Source code for kuha_oai_pmh_repo_handler.serve

#!/usr/bin/env python3
# Author(s): Toni Sissala
# Copyright 2021 Finnish Social Science Data Archive FSD / University of Tampere
# Licensed under the EUPL. See LICENSE.txt for full license.
"""Main entry point for starting OAI-PMH Repo Handler.
"""
import sys
import logging
import importlib.metadata

from kuha_common import (
    conf,
    cli_setup
)
from kuha_common.server import serve
from kuha_oai_pmh_repo_handler import (
    http_api,
    controller,
    constants
)


_logger = logging.getLogger(__name__)
MDFORMATS_EPGROUP = 'kuha.oai.metadataformats'


[docs]class KuhaEntryPointException(Exception): """Base for entry point exceptions."""
[docs]class InvalidMetadataFormatException(KuhaEntryPointException): """A loaded metadataformat is constructed wrong. Raised for instance, when a metadataformat implements different sets that other metadataformats in same repository. """
[docs]class ConflictingMetadataPrefixException(KuhaEntryPointException): """Raise when non-overridable metadataformats implement the same metadataprefix"""
[docs]class NoMetadataFormatsException(KuhaEntryPointException): """Unable to load any metadataformats for OAI-PMH Repo Handler"""
def _iter_entry_points_by_group(entry_point_group): """Iterate distribution entry points by group Provides a compatibility layer for python versions 3.8 - 3.12. The behaviour for :func:`importlib.metadata.entry_points()` was changed in python 3.10 in a way that provides no backwards compatible way to get entry points by group. Furthermore, there is a bug in importlib.metadata.entry_points() in python 3.8 and python 3.9., which yields duplicate entry_points when there is a .egg-info findable in sys.path along with a normally installed site-packages entry. See https://github.com/python/importlib_metadata/issues/410 and https://github.com/pypa/setuptools/issues/3649 This function may be removed when support for Python < 3.10 is dropped and use `importlib.metadata.entry_points(group=entry_point_group)` :param str entry_point_group: Group to iterate :returns: generator yielding every entry point in group """ if sys.version_info.minor < 10: found = [] for entry_point in importlib.metadata.entry_points().get(entry_point_group): name_value = (entry_point.name, entry_point.value) if name_value in found: continue found.append(name_value) yield entry_point return yield from importlib.metadata.entry_points(group=entry_point_group)
[docs]def load_metadataformats(entry_point_group): """Load metadataformat using plugin discovery via setuptools entry-points. The following constraints apply to loaded metadataformats: * Every loaded metadataformat must have a unique mdprefix. Consults overridable (bool) attribute to check if a certain mdprefix can be overridden by another metadataformat. Raises ConflictingMetadataPrefixException, if metadataformats have same mdprefix and are non-overridable. * Every loaded metadataformat must implement the same sets; sets-attribute and list_sets method must be the same for every loaded metadataformat. Note that overridden metadataformats can have different sets that the ones that are finally loaded, as long as the loaded ones implement the same sets. Raises InvalidMetadataFormatException if loaded metadataformats implement different sets. Also, it is mandatory to load at least one metadataformat. Raises NoMetadataFormatsException if no metadataformats are loaded. :param str entry_point_group: Entry point group for metadataformats. :returns: Loaded metadataformat classes. :rtype: list """ mdformats = {} for entry_point in _iter_entry_points_by_group(entry_point_group): candidate = entry_point.load() loaded = mdformats.get(candidate.mdprefix) if loaded and loaded.overridable is False: if candidate.overridable is False: raise ConflictingMetadataPrefixException( "Conflicting non-overridable metadata prefix '%s'" % (candidate.mdprefix,)) continue mdformats.update({candidate.mdprefix: candidate}) if mdformats == {}: raise NoMetadataFormatsException( "Could not load any metadataformats from entry point group '%s'" % (entry_point_group,)) mdformats = list(mdformats.values()) sets_list_sets = (mdformats[0].sets, mdformats[0].list_sets) if any((mdf.sets, mdf.list_sets) != sets_list_sets for mdf in mdformats[1:]): raise InvalidMetadataFormatException("Metadataformats must implement same OAI sets.") return mdformats
[docs]def configure(mdformats): """Configure OAI-PMH Server. :param list mdformats: Metadataformats to serve :returns: Server settings :rtype: :obj:`argparse.Namespace` """ conf.load('Kuha OAI-PMH Repo Handler', package='kuha_oai_pmh_repo_handler', env_var_prefix='KUHA_') conf.add('--api-version', help='OAI-PMH api version', default=constants.API_VERSION, env_var='OPRH_API_VERSION', type=str) conf.add('--port', help='Server port', default=constants.SERVER_PORT, env_var='OPRH_PORT', type=int) conf.add_config_arg() conf.add_print_arg() controller.add_cli_args() for mdformat in mdformats: mdformat.add_cli_args(conf) settings = cli_setup.setup_common_modules( cli_setup.MOD_SERVER, cli_setup.MOD_LOGGING) for mdformat in mdformats: mdformat.configure(settings) return settings
[docs]def main(): """Application main function. Parse commandline for settings. Setup and serve webapp. Exit on exceptions propagated at this level. :returns: None """ mdformats = load_metadataformats(MDFORMATS_EPGROUP) settings = configure(mdformats) if settings.print_configuration: print("Print active configuration and exit\n") conf.print_conf() return ctrl = controller.from_settings(settings, mdformats) web_app = http_api.get_app(settings.api_version, controller=ctrl) try: serve(web_app, settings.port) except KeyboardInterrupt as exc: _logger.warning("Shutdown by CTRL + C") _logger.warning(str(exc)) except: _logger.exception("Exception in main()") raise finally: # Cleanup _logger.info("Shutdown")
if __name__ == '__main__': sys.exit(main())