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

Refactor LoanInfo and HoldInfo to be dataclasses (PP-1728) #2113

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Remove Hold.external_identifier
Revision ID: 1938277e993f
Revises: 87901a6323d6
Create Date: 2024-10-15 19:47:55.697280+00:00
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "1938277e993f"
down_revision = "87901a6323d6"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.drop_constraint("holds_external_identifier_key", "holds", type_="unique")
op.drop_column("holds", "external_identifier")


def downgrade() -> None:
op.add_column(
"holds",
sa.Column(
"external_identifier", sa.VARCHAR(), autoincrement=False, nullable=True
),
)
op.create_unique_constraint(
"holds_external_identifier_key", "holds", ["external_identifier"]
)
101 changes: 28 additions & 73 deletions src/palace/manager/api/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
AlreadyCheckedOut,
AlreadyOnHold,
CannotFulfill,
CannotHold,
CannotLoan,
CurrentlyAvailable,
InvalidInputException,
Expand Down Expand Up @@ -415,9 +414,7 @@
patron_id = patron.authorization_identifier
response = self._checkin(title_id, patron_id)
try:
CheckinResponseParser(licensepool.collection).process_first(
response.content
)
CheckinResponseParser().process_first(response.content)
except etree.XMLSyntaxError:
raise RemoteInitiatedServerError(response.text, self.label())

Expand Down Expand Up @@ -454,12 +451,8 @@
title_id, patron_id, self.internal_format(delivery_mechanism)
)
try:
loan_info = CheckoutResponseParser(licensepool.collection).process_first(
response.content
)
if loan_info is None:
raise CannotLoan()
return loan_info
expiration_date = CheckoutResponseParser().process_first(response.content)
return LoanInfo.from_license_pool(licensepool, end_date=expiration_date)

Check warning on line 455 in src/palace/manager/api/axis.py

View check run for this annotation

Codecov / codecov/patch

src/palace/manager/api/axis.py#L454-L455

Added lines #L454 - L455 were not covered by tests
except etree.XMLSyntaxError:
raise RemoteInitiatedServerError(response.text, self.label())

