Skip to content

Commit

Permalink
OPDS ODL API changes for UL Feed (PP-1769) (#2095)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathangreen authored Oct 5, 2024
1 parent 4f4295e commit ea60979
Show file tree
Hide file tree
Showing 45 changed files with 2,249 additions and 928 deletions.
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

0 comments on commit ea60979

Please sign in to comment.