###############################################################################
# Project: beelzebub
# Purpose: Classes to manage transformation of input to output
# Author:  <NAME>
# Date:    2020-10-01
###############################################################################

__version__ = '0.1.1'

import logging.config
import io
import jinja2

class BaseContextManager(object):
    """
    Context manager base class
    """

    def __init__(self, iostream=None, iotype='', conf={}):
        """
        Constructor

        :param iostream: Iostream (file path, URL, or str)
        :type iostream: str
        :param iotype: Type of the iostream ['file','url','str']
        :type iotype: str
        :param conf: Optional configuration
        :type conf: dict
        """

        self.iostream = iostream
        self.iotype = iotype
        self.conf = conf
        self.fp = None

        if not self.iotype:
            try:
                self.iotype = self.conf['iotype']
            except KeyError:
                pass

    def __enter__(self):
        """
        Enter the runtime context for this object

        The iostream is opened

        :returns: This object
        :rtype: BeelzebubBaseContextManager
        """

        return self.open(iostream=self.iostream)

    def __exit__(self, exc_type, exc_value, exc_traceback):
        """
        Exit the runtime context for this object

        The iostream is closed

        :returns: False
        :rtype: bool
        """

        self.close()

        return False         # This ensures any exception is re-raised

    def open(self, iostream=None, iotype='', mode='r', encoding='utf-8'):
        """
        Open the given iostream

        :param iostream: Iostream (file path, URL, or str)
        :type iostream: str
        :param iotype: Type of the iostream ['file','url','str']
        :type iotype: str
        :param mode: Mode in which to open if iostream is a file
        :type mode: str
        :param encoding: Encoding if iostream is a file
        :type encoding: str
        :returns: This object
        :rtype: BeelzebubBaseContextManager
        """

        if iostream:
            self.iostream = iostream

        if iotype:
            self.iotype = iotype

        if self.iotype.lower() == 'file':
            self.fp = io.open(self.iostream, mode, encoding=encoding)
        elif self.iotype.lower() == 'url':
            self.fp = io.open(self.iostream)
        else:
            self.fp = io.StringIO(self.iostream)

        return self

    def close(self):
        """
        Close the iostream

        :returns: This object
        :rtype: BeelzebubBaseContextManager
        """

        self.fp.close()
        self.fp = None

        return self

class BaseReader(BaseContextManager):
    """
    Context manager for reading input
    """

    def __init__(self, **kwargs):
        """
        Constructor

        Takes the same arguments as super.__init__()
        """

        super().__init__(**kwargs)
        self.input = None

    def open(self, mode='r', **kwargs):
        """
        Open the given iostream

        Takes the same arguments as super.open()
        """

        return super().open(mode=mode, **kwargs)

    def read(self):
        """
        Read the input iostream

        The input is loaded as a str and accessible via self.input

        :returns: The input
        :rtype: str
        """

        self.input = self.fp.read()

        return self.input

class BaseWriter(BaseContextManager):
    """
    Context manager for writing output
    """

    DEFAULTS = {
       'template': None
    }

    def __init__(self, **kwargs):
        """
        Constructor

        Takes the same arguments as super.__init__()
        """

        super().__init__(**kwargs)
        self.output = None

    def open(self, mode='w', **kwargs):
        """
        Open the given iostream

        Takes the same arguments as super.open()
        """

        return super().open(mode=mode, **kwargs)

    def infill_template(self, data, template=None):
        """
        Use the input dict to infill the template

        The infilled template ouput is accessible via self.output

        If no template is specified nor set as default, then self.output is
        just the input data

        :param data: Input data
        :type data: dict
        :param template: Optionally specify an alternative template
        :type template: str
        :returns: The output
        :rtype: str
        """

        if not template:
            template = self.DEFAULTS['template']

        if not template:
            self.output = data
        else:
            self.output = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True, lstrip_blocks=True).get_template(template).render(data)

        return self.output

    def transform(self, data):
        """
        Transform the input dict to the output str

        The transformed ouput str is accessible via self.output

        :param data: Input data
        :type data: dict
        :returns: The output
        :rtype: str
        """

        self.infill_template(data)

        return self.output

    def write(self, data):
        """
        Write the output iostream

        The output is written as a str and accessible via self.output

        :param data: Input data
        :type data: dict
        :returns: The output
        :rtype: str
        """

        self.transform(data)
        self.fp.write(self.output)

        return self.output

