#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention UCS@school
# Copyright 2017-2021 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.
"""
Base class for all Python based import hooks.
"""
from __future__ import absolute_import
import logging
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Type, TypeVar, Union
from ucsschool.lib.pyhooks import PyHook, PyHooksLoader
from ..exceptions import InitialisationError
from .ldap_connection import get_admin_connection, get_readonly_connection
if TYPE_CHECKING:
    import univention.admin.uldap
ImportPyHookTV = TypeVar("ImportPyHookTV", bound="ImportPyHook")
__import_pyhook_loader_instance = None  # type: Optional["ImportPyHookLoader"]
[docs]class ImportPyHook(PyHook):
    """
    Base class for Python based import hooks.
    * self.dry_run     # whether hook is executed during a dry-run (1)
    * self.lo          # LDAP connection object (2)
    * self.logger      # Python logging instance
    If multiple hook classes are found, hook functions with higher
    priority numbers run before those with lower priorities. None disables
    a function (no need to remove it / comment it out).
    (1) Hooks are only executed during dry-runs, if the class attribute
    :py:attr:`supports_dry_run` is set to `True` (default is `False`). Hooks
    with `supports_dry_run == True` must not modify LDAP objects.
    Therefore the LDAP connection object self.lo will be a read-only connection
    during a dry-run.
    (2) Read-write cn=admin connection in a real run, read-only cn=admin
    connection during a dry-run.
    """
    supports_dry_run = False  # if True hook will be executed during a dry-run
    def __init__(self, lo=None, dry_run=None, *args, **kwargs):
        # type: (Optional[univention.admin.uldap.access], Optional[bool], *Any, **Any) -> None
        """
        :param univention.admin.uldap.access lo: optional LDAP connection object
        :param bool dry_run: whether hook is executed during a dry-run
        """
        super(ImportPyHook, self).__init__(*args, **kwargs)
        if dry_run is None:
            from ..configuration import Configuration
            try:
                config = Configuration()
                self.dry_run = config["dry_run"]
                """Whether this is a dry-run"""
            except InitialisationError:
                self.dry_run = False
        else:
            self.dry_run = dry_run
        if lo is None:
            self.lo = (
                get_readonly_connection()[0] if self.dry_run else get_admin_connection()[0]
            )  # type: univention.admin.uldap.access
            """LDAP connection object"""
        else:
            self.lo = lo  # reuse LDAP object
            """LDAP connection object"""
        self.logger = logging.getLogger(__name__)  # type: logging.Logger
        """Python logging instance""" 
[docs]class ImportPyHookLoader(object):
    """
    Load and initialize hooks.
    If hooks should be instantiated with arguments, use :py:meth:`init_hook()`
    before :py:meth:`call_hook()`.
    """
    _pyhook_obj_cache = {}  # type: Dict[Type[ImportPyHookTV], Dict[str, List[Callable[..., Any]]]]
    def __init__(self, pyhooks_base_path):  # type: (str) -> None
        self.pyhooks_base_path = pyhooks_base_path
        self.logger = logging.getLogger(__name__)  # type: logging.Logger
[docs]    def init_hook(self, hook_cls, filter_func=None, *args, **kwargs):
        # type: (Union[Type[ImportPyHookTV], str], Optional[Callable[[Type[ImportPyHookTV]], bool]], *Any, **Any) -> Dict[str, List[Callable[..., Any]]]  # noqa: E501
        """
        Load and initialize hook class `hook_cls`.
        :param hook_cls: class object - load and run hooks that are a subclass of this
        :type hook_cls: ucsschool.importer.utils.import_pyhook.ImportPyHook or str
        :param filter_func: function to filter out undesired hook classes (takes a
            class and returns a bool), passed to PyHooksLoader
        :type filter_func: callable or None
        :param tuple args: arguments to pass to __init__ of hooks
        :param dict kwargs: arguments to pass to __init__ of hooks
        :return: mapping from method names to list of methods of initialized
            hook objects, sorted by method priority
        :rtype: dict[str, list[callable]]
        """
        # The PyHook objects themselves are already cached by PyHooksLoader, but we don't want to
        # initialize a PyHooksLoader each time we run a hook, so we'll keep a dict linking directly to
        # all PyHooksLoader caches.
        base_class = PyHooksLoader.hook_cls2importpyhook(hook_cls, "hook_cls")
        if not issubclass(base_class, ImportPyHook):
            raise TypeError("Argument 'hook_cls' must be a subclass of ImportPyHook.")
        if base_class not in self._pyhook_obj_cache:
            pyhooks_loader = PyHooksLoader(self.pyhooks_base_path, base_class, self.logger, filter_func)
            self._pyhook_obj_cache[base_class] = pyhooks_loader.get_hook_objects(*args, **kwargs)
        return self._pyhook_obj_cache[base_class] 
