Skip to content

oarepo_oaipmh_harvester.oai_harvester.service

oarepo_oaipmh_harvester.oai_harvester.service

Service layer for OAI-PMH Harvester.

OAIHarvesterSearchOptions

Bases: SearchOptions, SearchOptionsMixin

Search options for harvesters.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
class OAIHarvesterSearchOptions(SearchOptions, SearchOptionsMixin):
    """Search options for harvesters."""

    pagination_options: Mapping[str, Any] = {
        "default_results_per_page": 25,
        "default_max_results": 10000,
    }

    suggest_parser_cls = SuggestQueryParser.factory(
        fields=["comment^1", "name^2", "id^3"],
        type="most_fields",
        fuzziness="AUTO",
    )

    params_interpreters_cls = (
        QueryStrParam,
        SortParam,
        PaginationParam,
        FacetsParam,
    )

    facets: Mapping[str, Any] = {
        "base_url": facets.base_url,
        "name": facets.name,
        "metadata_prefix": facets.metadata_prefix,
        "setspec": facets.setspec,
        "loader": facets.loader,
        "transformers": facets.transformers,
        "writers": facets.writers,
        "harvest_managers": facets.harvest_managers,
    }

Bases: Link

Short cut for writing record links.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
class RecordLink(Link):
    """Short cut for writing record links."""

    @staticmethod
    def vars(
        record: OAIHarvesterAggregate,
        link_vars: dict[str, Any],
    ) -> None:
        """Variables for the URI template."""
        link_vars.update({"id": record.id})

vars(record: OAIHarvesterAggregate, link_vars: dict[str, Any]) -> None staticmethod

Variables for the URI template.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
@staticmethod
def vars(
    record: OAIHarvesterAggregate,
    link_vars: dict[str, Any],
) -> None:
    """Variables for the URI template."""
    link_vars.update({"id": record.id})

OAIHarvesterServiceConfig

Bases: RecordServiceConfig, ConfiguratorMixin

Harvester service configuration.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
class OAIHarvesterServiceConfig(RecordServiceConfig, ConfiguratorMixin):
    """Harvester service configuration."""

    # common configuration
    permission_policy_cls = OAIHarvesterPermissionPolicy
    result_item_cls = OAIHarvesterItem
    result_list_cls = OAIHarvesterList

    search = OAIHarvesterSearchOptions

    service_id = "oai-harvester"
    record_cls = OAIHarvesterAggregate
    schema = cast("type[ma.Schema]", FromConfig("OAI_RECORD_SERVICE_SCHEMA", OAIHarvesterSchema))
    indexer_queue_name = "oai-harvester"
    index_dumper = None

    # links configuration
    links_item: Mapping[str, Any] = {
        "self": RecordLink("{+api}/oai/harvest/harvesters/{id}"),
        "actions": ActionLinks(
            {
                "harvest": RecordLink(
                    "{+api}/oai/harvest/harvesters/{id}/start",
                ),
            }
        ),
    }
    links_search_item: Mapping[str, Any] = {
        "self": RecordLink("{+api}/oai/harvest/harvesters/{id}"),
        "actions": ActionLinks(
            {
                "harvest": RecordLink(
                    "{+api}/oai/harvest/harvesters/{id}/start",
                ),
            }
        ),
    }
    links_search = pagination_links("{+api}/oai/harvest/harvesters/{?args*}")

    components = ()

OAIHarvesterService

Bases: RecordService

