tools.py 3.88 KB
'''
File:           perguntations/tools.py
Description:    Helper functions to load yaml files and run external programs.
'''


# python standard library
import asyncio
import logging
from os import path
import subprocess
from typing import Any, List

# third party libraries
import yaml


# setup logger for this module
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
def load_yaml(filename: str, default: Any = None) -> Any:
    '''load data from yaml file'''

    filename = path.expanduser(filename)
    try:
        file = open(filename, 'r', encoding='utf-8')
    except Exception as exc:
        logger.error(exc)
        if default is not None:
            return default
        raise

    with file:
        try:
            return yaml.safe_load(file)
        except yaml.YAMLError as exc:
            logger.error(str(exc).replace('\n', ' '))
            if default is not None:
                return default
            raise


# ---------------------------------------------------------------------------
def run_script(script: str,
               args: List[str],
               stdin: str = '',
               timeout: int = 3) -> Any:
    '''
    Runs a script and returns its stdout parsed as yaml, or None on error.
    The script is run in another process but this function blocks waiting
    for its termination.
    '''
    logger.info('run_script "%s"', script)

    output = None
    script = path.expanduser(script)
    cmd = [script] + [str(a) for a in args]

    # --- run process
    try:
        proc = subprocess.run(cmd,
                              input=stdin,
                              stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT,
                              universal_newlines=True,
                              timeout=timeout,
                              check=True,
                              )
    except subprocess.TimeoutExpired:
        logger.error('Timeout %ds exceeded running "%s".', timeout, script)
        return output
    except subprocess.CalledProcessError as exc:
        logger.error('Return code %d running "%s".', exc.returncode, script)
        return output
    except OSError:
        logger.error('Can not execute script "%s".', script)
        return output

    # --- parse yaml
    try:
        output = yaml.safe_load(proc.stdout)
    except yaml.YAMLError:
        logger.error('Error parsing yaml output of "%s".', script)

    return output


# ----------------------------------------------------------------------------
async def run_script_async(script: str,
                           args: List[str],
                           stdin: str = '',
                           timeout: int = 3) -> Any:
    '''Same as above, but asynchronous'''

    script = path.expanduser(script)
    args = [str(a) for a in args]
    output = None

    # --- start process
    try:
        proc = await asyncio.create_subprocess_exec(
            script, *args,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.DEVNULL,
            )
    except OSError:
        logger.error('Can not execute script "%s".', script)
        return output

    # --- send input and wait for termination
    try:
        stdout, _ = await asyncio.wait_for(
            proc.communicate(input=stdin.encode('utf-8')),
            timeout=timeout)
    except asyncio.TimeoutError:
        logger.warning('Timeout %ds running script "%s".', timeout, script)
        return output

    # --- check return code
    if proc.returncode != 0:
        logger.error('Return code %d running "%s".', proc.returncode, script)
        return output

    # --- parse yaml
    try:
        output = yaml.safe_load(stdout.decode('utf-8', 'ignore'))
    except yaml.YAMLError:
        logger.error('Error parsing yaml output of "%s"', script)

    return output