Source code for kuha_oai_pmh_repo_handler.controller

"""OAI-PMH Repo Handler controller

Connects backend components together, mainly metadataformats, XML
templates and the oai.Protocol. Is responsible for cathing oai.errors
and routing them to the correct template.
"""
import logging
from kuha_common import conf
from kuha_oai_pmh_repo_handler.oai.constants import OAI_RESPOND_WITH_REQ_URL
from kuha_oai_pmh_repo_handler import oai
from kuha_oai_pmh_repo_handler.genshi_loader import GenPlate
from kuha_oai_pmh_repo_handler.constants import API_VERSION


_logger = logging.getLogger(__name__)


class _MDFormatsWrapper:
    """Helper wraps metadataformats and their initialization
    arguments. Returns initialized metadataformat objects.

    The __init__ takes in loaded metadataformats as the first argument.
    Additional arguments are used when initialising the metadataformats.

    Usage::

        mdfwrapper = _MDFormatsWrapper(loader_metadataformats, <metadataformats_initialization_arguments>)
        # iterate all metadataformats
        for mdf in mdfwrapper:
            # Do something with initialized metadataformat
            pass
        # get specific metadataformat using metadataprefix
        mdf = mdfwrapper.get('oai_dc')
        # get metadataformat using list index. This is mainly used when any of the metadataformats will do.
        mdf = mdfwrapper[0]

    :param list mdformats: loaded metadataformats
    """
    def __init__(self, mdformats, *args, **kwargs):
        self._mdformats = mdformats
        self._args = {'args': args,
                      'kwargs': kwargs}

    def __iter__(self):
        for mdclass in self._mdformats:
            yield mdclass(*self._args['args'], **self._args['kwargs'])

    def __getitem__(self, index):
        return self._mdformats[index](*self._args['args'], **self._args['kwargs'])

    def get(self, prefix):
        """Get initialized metadataformat by prefix.

        :param str prefix: metadataformat prefix to lookup
        :returns: Initialized metadataformat object
        :raises: :exc:`oai.erros.CannotDisseminateFormat` if no
                 metadataformat is found using the prefix.
        """
        for mdclass in self._mdformats:
            if mdclass.mdprefix == prefix:
                return mdclass(*self._args['args'], **self._args['kwargs'])
        raise oai.errors.CannotDisseminateFormat()