Harvester service.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
class OAIHarvesterService(RecordService):
    """Harvester service."""

    @property
    def oai_harvester_cls(self) -> type[OAIHarvesterAggregate]:
        """Alias for record_cls."""
        return cast("type[OAIHarvesterAggregate]", self.record_cls)

    @override
    @unit_of_work()
    def create(
        self,
        identity: Identity,
        data: dict[str, Any],
        uow: UnitOfWork,
        expand: bool = False,
        raise_errors: bool = False,
    ) -> RecordItem:
        """Create a new OAI harvester."""
        # can not use super().create because it creates an empty record first
        # which is incompatible with our database constraints
        self.require_permission(identity, "create", data=data)

        # Validate data and create record with pid
        data, errors = self.schema.load(
            data,
            context={"identity": identity},
            raise_errors=raise_errors,  # if False, flow is continued with data
            # only containing valid data, but errors
            # are reported (as warnings)
        )

        record = self.record_cls.create(data, id_=data["id"])

        self.run_components(
            "create",
            identity,
            data=data,
            record=record,
            errors=errors,
            uow=uow,
        )

        # Persist record (DB and index)
        uow.register(RecordCommitOp(record, self.indexer, index_refresh=True))

        return self.result_item(
            self,
            identity,
            record,
            links_tpl=self.links_item_tpl,
            nested_links_item=getattr(self.config, "nested_links_item", None),
            errors=errors,
            expandable_fields=self.expandable_fields,
            expand=expand,
        )

    @override
    def search(
        self,
        identity: Identity,
        params: dict[str, Any] | None = None,
        search_preference: str | None = None,
        expand: bool = False,
        **kwargs: Any,
    ) -> RecordList:
        """Search harvesters."""
        self.require_permission(identity, "search")

        return super().search(
            identity,
            params=params,
            search_preference=search_preference,
            search_opts=self.config.search,
            permission_action="read",
            **kwargs,
        )

    @override
    def read(self, identity: Identity, id_: str, expand: bool = False, action: str = "read") -> RecordItem:
        """Retrieve a single harvester."""
        # resolve and require permission
        oai_record = OAIHarvesterAggregate.get_record(id_)
        if oai_record is None:
            raise PermissionDeniedError

        self.require_permission(identity, "read", record=oai_record)

        # record components
        for component in self.components:
            if hasattr(component, "read"):
                component.read(identity, oai_record=oai_record)

        return self.result_item(self, identity, oai_record, links_tpl=self.links_item_tpl)

    @override
    @unit_of_work()
    def update(
        self,
        identity: Identity,
        id_: str,
        data: dict[str, Any],
        uow: UnitOfWork,
        revision_id: int | None = None,
        expand: bool = False,
        **kwargs: Any,
    ) -> RecordItem:
        """Update a harvester."""
        record = OAIHarvesterAggregate.get_record(id_)

        # Permissions
        self.require_permission(identity, "update", record=record, data=data, **kwargs)

        data["id"] = record.id  # overwrite the id to prevent changing it
        data, _ = self.schema.load(data, context={"identity": identity, "record": record})

        # model fields can not use update method directly, we need to iterate and set attribute
        for key, value in data.items():
            if key != "id":
                setattr(record, key, value)

        uow.register(RecordCommitOp(record, self.indexer))

        return self.result_item(
            self,
            identity,
            record,
            links_tpl=self.links_item_tpl,
            nested_links_item=getattr(self.config, "nested_links_item", None),
            expandable_fields=self.expandable_fields,
            expand=expand,
        )

    @override
    @unit_of_work()
    def delete(
        self,
        identity: Identity,
        id_: str,
        uow: UnitOfWork,
        revision_id: int | None = None,
        **kwargs: Any,
    ) -> bool:
        """Delete a harvester from the database and the search index."""
        record = OAIHarvesterAggregate.get_record(id_)

        # Permissions
        self.require_permission(identity, "delete", record=record, **kwargs)

        uow.register(RecordDeleteOp(record, self.indexer, index_refresh=True, force=True))

        return True

    @override
    def rebuild_index(self, identity: Identity, uow: UnitOfWork | None = None) -> bool:
        """Reindex all harvesters."""
        oai_harvesters = db.session.query(OAIHarvester.id).yield_per(1000)
        self.indexer.bulk_index([u.id for u in oai_harvesters])
        return True

    def harvest(self, identity: Identity, id_: str, all_records: bool = True) -> RecordItem:
        """Start a harvest of the harvester's source.

        With `all_records=False` only records changed at the source since the newest OAI
        datestamp already harvested are harvested (all records if nothing was harvested yet).
        """
        # resolve and require permission
        oai_harvester = OAIHarvesterAggregate.get_record(id_)
        if oai_harvester is None:
            raise PermissionDeniedError

        self.require_permission(identity, "run_harvest", harvester=oai_harvester, record=oai_harvester)

        from ..tasks import harvest_oaipmh_records

        since = (
            None
            if all_records
            else db.session.query(func.max(OAIHarvestedRecord.datestamp))
            .filter_by(harvester_id=oai_harvester.id)
            .scalar()
        )
        # Fire-and-forget: we do not need the result of the Celery task here.
        harvest_oaipmh_records.delay(harvester_id=oai_harvester.id, **({"since": since.isoformat()} if since else {}))

        return self.result_item(self, identity, oai_harvester, links_tpl=self.links_item_tpl)

oai_harvester_cls: type[OAIHarvesterAggregate] property

Alias for record_cls.

create(identity: Identity, data: dict[str, Any], uow: UnitOfWork, expand: bool = False, raise_errors: bool = False) -> RecordItem

Create a new OAI harvester.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
@override
@unit_of_work()
def create(
    self,
    identity: Identity,
    data: dict[str, Any],
    uow: UnitOfWork,
    expand: bool = False,
    raise_errors: bool = False,
) -> RecordItem:
    """Create a new OAI harvester."""
    # can not use super().create because it creates an empty record first
    # which is incompatible with our database constraints
    self.require_permission(identity, "create", data=data)

    # Validate data and create record with pid
    data, errors = self.schema.load(
        data,
        context={"identity": identity},
        raise_errors=raise_errors,  # if False, flow is continued with data
        # only containing valid data, but errors
        # are reported (as warnings)
    )

    record = self.record_cls.create(data, id_=data["id"])

    self.run_components(
        "create",
        identity,
        data=data,
        record=record,
        errors=errors,
        uow=uow,
    )

    # Persist record (DB and index)
    uow.register(RecordCommitOp(record, self.indexer, index_refresh=True))

    return self.result_item(
        self,
        identity,
        record,
        links_tpl=self.links_item_tpl,
        nested_links_item=getattr(self.config, "nested_links_item", None),
        errors=errors,
        expandable_fields=self.expandable_fields,
        expand=expand,
    )

