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 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())