class BaseProcessor(object):
    """
    Execute an input to output processing workflow
    """

    def __init__(self, reader, writer, conf={}):
        """
        Constructor

        :param reader: reader class
        :type reader: class
        :param writer: writer class
        :type writer: class
        :param conf: Optional configuration
        :type conf: dict
        """

        self.reader = reader
        self.writer = writer
        self.conf = conf
        self.input = None
        self.output = None

    def process(self):
        """
        Process the input to output
        """

        with self.reader as fp:
            self.input = fp.read()

        with self.writer as fp:
            fp.write(self.input)

class BaseWorkflow(object):
    """
    Setup an input to output processing workflow
    """

    def __init__(self, reader_class=BaseReader, writer_class=BaseWriter, processor_class=BaseProcessor, conf={}):
        """
        Constructor

        :param reader: reader class
        :type reader: class
        :param writer: writer class
        :type writer: class
        :param processor: input to output processor class
        :type processor: class
        :param conf: Optional configuration
        :type conf: dict
        """

        self.reader_class = reader_class
        self.writer_class = writer_class
        self.processor_class = processor_class
        self.conf = conf
        self.source = None
        self.sink = None

    def get_conf_section(self, section):
        """
        Get the named section from the configuration

        :param section: The name of the configuration section
        :type section: str
        :returns: The configuration section
        :rtype: dict
        """

        conf_section = None

        try:
            conf_section = self.conf[section]
        except KeyError:
            pass

        return conf_section

    def setup_logging(self):
        """
        Optionally setup logging for the workflow

        If the optional configuration contains a `logger` section, then it
        is used to configure logging for the workflow
        """

        logging_conf = self.get_conf_section('logger')

        if logging_conf:
            logging.config.dictConfig(logging_conf)

    def process(self):
        """
        Process the input to output
        """

        self.reader = self.reader_class(iostream=self.source, conf=self.get_conf_section('reader'))
        self.writer = self.writer_class(iostream=self.sink, conf=self.get_conf_section('writer'))
        self.processor = self.processor_class(self.reader, self.writer, conf=self.get_conf_section('processor'))
        self.processor.process()

    def run(self, source, sink):
        """
        Run the input to output workflow
        """

        self.source = source
        self.sink = sink
        self.setup_logging()
        self.process()


###############################################################################
# Project: beelzebub
# Purpose: Classes to manage transformation of input to output
# Author:  <NAME>
# Date:    2020-10-01
###############################################################################

__version__ = '0.1.1'

import logging.config
import io
import jinja2

class BaseContextManager(object):
    """
    Context manager base class
    """

    def __init__(self, iostream=None, iotype='', conf={}):
        """
        Constructor

        :param iostream: Iostream (file path, URL, or str)
        :type iostream: str
        :param iotype: Type of the iostream ['file','url','str']
        :type iotype: str
        :param conf: Optional configuration
        :type conf: dict
        """

        self.iostream = iostream
        self.iotype = iotype
        self.conf = conf
        self.fp = None

        if not self.iotype:
            try:
                self.iotype = self.conf['iotype']
            except KeyError:
                pass

    def __enter__(self):
        """
        Enter the runtime context for this object

        The iostream is opened

        :returns: This object
        :rtype: BeelzebubBaseContextManager
        """

        return self.open(iostream=self.iostream)

    def __exit__(self, exc_type, exc_value, exc_traceback):
        """
        Exit the runtime context for this object

        The iostream is closed

        :returns: False
        :rtype: bool
        """

        self.close()

        return False         # This ensures any exception is re-raised

    def open(self, iostream=None, iotype='', mode='r', encoding='utf-8'):
        """
        Open the given iostream

        :param iostream: Iostream (file path, URL, or str)
        :type iostream: str
        :param iotype: Type of the iostream ['file','url','str']
        :type iotype: str
        :param mode: Mode in which to open if iostream is a file
        :type mode: str
        :param encoding: Encoding if iostream is a file
        :type encoding: str
        :returns: This object
        :rtype: BeelzebubBaseContextManager
        """

        if iostream:
            self.iostream = iostream

        if iotype:
            self.iotype = iotype

        if self.iotype.lower() == 'file':
            self.fp = io.open(self.iostream, mode, encoding=encoding)
        elif self.iotype.lower() == 'url':
            self.fp = io.open(self.iostream)
        else:
            self.fp = io.StringIO(self.iostream)

        return self

    def close(self):
        """
        Close the iostream

        :returns: This object
        :rtype: BeelzebubBaseContextManager
        """

        self.fp.close()
        self.fp = None

        return self

