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

OPDS ODL API changes for UL Feed (PP-1769) #2095

Merged
merged 17 commits into from
Oct 5, 2024
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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ module = [
"palace.manager.api.controller.circulation_manager",
"palace.manager.api.controller.loan",
"palace.manager.api.controller.marc",
"palace.manager.api.controller.odl_notification",
"palace.manager.api.discovery.*",
"palace.manager.api.enki",
"palace.manager.api.lcp.hash",
Expand All @@ -99,6 +100,7 @@ module = [
"palace.manager.core.selftest",
"palace.manager.feed.*",
"palace.manager.integration.*",
"palace.manager.opds.*",
"palace.manager.scripts.initialization",
"palace.manager.scripts.rotate_jwe_key",
"palace.manager.scripts.search",
Expand Down
6 changes: 5 additions & 1 deletion src/palace/manager/api/circulation_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,11 @@ def setup_one_time_controllers(self):
self.profiles = ProfileController(self)
self.patron_devices = DeviceTokensController(self)
self.version = ApplicationVersionController()
self.odl_notification_controller = ODLNotificationController(self)
self.odl_notification_controller = ODLNotificationController(
self._db,
self,
self.services.integration_registry.license_providers(),
)
self.patron_auth_token = PatronAuthTokenController(self)
self.playtime_entries = PlaytimeEntriesController(self)

Expand Down
77 changes: 58 additions & 19 deletions src/palace/manager/api/controller/odl_notification.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,81 @@
from __future__ import annotations

import json
from typing import TYPE_CHECKING

import flask
from flask import Response
from flask_babel import lazy_gettext as _
from pydantic import ValidationError
from sqlalchemy.orm import Session

from palace.manager.api.controller.circulation_manager import (
CirculationManagerController,
)
from palace.manager.api.odl.api import OPDS2WithODLApi
from palace.manager.api.problem_details import (
INVALID_LOAN_FOR_ODL_NOTIFICATION,
NO_ACTIVE_LOAN,
)
from palace.manager.core.problem_details import INVALID_INPUT
from palace.manager.opds.lcp.status import LoanStatus
from palace.manager.service.integration_registry.license_providers import (
LicenseProvidersRegistry,
)
from palace.manager.sqlalchemy.model.library import Library
from palace.manager.sqlalchemy.model.patron import Loan
from palace.manager.sqlalchemy.util import get_one
from palace.manager.util.log import LoggerMixin
from palace.manager.util.problem_detail import ProblemDetail

if TYPE_CHECKING:
from palace.manager.api.circulation_manager import CirculationManager


class ODLNotificationController(CirculationManagerController):
class ODLNotificationController(LoggerMixin):
"""Receive notifications from an ODL distributor when the
status of a loan changes.
"""

def notify(self, loan_id):
library = flask.request.library
status_doc = flask.request.data
loan = get_one(self._db, Loan, id=loan_id)
def __init__(
self,
db: Session,
manager: CirculationManager,
registry: LicenseProvidersRegistry,
) -> None:
self.db = db
self.manager = manager
self.registry = registry

if not loan:
return NO_ACTIVE_LOAN.detailed(_("No loan was found for this identifier."))

collection = loan.license_pool.collection
if collection.protocol != OPDS2WithODLApi.label():
return INVALID_LOAN_FOR_ODL_NOTIFICATION

api = self.manager.circulation_apis[library.id].api_for_license_pool(
def get_api(self, library: Library, loan: Loan) -> OPDS2WithODLApi:
return self.manager.circulation_apis[library.id].api_for_license_pool( # type: ignore[no-any-return]
loan.license_pool
)
api.update_loan(loan, json.loads(status_doc))
return Response(_("Success"), 200)

def notify(self, loan_id: int) -> Response | ProblemDetail:
library = flask.request.library # type: ignore[attr-defined]
status_doc_json = flask.request.data
loan = get_one(self.db, Loan, id=loan_id)

try:
status_doc = LoanStatus.model_validate_json(status_doc_json)
except ValidationError as e:
self.log.exception(f"Unable to parse loan status document. {e}")
return INVALID_INPUT

# We don't have a record of this loan. This likely means that the loan has been returned
# and our local record has been deleted. This is expected, except in the case where the
# distributor thinks the loan is still active.
if loan is None and status_doc.active:
return NO_ACTIVE_LOAN.detailed(
_("No loan was found for this identifier."), status_code=404
)

if loan:
integration = loan.license_pool.collection.integration_configuration
if (
not integration.protocol
or self.registry.get(integration.protocol) != OPDS2WithODLApi
):
return INVALID_LOAN_FOR_ODL_NOTIFICATION

api = self.get_api(library, loan)
api.update_loan(loan, status_doc)

return Response(status=204)
Loading