[docs]    def call_hooks(self, hook_cls, func_name, *args, **kwargs):
        # type: (Type[ImportPyHookTV], str, *Any, **Any) -> List[Any]
        """
        Run hooks with name `func_name` from class `hook_cls`.
        :param hook_cls: class object - load and run hooks that are a
            subclass of this
        :param str func_name: name of method to run in each hook
        :param args: arguments to pass to hook function
        :param kwargs: arguments to pass to hook function
        :return: list of when all executed hooks returned
        :rtype: list
        """
        hooks = self.init_hook(hook_cls)
        res = []
        for func in hooks.get(func_name, []):
            self.logger.info("Running %s %s hook %s ...", self.__class__.__name__, func_name, func)
            res.append(func(*args, **kwargs))
        return res  
[docs]def get_import_pyhooks(hook_cls, filter_func=None, *args, **kwargs):
    # type: (Union[Type[ImportPyHookTV], str], Optional[Callable[[Type[ImportPyHookTV]], bool]], *Any, **Any) -> Dict[str, List[Callable[..., Any]]]  # noqa: E501
    """
    Retrieve (and initialize subclasses of :py:class:`hook_cls`, if not yet
    done) pyhooks of type `hook_cls`. Results are cached.
    If no argument must be passed to the `hook_cls` :py:meth:`__init__()` or
    :py:class:`PyHooksLoader`, then it is not necessary to call this function,
    just use :py:func:`run_import_pyhooks` directly.
    Convenience function for easy usage of PyHooksLoader.
    :param hook_cls: class object or fully dotted Python path to a class
        definition - load and run hooks that are a subclass of this
    :type hook_cls: ucsschool.importer.utils.import_pyhook.ImportPyHook or str
    :param filter_func: function to filter out undesired hook classes (takes a
        class and returns a bool), passed to PyHooksLoader
    :type filter_func: callable or None
    :param args: arguments to pass to __init__ of hooks
    :param kwargs: arguments to pass to __init__ of hooks
    :return: mapping from method names to list of methods of initialized
        hook objects, sorted by method priority
    :rtype: dict[str, list[callable]]
    """
    global __import_pyhook_loader_instance
    if not __import_pyhook_loader_instance:
        from ..configuration import Configuration
        try:
            config = Configuration()
            path = config.get("hooks_dir_pyhook", "/usr/share/ucs-school-import/pyhooks")
            if "dry_run" not in kwargs:
                kwargs["dry_run"] = config["dry_run"]
        except InitialisationError:
            path = "/usr/share/ucs-school-import/pyhooks"
        __import_pyhook_loader_instance = ImportPyHookLoader(path)
    return __import_pyhook_loader_instance.init_hook(hook_cls, filter_func, *args, **kwargs) 
[docs]def run_import_pyhooks(hook_cls, func_name, *args, **kwargs):
    # type: (Union[Type[ImportPyHookTV], str], str, *Any, **Any) -> List[Any]
    """
    Execute method `func_name` of subclasses of `hook_cls`, load and
    initialize if required.
    Convenience function for easy usage of PyHooksLoader.
    :param hook_cls: class object or fully dotted Python path to a class
        definition - load and run hooks that are a subclass of this
    :type hook_cls: ucsschool.importer.utils.import_pyhook.ImportPyHook or str
    :param str func_name: name of method to run in each hook
    :param args: arguments to pass to hook function `func_name`
    :param kwargs: arguments to pass to hook function `func_name`
    :return: list of when all executed hooks returned
    :rtype: list
    """
    get_import_pyhooks(hook_cls)
    return __import_pyhook_loader_instance.call_hooks(hook_cls, func_name, *args, **kwargs)