class BaseReader(BaseContextManager):
    """
    Context manager for reading input
    """

    def __init__(self, **kwargs):
        """
        Constructor

        Takes the same arguments as super.__init__()
        """

        super().__init__(**kwargs)
        self.input = None

    def open(self, mode='r', **kwargs):
        """
        Open the given iostream

        Takes the same arguments as super.open()
        """

        return super().open(mode=mode, **kwargs)

    def read(self):
        """
        Read the input iostream

        The input is loaded as a str and accessible via self.input

        :returns: The input
        :rtype: str
        """

        self.input = self.fp.read()

        return self.input

class BaseWriter(BaseContextManager):
    """
    Context manager for writing output
    """

    DEFAULTS = {
       'template': None
    }

    def __init__(self, **kwargs):
        """
        Constructor

        Takes the same arguments as super.__init__()
        """

        super().__init__(**kwargs)
        self.output = None

    def open(self, mode='w', **kwargs):
        """
        Open the given iostream

        Takes the same arguments as super.open()
        """

        return super().open(mode=mode, **kwargs)

    def infill_template(self, data, template=None):
        """
        Use the input dict to infill the template

        The infilled template ouput is accessible via self.output

        If no template is specified nor set as default, then self.output is
        just the input data

        :param data: Input data
        :type data: dict
        :param template: Optionally specify an alternative template
        :type template: str
        :returns: The output
        :rtype: str
        """

        if not template:
            template = self.DEFAULTS['template']

        if not template:
            self.output = data
        else:
            self.output = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True, lstrip_blocks=True).get_template(template).render(data)

        return self.output

    def transform(self, data):
        """
        Transform the input dict to the output str

        The transformed ouput str is accessible via self.output

        :param data: Input data
        :type data: dict
        :returns: The output
        :rtype: str
        """

        self.infill_template(data)

        return self.output

    def write(self, data):
        """
        Write the output iostream

        The output is written as a str and accessible via self.output

        :param data: Input data
        :type data: dict
        :returns: The output
        :rtype: str
        """

        self.transform(data)
        self.fp.write(self.output)

        return self.output

class BaseProcessor(object):
    """
    Execute an input to output processing workflow
    """

    def __init__(self, reader, writer, conf={}):
        """
        Constructor

        :param reader: reader class
        :type reader: class
        :param writer: writer class
        :type writer: class
        :param conf: Optional configuration
        :type conf: dict
        """

        self.reader = reader
        self.writer = writer
        self.conf = conf
        self.input = None
        self.output = None

    def process(self):
        """
        Process the input to output
        """

        with self.reader as fp:
            self.input = fp.read()

        with self.writer as fp:
            fp.write(self.input)

