Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add OAI-PMH API #891

Merged
merged 8 commits into from
Mar 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,14 @@ jobs:
python-version: [3.7, 3.8, 3.9]
requirements-level: [pypi]
db-service: [postgresql12]
search-service: [elasticsearch6, elasticsearch7]
search-service: [elasticsearch7]
exclude:
- python-version: 3.7
search-service: elasticsearch7
- python-version: 3.8
search-service: elasticsearch6
- python-version: 3.9
search-service: elasticsearch6
include:
- db-service: postgresql12
DB_EXTRAS: "postgresql"

- search-service: elasticsearch6
SEARCH_EXTRAS: "elasticsearch6"

- search-service: elasticsearch7
SEARCH_EXTRAS: "elasticsearch7"

Expand Down
19 changes: 19 additions & 0 deletions invenio_rdm_records/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@
SubjectsResourceConfig, SubjectsService, SubjectsServiceConfig
from itsdangerous import SignatureExpired

from invenio_rdm_records.oaiserver.resources.config import \
OAIPMHServerResourceConfig
from invenio_rdm_records.oaiserver.resources.resources import \
OAIPMHServerResource
from invenio_rdm_records.oaiserver.services.config import \
OAIPMHServerServiceConfig
from invenio_rdm_records.oaiserver.services.services import OAIPMHServerService

from . import config
from .resources import RDMDraftFilesResourceConfig, \
RDMParentRecordLinksResource, RDMParentRecordLinksResourceConfig, \
Expand Down Expand Up @@ -138,6 +146,7 @@ class ServiceConfigs:
affiliations = AffiliationsServiceConfig
names = NamesServiceConfig
subjects = SubjectsServiceConfig
oaipmh_server = OAIPMHServerServiceConfig

return ServiceConfigs

Expand All @@ -164,6 +173,10 @@ def init_services(self, app):
config=service_configs.subjects
)

self.oaipmh_server_service = OAIPMHServerService(
config=service_configs.oaipmh_server,
)

def init_resource(self, app):
"""Initialize vocabulary resources."""
self.records_resource = RDMRecordResource(
Expand Down Expand Up @@ -203,6 +216,12 @@ def init_resource(self, app):
config=SubjectsResourceConfig,
)

# OAI-PMH
self.oaipmh_server_resource = OAIPMHServerResource(
service=self.oaipmh_server_service,
config=OAIPMHServerResourceConfig,
)