Expand Down Expand Up @@ -525,17 +518,13 @@
titleId=title_id, patronId=patron_id, email=hold_notification_email
)
response = self.request(url, params=params)
hold_info = HoldResponseParser(licensepool.collection).process_first(
response.content
hold_position = HoldResponseParser().process_first(response.content)
hold_info = HoldInfo.from_license_pool(
licensepool,
start_date=utc_now(),
end_date=None,
hold_position=hold_position,
)
if not hold_info:
raise CannotHold()
if not hold_info.identifier:
# The Axis 360 API doesn't return the identifier of the
# item that was placed on hold, so we have to fill it in
# based on our own knowledge.
hold_info.identifier_type = identifier.type
hold_info.identifier = identifier.identifier
return hold_info

def release_hold(self, patron: Patron, pin: str, licensepool: LicensePool) -> None:
Expand All @@ -546,9 +535,7 @@
params = dict(titleId=title_id, patronId=patron_id)
response = self.request(url, params=params)
try:
HoldReleaseResponseParser(licensepool.collection).process_first(
response.content
)
HoldReleaseResponseParser().process_first(response.content)

Check warning on line 538 in src/palace/manager/api/axis.py

View check run for this annotation

Codecov / codecov/patch

src/palace/manager/api/axis.py#L538

Added line #L538 was not covered by tests
except NotOnHold:
# Fine, it wasn't on hold and now it's still not on hold.
pass
Expand Down Expand Up @@ -1423,14 +1410,6 @@


class XMLResponseParser(ResponseParser, Axis360Parser[T], ABC):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here make the XMLResponseParser subclasses return the parsed data directly, rather then a LoanInfo or HoldInfo object. Previously they returned a partial object, and the caller had to fill in the missing details, now the caller creates the whole LoanInfo or HoldInfo object.

def __init__(self, collection: Collection):
"""Constructor.
:param collection: A Collection, in case parsing this document
results in the creation of LoanInfo or HoldInfo objects.
"""
self.collection = collection

def raise_exception_on_error(
self,
e: _Element,
Expand Down Expand Up @@ -1477,47 +1456,37 @@
return True


class CheckoutResponseParser(XMLResponseParser[LoanInfo]):
class CheckoutResponseParser(XMLResponseParser[datetime.datetime | None]):
@property
def xpath_expression(self) -> str:
return "//axis:checkoutResult"

def process_one(self, e: _Element, namespaces: dict[str, str] | None) -> LoanInfo:
def process_one(
self, e: _Element, namespaces: dict[str, str] | None
) -> datetime.datetime | None:
"""Either turn the given document into a LoanInfo
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the doc needs to be updated

object, or raise an appropriate exception.
"""
self.raise_exception_on_error(e, namespaces)

# If we get to this point it's because the checkout succeeded.
expiration_date = self._xpath1(e, "//axis:expirationDate", namespaces)
fulfillment_url = self._xpath1(e, "//axis:url", namespaces)
if fulfillment_url is not None:
fulfillment_url = fulfillment_url.text
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was parsed, but never used.


if expiration_date is not None:
expiration_date = expiration_date.text
expiration_date = self._pd(expiration_date)

loan_start = utc_now()
loan = LoanInfo(
collection=self.collection,
data_source_name=DataSource.AXIS_360,
identifier_type=self.id_type,
identifier=None,
start_date=loan_start,
end_date=expiration_date,
)
return loan
return expiration_date # type: ignore[no-any-return]


class HoldResponseParser(XMLResponseParser[HoldInfo]):
class HoldResponseParser(XMLResponseParser[int | None], LoggerMixin):
@property
def xpath_expression(self) -> str:
return "//axis:addtoholdResult"

def process_one(self, e: _Element, namespaces: dict[str, str] | None) -> HoldInfo:
"""Either turn the given document into a HoldInfo
object, or raise an appropriate exception.
def process_one(self, e: _Element, namespaces: dict[str, str] | None) -> int | None:
"""Either turn the given document into an int representing the hold position,
or raise an appropriate exception.
"""
self.raise_exception_on_error(e, namespaces, {3109: AlreadyOnHold})

Expand All @@ -1529,22 +1498,10 @@
try:
queue_position = int(queue_position.text)
except ValueError:
print("Invalid queue position: %s" % queue_position)
self.log.warning("Invalid queue position: %s" % queue_position)

Check warning on line 1501 in src/palace/manager/api/axis.py

View check run for this annotation

Codecov / codecov/patch

src/palace/manager/api/axis.py#L1501

Added line #L1501 was not covered by tests
queue_position = None

hold_start = utc_now()
# NOTE: The caller needs to fill in Collection -- we have no idea
# what collection this is.
hold = HoldInfo(
collection=self.collection,
data_source_name=DataSource.AXIS_360,
identifier_type=self.id_type,
identifier=None,
start_date=hold_start,
end_date=None,
hold_position=queue_position,
)
return hold
return queue_position


class HoldReleaseResponseParser(XMLResponseParser[Literal[True]]):
Expand Down Expand Up @@ -1575,11 +1532,12 @@
"""
self.api = api
self.internal_format = internal_format
if api.collection is None:
if api.collection_id is None:
raise ValueError(
"Cannot use an Axis360AvailabilityResponseParser without a Collection."
"Cannot use an Axis360AvailabilityResponseParser without a collection_id."
)
super().__init__(api.collection)
self.collection_id = api.collection_id
super().__init__()

@property
def xpath_expression(self) -> str:
Expand Down Expand Up @@ -1643,8 +1601,7 @@
# We're out of luck -- we can't fulfill this loan.
fulfillment = None
info = LoanInfo(
collection=self.collection,
data_source_name=DataSource.AXIS_360,
collection_id=self.collection_id,
identifier_type=self.id_type,
identifier=axis_identifier,
start_date=start_date,
Expand All @@ -1655,8 +1612,7 @@
elif reserved:
end_date = self._xpath1_date(availability, "axis:reservedEndDate", ns)
info = HoldInfo(
collection=self.collection,
data_source_name=DataSource.AXIS_360,
collection_id=self.collection_id,
identifier_type=self.id_type,
identifier=axis_identifier,
start_date=None,
Expand All @@ -1668,8 +1624,7 @@
availability, "axis:holdsQueuePosition", ns
)
info = HoldInfo(
collection=self.collection,
data_source_name=DataSource.AXIS_360,
collection_id=self.collection_id,
identifier_type=self.id_type,
identifier=axis_identifier,
start_date=None,
Expand Down
38 changes: 13 additions & 25 deletions src/palace/manager/api/bibliotheca.py
Original file line number Diff line number Diff line change
Expand Up @@ -472,12 +472,8 @@

# At this point we know we have a loan.
loan_expires = CheckoutResponseParser().process_first(response.content)
loan = LoanInfo(
licensepool.collection,
DataSource.BIBLIOTHECA,
licensepool.identifier.type,
licensepool.identifier.identifier,
start_date=None,
loan = LoanInfo.from_license_pool(

Check warning on line 475 in src/palace/manager/api/bibliotheca.py

View check run for this annotation

Codecov / codecov/patch

src/palace/manager/api/bibliotheca.py#L475

Added line #L475 was not covered by tests
licensepool,
end_date=loan_expires,
)
return loan
Expand Down Expand Up @@ -561,11 +557,8 @@
if response.status_code in (200, 201):
start_date = utc_now()
end_date = HoldResponseParser().process_first(response_content)
return HoldInfo(
licensepool.collection,
DataSource.BIBLIOTHECA,
licensepool.identifier.type,
licensepool.identifier.identifier,
return HoldInfo.from_license_pool(
licensepool,
start_date=start_date,
end_date=end_date,
hold_position=None,
Expand Down Expand Up @@ -1079,21 +1072,16 @@
identifier = self.text_of_subtag(tag, "ItemId")
start_date = datevalue("EventStartDateInUTC")
end_date = datevalue("EventEndDateInUTC")
a = [
self.collection,
DataSource.BIBLIOTHECA,
self.id_type,
identifier,
start_date,
end_date,
]
kwargs = {
"collection_id": self.collection.id,
"identifier_type": self.id_type,
"identifier": identifier,
"start_date": start_date,
"end_date": end_date,
}
if source_class is HoldInfo:
hold_position = self.int_of_subtag(tag, "Position")
a.append(hold_position)
else:
# Fulfillment info -- not available from this API
a.append(None)
return source_class(*a)
kwargs["hold_position"] = self.int_of_subtag(tag, "Position")
return source_class(**kwargs)


class DateResponseParser(BibliothecaParser[Optional[datetime]], ABC):
Expand Down
Loading