Skip to content

Commit

Permalink
feature: extend OAI API with search and format
Browse files Browse the repository at this point in the history
test: add tests for OAI API
  • Loading branch information
rekt-hard committed Feb 16, 2022
1 parent 24d3f98 commit b643d00
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 92 deletions.
8 changes: 8 additions & 0 deletions invenio_rdm_records/oaiserver/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022 Graz University of Technology.
#
# Invenio-RDM-Records is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""OAI-PMH API for InvenioRDM."""
8 changes: 8 additions & 0 deletions invenio_rdm_records/oaiserver/resources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022 Graz University of Technology.
#
# Invenio-RDM-Records is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""OAI-PMH resources for InvenioRDM."""
21 changes: 19 additions & 2 deletions invenio_rdm_records/oaiserver/resources/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,40 @@
SearchRequestArgsSchema,
)

from ..services.errors import OAIPMHError
from ..services.errors import (
OAIPMHError,
OAIPMHSetDoesNotExistError,
OAIPMHSetIDDoesNotExistError,
)

oaipmh_error_handlers = {
**ErrorHandlersMixin.error_handlers,
OAIPMHError: create_error_handler(
OAIPMHSetDoesNotExistError: create_error_handler(
lambda e: HTTPJSONException(
code=404,
description=e.description,
)
),
OAIPMHSetIDDoesNotExistError: create_error_handler(
lambda e: HTTPJSONException(
code=404,
description=e.description,
)
),
OAIPMHError: create_error_handler(
lambda e: HTTPJSONException(
code=400,
description=e.description,
)
),
}


class OAIPMHServerSearchRequestArgsSchema(SearchRequestArgsSchema):
"""OAI-PMH request parameters."""

managed = ma.fields.Boolean()
sort_direction = ma.fields.Str()


class OAIPMHServerResourceConfig(ResourceConfig):
Expand Down
6 changes: 4 additions & 2 deletions invenio_rdm_records/oaiserver/resources/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ def create_url_rules(self):
route("POST", routes["set-prefix"], self.create),
route("GET", routes["set-prefix"] + routes["item"], self.read),
route("PUT", routes["set-prefix"] + routes["item"], self.update),
route("DELETE", routes["set-prefix"] + routes["item"], self.delete),
route(
"DELETE", routes["set-prefix"] + routes["item"], self.delete
),
route(
"GET",
routes["format-prefix"] + routes["list"],
Expand Down Expand Up @@ -117,4 +119,4 @@ def read_formats(self):
hits = self.service.read_all_formats(
identity=identity,
)
return hits, 200
return hits.to_dict(), 200
8 changes: 8 additions & 0 deletions invenio_rdm_records/oaiserver/services/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2022 Graz University of Technology.
#
# Invenio-RDM-Records is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""OAI-PMH services for InvenioRDM."""
100 changes: 55 additions & 45 deletions invenio_rdm_records/oaiserver/services/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,27 @@
#
# Copyright (C) 2022 Graz University of Technology.
#
# Invenio-Records-Resources is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
# Invenio-RDM-Records is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.

"""OAI-PMH service API configuration."""

from flask_babelex import gettext as _
from invenio_indexer.api import RecordIndexer
from invenio_oaiserver.models import OAISet
from invenio_records_resources.services import ServiceConfig
from invenio_records_resources.services.base import Link
from invenio_records_resources.services.records.links import pagination_links
from invenio_records_resources.services.records.params import (
FacetsParam,
PaginationParam,
QueryParser,
QueryStrParam,
SortParam,
)
from invenio_search import RecordsSearchV2
from marshmallow import Schema, fields, validate
from marshmallow_utils.fields import SanitizedUnicode
from sqlalchemy import asc, desc

from invenio_rdm_records.oaiserver.services.links import OAIPMHSetLink
from invenio_rdm_records.oaiserver.services.permissions import (
from ..services.links import OAIPMHSetLink
from ..services.permissions import (
OAIPMHServerPermissionPolicy,
)
from invenio_rdm_records.oaiserver.services.results import (
from ..services.results import (
OAIMetadataFormatItem,
OAIMetadataFormatList,
OAISetItem,
OAISetList,
)
Expand All @@ -37,45 +31,58 @@
class SearchOptions:
"""Search options."""

search_cls = RecordsSearchV2
query_parser_cls = QueryParser
suggest_parser_cls = None
sort_default = 'bestmatch'
sort_default_no_query = 'newest'
sort_default = 'created'
sort_direction_default = 'asc'

sort_direction_options = {
"asc": dict(
title=_('Ascending'),
fn=asc,
),
"desc": dict(
title=_('Descending'),
fn=desc,
),
}

sort_options = {
"bestmatch": dict(
title=_('Best match'),
fields=['_score'], # ES defaults to desc on `_score` field
"name": dict(
title=_('Name'),
fields=['name'],
),
"newest": dict(
title=_('Newest'),
fields=['-created'],
"spec": dict(
title=_('Spec'),
fields=['spec'],
),
"oldest": dict(
title=_('Oldest'),
"created": dict(
title=_('Created'),
fields=['created'],
),
"updated": dict(
title=_('Updated'),
fields=['updated'],
),
}
facets = {}
pagination_options = {
"default_results_per_page": 25,
"default_max_results": 10000,
}
params_interpreters_cls = [
QueryStrParam,
PaginationParam,
SortParam,
FacetsParam,
]


class OAIPMHMetadataFormat(Schema):
"""Marshmallow schema for OAI-PMH metadata format."""

id = fields.Str(read_only=True)
schema = fields.URL(read_only=True)
namespace = fields.URL(read_only=True)


class OAIPMHSetSchema(Schema):
"""Marshmallow schema for OAI-PMH set."""

description = fields.Str(required=True)
name = fields.Str(required=True, validate=validate.Length(min=1))
search_pattern = fields.Str(required=True, validate=validate.Length(min=1))
spec = fields.Str(required=True, validate=validate.Length(min=1))
description = SanitizedUnicode(missing=None, default=None)
name = SanitizedUnicode(required=True, validate=validate.Length(min=1))
search_pattern = SanitizedUnicode(required=True)
spec = SanitizedUnicode(required=True, validate=validate.Length(min=1))
created = fields.DateTime(read_only=True)
updated = fields.DateTime(read_only=True)
id = fields.Int(read_only=True)
Expand All @@ -84,9 +91,9 @@ class OAIPMHSetSchema(Schema):
class OAIPMHSetUpdateSchema(Schema):
"""Marshmallow schema for OAI-PMH set update request."""

description = fields.Str(validate=validate.Length(min=1))
name = fields.Str(validate=validate.Length(min=1))
search_pattern = fields.Str(validate=validate.Length(min=1))
description = SanitizedUnicode(missing=None)
name = fields.Str(required=True, validate=validate.Length(min=1))
search_pattern = fields.Str(required=True)


class OAIPMHServerServiceConfig(ServiceConfig):
Expand All @@ -97,10 +104,11 @@ class OAIPMHServerServiceConfig(ServiceConfig):
result_item_cls = OAISetItem
result_list_cls = OAISetList

metadata_format_result_item_cls = OAIMetadataFormatItem
metadata_format_result_list_cls = OAIMetadataFormatList

# Record specific configuration
record_cls = OAISet
indexer_cls = None
index_dumper = None

# Search configuration
search = SearchOptions
Expand All @@ -109,6 +117,8 @@ class OAIPMHServerServiceConfig(ServiceConfig):
schema = OAIPMHSetSchema
update_schema = OAIPMHSetUpdateSchema

metadata_format_schema = OAIPMHMetadataFormat

links_item = {
"self": OAIPMHSetLink("{+api}/oaipmh/sets/{id}"),
"oai-listrecords": OAIPMHSetLink(
Expand Down
5 changes: 0 additions & 5 deletions invenio_rdm_records/oaiserver/services/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,3 @@ def vars(set, vars):
"spec": set.spec,
}
)


class OAIPMHLink(Link):
def __init__(self, verb, metadataFormat=None, params=None):
super().__init__()
7 changes: 5 additions & 2 deletions invenio_rdm_records/oaiserver/services/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
"""Permissions for OAI-PMH service."""

from invenio_records_permissions import BasePermissionPolicy
from invenio_records_permissions.generators import AnyUser, Admin, SystemProcess
from invenio_records_permissions.generators import (
Admin,
AnyUser,
SystemProcess,
)


class OAIPMHServerPermissionPolicy(BasePermissionPolicy):
Expand All @@ -18,5 +22,4 @@ class OAIPMHServerPermissionPolicy(BasePermissionPolicy):
can_create = [Admin(), SystemProcess()]
can_delete = [Admin(), SystemProcess()]
can_update = [Admin(), SystemProcess()]
can_search = [AnyUser(), SystemProcess()]
can_read_format = [AnyUser(), SystemProcess()]
35 changes: 26 additions & 9 deletions invenio_rdm_records/oaiserver/services/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@
)


class OAISetItem(ServiceItemResult):
"""Single OAI-PMH set result item."""
class BaseServiceItemResult(ServiceItemResult):
"""Single result item."""

def __init__(self, service, identity, set, links_tpl, schema=None):
def __init__(self, service, identity, item, links_tpl, schema=None):
self._identity = identity
self._set = set
self._item = item
self._schema = schema or service.schema
self._links_tpl = links_tpl
self._data = None

@property
def links(self):
"""Get links for this result item."""
return self._links_tpl.expand(self._set)
return self._links_tpl.expand(self._item)

@property
def data(self):
Expand All @@ -36,7 +36,7 @@ def data(self):
return self._data

self._data = self._schema.dump(
self._set,
self._item,
context=dict(
identity=self._identity,
),
Expand All @@ -52,8 +52,8 @@ def to_dict(self):
return res


class OAISetList(ServiceListResult):
"""List of records result."""
class BaseServiceListResult(ServiceListResult):
"""List of result items."""

def __init__(
self,
Expand All @@ -69,7 +69,7 @@ def __init__(
:params service: a service instance
:params identity: an identity that performed the service request
:params results: the search results
:params results: the db search results
:params params: dictionary of the query parameters
"""
self._identity = identity
Expand Down Expand Up @@ -122,6 +122,7 @@ def to_dict(self):
"""Return result as a dictionary."""
# TODO: This part should imitate the result item above. I.e. add a
# "data" property which uses a ServiceSchema to dump the entire object.

res = {
"hits": {
"hits": list(self.hits),
Expand All @@ -134,3 +135,19 @@ def to_dict(self):
res['links'] = self._links_tpl.expand(self.pagination)

return res


class OAISetItem(BaseServiceItemResult):
"""Single OAI-PMH set result item."""


class OAISetList(BaseServiceListResult):
"""List of OAI-PMH set result items."""


class OAIMetadataFormatItem(BaseServiceItemResult):
"""Single OAI-PMH metadata format result item."""


class OAIMetadataFormatList(BaseServiceListResult):
"""List of OAI-PMH metadata format result items."""
Loading

0 comments on commit b643d00

Please sign in to comment.