def fix_datacite_configs(self, app):
"""Make sure that the DataCite config items are strings."""
datacite_config_items = [
Expand Down
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."""
69 changes: 69 additions & 0 deletions invenio_rdm_records/oaiserver/resources/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- 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 resource configuration."""

import marshmallow as ma
from flask_babelex import lazy_gettext as _
from flask_resources import HTTPJSONException, ResourceConfig, \
create_error_handler
from invenio_records_resources.resources.errors import ErrorHandlersMixin
from invenio_records_resources.resources.records.args import \
SearchRequestArgsSchema

from ..services.errors import OAIPMHError, OAIPMHSetDoesNotExistError, \
OAIPMHSetIDDoesNotExistError

oaipmh_error_handlers = {
**ErrorHandlersMixin.error_handlers,
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):
"""OAI-PMH resource config."""

# Blueprint configuration
blueprint_name = "oaipmh-server"
url_prefix = "/oaipmh"
routes = {
"set-prefix": "/sets",
"list": "",
"item": "/<id>",
"format-prefix": "/formats",
}

# Request parsing
request_read_args = {}
request_view_args = {"id": ma.fields.Int()}
request_search_args = OAIPMHServerSearchRequestArgsSchema

error_handlers = oaipmh_error_handlers
114 changes: 114 additions & 0 deletions invenio_rdm_records/oaiserver/resources/resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# -*- 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 resource."""

from flask import abort, g
from flask.globals import request
from flask_resources import Resource, resource_requestctx, response_handler, \
route
from invenio_records_resources.resources.errors import ErrorHandlersMixin
from invenio_records_resources.resources.records.resource import \
request_data, request_headers, request_search_args, request_view_args
from invenio_records_resources.resources.records.utils import es_preference


class OAIPMHServerResource(ErrorHandlersMixin, Resource):
"""OAI-PMH server resource."""

def __init__(self, config, service):
"""Constructor."""
super().__init__(config)
self.service = service

def create_url_rules(self):
"""Create the URL rules for the OAI-PMH server resource."""
routes = self.config.routes
url_rules = [
route("GET", routes["set-prefix"] + routes["list"], self.search),
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(
"GET",
routes["format-prefix"] + routes["list"],
self.read_formats,
),
]

return url_rules

#
# Primary Interface
#
@request_search_args
@response_handler(many=True)
def search(self):
"""Perform a search over the items."""
identity = g.identity
hits = self.service.search(
identity=identity,
params=resource_requestctx.args,
)
return hits.to_dict(), 200

@request_data
@response_handler()
def create(self):
"""Create an item."""
item = self.service.create(
g.identity,
resource_requestctx.data or {},
)
return item.to_dict(), 201

# @request_read_args
@request_view_args
@response_handler()
def read(self):
"""Read an item."""
item = self.service.read(
g.identity,
resource_requestctx.view_args["id"],
)
return item.to_dict(), 200

@request_headers
@request_view_args
@request_data
@response_handler()
def update(self):
"""Update an item."""
item = self.service.update(
g.identity,
resource_requestctx.view_args["id"],
resource_requestctx.data,
)
return item.to_dict(), 200

@request_headers
@request_view_args
def delete(self):
"""Delete an item."""
self.service.delete(
g.identity,
resource_requestctx.view_args["id"],
)
return "", 204

@request_search_args
@response_handler(many=True)
def read_formats(self):
"""Perform a search over the formats."""
identity = g.identity
hits = self.service.read_all_formats(
identity=identity,
)
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."""
108 changes: 108 additions & 0 deletions invenio_rdm_records/oaiserver/services/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# -*- 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 service API configuration."""

from flask_babelex import gettext as _
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 sqlalchemy import asc, desc

from invenio_rdm_records.oaiserver.services.schema import \
OAIPMHMetadataFormat, OAIPMHSetSchema

from ..services.links import OAIPMHSetLink
from ..services.permissions import OAIPMHServerPermissionPolicy
from ..services.results import OAIMetadataFormatItem, OAIMetadataFormatList, \
OAISetItem, OAISetList


class SearchOptions:
"""Search options."""

sort_default = 'created'
sort_direction_default = 'asc'

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

sort_options = {
"name": dict(
title=_('Name'),
fields=['name'],
),
"spec": dict(
title=_('Spec'),
fields=['spec'],
),
"created": dict(
title=_('Created'),
fields=['created'],
),
"updated": dict(
title=_('Updated'),
fields=['updated'],
),
}
pagination_options = {
"default_results_per_page": 25,
}


class OAIPMHServerServiceConfig(ServiceConfig):
"""Service factory configuration."""

# Common configuration
permission_policy_cls = OAIPMHServerPermissionPolicy
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

# Search configuration
search = SearchOptions

# Service schema
schema = OAIPMHSetSchema

metadata_format_schema = OAIPMHMetadataFormat

links_item = {
"self": OAIPMHSetLink("{+api}/oaipmh/sets/{id}"),
"oai-listrecords": OAIPMHSetLink(
"{+ui}/oai2d?verb=ListRecords&metadataPrefix=oai_dc&set={spec}"
),
"oai-listidentifiers": OAIPMHSetLink(
"{+ui}/oai2d?verb=ListIdentifiers&metadataPrefix=oai_dc&set={spec}"
),
}

links_search = {
**pagination_links("{+api}/oaipmh/sets{?args*}"),
"oai-listsets": Link("{+ui}/oai2d?verb=ListSets"),
"oai-listrecords": Link(
"{+ui}/oai2d?verb=ListRecords&metadataPrefix=oai_dc"
),
"oai-listidentifiers": Link(
"{+ui}/oai2d?verb=ListIdentifiers&metadataPrefix=oai_dc"
),
"oai-identify": Link("{+ui}/oai2d?verb=Identify"),
}
Loading