#!/usr/bin/env python3
# Author(s): Toni Sissala
# Copyright 2020 Finnish Social Science Data Archive FSD / University of Tampere
# Licensed under the EUPL. See LICENSE.txt for full license.
"""Define handlers for responding to HTTP-requests.
:note: OSMH protocol only supports HTTP-GET
"""
#: OSMH requires returning a list,
#: which is potentially dangerous
#: and thus not supported by tornados encoder.
#: Use built-in json module for list encoding.
import json
from kuha_common.server import RequestHandler
from kuha_common.query import QueryController
from kuha_osmh_repo_handler.response import RecordsResponse
from kuha_osmh_repo_handler.osmh import records
[docs]class BaseHandler(RequestHandler):
"""BaseHandler class for handling OSMH requests.
Derived from :class:`kuha_common.server.RequestHandler`.
Defines common attributes.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._records_response = None
self._query_ctrl = None
self._api_version = None
self._query_limit = 0
[docs] async def prepare(self):
"""Prepare for responding to request.
Set output content type. Initiate :obj:`kuha_osmh_repo_handler.response.RecordsResponse`
and :obj:`kuha_common.query.QueryController` as instance attributes.
"""
await super().prepare()
self.set_output_content_type(self.CONTENT_TYPE_JSON)
self._records_response = RecordsResponse()
self._query_limit = self.application.settings['kuha_settings'].query_limit
self._query_ctrl = QueryController(
headers=self._correlation_id.as_header(),
record_limit=self._query_limit
)
self._api_version = self.application.settings['kuha_settings'].osmh_repo_handler_api_version
[docs]class ListRecordHeadersHandler(BaseHandler):
"""Handle list responses.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._stream_response = None
self._ongoing_response = None
self._get_response_callback = None
[docs] async def prepare(self):
"""Prepare for each request.
Depending on stream-configuration choose which response callback to use.
If streaming is enabled write output once a single record has been built.
Otherwise store all records in a list and write output when all records are built.
:note: With streaming enabled memory consumption will be lower since
the records will not be gathered in a single list
and encoded to JSON all at once. When dealing with large repositories
the amount of memory consumed without streaming could be an issue.
"""
await super().prepare()
self._stream_response = self.application.settings['kuha_settings'].stream_response
if self._stream_response:
self._get_response_callback = self._get_response_streamer
else:
self._get_response_callback = self._records_response.get_payload_appender
self._ongoing_response = False
def _get_response_streamer(self, constructor):
def _streaming_response(ds_payload):
if not ds_payload:
self.flush()
return
if self._ongoing_response:
self.write(', ')
else:
self._ongoing_response = True
record = constructor(ds_payload)
self.write(record.get_payload())
self.flush()
return _streaming_response
async def _query_recordtype(self, record_type):
record_class = records.get_osmh_record_for_type(record_type)
fields = record_class.fields_for_header()
_call = self._get_response_callback(record_class.for_header_response)
return await self._query_ctrl.query_multiple(
record_class.get_query_document(),
_call,
fields=fields
)
[docs] async def get(self, record_type=None):
"""Handles HTTP-GET requests to endpoint.
:param record_type: Optional record_type parameter defines if the request
limits the list to a certain OSMH type. If the parameter
is None, shall output every record in repository.
Valid values are defined in handler configuration.
:type record_type: str or None
"""
if self._stream_response:
self.write('[')
if not record_type:
for _type in records.OSMH_RECORD_TYPES:
flush_queue = await self._query_recordtype(_type)
else:
flush_queue = await self._query_recordtype(record_type)
if self._query_limit:
await flush_queue()
if self._stream_response:
self.finish(']')
else:
self.finish(json.dumps(self._records_response.get_response()))
[docs]class GetRecordHandler(BaseHandler):
"""Handle responses for single record.
"""
[docs] async def get(self, record_type, identifier):
"""Handle HTTP-GET requests to endpoint.
Gathers the needed information by querying Document Store.
Builds the OSMH record response.
:param record_type: requested OSMH record type.
:type record_type: str
:param identifier: requested record identifier.
:type identifier: str
"""
record_class = records.get_osmh_record_for_type(record_type)
has_relative_records = record_class.requires_relative_queries_for_record()
if has_relative_records:
_call = self._records_response.get_record_appender(
record_class.for_record_response
)
else:
_call = self._records_response.get_payload_appender(
record_class.for_record_response
)
await self._query_ctrl.query_single(record_class.query_document,
_filter=record_class.query_filter_for_record(
identifier
),
fields=record_class.fields_for_record(),
on_record=_call)
if has_relative_records:
flush_queue = None
for record in self._records_response.iterate_records():
_filter = record.get_secondary_query_filter_for_record()
fields = record.get_secondary_query_fields_for_record()
flush_queue = await self._query_ctrl.query_multiple(
record.get_secondary_query_document(),
record.build_relative_record_payload,
_filter=_filter,
fields=fields,
)
if self._query_limit and flush_queue:
await flush_queue()
self.finish(self._records_response.get_single_response())
[docs]class ListSupportedRecordTypesHandler(BaseHandler):
"""Handle response to endpoint that lists
supported record types.
"""
[docs] async def get(self):
"""Handle HTTP-GET request.
:note: returning object will be a list, so will encode to
JSON by using build-in json-module.
"""
self.finish(json.dumps(records.OSMH_RECORD_TYPES))
[docs]class SupportedVersionsHandler(BaseHandler):
"""Handle response to endpoint that lists
supported api version.
:note: Currently only single version is supported at a time.
"""
[docs] async def get(self):
"""Handle HTTP-GET request.
:note: returning object will be a list, so will encode to
JSON by using build-in json-module.
"""
self.finish(json.dumps([self._api_version]))