1818from palace .manager .integration .license .overdrive .constants import (
1919 OVERDRIVE_MAIN_ACCOUNT_ID ,
2020)
21+ from palace .manager .integration .license .overdrive .model import (
22+ Availability ,
23+ AvailabilityAccount ,
24+ )
2125from palace .manager .integration .license .overdrive .util import _make_link_safe
2226from palace .manager .sqlalchemy .constants import MediaTypes
2327from palace .manager .sqlalchemy .model .classification import Classification , Subject
@@ -219,31 +223,41 @@ def parse_roles(cls, id: str, rolestring: str) -> list[Contributor.Role]:
219223 processed .append (cls .overdrive_role_to_simplified_role [x ])
220224 return processed
221225
222- def book_info_to_circulation (self , book : dict [str , Any ]) -> CirculationData :
223- """Note: The json data passed into this method is from a different file/stream
224- from the json data that goes into the book_info_to_metadata() method.
226+ def book_info_to_circulation (
227+ self , availability : Availability , book_id : str | None = None
228+ ) -> CirculationData :
229+ """Convert an Overdrive availability document into a :class:`CirculationData` object.
230+
231+ Note: The availability data passed into this method is from a different
232+ API endpoint than the metadata data that goes into
233+ :meth:`book_info_to_bibliographic`.
234+
235+ :param availability: The parsed Overdrive availability document.
236+ :param book_id: Optional Overdrive ID to use when the availability
237+ document does not include a ``reserveId`` (e.g. a NotFound error
238+ response). If neither ``availability.reserve_id`` nor ``book_id``
239+ is present, a :exc:`PalaceValueError` is raised.
225240 """
226- # In Overdrive, 'reserved' books show up as books on
227- # hold. There is no separate notion of reserved books.
241+ # In Overdrive, 'reserved' books show up as books on hold.
228242 licenses_reserved = 0
229243
230244 licenses_owned = None
231245 licenses_available = None
232246 patrons_in_hold_queue = None
233247
234- # TODO: The only reason this works for a NotFound error is the
235- # circulation code sticks the known book ID into `book` ahead
236- # of time. That's a code smell indicating that this system
237- # needs to be refactored.
238- if "reserveId" in book and not "id" in book :
239- book ["id" ] = book ["reserveId" ]
240- if not "id" in book :
241- self .log .error ("Book has no ID: %r" , book )
248+ # book_id takes precedence over the document's reserveId so that the
249+ # caller can override the identifier (e.g. after a circulation lookup
250+ # where the caller already knows the book's ID). Fall back to reserveId
251+ # when no external book_id is provided (e.g. from the importer).
252+ overdrive_id = book_id or availability .reserve_id
253+ if not overdrive_id :
254+ self .log .error ("Availability has no ID: %r" , availability )
242255 raise PalaceValueError ("Book must have an id to be processed" )
243- overdrive_id = book [ "id" ]
256+
244257 primary_identifier = IdentifierData (
245258 type = Identifier .OVERDRIVE_ID , identifier = overdrive_id
246259 )
260+
247261 # TODO: We might be able to use this information to avoid the
248262 # need for explicit configuration of Advantage collections, or
249263 # at least to keep Advantage collections more up-to-date than
@@ -261,28 +275,24 @@ def book_info_to_circulation(self, book: dict[str, Any]) -> CirculationData:
261275 # similarly, though those can abruptly become unavailable, so
262276 # UNLIMITED_ACCESS is probably not appropriate.
263277
264- error_code = book .get ("errorCode" )
265278 # TODO: It's not clear what other error codes there might be.
266279 # The current behavior will respond to errors other than
267280 # NotFound by leaving the book alone, but this might not be
268281 # the right behavior.
269- if error_code == "NotFound" :
282+ if availability . error_code == "NotFound" :
270283 licenses_owned = 0
271284 licenses_available = 0
272285 patrons_in_hold_queue = 0
273- elif book . get ( "isOwnedByCollections" ) is not False :
286+ elif availability . is_owned_by_collections is not False :
274287 # We own this book.
275288 licenses_owned = 0
276289 licenses_available = 0
277290
278- for account in self ._get_applicable_accounts (book . get ( " accounts" , []) ):
279- licenses_owned += int ( account .get ( "copiesOwned" , 0 ))
280- licenses_available += int ( account .get ( "copiesAvailable" , 0 ))
291+ for account in self ._get_applicable_accounts (availability . accounts ):
292+ licenses_owned += account .copies_owned
293+ licenses_available += account .copies_available
281294
282- if "numberOfHolds" in book :
283- if patrons_in_hold_queue is None :
284- patrons_in_hold_queue = 0
285- patrons_in_hold_queue += book ["numberOfHolds" ]
295+ patrons_in_hold_queue = availability .number_of_holds
286296
287297 if licenses_owned is None :
288298 license_status = None
@@ -302,38 +312,26 @@ def book_info_to_circulation(self, book: dict[str, Any]) -> CirculationData:
302312 )
303313
304314 def _get_applicable_accounts (
305- self , accounts : list [dict [str , Any ]]
306- ) -> list [dict [str , Any ]]:
307- """
308- Returns those accounts from the accounts array that apply the
309- current overdrive collection context.
315+ self , accounts : list [AvailabilityAccount ]
316+ ) -> list [AvailabilityAccount ]:
317+ """Return the accounts from the availability document that apply to the
318+ current Overdrive collection context.
310319
311- If this is an overdrive parent collection, we want to return accounts
312- associated with the main OverDrive "library" and any non-main account
313- with sharing enabled.
320+ For a parent collection, returns accounts for the main OverDrive
321+ "library" and any sub-account with sharing enabled.
314322
315- If this is a child OverDrive collection, then we return only the
316- account associated with that child's OverDrive Advantage "library".
317- Additionally, we want to exclude the account if it is "shared" since
318- we will be counting it with the parent collection.
323+ For a child Overdrive Advantage collection, returns only the account
324+ matching that child's library ID (excluding shared copies, which are
325+ counted with the parent).
319326 """
320-
321327 if self .library_id == OVERDRIVE_MAIN_ACCOUNT_ID :
322- # this is a parent collection
323- filtered_result = filter (
324- lambda account : account .get ("id" ) == OVERDRIVE_MAIN_ACCOUNT_ID
325- or account .get ("shared" , False ),
326- accounts ,
327- )
328+ # parent collection
329+ return [
330+ a for a in accounts if a .id == OVERDRIVE_MAIN_ACCOUNT_ID or a .shared
331+ ]
328332 else :
329- # this is child collection
330- filtered_result = filter (
331- lambda account : account .get ("id" ) == self .library_id
332- and not account .get ("shared" , False ),
333- accounts ,
334- )
335-
336- return list (filtered_result )
333+ # child Advantage collection
334+ return [a for a in accounts if a .id == self .library_id and not a .shared ]
337335
338336 @classmethod
339337 def image_link_to_linkdata (
0 commit comments