class BaseWorkflow(object):
    """
    Setup an input to output processing workflow
    """

    def __init__(self, reader_class=BaseReader, writer_class=BaseWriter, processor_class=BaseProcessor, conf={}):
        """
        Constructor

        :param reader: reader class
        :type reader: class
        :param writer: writer class
        :type writer: class
        :param processor: input to output processor class
        :type processor: class
        :param conf: Optional configuration
        :type conf: dict
        """

        self.reader_class = reader_class
        self.writer_class = writer_class
        self.processor_class = processor_class
        self.conf = conf
        self.source = None
        self.sink = None

    def get_conf_section(self, section):
        """
        Get the named section from the configuration

        :param section: The name of the configuration section
        :type section: str
        :returns: The configuration section
        :rtype: dict
        """

        conf_section = None

        try:
            conf_section = self.conf[section]
        except KeyError:
            pass

        return conf_section

    def setup_logging(self):
        """
        Optionally setup logging for the workflow

        If the optional configuration contains a `logger` section, then it
        is used to configure logging for the workflow
        """

        logging_conf = self.get_conf_section('logger')

        if logging_conf:
            logging.config.dictConfig(logging_conf)

    def process(self):
        """
        Process the input to output
        """

        self.reader = self.reader_class(iostream=self.source, conf=self.get_conf_section('reader'))
        self.writer = self.writer_class(iostream=self.sink, conf=self.get_conf_section('writer'))
        self.processor = self.processor_class(self.reader, self.writer, conf=self.get_conf_section('processor'))
        self.processor.process()

    def run(self, source, sink):
        """
        Run the input to output workflow
        """

        self.source = source
        self.sink = sink
        self.setup_logging()
        self.process()


############################################################################### # Project: beelzebub # Purpose: Classes to manage transformation of input to output # Author:  <NAME> # Date:    2020-10-01 ############################################################################### Context manager base class Constructor          :param iostream: Iostream (file path, URL, or str)         :type iostream: str         :param iotype: Type of the iostream ['file','url','str']         :type iotype: str         :param conf: Optional configuration         :type conf: dict Enter the runtime context for this object          The iostream is opened          :returns: This object         :rtype: BeelzebubBaseContextManager Exit the runtime context for this object          The iostream is closed          :returns: False         :rtype: bool # This ensures any exception is re-raised Open the given iostream          :param iostream: Iostream (file path, URL, or str)         :type iostream: str         :param iotype: Type of the iostream ['file','url','str']         :type iotype: str         :param mode: Mode in which to open if iostream is a file         :type mode: str         :param encoding: Encoding if iostream is a file         :type encoding: str         :returns: This object         :rtype: BeelzebubBaseContextManager Close the iostream          :returns: This object         :rtype: BeelzebubBaseContextManager Context manager for reading input Constructor          Takes the same arguments as super.__init__() Open the given iostream          Takes the same arguments as super.open() Read the input iostream          The input is loaded as a str and accessible via self.input          :returns: The input         :rtype: str Context manager for writing output Constructor          Takes the same arguments as super.__init__() Open the given iostream          Takes the same arguments as super.open() Use the input dict to infill the template          The infilled template ouput is accessible via self.output          If no template is specified nor set as default, then self.output is         just the input data          :param data: Input data         :type data: dict         :param template: Optionally specify an alternative template         :type template: str         :returns: The output         :rtype: str Transform the input dict to the output str          The transformed ouput str is accessible via self.output          :param data: Input data         :type data: dict         :returns: The output         :rtype: str Write the output iostream          The output is written as a str and accessible via self.output          :param data: Input data         :type data: dict         :returns: The output         :rtype: str Execute an input to output processing workflow Constructor          :param reader: reader class         :type reader: class         :param writer: writer class         :type writer: class         :param conf: Optional configuration         :type conf: dict Process the input to output Setup an input to output processing workflow Constructor          :param reader: reader class         :type reader: class         :param writer: writer class         :type writer: class         :param processor: input to output processor class         :type processor: class         :param conf: Optional configuration         :type conf: dict Get the named section from the configuration          :param section: The name of the configuration section         :type section: str         :returns: The configuration section         :rtype: dict Optionally setup logging for the workflow          If the optional configuration contains a `logger` section, then it         is used to configure logging for the workflow Process the input to output Run the input to output workflow