Source code for ontocode.instantiation

# Copyright, 2018-2019, Deutsches Zentrum für Luft- und Raumfahrt e.V.
# Licensed under LGPLv3+, see LICENSE for details.
"""
Instantiations tie everything together. An
:class:`~ontocode.instantiation.Instantiation` aggregates a list of
:ref:`ontology locators<ontology-locators>`, a :ref:`template<templates>`, and
list, as well as optionally a single instance, of
:ref:`template inputs<template-inputs>`.

Calling one of their execution methods will cause it to load the ontologies
specified by the :ref:`ontology locators<ontology-locators>`, let the
:ref:`template inputs<template-inputs>` generate arguments for the
:ref:`template<templates>` and pass the results to its
:ref:`template<templates>`.

A :class:`~ontocode.instantiation.Instantiation` object can instantiate its
:ref:`template<templates>` multiple times for a single execution. This is
controlled by the second :ref:`template input<template-inputs>` argument to
:class:`~ontocode.instantiation.Instantiation`\\´s constructor:
``per_row_input``.

:ref:`Template inputs<template-inputs>` passed as arguments to
``per_row_input`` are expected to return a list of dictionaries, while all
:ref:`template inputs<template-inputs>` in lists passed as arguments to
``at_once_inputs`` are expected to return dictionaries. The number of elements
in the list generated by the ``per_row_input`` argument determines the number
of :ref:`template<templates>` instantiations.

When :func:`~ontocode.instantiation.Instantiation.execute` is called (directly
or via :func:`~ontocode.instantiation.Instantiation.execute_and_write_to_file`)
with arguments ``*args`` and ``**kwargs``, the
:ref:`template inputs<template-inputs>` passed to ``at_once_inputs`` are called
to generate a set of dictionaries and, if not ``None``, the argument passed to
``per_row_input`` is called to generate a list of dictionaries.

Now two cases have to be distinguished: ``per_row_input`` is ``None`` or
generated an empty list and ``per_row_input`` generated a non-empty list.

In the former case, the :ref:`template<templates>` is instantiated once.
``*arg`` is passed right through from
:func:`~ontocode.instantiation.Instantiation.execute`. The argument to
``**kwargs`` is a merge of ``**kwargs`` as passed to
:func:`~ontocode.instantiation.Instantiation.execute` with the dictionaries
generated by the argument to ``at_once_inputs``, where
:func:`~ontocode.instantiation.Instantiation.execute`\\´s ``**kwargs`` take
precedence over the generated dictionaries and the precedence order within
the dictionaries is determined by their order as part of the list passed to
``at_once_inputs``.

In the latter case, the :ref:`template<templates>` is instantiated once for
each dictionary generated by the argument to ``per_row_input``. The arguments
to the :ref:`template<templates>` are constructed similarly to the previous
case. The only difference is, that for each invocation a different dictionary
of those generated by the argument to ``per_row_input`` is merged with the
other dictionaries to produce the ``**kwargs`` argument. In the precedence
order, it is located between the ``**kwargs`` as passed to
:func:`~ontocode.instantiation.Instantiation.execute` and the dictionaries
generated by the argument to ``at_once_inputs``.
"""
import codecs
import errno
import os
import owlready2 as owl

__all__ = ['Instantiation', 'TemplateInputArgumentError',
           'TemplateInputResultError']


def _merge_dicts(dicts):
    result = {}
    for dic in dicts:
        result.update(dic)
    return result


def _write_to_file(path, content):
    abspath = os.path.abspath(path)
    dirname = os.path.dirname(abspath)
    try:
        os.makedirs(dirname)
    except OSError as error:  # pragma: no cover
        if errno.EEXIST != error.errno:
            raise
    with codecs.open(abspath, 'w', 'utf-8') as result_file:
        result_file.write(content)


[docs]class Instantiation(): """Instantiation of a template based on data queried from ontologies. :param list ontology_locators: a list of :ref:`ontology locators<ontology-locators>` :param ontocode.template.Template template: a template :param list at_once_inputs: a list of :class:`~ontocode.template_input.TemplateInput`\\s :param ontocode.template_input.TemplateInput per_row_input: a template input """ def __init__(self, ontology_locators, template, at_once_inputs, per_row_input=None): self._ontology_locators = ontology_locators self._at_once_inputs = at_once_inputs self._per_row_input = per_row_input self._template = template def _execute(self, *args, **kwargs): world = owl.World() self._load_ontologies(world) at_once_input = self._generate_at_once_input(world) if self._per_row_input: per_row_input = self._generate_per_row_input(world) render_kwargs_list = [{**at_once_input, **row, **kwargs} for row in per_row_input] return [(args, render_kwargs, self._template.render(*args, **render_kwargs)) for render_kwargs in render_kwargs_list] render_kwargs = {**at_once_input, **kwargs} return [(args, render_kwargs, self._template.render(*args, **render_kwargs))]
[docs] def execute(self, *args, **kwargs): """Executes the instantiation and returns a list of the results. The :ref:`instantiations` module documentation describes the instantiation process.""" return [content for (_, __, content) in self._execute(*args, **kwargs)]
[docs] def execute_and_write_to_file(self, path, *args, **kwargs): """Executes the instantiation and writes result to file. Raises an :class:`ontocode.instantiation.TemplateInputArgumentError`, if a non-null value for ``per_row_input`` was passed into the constructor of this :class:`~ontocode.instantiation.Instantiation` instance. The :ref:`instantiations` module documentation describes the instantiation process. :param str path: path to target file """ if self._per_row_input: raise TemplateInputArgumentError() content = self.execute(*args, **kwargs)[0] _write_to_file(path, content)
[docs] def execute_and_write_to_files(self, path_function, *args, **kwargs): """Executes the instantiation and writes result to files. The :ref:`instantiations` module documentation describes the instantiation process. :param generator path_function: a function that takes the same arguments as the instances template and returns a path for the corresponding template invocation """ for (args, kwargs, content) in self._execute(*args, **kwargs): path = path_function(*args, **kwargs) _write_to_file(path, content)
def _load_ontologies(self, world): for locator in self._ontology_locators: locator.load(world) def _generate_at_once_input(self, world): def generate_at_once_input(input_, world): result = input_.generate(world) if not isinstance(result, dict): raise TemplateInputResultError('at_once_inputs argument \ element must return a dict type object') return result at_once_inputs = [generate_at_once_input(input_, world) for input_ in self._at_once_inputs] return _merge_dicts(at_once_inputs) def _generate_per_row_input(self, world): result = self._per_row_input.generate(world) try: iter(result) except TypeError: raise TemplateInputResultError('per_row_input argument must \ return an iterable') return result
[docs]class TemplateInputResultError(TypeError): """Raised when a template input´s result is of the wrong type."""
[docs]class TemplateInputArgumentError(RuntimeError): """Raised when :func:`~ontocode.instantiation.Instantiation.execute_and_write_to_file` is called on an :class:`~ontocode.instantiation.Instantiation`\\´s instance that was created with a non-null ``per_row_input`` argument."""