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