search(identity: Identity, params: dict[str, Any] | None = None, search_preference: str | None = None, expand: bool = False, **kwargs: Any) -> RecordList

Search harvesters.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
@override
def search(
    self,
    identity: Identity,
    params: dict[str, Any] | None = None,
    search_preference: str | None = None,
    expand: bool = False,
    **kwargs: Any,
) -> RecordList:
    """Search harvesters."""
    self.require_permission(identity, "search")

    return super().search(
        identity,
        params=params,
        search_preference=search_preference,
        search_opts=self.config.search,
        permission_action="read",
        **kwargs,
    )

read(identity: Identity, id_: str, expand: bool = False, action: str = 'read') -> RecordItem

Retrieve a single harvester.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
@override
def read(self, identity: Identity, id_: str, expand: bool = False, action: str = "read") -> RecordItem:
    """Retrieve a single harvester."""
    # resolve and require permission
    oai_record = OAIHarvesterAggregate.get_record(id_)
    if oai_record is None:
        raise PermissionDeniedError

    self.require_permission(identity, "read", record=oai_record)

    # record components
    for component in self.components:
        if hasattr(component, "read"):
            component.read(identity, oai_record=oai_record)

    return self.result_item(self, identity, oai_record, links_tpl=self.links_item_tpl)

update(identity: Identity, id_: str, data: dict[str, Any], uow: UnitOfWork, revision_id: int | None = None, expand: bool = False, **kwargs: Any) -> RecordItem

Update a harvester.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
@override
@unit_of_work()
def update(
    self,
    identity: Identity,
    id_: str,
    data: dict[str, Any],
    uow: UnitOfWork,
    revision_id: int | None = None,
    expand: bool = False,
    **kwargs: Any,
) -> RecordItem:
    """Update a harvester."""
    record = OAIHarvesterAggregate.get_record(id_)

    # Permissions
    self.require_permission(identity, "update", record=record, data=data, **kwargs)

    data["id"] = record.id  # overwrite the id to prevent changing it
    data, _ = self.schema.load(data, context={"identity": identity, "record": record})

    # model fields can not use update method directly, we need to iterate and set attribute
    for key, value in data.items():
        if key != "id":
            setattr(record, key, value)

    uow.register(RecordCommitOp(record, self.indexer))

    return self.result_item(
        self,
        identity,
        record,
        links_tpl=self.links_item_tpl,
        nested_links_item=getattr(self.config, "nested_links_item", None),
        expandable_fields=self.expandable_fields,
        expand=expand,
    )

delete(identity: Identity, id_: str, uow: UnitOfWork, revision_id: int | None = None, **kwargs: Any) -> bool

Delete a harvester from the database and the search index.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
@override
@unit_of_work()
def delete(
    self,
    identity: Identity,
    id_: str,
    uow: UnitOfWork,
    revision_id: int | None = None,
    **kwargs: Any,
) -> bool:
    """Delete a harvester from the database and the search index."""
    record = OAIHarvesterAggregate.get_record(id_)

    # Permissions
    self.require_permission(identity, "delete", record=record, **kwargs)

    uow.register(RecordDeleteOp(record, self.indexer, index_refresh=True, force=True))

    return True

rebuild_index(identity: Identity, uow: UnitOfWork | None = None) -> bool

Reindex all harvesters.

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
@override
def rebuild_index(self, identity: Identity, uow: UnitOfWork | None = None) -> bool:
    """Reindex all harvesters."""
    oai_harvesters = db.session.query(OAIHarvester.id).yield_per(1000)
    self.indexer.bulk_index([u.id for u in oai_harvesters])
    return True

harvest(identity: Identity, id_: str, all_records: bool = True) -> RecordItem

Start a harvest of the harvester's source.

With all_records=False only records changed at the source since the newest OAI datestamp already harvested are harvested (all records if nothing was harvested yet).

Source code in oarepo_oaipmh_harvester/oai_harvester/service.py
def harvest(self, identity: Identity, id_: str, all_records: bool = True) -> RecordItem:
    """Start a harvest of the harvester's source.

    With `all_records=False` only records changed at the source since the newest OAI
    datestamp already harvested are harvested (all records if nothing was harvested yet).
    """
    # resolve and require permission
    oai_harvester = OAIHarvesterAggregate.get_record(id_)
    if oai_harvester is None:
        raise PermissionDeniedError

    self.require_permission(identity, "run_harvest", harvester=oai_harvester, record=oai_harvester)

    from ..tasks import harvest_oaipmh_records

    since = (
        None
        if all_records
        else db.session.query(func.max(OAIHarvestedRecord.datestamp))
        .filter_by(harvester_id=oai_harvester.id)
        .scalar()
    )
    # Fire-and-forget: we do not need the result of the Celery task here.
    harvest_oaipmh_records.delay(harvester_id=oai_harvester.id, **({"since": since.isoformat()} if since else {}))

    return self.result_item(self, identity, oai_harvester, links_tpl=self.links_item_tpl)