[docs]class Controller: """Controls processing of OAI requests Holds information about the expected behaviour and routes requests to correct handlers using defined metadataformats. Interprets the OAI request and calls the correct metadataformats. Catches oai.errors and routes them to the error handler & template. :param bool respond_with_requested_url: Configures how to create the OAI response url. If True, the url is generated based on the incoming OAI request. Otherwise, the configured base_url is used. :seealso: :class:`oai.protocol.Response` :param list mdformats: Loaded metadataformats :param str stylesheet_url: Optional XML stylesheet URL that is added to templates. Defaults to an empty string. """ _mdfwrapperclass = _MDFormatsWrapper def __init__(self, respond_with_requested_url, mdformats, stylesheet_url=None): self._respond_with_requested_url = respond_with_requested_url self._mdformats = mdformats self.stylesheet_url = stylesheet_url or '' GenPlate.set_stylesheet_url(self.stylesheet_url) @staticmethod @GenPlate('error.xml') async def oai_error(oai_protocol): """Handle OAI errors :param :obj:`oai.Protocol` oai_protocol: OAI protocol instantiated with the current request. :returns: Response context :rtype: dict """ return oai_protocol.response.context @staticmethod @GenPlate('identify.xml') async def identify(mdformats, oai_protocol): """Handle Identify OAI request :param :obj:`Controller._mdfwrapperclass` mdformats: Wrapped & loaded metadataformats :param :obj:`oai.Protocol` oai_protocol: OAI protocol instantiated with the current request :returns: Response context :rtype: dict """ datestamp = None for mdf in mdformats: candidate = await mdf.get_earliest_datestamp() if datestamp is None or datestamp > candidate: datestamp = candidate return await oai_protocol.response.identify_response( datestamp, deleted_records=mdformats[0].get_deleted_record(), granularity=mdformats[0].datestamp_granularity) @staticmethod @GenPlate('list_sets.xml') async def listsets(mdformats, oai_protocol): """Handle ListSets OAI request :param :obj:`Controller._mdfwrapperclass` mdformats: Wrapped & loaded metadataformats :param :obj:`oai.Protocol` oai_protocol: OAI protocol instantiated with the current request :returns: Response context :rtype: dict """ # Take any md, since they should all implement same sets mdf = mdformats[0] await mdf.list_sets() return await oai_protocol.response.list_sets_response() @staticmethod @GenPlate('list_metadata_formats.xml') async def listmetadataformats(mdformats, oai_protocol): """Handle ListMetadataFormats OAI request :param :obj:`Controller._mdfwrapperclass` mdformats: Wrapped & loaded metadataformats :param :obj:`oai.Protocol` oai_protocol: OAI protocol instantiated with the current request :returns: Response context :rtype: dict """ for mdf in mdformats: await mdf.list_metadata_formats() return oai_protocol.response.context @staticmethod @GenPlate('list_identifiers.xml') async def listidentifiers(mdformats, oai_protocol): """Handle ListIdentifiers OAI request :param :obj:`Controller._mdfwrapperclass` mdformats: Wrapped & loaded metadataformats :param :obj:`oai.Protocol` oai_protocol: OAI protocol instantiated with the current request :returns: Response context :rtype: dict """ mdf = mdformats.get(oai_protocol.arguments.metadata_prefix) await mdf.list_identifiers() return await oai_protocol.response.list_identifiers_response()
[docs] @staticmethod async def listrecords(mdformats, oai_protocol): """Handle ListRecords OAI request :param :obj:`Controller._mdfwrapperclass` mdformats: Wrapped & loaded metadataformats :param :obj:`oai.Protocol` oai_protocol: OAI protocol instantiated with the current request :returns: Response context :rtype: dict """ mdf = mdformats.get(oai_protocol.arguments.metadata_prefix) return await mdf.list_records()
[docs] @staticmethod async def getrecord(mdformats, oai_protocol): """Handle GetRecord OAI request :param :obj:`Controller._mdfwrapperclass` mdformats: Wrapped & loaded metadataformats :param :obj:`oai.Protocol` oai_protocol: OAI protocol instantiated with the current request :returns: Response context :rtype: dict """ mdf = mdformats.get(oai_protocol.arguments.metadata_prefix) return await mdf.get_record()
[docs] async def oai_request(self, args, correlation_id_header, tornado_request): """Create a request from requested arguments. :param list args: List of 2-item tuples [(key, value]] containing request arguments. :param dict correlation_id: Correlation id header as dict :param tornado_request: Current request from tornado handler :returns: Two-tuple. First item is an iterable with the full HTTP response body. Second item is the current oai.Protocol instance. """ req_url = '{}://{}{}'.format( tornado_request.protocol, tornado_request.host, tornado_request.uri.split('?')[0]) if self._respond_with_requested_url else None oai_protocol = oai.Protocol(args, req_url) mdformats = self._mdfwrapperclass(self._mdformats, oai_protocol, correlation_id_header) try: return await getattr(self, oai_protocol.arguments.verb.lower())(mdformats, oai_protocol), oai_protocol except oai.errors.OAIError as exc: _logger.warning("OAIError: %s", exc) oai_protocol.response.set_error(exc) return await self.oai_error(oai_protocol), oai_protocol
[docs]def from_settings(settings, mdformats): """Get Controller from settings. :param :obj:`argparse.Namespace` settings: Loaded settings. :param list mdformats: Loaded metadataformats. :returns: Initialized controller. :rtype: :obj:`Controller` """ oai.configure(settings) return Controller(settings.oai_pmh_respond_with_requested_url, mdformats, stylesheet_url=settings.oai_pmh_stylesheet_url)
[docs]def add_cli_args(): """Add command line arguments required by controller. """ oai.add_cli_args(conf) conf.add('--oai-pmh-respond-with-requested-url', help='OAI-PMH response uses base url of the request, rather than the configured base url.', default=OAI_RESPOND_WITH_REQ_URL, env_var='OPRH_OP_RESP_WITH_REQ_URL', action='store_true') conf.add('--oai-pmh-stylesheet-url', help='XML stylesheet URL that is added to OAI-PMH responses. ' 'Set empty string to not add XML stylesheet to OAI-PMH responses. ' 'If this values start with a slash (\'/\') it is considered ' 'relative to the server and will be served by Kuha OAI-PMH Repo Handler server. ' 'If this is a full URL, it is considered an external asset and will not ' 'be served by Kuha OAI-PMH Repo Handler server.', env_var='OPRH_OP_XSL_URL', default=f'/{API_VERSION}/oai/static/oai2.xsl', type=str)