api package

Subpackages

Submodules

api.annotations module

class api.annotations.AnnotationParser[source]

Bases: object

classmethod parse(_db, data, patron)[source]
class api.annotations.AnnotationWriter[source]

Bases: object

CONTENT_TYPE = 'application/ld+json; profile="http://www.w3.org/ns/anno.jsonld"'
JSONLD_CONTEXT = 'http://www.w3.org/ns/anno.jsonld'
LDP_CONTEXT = 'http://www.w3.org/ns/ldp.jsonld'
classmethod annotation_container_for(patron, identifier=None)[source]
classmethod annotation_page_for(patron, identifier=None, with_context=True)[source]
classmethod annotations_for(patron, identifier=None)[source]
classmethod detail(annotation, with_context=True)[source]
api.annotations.load_document(url)[source]

Retrieves JSON-LD for the given URL from a local file if available, and falls back to the network.

api.announcements module

class api.announcements.Announcement(**kwargs)[source]

Bases: object

Data model class for a single library-wide announcement.

property for_authentication_document

The publishable representation of this announcement, for use in an authentication document.

Basically just the ID and the content.

property is_active

Should this announcement be displayed now?

property json_ready
class api.announcements.Announcements(announcements)[source]

Bases: object

Data model class for a library’s announcements.

This entire list is stored as a single ConfigurationSetting, which is why this isn’t in core/model.

SETTING_NAME = 'announcements'
property active

Yield only the active announcements.

classmethod for_library(library)[source]

Load an Announcements object for the given Library.

Parameters:

library – A Library

api.app module

api.app.get_locale()[source]
api.app.initialize_database(autoinitialize=True)[source]
api.app.run(url=None)[source]

api.authenticator module

class api.authenticator.AuthenticationProvider(library, integration, analytics=None)[source]

Bases: OPDSAuthenticationFlow

Handle a specific patron authentication scheme.

DESCRIPTION = ''
EXTERNAL_TYPE_REGULAR_EXPRESSION = 'external_type_regular_expression'
FLOW_TYPE = None
IDENTIFIES_INDIVIDUALS = True
INSTITUTION_ID = 'institution_id'
LIBRARY_IDENTIFIER_FIELD = 'library_identifier_field'
LIBRARY_IDENTIFIER_RESTRICTION = 'library_identifier_restriction'
LIBRARY_IDENTIFIER_RESTRICTION_BARCODE = 'barcode'
LIBRARY_IDENTIFIER_RESTRICTION_TYPE = 'library_identifier_restriction_type'
LIBRARY_IDENTIFIER_RESTRICTION_TYPE_LIST = 'list'
LIBRARY_IDENTIFIER_RESTRICTION_TYPE_NONE = 'none'
LIBRARY_IDENTIFIER_RESTRICTION_TYPE_PREFIX = 'prefix'
LIBRARY_IDENTIFIER_RESTRICTION_TYPE_REGEX = 'regex'
LIBRARY_IDENTIFIER_RESTRICTION_TYPE_STRING = 'string'
LIBRARY_SETTINGS = [{'key': 'external_type_regular_expression', 'label': l'External Type Regular Expression', 'description': l'Derive a patron's type from their identifier.'}, {'key': 'library_identifier_restriction_type', 'label': l'Library Identifier Restriction Type', 'type': 'select', 'description': l'When multiple libraries share an ILS, a person may be able to authenticate with the ILS but not be considered a patron of <em>this</em> library. This setting contains the rule for determining whether an identifier is valid for this specific library. <p/> If this setting it set to 'No Restriction' then the values for <em>Library Identifier Field</em> and <em>Library Identifier Restriction</em> will not be used.', 'options': [{'key': 'none', 'label': l'No restriction'}, {'key': 'prefix', 'label': l'Prefix Match'}, {'key': 'string', 'label': l'Exact Match'}, {'key': 'regex', 'label': l'Regex Match'}, {'key': 'list', 'label': l'Exact Match, comma separated list'}], 'default': 'none'}, {'key': 'library_identifier_field', 'label': l'Library Identifier Field', 'type': 'select', 'options': [{'key': 'barcode', 'label': l'Barcode'}], 'description': l'This is the field on the patron record that the <em>Library Identifier Restriction Type</em> is applied to, different patron authentication methods provide different values here. This value is not used if <em>Library Identifier Restriction Type</em> is set to 'No restriction'.', 'default': 'barcode'}, {'key': 'library_identifier_restriction', 'label': l'Library Identifier Restriction', 'description': l'This is the restriction applied to the <em>Library Identifier Field</em> using the method chosen in <em>Library Identifier Restriction Type</em>. This value is not used if <em>Library Identifier Restriction Type</em> is set to 'No restriction'.'}, {'key': 'institution_id', 'label': l'Institution ID', 'description': l'A specific identifier for the library or branch, if used in patron authentication'}]
LOGIN_BUTTON_IMAGE = None
SETTINGS = []
authenticate(_db, header)[source]

Authenticate a patron based on a WWW-Authenticate header (or equivalent).

Returns:

A Patron if one can be authenticated; a ProblemDetail if an error occurs; None if the credentials are missing or wrong.

authenticated_patron(_db, header)[source]

Go from a WWW-Authenticate header (or equivalent) to a Patron object.

If the Patron needs to have their metadata updated, it happens transparently at this point.

Returns:

A Patron if one can be authenticated; a ProblemDetail if an error occurs; None if the credentials are missing or wrong.

enforce_library_identifier_restriction(identifier, patrondata)[source]

Does the given patron match the configured library identifier restriction?

external_integration(_db)[source]
get_credential_from_header(header)[source]

Extract a password credential from a WWW-Authenticate header (or equivalent).

This is used to pass on a patron’s credential to a content provider, such as Overdrive, which performs independent validation of a patron’s credentials.

Returns:

The patron’s password, or None if not available.

library(_db)[source]
remote_patron_lookup(patron_or_patrondata)[source]

Ask the remote for detailed information about a patron’s account.

This may be called in the course of authenticating a patron, or it may be called when the patron isn’t around, for purposes of learning some personal information (primarily email address) that can’t be stored in the database.

The default implementation assumes there is no special lookup functionality, and returns exactly the information present in the object that was passed in.

Parameters:

patron_or_patrondata – Either a Patron object, a PatronData object, or None (if no further information could be provided).

Returns:

An updated PatronData object.

update_patron_external_type(patron)[source]

Make sure the patron’s external type reflects what external_type_regular_expression says.

update_patron_metadata(patron)[source]

Refresh our local record of this patron’s account information.

Parameters:

patron – A Patron object.

class api.authenticator.Authenticator(_db, analytics=None)[source]

Bases: object

Route requests to the appropriate LibraryAuthenticator.

authenticated_patron(_db, header)[source]
bearer_token_provider_lookup(*args, **kwargs)[source]
create_authentication_document()[source]
create_authentication_headers()[source]
create_bearer_token(*args, **kwargs)[source]
property current_library_short_name
decode_bearer_token(*args, **kwargs)[source]
get_credential_from_header(header)[source]
invoke_authenticator_method(method_name, *args, **kwargs)[source]
populate_authenticators(_db, analytics)[source]
class api.authenticator.BaseSAMLAuthenticationProvider(library, integration, analytics=None)[source]

Bases: AuthenticationProvider, BearerTokenSigner

Base class for SAML authentication providers

DESCRIPTION = l'SAML 2.0 authentication provider'
DISPLAY_NAME = 'SAML 2.0'
FLOW_TYPE = 'http://librarysimplified.org/authtype/SAML-2.0'
LIBRARY_SETTINGS = []
NAME = 'SAML 2.0'
SETTINGS = [{'key': 'sp_xml_metadata', 'label': l'Service Provider's XML Metadata', 'description': l'SAML metadata of the Circulation Manager's Service Provider in an XML format. MUST contain exactly one SPSSODescriptor tag with at least one AssertionConsumerService tag with Binding attribute set to urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST.', 'type': 'textarea', 'required': True, 'default': None, 'options': None, 'category': None}, {'key': 'sp_private_key', 'label': l'Service Provider's Private Key', 'description': l'Private key used for encrypting SAML requests.', 'type': 'textarea', 'required': False, 'default': None, 'options': None, 'category': None}, {'key': 'saml_federated_idp_entity_ids', 'label': l'List of Federated IdPs', 'description': l'List of federated (for example, from InCommon Federation) IdPs supported by this authentication provider. Try to type the name of the IdP to find it in the list.', 'type': 'menu', 'required': False, 'default': [], 'options': None, 'category': None}, {'key': 'saml_patron_id_use_name_id', 'label': l'Patron ID: SAML NameID', 'description': l'Configuration setting indicating whether SAML NameID should be searched for a unique patron ID. If NameID found, it will supersede any SAML attributes selected in the next section.', 'type': 'select', 'required': False, 'default': 'true', 'options': [{'key': 'true', 'label': 'Use SAML NameID'}, {'key': 'false', 'label': 'Do NOT use SAML NameID'}], 'category': None}, {'key': 'saml_patron_id_attributes', 'label': l'Patron ID: SAML Attributes', 'description': l'List of SAML attributes that MAY contain a unique patron ID. The attributes will be scanned sequentially in the order you chose them, and the first existing attribute will be used to extract a unique patron ID.<br>NOTE: If a SAML attribute contains several values, only the first will be used.', 'type': 'menu', 'required': False, 'default': ['eduPersonUniqueId', 'eduPersonTargetedID', 'eduPersonPrincipalName', 'uid'], 'options': [{'key': 'uid', 'label': 'uid'}, {'key': 'givenName', 'label': 'givenName'}, {'key': 'surname', 'label': 'surname'}, {'key': 'mail', 'label': 'mail'}, {'key': 'displayName', 'label': 'displayName'}, {'key': 'eduPerson', 'label': 'eduPerson'}, {'key': 'eduPersonAffiliation', 'label': 'eduPersonAffiliation'}, {'key': 'eduPersonNickname', 'label': 'eduPersonNickname'}, {'key': 'eduPersonOrgDN', 'label': 'eduPersonOrgDN'}, {'key': 'eduPersonOrgUnitDN', 'label': 'eduPersonOrgUnitDN'}, {'key': 'eduPersonPrimaryAffiliation', 'label': 'eduPersonPrimaryAffiliation'}, {'key': 'eduPersonPrincipalName', 'label': 'eduPersonPrincipalName'}, {'key': 'eduPersonEntitlement', 'label': 'eduPersonEntitlement'}, {'key': 'eduPersonPrimaryOrgUnitDN', 'label': 'eduPersonPrimaryOrgUnitDN'}, {'key': 'eduPersonScopedAffiliation', 'label': 'eduPersonScopedAffiliation'}, {'key': 'eduPersonTargetedID', 'label': 'eduPersonTargetedID'}, {'key': 'eduPersonAssurance', 'label': 'eduPersonAssurance'}, {'key': 'eduPersonOrcid', 'label': 'eduPersonOrcid'}, {'key': 'eduPersonUniqueId', 'label': 'eduPersonUniqueId'}, {'key': 'eduPersonPrincipalNamePrior', 'label': 'eduPersonPrincipalNamePrior'}, {'key': 'eduOrg', 'label': 'eduOrg'}, {'key': 'eduOrgHomePageURI', 'label': 'eduOrgHomePageURI'}, {'key': 'eduOrgIdentityAuthNPolicyURI', 'label': 'eduOrgIdentityAuthNPolicyURI'}, {'key': 'eduOrgLegalName', 'label': 'eduOrgLegalName'}, {'key': 'eduOrgSuperiorURI', 'label': 'eduOrgSuperiorURI'}, {'key': 'eduOrgWhitePagesURI', 'label': 'eduOrgWhitePagesURI'}], 'category': None}, {'key': 'saml_patron_id_regular_expression', 'label': l'Patron ID: Regular expression', 'description': "Regular expression used to extract a unique patron ID from the attributes specified in <b>Patron ID: SAML Attributes</b> and/or NameID (if it's enabled in <b>Patron ID: SAML NameID</b>). <br>The expression MUST contain a named group <b>patron_id</b> used to match the patron ID. For example:<br><pre>(?P&lt;patron_id&gt;.+)@university\\.org</pre>The expression will extract the <b>patron_id</b> from the first SAML attribute that matches or NameID if it matches the expression.", 'type': None, 'required': False, 'default': None, 'options': None, 'category': None}, {'key': 'idp_xml_metadata', 'label': l'Identity Provider's XML metadata', 'description': l'SAML metadata of Identity Providers in an XML format. MAY contain multiple IDPSSODescriptor tags but each of them MUST contain at least one SingleSignOnService tag with Binding attribute set to urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect.', 'type': 'textarea', 'required': False, 'default': None, 'options': None, 'category': None}, {'key': 'saml_session_lifetime', 'label': l'Session Lifetime', 'description': l'This configuration setting determines how long a session created by the SAML authentication provider will live in days. By default it's empty meaning that the lifetime of the Circulation Manager's session is exactly the same as the lifetime of the IdP's session. Setting this value to a specific number will override this behaviour.<br>NOTE: This setting affects the session's lifetime only Circulation Manager's side. Accessing content protected by SAML will still be governed by the IdP and patrons will have to reauthenticate each time the IdP's session expires.', 'type': 'number', 'required': False, 'default': None, 'options': None, 'category': None}, {'key': 'saml_filter_expression', 'label': l'Filter Expression', 'description': l'Python expression used for filtering out patrons by their SAML attributes.<br><br>For example, if you want to authenticate using SAML only patrons having "eresources" as the value of their "eduPersonEntitlement" then you need to use the following expression:<br><pre> "urn:mace:nyu.edu:entl:lib:eresources" == subject.attribute_statement.attributes["eduPersonEntitlement"].values[0] </pre><br>If "eduPersonEntitlement" can have multiple values, you can use the following expression:<br><pre> "urn:mace:nyu.edu:entl:lib:eresources" in subject.attribute_statement.attributes["eduPersonEntitlement"].values </pre>', 'type': 'textarea', 'required': False, 'default': None, 'options': None, 'category': None}, {'key': 'strict', 'label': l'Service Provider's Strict Mode', 'description': l'If strict is 1, then the Python Toolkit will reject unsigned or unencrypted messages if it expects them to be signed or encrypted. Also, it will reject the messages if the SAML standard is not strictly followed.', 'type': 'number', 'required': False, 'default': 0, 'options': None, 'category': None}, {'key': 'debug', 'label': l'Service Provider's Debug Mode', 'description': l'Enable debug mode (outputs errors).', 'type': 'number', 'required': False, 'default': 0, 'options': None, 'category': None}]
TOKEN_DATA_SOURCE_NAME = 'SAML 2.0'
TOKEN_TYPE = 'SAML 2.0 token'
class api.authenticator.BasicAuthTempTokenController(authenticator)[source]

Bases: object

A controller that handles requests for issuing temporary tokens to HTTP Basic Auth credentials.

DO_NOT_GENERATE_NEW_TOKEN_PERIOD = 3540
TOKEN_DURATION = datetime.timedelta(seconds=3600)
basic_auth_temp_token(params, _db)[source]

Generate and return a temporary token from HTTP Basic Auth credentials.

get_or_create_token(_db, patron)[source]

Retrieve a patron’s Credential or create a new one.

class api.authenticator.BasicAuthenticationProvider(library, integration, analytics=None)[source]

Bases: AuthenticationProvider, HasSelfTests

Verify a username/password, obtained through HTTP Basic Auth, with a remote source of truth.

AUTHENTICATION_REALM = l'Library card'
BARCODE_FORMAT_CODABAR = 'Codabar'
BARCODE_FORMAT_NONE = ''
BEARER_TOKEN_PROVIDER_NAME = 'HTTPBasicBearerToken'
COMMON_IDENTIFIER_LABELS = {'Barcode': l'Barcode', 'Card Number': l'Card Number', 'Email Address': l'Email Address', 'Library Card': l'Library Card', 'Username': l'Username'}
COMMON_PASSWORD_LABELS = {'PIN': l'PIN', 'Password': l'Password'}
DEFAULT_IDENTIFIER_LABEL = 'Barcode'
DEFAULT_IDENTIFIER_REGULAR_EXPRESSION = re.compile('^[A-Za-z0-9@.-]+$')
DEFAULT_KEYBOARD = 'Default'
DEFAULT_PASSWORD_LABEL = 'PIN'
DEFAULT_PASSWORD_REGULAR_EXPRESSION = None
DISPLAY_NAME = l'Library Barcode'
EMAIL_ADDRESS_KEYBOARD = 'Email address'
FLOW_TYPE_BASIC = 'http://opds-spec.org/auth/basic'
FLOW_TYPE_OAUTH = 'http://librarysimplified.org/authtype/OAuth-Client-Credentials'
HTTP_BASIC_OAUTH_ENABLED = 'http_basic_oauth_enabled'
HTTP_BASIC_OAUTH_ENABLED_DEFAULT = False
IDENTIFIER_BARCODE_FORMAT = 'identifier_barcode_format'
IDENTIFIER_KEYBOARD = 'identifier_keyboard'
IDENTIFIER_LABEL = 'identifier_label'
IDENTIFIER_MAXIMUM_LENGTH = 'identifier_maximum_length'
IDENTIFIER_REGULAR_EXPRESSION = 'identifier_regular_expression'
LIBRARY_SETTINGS = [{'key': 'http_basic_oauth_enabled', 'label': l'Enable OAuth for HTTP Basic Auth', 'description': l'Enable authentication with bearer tokens generated via basic auth credentials', 'type': 'select', 'options': [{'key': 'false', 'label': l'Disabled'}, {'key': 'true', 'label': l'Enabled'}], 'default': 'false'}, {'key': 'external_type_regular_expression', 'label': l'External Type Regular Expression', 'description': l'Derive a patron's type from their identifier.'}, {'key': 'library_identifier_restriction_type', 'label': l'Library Identifier Restriction Type', 'type': 'select', 'description': l'When multiple libraries share an ILS, a person may be able to authenticate with the ILS but not be considered a patron of <em>this</em> library. This setting contains the rule for determining whether an identifier is valid for this specific library. <p/> If this setting it set to 'No Restriction' then the values for <em>Library Identifier Field</em> and <em>Library Identifier Restriction</em> will not be used.', 'options': [{'key': 'none', 'label': l'No restriction'}, {'key': 'prefix', 'label': l'Prefix Match'}, {'key': 'string', 'label': l'Exact Match'}, {'key': 'regex', 'label': l'Regex Match'}, {'key': 'list', 'label': l'Exact Match, comma separated list'}], 'default': 'none'}, {'key': 'library_identifier_field', 'label': l'Library Identifier Field', 'type': 'select', 'options': [{'key': 'barcode', 'label': l'Barcode'}], 'description': l'This is the field on the patron record that the <em>Library Identifier Restriction Type</em> is applied to, different patron authentication methods provide different values here. This value is not used if <em>Library Identifier Restriction Type</em> is set to 'No restriction'.', 'default': 'barcode'}, {'key': 'library_identifier_restriction', 'label': l'Library Identifier Restriction', 'description': l'This is the restriction applied to the <em>Library Identifier Field</em> using the method chosen in <em>Library Identifier Restriction Type</em>. This value is not used if <em>Library Identifier Restriction Type</em> is set to 'No restriction'.'}, {'key': 'institution_id', 'label': l'Institution ID', 'description': l'A specific identifier for the library or branch, if used in patron authentication'}]
NAME = 'Generic Basic Authentication provider'
NULL_KEYBOARD = 'No input'
NUMBER_PAD = 'Number pad'
PASSWORD_KEYBOARD = 'password_keyboard'
PASSWORD_LABEL = 'password_label'
PASSWORD_MAXIMUM_LENGTH = 'password_maximum_length'
PASSWORD_REGULAR_EXPRESSION = 'password_regular_expression'
SETTINGS = [{'key': 'test_identifier', 'label': l'Test Identifier', 'description': l'A valid identifier that can be used to test that patron authentication is working. An optional Test Password for this identifier can be set in the next section.', 'required': True}, {'key': 'test_password', 'label': l'Test Password', 'description': l'The password for the Test Identifier (above, in previous section).'}, {'key': 'identifier_barcode_format', 'label': l'Patron identifier barcode format', 'description': l'Many libraries render patron identifiers as barcodes on physical library cards. If you specify the barcode format, patrons will be able to scan their library cards with a camera instead of manually typing in their identifiers.', 'type': 'select', 'options': [{'key': 'Codabar', 'label': l'Patron identifiers are are rendered as barcodes in Codabar format'}, {'key': '', 'label': l'Patron identifiers are not rendered as barcodes'}], 'default': '', 'required': True}, {'key': 'identifier_regular_expression', 'label': l'Identifier Regular Expression', 'description': l'A patron's identifier will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'password_regular_expression', 'label': l'Password Regular Expression', 'description': l'A patron's password will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'identifier_keyboard', 'label': l'Keyboard for identifier entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Email address', 'label': l'Email address entry'}, {'key': 'Number pad', 'label': l'Number pad'}], 'default': 'Default', 'required': True}, {'key': 'password_keyboard', 'label': l'Keyboard for password entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Number pad', 'label': l'Number pad'}, {'key': 'No input', 'label': l'Patrons have no password and should not be prompted for one.'}], 'default': 'Default'}, {'key': 'identifier_maximum_length', 'label': l'Maximum identifier length', 'type': 'number'}, {'key': 'password_maximum_length', 'label': l'Maximum password length', 'type': 'number'}, {'key': 'identifier_label', 'label': l'Label for identifier entry'}, {'key': 'password_label', 'label': l'Label for password entry'}]
TEST_IDENTIFIER = 'test_identifier'
TEST_IDENTIFIER_DESCRIPTION_FOR_OPTIONAL_PASSWORD = l'A valid identifier that can be used to test that patron authentication is working. An optional Test Password for this identifier can be set in the next section.'
TEST_IDENTIFIER_DESCRIPTION_FOR_REQUIRED_PASSWORD = l'A valid identifier that can be used to test that patron authentication is working.'
TEST_PASSWORD = 'test_password'
TEST_PASSWORD_DESCRIPTION_OPTIONAL = l'The password for the Test Identifier (above, in previous section).'
TEST_PASSWORD_DESCRIPTION_REQUIRED = l'The password for the Test Identifier.'
TOKEN_TYPE = 'HTTP Basic'
alphanumerics_plus = re.compile('^[A-Za-z0-9@.-]+$')
apply_patrondata(patrondata, patron)[source]

Apply a PatronData object to the given patron and make sure any fields that need to be updated as a result of new data are updated.

authenticate(_db, credentials)[source]

Turn a set of credentials into a Patron object.

Parameters:

credentials – A dictionary with keys username and password or a bearer token string.

Returns:

A Patron if one can be authenticated; a ProblemDetail if an error occurs; None if the credentials are missing or wrong.

property authentication_header
class_default = <object object>
property collects_password

Does this BasicAuthenticationProvider expect a username and a password, or just a username?

get_credential_from_header(header)[source]

Extract a password credential from a WWW-Authenticate header (or equivalent).

This is used to pass on a patron’s credential to a content provider, such as Overdrive, which performs independent validation of a patron’s credentials.

Parameters:

header – A dictionary with keys username and password.

local_patron_lookup(_db, username, patrondata)[source]

Try to find a Patron object in the local database.

Parameters:
  • username – An HTTP Basic Auth username. May or may not correspond to the Patron.username field.

  • patrondata – A PatronData object recently obtained from the source of truth, possibly as a side effect of validating the username and password. This may make it possible to identify the patron more precisely. Or it may be None, in which case it’s no help at all.

patron_is_new = False
remote_authenticate(username, password)[source]

Does the source of truth approve of these credentials?

Returns:

If the credentials are valid, but nothing more is known about the patron, return True.

If the credentials are valid, _and_ enough information came back in the request to also create a PatronInfo object, you may create that object and return it to save a remote patron lookup later.

If the credentials are invalid, return False or None.

remote_patron_lookup(patron_or_patrondata)[source]

Ask the remote for information about this patron, and then make sure the patron belongs to the library associated with thie BasicAuthenticationProvider.

scrub_credential(value)[source]

Scrub an incoming value that is part of a patron’s set of credentials.

server_side_validation(username, password)[source]

Do these credentials even look right?

Sometimes egregious problems can be caught without needing to check with the ILS.

testing_patron(_db)[source]

Look up a Patron object reserved for testing purposes.

Returns:

A 2-tuple (Patron, password)

testing_patron_or_bust(_db)[source]

Look up the Patron object reserved for testing purposes.

Raise:CannotLoadConfiguration:

If no test patron is configured.

Raise:IntegrationException:

If the returned patron is not a Patron object.

Returns:

A 2-tuple (Patron, password)

class api.authenticator.BearerTokenSigner[source]

Bases: object

Mixin class used for storing a secret used for signing Bearer tokens

BEARER_TOKEN_SIGNING_SECRET = 'bearer_token_signing_secret'
classmethod bearer_token_signing_secret(db)[source]

Find or generate the site-wide bearer token signing secret.

Parameters:

db (sqlalchemy.orm.session.Session) – Database session

Returns:

ConfigurationSetting object containing the signing secret

Return type:

ConfigurationSetting

exception api.authenticator.CannotCreateLocalPatron[source]

Bases: Exception

A remote system provided information about a patron, but we could not put it into our database schema.

Probably because it was too vague.

class api.authenticator.CirculationPatronProfileStorage(patron, url_for=None)[source]

Bases: PatronProfileStorage

A patron profile storage that can also provide short client tokens

property profile_document

Create a Profile document representing the patron’s current status.

class api.authenticator.LibraryAuthenticator(_db, library, basic_auth_provider=None, oauth_providers=None, saml_providers=None, bearer_token_signing_secret=None, authentication_document_annotator=None)[source]

Bases: object

Use the registered AuthenticationProviders to turn incoming credentials into Patron objects.

assert_ready_for_token_signing()[source]

If this LibraryAuthenticator has OAuth providers, ensure that it also has a secret it can use to sign bearer tokens.

authenticated_patron(_db, header)[source]

Go from an Authorization header value to a Patron object.

Parameters:

header – If Basic Auth is in use, this is a dictionary with ‘user’ and ‘password’ components, derived from the HTTP header Authorization. Otherwise, this is the literal value of the Authorization HTTP header.

Returns:

A Patron, if one can be authenticated. None, if the credentials do not authenticate any particular patron. A ProblemDetail if an error occurs.

authentication_document_url(library)[source]

Return the URL of the authentication document for the given library.

bearer_token_provider_lookup(provider_name)[source]

Look up the relevant bearer token authentication provider with the given name. If that doesn’t work, return an appropriate ProblemDetai.

create_authentication_document()[source]

Create the Authentication For OPDS document to be used when a request comes in with no authentication.

create_authentication_headers()[source]

Create the HTTP headers to return with the OPDS authentication document.

create_bearer_token(provider_name, provider_token)[source]

Create a JSON web token with the given provider name and access token.

The patron will use this as a bearer token in lieu of the token we got from their OAuth provider. The big advantage of this token is that it tells us _which_ OAuth provider the patron authenticated against.

When the patron uses the bearer token in the Authenticate header, it will be decoded with decode_bearer_token_from_header.

decode_bearer_token(token)[source]

Extract auth provider name and access token from JSON web token.

decode_bearer_token_from_header(header)[source]

Extract auth provider name and access token from an Authenticate header value.

classmethod from_config(_db, library, analytics=None, custom_catalog_source=<class 'api.custom_patron_catalog.CustomPatronCatalog'>)[source]

Initialize an Authenticator for the given Library based on its configured ExternalIntegrations.

Parameters:

custom_catalog_source – The lookup class for CustomPatronCatalogs. Intended for mocking during tests.

get_credential_from_header(header)[source]

Extract a password credential from a WWW-Authenticate header (or equivalent).

This is used to pass on a patron’s credential to a content provider, such as Overdrive, which performs independent validation of a patron’s credentials.

Returns:

The patron’s password, or None if not available.

property identifies_individuals

Does this library require that individual patrons be identified?

Most libraries require authentication as an individual. Some libraries don’t identify patrons at all; others may have a way of identifying the patron population without identifying individuals, such as an IP gate.

If some of a library’s authentication mechanisms identify individuals, and others do not, the library does not identify individuals.

property key_pair

Look up or create a public/private key pair for use by this library.

property library
property providers

An iterator over all registered AuthenticationProviders.

register_basic_auth_provider(provider)[source]
register_bearer_token_auth_provider(provider)[source]
register_provider(integration, analytics=None)[source]

Turn an ExternalIntegration object into an AuthenticationProvider object, and register it.

Parameters:

integration – An ExternalIntegration that configures a way of authenticating patrons.

property supports_patron_authentication

Does this library have any way of authenticating patrons at all?

class api.authenticator.OAuthAuthenticationProvider(library, integration, analytics=None)[source]

Bases: AuthenticationProvider, BearerTokenSigner

DEFAULT_TOKEN_EXPIRATION_DAYS = 42
FLOW_TYPE = 'http://librarysimplified.org/authtype/OAuth-with-intermediary'
OAUTH_TOKEN_EXPIRATION_DAYS = 'token_expiration_days'
SETTINGS = [{'key': 'token_expiration_days', 'type': 'number', 'label': l'Days until OAuth token expires'}]
authenticated_patron(_db, token)[source]

Go from an OAuth provider token to an authenticated Patron.

Parameters:

token – The provider token extracted from the Authorization header. This is _not_ the bearer token found in the Authorization header; it’s the provider-specific token embedded in that token.

Returns:

A Patron, if one can be authenticated. None, if the credentials do not authenticate any particular patron. A ProblemDetail if an error occurs.

create_token(_db, patron, token)[source]

Create a Credential object that ties the given patron to the given provider token.

external_authenticate_url(state, _db)[source]

Generate the URL provided by the OAuth provider which will present the patron with a login form.

Parameters:

state – A state variable to be propagated through to the OAuth callback.

external_authenticate_url_parameters(state, _db)[source]

Arguments used to fill in the template EXTERNAL_AUTHENTICATE_URL.

oauth_callback(_db, code)[source]

Verify the incoming parameters with the OAuth provider. Exchange the authorization code for an access token. Create or look up appropriate database records.

Parameters:

code – The authorization code generated by the authorization server, as per section 4.1.2 of RFC 6749. This method will exchange the authorization code for an access token.

Returns:

A ProblemDetail if there’s a problem. Otherwise, a 3-tuple (Credential, Patron, PatronData). The Credential contains the access token provided by the OAuth provider. The Patron object represents the authenticated Patron, and the PatronData object includes information about the patron obtained from the OAuth provider which cannot be stored in the circulation manager’s database, but which should be passed on to the client.

remote_exchange_authorization_code_for_access_token(_db, code)[source]

Ask the OAuth provider to convert a code (passed in to the OAuth callback) into a bearer token.

We can use the bearer token to act on behalf of a specific patron. It also gives us confidence that the patron authenticated correctly with the OAuth provider.

Returns:

A ProblemDetail if there’s a problem; otherwise, the bearer token.

remote_patron_lookup(access_token)[source]

Use a bearer token to look up as much information as possible about a patron.

Returns:

A ProblemDetail if there’s a problem. Otherwise, a PatronData.

token_data_source(_db)[source]
class api.authenticator.OAuthController(authenticator)[source]

Bases: object

A controller for handling requests that are part of the OAuth credential dance.

oauth_authentication_callback(_db, params)[source]

Create a Patron object and a bearer token for a patron who has just authenticated with one of our OAuth providers.

Returns:

A redirect to the redirect_uri kept in params[‘state’], with the bearer token encoded into the fragment identifier as access_token and useful information about the patron encoded into the fragment identifier as patron_info. For example, if params is

dict(state=”http://oauthprovider.org/success”)

Then the redirect URI might be:

It’s the client’s responsibility to extract the access_token, start using it as a bearer token, and make sense of the patron_info.

classmethod oauth_authentication_callback_url(library_short_name)[source]

The URL to the oauth_authentication_callback controller.

This is its own method because sometimes an OAuthAuthenticationProvider needs to send it to the OAuth provider to demonstrate that it knows which URL a patron was redirected to.

oauth_authentication_redirect(params, _db)[source]

Redirect an unauthenticated patron to the authentication URL of the appropriate OAuth provider.

Over on that other site, the patron will authenticate and be redirected back to the circulation manager, ending up in oauth_authentication_callback.

class api.authenticator.PatronData(permanent_id=None, authorization_identifier=None, username=None, personal_name=None, email_address=None, authorization_expires=None, external_type=None, fines=None, block_reason=None, library_identifier=None, neighborhood=None, cached_neighborhood=None, complete=True, is_new=False)[source]

Bases: object

A container for basic information about a patron.

Like Metadata and CirculationData, this offers a layer of abstraction between various account managment systems and the circulation manager database. Unlike with those classes, some of this data cannot be written to the database for data retention reasons. But it can be passed from the account management system to the client application.

CARD_REPORTED_LOST = 'card reported lost'
EXCESSIVE_FEES = 'excessive fees'
EXCESSIVE_FINES = 'excessive fines'
NO_BORROWING_PRIVILEGES = 'no borrowing privileges'
NO_VALUE = <api.authenticator.PatronData.NoValue object>
class NoValue[source]

Bases: object

RECALL_OVERDUE = 'recall overdue'
TOO_MANY_ITEMS_BILLED = 'too many items billed'
TOO_MANY_LOANS = 'too many active loans'
TOO_MANY_LOST = 'too many items lost'
TOO_MANY_OVERDUE = 'too many items overdue'
TOO_MANY_RENEWALS = 'too many renewals'
UNKNOWN_BLOCK = 'unknown'
apply(patron)[source]

Take the portion of this data that can be stored in the database and write it to the given Patron record.

fines
get_or_create_patron(_db, library_id, analytics=None)[source]

Create a Patron with this information.

TODO: I’m concerned in the general case with race conditions. It’s theoretically possible that two newly created patrons could have the same username or authorization identifier, violating a uniqueness constraint. This could happen if one was identified by permanent ID and the other had no permanent ID and was identified by username. (This would only come up if the authentication provider has permanent IDs for some patrons but not others.)

Something similar can happen if the authentication provider provides username and authorization identifier, but not permanent ID, and the patron’s authorization identifier (but not their username) changes while two different circulation manager authentication requests are pending.

When these race conditions do happen, I think the worst that will happen is the second request will fail. But it’s very important that authorization providers give some unique, preferably unchanging way of identifying patrons.

Parameters:
  • library_id – Database ID of the Library with which this patron is associated.

  • analytics – Analytics instance to track the new patron creation event.

set_authorization_identifier(authorization_identifier)[source]

Helper method to set both .authorization_identifier and .authorization_identifiers appropriately.

set_value(patron, field_name, value)[source]
property to_dict

Convert the information in this PatronData to a dictionary which can be converted to JSON and sent out to a client.

property to_response_parameters

Return information about this patron which the client might find useful.

This information will be sent to the client immediately after a patron’s credentials are verified by an OAuth provider.

api.axis module

class api.axis.AudiobookMetadataParser(collection)[source]

Bases: JSONResponseParser

Parse the results of Axis 360’s audiobook metadata API call.

class api.axis.AvailabilityResponseParser(api, internal_format=None)[source]

Bases: ResponseParser

process_all(string)[source]
process_one(e, ns)[source]
class api.axis.Axis360API(_db, collection)[source]

Bases: Authenticator, BaseCirculationAPI, Axis360APIConstants, HasCollectionSelfTests

ALLOW_ANONYMOUS_ACCESS_SETTING = 'allow_anonymous_access'
AXISNOW = 'AxisNow'
DATE_FORMAT = '%m-%d-%Y %H:%M:%S'
LIBRARY_SETTINGS = [{'key': 'ebook_loan_duration', 'label': l'Default Loan Period (in Days)', 'default': 21, 'type': 'number', 'description': l'Until it hears otherwise from the distributor, this server will assume that any given loan for this library from this collection will last this number of days. This number is usually a negotiated value between the library and the distributor. This only affects estimates&mdash;it cannot affect the actual length of loans.'}]
NAME = 'Axis 360'
PRODUCTION_BASE_URL = 'https://axis360api.baker-taylor.com/Services/VendorAPI/'
QA_BASE_URL = 'http://axis360apiqa.baker-taylor.com/Services/VendorAPI/'
SERVER_NICKNAMES = {'production': 'https://axis360api.baker-taylor.com/Services/VendorAPI/', 'qa': 'http://axis360apiqa.baker-taylor.com/Services/VendorAPI/'}
SERVICE_NAME = 'Axis 360'
SETTINGS = [{'key': 'username', 'label': l'Username', 'required': True}, {'key': 'password', 'label': l'Password', 'required': True}, {'key': 'external_account_id', 'label': l'Library ID', 'required': True}, {'key': 'url', 'label': l'Server', 'default': 'https://axis360api.baker-taylor.com/Services/VendorAPI/', 'required': True, 'format': 'url', 'allowed': ['production', 'qa']}, {'key': 'verify_certificate', 'label': l'Verify SSL Certificate', 'description': l'This should always be True in production, it may need to be set to False to use theAxis 360 QA Environment.', 'type': 'select', 'options': [{'label': l'True', 'key': 'True'}, {'label': l'False', 'key': 'False'}], 'default': True}, {'key': 'allow_anonymous_access', 'label': l'Allow anonymous access', 'description': l'If you're associating an Axis 360 collection with a library that doesn't take any steps to authenticate its patrons, you'll need to disable this safety setting to explicitly allow anonymous access to the collection. Most libraries authenticate their patrons, so allowing only authenticated access is the right choice in almost all situations.', 'type': 'select', 'options': [{'key': 'false', 'label': l'Allow only authenticated access to this collection's titles'}, {'key': 'true', 'label': l'Allow anonymous access to this collection's titles'}], 'default': 'false'}]
SET_DELIVERY_MECHANISM_AT = 'borrow'
access_token_endpoint = 'accesstoken'
adobe_drm = 'application/vnd.adobe.adept+xml'
audiobook_metadata_endpoint = 'getaudiobookmetadata/v2'
property authorization_headers
availability(patron_id=None, since=None, title_ids=[])[source]
availability_endpoint = 'availability/v2'
axisnow_drm = 'application/vnd.librarysimplified.axisnow+json'
can_fulfill_without_loan(patron, pool, lpdm)[source]

A LicensePool can be fulfilled without a loan if a) the library allows for it, and b) the delivery mechanism is either AxisNow or unspecified (in which case AxisNow is an option).

checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – The Patron who wants to return their book.

  • pin – Not used.

  • licensepool – LicensePool for the book to be returned.

Raises:
checkout(patron, pin, licensepool, internal_format)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

property collection
classmethod create_identifier_strings(identifiers)[source]
delivery_mechanism_to_internal_format = {('application/epub+zip', None): 'ePub', ('application/epub+zip', 'application/vnd.adobe.adept+xml'): 'ePub', ('application/pdf', None): 'PDF', ('application/pdf', 'application/vnd.adobe.adept+xml'): 'PDF', (None, 'application/vnd.librarysimplified.findaway.license+json'): 'Acoustik', (None, 'application/vnd.librarysimplified.axisnow+json'): 'AxisNow'}
epub = 'application/epub+zip'
external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

findaway_drm = 'application/vnd.librarysimplified.findaway.license+json'
fulfill(patron, pin, licensepool, internal_format, **kwargs)[source]

Fulfill a patron’s request for a specific book.

Parameters:

kwargs – A container for arguments to fulfill() which are not relevant to this vendor.

Returns:

a FulfillmentInfo object.

fulfillment_endpoint = 'getfullfillmentInfo/v2'
get_audiobook_metadata(findaway_content_id)[source]

Make a call to the getaudiobookmetadata endpoint.

get_fulfillment_info(transaction_id)[source]

Make a call to the getFulfillmentInfoAPI.

log = <Logger Axis 360 API (WARNING)>
no_drm = None
classmethod parse_token(token)[source]
patron_activity(patron, pin, identifier=None, internal_format=None)[source]

Return a patron’s current checkouts and holds.

pdf = 'application/pdf'
place_hold(patron, pin, licensepool, hold_notification_email)[source]

Place a book on hold.

Returns:

A HoldInfo object

recent_activity(since)[source]

Find books that have had recent activity.

Yield:

A sequence of (Metadata, CirculationData) 2-tuples

refresh_bearer_token()[source]
release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Raises:

CannotReleaseHold – If there is an error communicating with the provider, or the provider refuses to release the hold for any reason.

request(url, method='get', extra_headers={}, data=None, params=None, exception_on_401=False, **kwargs)[source]

Make an HTTP request, acquiring/refreshing a bearer token if necessary.

property source
update_availability(licensepool)[source]

Update the availability information for a single LicensePool.

Part of the CirculationAPI interface.

update_book(bibliographic, availability, analytics=None)[source]

Create or update a single book based on bibliographic and availability data from the Axis 360 API.

Parameters:
  • bibliographic – A Metadata object containing bibliographic data about this title.

  • availability – A CirculationData object containing availability data about this title.

update_licensepools_for_identifiers(identifiers)[source]

Update availability and bibliographic information for a list of books.

If the book has never been seen before, a new LicensePool will be created for the book.

The book’s LicensePool will be updated with current circulation information.

class api.axis.Axis360APIConstants[source]

Bases: object

VERIFY_SSL = 'verify_certificate'
class api.axis.Axis360AcsFulfillmentInfo(verify, **kwargs)[source]

Bases: FulfillmentInfo

This implements a Axis 360 specific FulfillmentInfo for ACS content served through AxisNow. The AxisNow API gives us a link that we can use to get the ACSM file that we serve to the mobile apps. This link resolves to a redirect, which resolves to the actual ACSM file. The URL we are given in the redirect has a percent encoded query string in it. The encoding used in this string has lower case characters in it like “%3a” for :. In versions of urllib3 > 1.24.3 the library normalizes the query string before doing the actual request. In doing the normalization it follows the recommendation of RFC 3986 and uppercases the percent encoded bytes. This causes the Axis360 API to return an error from Adobe ACS: ` <error xmlns="http://ns.adobe.com/adept" data="E_URLLINK_AUTH https://acsqa.digitalcontentcafe.com/fulfillment/URLLink.acsm"/> ` instead of the correct ACSM file. Others have noted that this is a problem in the urllib3 github but they do not seem interested in providing an option to override this behavior and closed the ticket. https://github.com/urllib3/urllib3/issues/1677 This FulfillmentInfo implementation uses the built in Python urllib implementation instead of requests (and urllib3) to make this request to the Axis 360 API, sidestepping the problem, but taking a different code path than most of our external HTTP requests.

Copyright The Palace Project for code licensed under the Apache 2.0 License.

property as_response

Bypass the normal process of creating a Flask Response.

Returns:

A Response object, or None if you’re okay with the normal process.

logger = <Logger api.axis (WARNING)>
problem_detail_document(error_details)[source]
class api.axis.Axis360BibliographicCoverageProvider(collection, api_class=<class 'api.axis.Axis360API'>, **kwargs)[source]

Bases: BibliographicCoverageProvider

Fill in bibliographic metadata for Axis 360 records.

Currently this is only used by BibliographicRefreshScript. It’s not normally necessary because the Axis 360 API combines bibliographic and availability data. We rely on Monitors to fetch availability data and fill in the bibliographic data as necessary.

DATA_SOURCE_NAME = 'Axis 360'
DEFAULT_BATCH_SIZE = 25
INPUT_IDENTIFIER_TYPES = 'Axis 360 ID'
PROTOCOL = 'Axis 360'
SERVICE_NAME = 'Axis 360 Bibliographic Coverage Provider'
handle_success(identifier)[source]

Once a book has bibliographic coverage, it can be given a work and made presentation ready.

process_batch(identifiers)[source]

Do what it takes to give coverage records to a batch of items.

Returns:

A mixed list of coverage records and CoverageFailures.

process_item(identifier)[source]

Do the work necessary to give coverage to one specific item.

Since this is where the actual work happens, this is not implemented in IdentifierCoverageProvider or WorkCoverageProvider, and must be handled in a subclass.

class api.axis.Axis360CirculationMonitor(_db, collection, api_class=<class 'api.axis.Axis360API'>)[source]

Bases: CollectionMonitor, TimelineMonitor

Maintain LicensePools for Axis 360 titles.

DEFAULT_BATCH_SIZE = 50
DEFAULT_START_TIME = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=<UTC>)
INTERVAL_SECONDS = 60
PROTOCOL = 'Axis 360'
SERVICE_NAME = 'Axis 360 Circulation Monitor'
catch_up_from(start, cutoff, progress)[source]

Find Axis 360 books that changed recently.

Progress:

A TimestampData representing the time previously covered by this Monitor.

process_book(bibliographic, circulation)[source]
class api.axis.Axis360FulfillmentInfo(api, data_source_name, identifier_type, identifier, key)[source]

Bases: APIAwareFulfillmentInfo

An Axis 360-specific FulfillmentInfo implementation for audiobooks and books served through AxisNow.

We use these instead of normal FulfillmentInfo objects because putting all this information into FulfillmentInfo would require one or two extra HTTP requests, and there’s often no need to make those requests.

do_fetch()[source]

Actually make the API request.

When implemented, this method must set values for some or all of _content_link, _content_type, _content, and _content_expires.

class api.axis.Axis360FulfillmentInfoResponseParser(api)[source]

Bases: JSONResponseParser

Parse JSON documents into Findaway audiobook manifests or AxisNow manifests.

parse_axisnow(parsed)[source]
parse_date(date)[source]
parse_findaway(parsed, license_pool)[source]
class api.axis.Axis360Parser[source]

Bases: XMLParser

FULL_DATE_FORMAT_EXPLICIT_UTC = '%m/%d/%Y %I:%M:%S %p +00:00'
FULL_DATE_FORMAT_IMPLICIT_UTC = '%m/%d/%Y %I:%M:%S %p'
NS = {'axis': 'http://axis360api.baker-taylor.com/vendorAPI'}
SHORT_DATE_FORMAT = '%m/%d/%Y'
class api.axis.AxisCollectionReaper(_db, collection, api_class=<class 'api.axis.Axis360API'>)[source]

Bases: IdentifierSweepMonitor

Check for books that are in the local collection but have left our Axis 360 collection.

INTERVAL_SECONDS = 43200
PROTOCOL = 'Axis 360'
SERVICE_NAME = 'Axis Collection Reaper'
process_items(identifiers)[source]

Process a list of items.

class api.axis.AxisNowManifest(book_vault_uuid, isbn)[source]

Bases: object

A simple media type for conveying an entry point into the AxisNow access control system.

MEDIA_TYPE = 'application/vnd.librarysimplified.axisnow+json'
class api.axis.BibliographicParser(include_availability=True, include_bibliographic=True)[source]

Bases: Axis360Parser

DELIVERY_DATA_FOR_AXIS_FORMAT = {'Acoustik': (None, 'application/vnd.librarysimplified.findaway.license+json'), 'AxisNow': None, 'Blio': None, 'PDF': ('application/pdf', 'application/vnd.adobe.adept+xml'), 'ePub': ('application/epub+zip', 'application/vnd.adobe.adept+xml')}
extract_availability(circulation_data, element, ns)[source]
extract_bibliographic(element, ns)[source]

Turn bibliographic metadata into a Metadata and a CirculationData objects, and return them as a tuple.

generic_author = <object object>
log = <Logger Axis 360 Bibliographic Parser (WARNING)>
classmethod parse_contributor(author, primary_author_found=False, force_role=None)[source]

Parse an Axis 360 contributor string.

The contributor string looks like “Butler, Octavia” or “Walt Disney Pictures (COR)” or “Rex, Adam (ILT)”. The optional three-letter code describes the contributor’s role in the book.

Parameters:
  • author – The string to parse.

  • primary_author_found – If this is false, then a contributor with no three-letter code will be treated as the primary author. If this is true, then a contributor with no three-letter code will be treated as just a regular author.

  • force_role – If this is set, the contributor will be assigned this role, no matter what. This takes precedence over the value implied by primary_author_found.

classmethod parse_list(l)[source]

Turn strings like this into lists:

FICTION / Thrillers; FICTION / Suspense; FICTION / General Ursu, Anne ; Fortune, Eric (ILT)

process_all(string)[source]
process_one(element, ns)[source]
role_abbreviation = re.compile('\\(([A-Z][A-Z][A-Z])\\)$')
role_abbreviation_to_role = {'ADP': <object object>, 'COR': <object object>, 'EDT': 'Editor', 'FRW': 'Foreword Author', 'ILT': 'Illustrator', 'INT': 'Introduction Author', 'PHT': 'Photographer', 'TRN': 'Translator'}
class api.axis.CheckinResponseParser(collection)[source]

Bases: ResponseParser

process_all(string)[source]
process_one(e, namespaces)[source]

Either raise an appropriate exception, or do nothing.

class api.axis.CheckoutResponseParser(collection)[source]

Bases: ResponseParser

process_all(string)[source]
process_one(e, namespaces)[source]

Either turn the given document into a LoanInfo object, or raise an appropriate exception.

class api.axis.HoldReleaseResponseParser(collection)[source]

Bases: ResponseParser

post_process(i)[source]

Unlike other ResponseParser subclasses, we don’t return any type of *Info object, so there’s no need to do any post-processing.

process_all(string)[source]
process_one(e, namespaces)[source]
class api.axis.HoldResponseParser(collection)[source]

Bases: ResponseParser

process_all(string)[source]
process_one(e, namespaces)[source]

Either turn the given document into a HoldInfo object, or raise an appropriate exception.

class api.axis.JSONResponseParser(collection)[source]

Bases: ResponseParser

Most ResponseParsers parse XML documents; subclasses of JSONResponseParser parse JSON documents.

This only subclasses ResponseParser so it can reuse _raise_exception_on_error.

parse(data, *args, **kwargs)[source]

Parse a JSON document.

classmethod verify_status_code(parsed)[source]

Assert that the incoming JSON document represents a successful response.

class api.axis.MockAxis360API(_db, collection, with_token=True, **kwargs)[source]

Bases: Axis360API

classmethod mock_collection(_db, name='Test Axis 360 Collection')[source]

Create a mock Axis 360 collection for use in tests.

queue_response(status_code, headers={}, content=None)[source]
class api.axis.ResponseParser(collection)[source]

Bases: Axis360Parser

SERVICE_NAME = 'Axis 360'
code_to_exception = {315: <class 'api.circulation_exceptions.InvalidInputException'>, 316: <class 'api.circulation_exceptions.InvalidInputException'>, 1000: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 1001: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 1002: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 1003: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 2000: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 2001: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 2002: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 2003: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 2004: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 2005: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 2007: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 2008: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 3100: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3101: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3102: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3103: <class 'api.circulation_exceptions.NotFoundOnRemote'>, 3104: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3105: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 3106: <class 'api.circulation_exceptions.InvalidInputException'>, 3108: <class 'api.circulation_exceptions.InvalidInputException'>, 3109: <class 'api.circulation_exceptions.InvalidInputException'>, 3110: <class 'api.circulation_exceptions.AlreadyCheckedOut'>, 3111: <class 'api.circulation_exceptions.CurrentlyAvailable'>, 3112: <class 'api.circulation_exceptions.CannotFulfill'>, 3113: <class 'api.circulation_exceptions.CannotLoan'>, (3113, 'Title ID is not available for checkout'): <class 'api.circulation_exceptions.NoAvailableCopies'>, 3114: <class 'api.circulation_exceptions.PatronLoanLimitReached'>, 3115: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3116: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3117: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3118: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3119: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 3120: <class 'api.circulation_exceptions.LibraryAuthorizationFailedException'>, 3123: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 3124: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 3126: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3127: <class 'api.circulation_exceptions.InvalidInputException'>, 3128: <class 'api.circulation_exceptions.InvalidInputException'>, 3129: <class 'api.circulation_exceptions.PatronAuthorizationFailedException'>, 3130: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3131: <class 'api.circulation_exceptions.RemoteInitiatedServerError'>, 3132: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3134: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 3135: <class 'api.circulation_exceptions.NoAcceptableFormat'>, 3136: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 4058: <class 'api.circulation_exceptions.NoActiveLoan'>, 5000: <class 'api.circulation_exceptions.RemoteInitiatedServerError'>, 5003: <class 'api.circulation_exceptions.LibraryInvalidInputException'>, 5004: <class 'api.circulation_exceptions.LibraryInvalidInputException'>}
id_type = 'Axis 360 ID'
raise_exception_on_error(e, ns, custom_error_classes={}, ignore_error_codes=None)[source]

Raise an error if the given lxml node represents an Axis 360 error condition.

Parameters:
  • e – An lxml Element

  • ns – A dictionary of namespaces

  • custom_error_classes – A dictionary of errors to map to custom classes rather than the defaults.

  • ignore_error_codes – A list of error codes to treat as success rather than as cause to raise an exception.

api.base_controller module

class api.base_controller.BaseCirculationManagerController(manager)[source]

Bases: object

Define minimal standards for a circulation manager controller, mainly around authentication.

authenticate()[source]

Sends a 401 response that demands authentication.

authenticated_patron(authorization_header)[source]

Look up the patron authenticated by the given authorization header.

The header could contain a barcode and pin or a token for an external service.

If there’s a problem, return a Problem Detail Document.

If there’s no problem, return a Patron object.

authenticated_patron_from_request()[source]

Try to authenticate a patron for the incoming request.

When this method returns, flask.request.patron will be set, though the value it’s set to may be None.

Returns:

A Patron, if possible. If no authentication was provided, a Flask Response. If a problem occured during authentication, a ProblemDetail.

authorization_header()[source]

Get the authentication header.

library_for_request(library_short_name)[source]

Look up the library the user is trying to access.

Since this is called on pretty much every request, it’s also an appropriate time to check whether the site configuration has been changed and needs to be updated.

library_through_external_loan_identifier(loan_external_identifier)[source]

Look up the library the user is trying to access using a loan’s external identifier. We assume that the external identifier is globally unique which is true, for example, in the case of using Readium LCP.

Parameters:

loan_external_identifier (basestring) – External identifier of the patron’s loan

Returns:

Library the patron is trying to access

Return type:

Library

property request_patron

The currently authenticated patron for this request, if any.

Most of the time you can use flask.request.patron, but sometimes it’s not clear whether authenticated_patron_from_request() (which sets flask.request.patron) has been called, and authenticated_patron_from_request has a complicated return value.

Returns:

A Patron, if one could be authenticated; None otherwise.

api.bibliotheca module

class api.bibliotheca.BibliothecaAPI(_db, collection)[source]

Bases: BaseCirculationAPI, HasSelfTests

ARGUMENT_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
AUTHORIZATION_FORMAT = '3MCLAUTH %s:%s'
AUTHORIZATION_HEADER = '3mcl-Authorization'
AUTH_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
CAN_REVOKE_HOLD_WHEN_RESERVED = False
DATETIME_HEADER = '3mcl-Datetime'
DEFAULT_BASE_URL = 'https://partner.yourcloudlibrary.com/'
DEFAULT_VERSION = '2.0'
LIBRARY_SETTINGS = [{'key': 'ebook_loan_duration', 'label': l'Default Loan Period (in Days)', 'default': 21, 'type': 'number', 'description': l'Until it hears otherwise from the distributor, this server will assume that any given loan for this library from this collection will last this number of days. This number is usually a negotiated value between the library and the distributor. This only affects estimates&mdash;it cannot affect the actual length of loans.'}]
MAX_AGE = 0
NAME = 'Bibliotheca'
SERVICE_NAME = 'Bibliotheca'
SETTINGS = [{'key': 'username', 'label': l'Account ID', 'required': True}, {'key': 'password', 'label': l'Account Key', 'required': True}, {'key': 'external_account_id', 'label': l'Library ID', 'required': True}]
SET_DELIVERY_MECHANISM_AT = None
TEMPLATE = '<%(request_type)s><ItemId>%(item_id)s</ItemId><PatronId>%(patron_id)s</PatronId></%(request_type)s>'
VERSION_HEADER = '3mcl-Version'
adobe_drm = 'application/vnd.adobe.adept+xml'
authorization(method, path)[source]
bibliographic_lookup(identifiers)[source]

Look up current bibliographic and circulation information for the given identifiers.

Parameters:

identifiers – A list containing either Identifier objects or Bibliotheca identifier strings.

bibliographic_lookup_request(identifiers)[source]

Make an HTTP request to look up current bibliographic and circulation information for the given identifiers.

Parameters:

identifiers – Strings containing Bibliotheca identifiers.

Returns:

A string containing an XML document, or None if there was an error not handled as an exception.

checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

checkout(patron_obj, patron_password, licensepool, delivery_mechanism)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron_obj – a Patron object for the patron who wants to check out the book.

  • patron_password – The patron’s alleged password. Not used here since Bibliotheca trusts Simplified to do the check ahead of time.

  • licensepool – LicensePool for the book to be checked out.

Returns:

a LoanInfo object

property collection
delivery_mechanism_to_internal_format = {('application/epub+zip', 'application/vnd.adobe.adept+xml'): 'ePub', ('application/pdf', 'application/vnd.adobe.adept+xml'): 'PDF', (None, 'application/vnd.librarysimplified.findaway.license+json'): 'MP3'}
external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

findaway_drm = 'application/vnd.librarysimplified.findaway.license+json'
classmethod findaway_license_to_webpub_manifest(license_pool, findaway_license)[source]

Convert a Bibliotheca license document to a FindawayManifest suitable for serving to a mobile client.

Parameters:
  • license_pool – A LicensePool for the title in question. This will be used to fill in basic bibliographic information.

  • findaway_license – A string containing a Findaway license document via Bibliotheca, or a dictionary representing such a document loaded into JSON form.

fulfill(patron, password, pool, internal_format, **kwargs)[source]

Get the actual resource file to the patron.

Parameters:

kwargs – A container for standard arguments to fulfill() which are not relevant to this implementation.

Returns:

a FulfillmentInfo object.

full_path(path)[source]
full_url(path)[source]
get_audio_fulfillment_file(patron_id, bibliotheca_id)[source]
get_bibliographic_info_for(editions, max_age=None)[source]
get_events_between(start, end, cache_result=False, no_events_error=False)[source]

Return event objects for events between the given times.

get_fulfillment_file(patron_id, bibliotheca_id)[source]
internal_format_to_delivery_mechanism = {'MP3': (None, 'application/vnd.librarysimplified.findaway.license+json'), 'PDF': ('application/pdf', 'application/vnd.adobe.adept+xml'), 'ePub': ('application/epub+zip', 'application/vnd.adobe.adept+xml')}
log = <Logger Bibliotheca API (WARNING)>
marc_request(start, end, offset=1, limit=50)[source]

Make an HTTP request to look up the MARC records for books purchased between two given dates.

Parameters:
  • start – A datetime to start looking for purchases.

  • end – A datetime to stop looking for purchases.

  • offset – An offset used to paginate results.

  • limit – A limit used to paginate results.

Raise:

An appropriate exception if the request did not return MARC records.

Yield:

A list of MARC records.

now()[source]

Return the current GMT time in the format 3M expects.

patron_activity(patron, pin)[source]

Return a patron’s current checkouts and holds.

place_hold(patron, pin, licensepool, hold_notification_email=None)[source]

Place a hold.

Returns:

a HoldInfo object.

release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Raises:

CannotReleaseHold – If there is an error communicating with the provider, or the provider refuses to release the hold for any reason.

classmethod replacement_policy(_db, analytics=None)[source]
request(path, body=None, method='GET', identifier=None, max_age=None)[source]
sign(method, headers, path)[source]

Add appropriate headers to a request.

signature(method, path)[source]
property source
update_availability(licensepool)[source]

Update the availability information for a single LicensePool.

class api.bibliotheca.BibliothecaBibliographicCoverageProvider(collection, api_class=<class 'api.bibliotheca.BibliothecaAPI'>, **kwargs)[source]

Bases: BibliographicCoverageProvider

Fill in bibliographic metadata for Bibliotheca records.

This will occasionally fill in some availability information for a single Collection, but we rely on Monitors to keep availability information up to date for all Collections.

DATA_SOURCE_NAME = 'Bibliotheca'
DEFAULT_BATCH_SIZE = 25
INPUT_IDENTIFIER_TYPES = 'Bibliotheca ID'
PROTOCOL = 'Bibliotheca'
SERVICE_NAME = 'Bibliotheca Bibliographic Coverage Provider'
process_item(identifier)[source]

Do the work necessary to give coverage to one specific item.

Since this is where the actual work happens, this is not implemented in IdentifierCoverageProvider or WorkCoverageProvider, and must be handled in a subclass.

class api.bibliotheca.BibliothecaCirculationSweep(_db, collection, api_class=<class 'api.bibliotheca.BibliothecaAPI'>, **kwargs)[source]

Bases: IdentifierSweepMonitor

Check on the current circulation status of each Bibliotheca book in our collection.

In some cases this will lead to duplicate events being logged, because this monitor and the main Bibliotheca circulation monitor will count the same event. However it will greatly improve our current view of our Bibliotheca circulation, which is more important.

If Bibliotheca has updated its metadata for a book, that update will also take effect during the circulation sweep.

If a Bibliotheca license has expired, and we didn’t hear about it for whatever reason, we’ll find out about it here, because Bibliotheca will act like they never heard of it.

DEFAULT_BATCH_SIZE = 25
PROTOCOL = 'Bibliotheca'
SERVICE_NAME = 'Bibliotheca Circulation Sweep'
process_items(identifiers)[source]

Process a list of items.

class api.bibliotheca.BibliothecaEventMonitor(_db, collection, api_class=<class 'api.bibliotheca.BibliothecaAPI'>, analytics=None)[source]

Bases: BibliothecaTimelineMonitor

Register CirculationEvents for Bibliotheca titles.

When run, this monitor will look at recent events as a way of keeping the local collection up to date.

Although useful in everyday situations, the events endpoint will not always give you all the events:

  • Any given call to the events endpoint will return at most 100-150 events. If there is a particularly busy 5-minute stretch, events will be lost.

  • The Bibliotheca API has, in the past, gone into a state where this endpoint returns an empty list of events rather than an error message.

Fortunately, we have the BibliothecaPurchaseMonitor to keep track of new license purchases, and the BibliothecaCirculationSweep to keep up to date on books we already know about. If the BibliothecaEventMonitor stopped working completely, the rest of the system would continue to work, but circulation data would always be a few hours out of date.

Thus, running the BibliothecaEventMonitor alongside the other two Bibliotheca monitors ensures that circulation data is kept up to date in near-real-time with good, but not perfect, consistency.

SERVICE_NAME = 'Bibliotheca Event Monitor'
catch_up_from(start, cutoff, progress)[source]

Make sure all events between start and cutoff are covered.

Parameters:
  • start – Start looking for events that happened at this time.

  • cutoff – You’re not responsible for events that happened after this time.

  • progress – A TimestampData representing the progress so far. Unlike with run_once(), you are encouraged to can modify this in place, for instance to set .achievements. However, you cannot change .start and .finish – any changes will be overwritten by run_once().

handle_event(bibliotheca_id, isbn, foreign_patron_id, start_time, end_time, internal_event_type)[source]
exception api.bibliotheca.BibliothecaException[source]

Bases: Exception

class api.bibliotheca.BibliothecaParser[source]

Bases: XMLParser

INPUT_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
date_from_subtag(tag, key, required=True)[source]
parse_date(value)[source]

Parse the string Bibliotheca sends as a date.

Usually this is a string in INPUT_TIME_FORMAT, but it might be None.

class api.bibliotheca.BibliothecaPurchaseMonitor(_db, collection, api_class=<class 'api.bibliotheca.BibliothecaAPI'>, default_start=None, override_timestamp=False, analytics=None)[source]

Bases: BibliothecaTimelineMonitor

Track purchases of licenses from Bibliotheca.

Most TimelineMonitors monitor the timeline starting at whatever time they’re first run. But it’s crucial that this monitor start at or before the first day on which a book was added to this collection, even if that date was years in the past. That’s because this monitor may be the only time we hear about a particular book.

Because of this, this monitor has a very old DEFAULT_START_TIME and special capabilities for customizing the start_time to go back even further.

DEFAULT_START_TIME = datetime.datetime(2014, 1, 1, 0, 0, tzinfo=<UTC>)
SERVICE_NAME = 'Bibliotheca Purchase Monitor'
catch_up_from(start, cutoff, progress)[source]

Ask the Bibliotheca API about new purchases for every day between start and cutoff.

Parameters:
  • start (datetime.datetime) – The first day to ask about.

  • cutoff (datetime.datetime) – The last day to ask about.

  • progress (core.metadata_layer.TimestampData) – Object used to record progress through the timeline.

process_record(record, purchase_time)[source]

Record the purchase of a new title.

Parameters:
  • record (pymarc.Record) – Bibliographic information about the new title.

  • purchase_time – Put down this time as the time the purchase happened.

Returns:

A LicensePool representing the new title.

Return type:

core.model.LicensePool

purchases(start, end)[source]

Ask Bibliotheca for a MARC record for each book purchased between start and end.

Yield:

A sequence of pymarc Record objects

timestamp()[source]

Find or create a Timestamp for this Monitor.

If we are overriding the normal start time with one supplied when the this class was instantiated, we do that here. The instance’s default_start_time will have been set to the specified datetime and setting`timestamp.finish` to None will cause the default to be used.

class api.bibliotheca.BibliothecaTimelineMonitor(_db, collection, api_class=<class 'api.bibliotheca.BibliothecaAPI'>, analytics=None)[source]

Bases: CollectionMonitor, TimelineMonitor

Common superclass for our two TimelineMonitors.

LOG_DATE_FORMAT = '%Y-%m-%dT%H:%M:%S'
PROTOCOL = 'Bibliotheca'
class api.bibliotheca.CheckoutResponseParser[source]

Bases: DateResponseParser

Extract due date from a checkout response.

DATE_TAG_NAME = 'DueDateInUTC'
RESULT_TAG_NAME = 'CheckoutResult'
class api.bibliotheca.DateResponseParser[source]

Bases: BibliothecaParser

Extract a date from a response.

DATE_TAG_NAME = None
RESULT_TAG_NAME = None
process_all(string)[source]
class api.bibliotheca.DummyBibliothecaAPIResponse(response_code, headers, content)[source]

Bases: object

class api.bibliotheca.ErrorParser[source]

Bases: BibliothecaParser

Turn an error document from the Bibliotheca web service into a CheckoutException

error_mapping = {'The patron does not have the book on hold': <class 'api.circulation_exceptions.NotOnHold'>, 'The patron has no eBooks checked out': <class 'api.circulation_exceptions.NotCheckedOut'>}
hold_limit_reached = re.compile('Patron cannot have more than [0-9]+ hold')
loan_limit_reached = re.compile('Patron cannot loan more than [0-9]+ document')
process_all(string)[source]
process_one(error_tag, namespaces)[source]
wrong_status = re.compile('the patron document status was ([^ ]+) and not one of ([^ ]+)')
class api.bibliotheca.EventParser[source]

Bases: BibliothecaParser

Parse Bibliotheca’s event file format into our native event objects.

EVENT_NAMES = {'CHECKIN': 'distributor_check_in', 'CHECKOUT': 'distributor_check_out', 'HOLD': 'distributor_hold_place', 'PURCHASE': 'distributor_license_add', 'REMOVED': 'distributor_license_remove', 'RESERVED': 'distributor_availability_notify'}
EVENT_SOURCE = 'Bibliotheca'
SET_DELIVERY_MECHANISM_AT = 'borrow'
process_all(string, no_events_error=False)[source]
process_one(tag, namespaces)[source]
class api.bibliotheca.HoldResponseParser[source]

Bases: DateResponseParser

Extract availability date from a hold response.

DATE_TAG_NAME = 'AvailabilityDateInUTC'
RESULT_TAG_NAME = 'PlaceHoldResult'
class api.bibliotheca.ItemListParser[source]

Bases: XMLParser

DATE_FORMAT = '%Y-%m-%d'
NAMESPACES = {}
YEAR_FORMAT = '%Y'
classmethod contributors_from_string(string, role='Author')[source]
format_data_for_bibliotheca_format = {'EPUB': ('application/epub+zip', 'application/vnd.adobe.adept+xml'), 'EPUB3': ('application/epub+zip', 'application/vnd.adobe.adept+xml'), 'MP3': (None, 'application/vnd.librarysimplified.findaway.license+json'), 'PDF': ('application/pdf', 'application/vnd.adobe.adept+xml')}
classmethod internal_formats(book_format)[source]

Convert the term Bibliotheca uses to refer to a book format into a (medium [formats]) 2-tuple.

parenthetical = re.compile(' \\([^)]+\\)$')
parse(xml)[source]
classmethod parse_genre_string(s)[source]
process_one(tag, namespaces)[source]

Turn an <item> tag into a Metadata and an encompassed CirculationData objects, and return the Metadata.

class api.bibliotheca.MockBibliothecaAPI(_db, collection, *args, **kwargs)[source]

Bases: BibliothecaAPI

classmethod mock_collection(_db, name='Test Bibliotheca Collection')[source]

Create a mock Bibliotheca collection for use in tests.

now()[source]

Return an unvarying time in the format Bibliotheca expects.

queue_response(status_code, headers={}, content=None)[source]
class api.bibliotheca.PatronCirculationParser(collection, *args, **kwargs)[source]

Bases: BibliothecaParser

Parse Bibliotheca’s patron circulation status document into a list of LoanInfo and HoldInfo objects.

id_type = 'Bibliotheca ID'
process_all(string)[source]
process_one(tag, namespaces, source_class)[source]
process_one_hold(tag, namespaces)[source]
process_one_loan(tag, namespaces)[source]
process_one_reserve(tag, namespaces)[source]
class api.bibliotheca.RunBibliothecaPurchaseMonitorScript(monitor_class, _db=None, cmd_args=None, **kwargs)[source]

Bases: RunCollectionMonitorScript

Adds the ability to specify a particular start date for the BibliothecaPurchaseMonitor. This is important because for a given collection, the start date needs to be before books started being licensed into that collection.

classmethod arg_parser()[source]
classmethod parse_command_line(_db=None, cmd_args=None, *args, **kwargs)[source]
exception api.bibliotheca.WorkflowException(actual_status, statuses_that_would_work)[source]

Bases: BibliothecaException

api.circulation module

class api.circulation.APIAwareFulfillmentInfo(api, data_source_name, identifier_type, identifier, key)[source]

Bases: FulfillmentInfo

This that acts like FulfillmentInfo but is prepared to make an API request on demand to get data, rather than having all the data ready right now.

This class is useful in situations where generating a full FulfillmentInfo object would be costly. We only want to incur that cost when the patron wants to fulfill this title and is not just looking at their loans.

property content
property content_expires
property content_type
do_fetch()[source]

Actually make the API request.

When implemented, this method must set values for some or all of _content_link, _content_type, _content, and _content_expires.

fetch()[source]

It’s time to tell the API that we want to fulfill this book.

class api.circulation.BaseCirculationAPI[source]

Bases: object

Encapsulates logic common to all circulation APIs.

AUDIOBOOK_LOAN_DURATION_SETTING = {'default': 21, 'description': l'When a patron uses SimplyE to borrow an audiobook from this collection, SimplyE will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.', 'key': 'audio_loan_duration', 'label': l'Audiobook Loan Duration (in Days)', 'type': 'number'}
BORROW_STEP = 'borrow'
CAN_REVOKE_HOLD_WHEN_RESERVED = True
DEFAULT_LOAN_DURATION_SETTING = {'default': 21, 'description': l'Until it hears otherwise from the distributor, this server will assume that any given loan for this library from this collection will last this number of days. This number is usually a negotiated value between the library and the distributor. This only affects estimates&mdash;it cannot affect the actual length of loans.', 'key': 'ebook_loan_duration', 'label': l'Default Loan Period (in Days)', 'type': 'number'}
EBOOK_LOAN_DURATION_SETTING = {'default': 21, 'description': l'When a patron uses SimplyE to borrow an ebook from this collection, SimplyE will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.', 'key': 'ebook_loan_duration', 'label': l'Ebook Loan Duration (in Days)', 'type': 'number'}
FULFILL_STEP = 'fulfill'
LIBRARY_SETTINGS = []
SETTINGS = []
SET_DELIVERY_MECHANISM_AT = 'fulfill'
can_fulfill_without_loan(patron, pool, lpdm)[source]

In general, you can’t fulfill a book without a loan.

checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

checkout(patron, pin, licensepool, internal_format)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

classmethod default_notification_email_address(library_or_patron, pin)[source]

What email address should be used to notify this library’s patrons of changes?

Parameters:

library_or_patron – A Library or a Patron.

delivery_mechanism_to_internal_format = {}
fulfill(patron, pin, licensepool, internal_format=None, part=None, fulfill_part_url=None)[source]

Get the actual resource file to the patron.

Implementations are encouraged to define **kwargs as a container for vendor-specific arguments, so that they don’t have to change as new arguments are added.

Parameters:
  • internal_format – A vendor-specific name indicating the format requested by the patron.

  • part – A vendor-specific identifier indicating that the patron wants to fulfill one specific part of the book (e.g. one chapter of an audiobook), not the whole thing.

  • fulfill_part_url – A function that takes one argument (a vendor-specific part identifier) and returns the URL to use when fulfilling that part.

Returns:

a FulfillmentInfo object.

internal_format(delivery_mechanism)[source]

Look up the internal format for this delivery mechanism or raise an exception.

Parameters:

delivery_mechanism – A LicensePoolDeliveryMechanism

patron_activity(patron, pin)[source]

Return a patron’s current checkouts and holds.

patron_email_address(patron, library_authenticator=None)[source]

Look up the email address that the given Patron shared with their library.

We do not store this information, but some API integrations need it, so we give the ability to look it up as needed.

Parameters:

patron – A Patron.

Returns:

The patron’s email address. None if the patron never shared their email address with their library, or if the authentication technique will not share that information with us.

place_hold(patron, pin, licensepool, notification_email_address)[source]

Place a book on hold.

Returns:

A HoldInfo object

release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Raises:

CannotReleaseHold – If there is an error communicating with the provider, or the provider refuses to release the hold for any reason.

update_availability(licensepool)[source]

Update availability information for a book.

class api.circulation.CirculationAPI(_db, library, analytics=None, api_map=None)[source]

Bases: object

Implement basic circulation logic and abstract away the details between different circulation APIs behind generic operations like ‘borrow’.

api_for_license_pool(licensepool)[source]

Find the API to use for the given license pool.

borrow(patron, pin, licensepool, delivery_mechanism, hold_notification_email=None)[source]

Either borrow a book or put it on hold. Don’t worry about fulfilling the loan yet.

Returns:

A 3-tuple (Loan, Hold, is_new). Either Loan or Hold must be None, but not both.

can_fulfill_without_loan(patron, pool, lpdm)[source]

Can we deliver the given book in the given format to the given patron, even though the patron has no active loan for that book?

In general this is not possible, but there are some exceptions, managed in subclasses of BaseCirculationAPI.

Parameters:
  • patron – A Patron. This is probably None, indicating that someone is trying to fulfill a book without identifying themselves.

  • delivery_mechanism – The LicensePoolDeliveryMechanism representing a format for a specific title.

can_revoke_hold(licensepool, hold)[source]

Some circulation providers allow you to cancel a hold when the book is reserved to you. Others only allow you to cancel a hold while you’re in the hold queue.

property default_api_map

When you see a Collection that implements protocol X, instantiate API class Y to handle that collection.

enforce_limits(patron, pool)[source]

Enforce library-specific patron loan and hold limits.

Parameters:
  • patron – A Patron.

  • pool – A LicensePool the patron is trying to access. As a side effect, this method may update pool with the latest availability information from the remote API.

Raises:
fulfill(patron, pin, licensepool, delivery_mechanism, part=None, fulfill_part_url=None, sync_on_failure=True)[source]

Fulfil a book that a patron has previously checked out.

Parameters:
  • delivery_mechanism – A LicensePoolDeliveryMechanism explaining how the patron wants the book to be delivered. If the book has previously been delivered through some other mechanism, this parameter is ignored and the previously used mechanism takes precedence.

  • part – A vendor-specific identifier indicating that the patron wants to fulfill one specific part of the book (e.g. one chapter of an audiobook), not the whole thing.

  • fulfill_part_url – A function that takes one argument (a vendor-specific part identifier) and returns the URL to use when fulfilling that part.

Returns:

A FulfillmentInfo object.

fulfill_open_access(licensepool, delivery_mechanism)[source]

Fulfill an open-access LicensePool through the requested DeliveryMechanism.

Parameters:
  • licensepool – The title to be fulfilled.

  • delivery_mechanism – A DeliveryMechanism.

property library
local_holds(patron)[source]
local_loans(patron)[source]
patron_activity(patron, pin)[source]

Return a record of the patron’s current activity vis-a-vis all relevant external loan sources.

We check each source in a separate thread for speed.

Returns:

A 2-tuple (loans, holds) containing HoldInfo and LoanInfo objects.

patron_at_hold_limit(patron)[source]

Is the given patron at their hold limit?

This doesn’t belong in Patron because the hold limit is not core functionality. Of course, Patron itself isn’t really core functionality…

Parameters:

patron – A Patron.

patron_at_loan_limit(patron)[source]

Is the given patron at their loan limit?

This doesn’t belong in Patron because the loan limit is not core functionality. Of course, Patron itself isn’t really core functionality…

Parameters:

patron – A Patron.

release_hold(patron, pin, licensepool)[source]

Remove a patron’s hold on a book.

revoke_loan(patron, pin, licensepool)[source]

Revoke a patron’s loan for a book.

sync_bookshelf(patron, pin, force=False)[source]

Sync our internal model of a patron’s bookshelf with any external vendors that provide books to the patron’s library.

Parameters:
  • patron – A Patron.

  • pin – The password authenticating the patron; used by some vendors that perform a cross-check against the library ILS.

  • force – If this is True, the method will call out to external vendors even if it looks like the system has up-to-date information about the patron.

class api.circulation.CirculationInfo(collection, data_source_name, identifier_type, identifier)[source]

Bases: object

collection(_db)[source]

Find the Collection to which this object belongs.

fd(d)[source]
license_pool(_db)[source]

Find the LicensePool model object corresponding to this object.

class api.circulation.DeliveryMechanismInfo(content_type, drm_scheme, rights_uri='http://librarysimplified.org/terms/rights-status/in-copyright', resource=None)[source]

Bases: CirculationInfo

A record of a technique that must be (but is not, currently, being) used to fulfill a certain loan.

Although this class is similar to FormatInfo in core/metadata.py, usage here is strictly limited to recording which LicensePoolDeliveryMechanism a specific loan is currently locked to.

If, in the course of investigating a patron’s loans, you discover general facts about a LicensePool’s availability or formats, that information needs to be stored in a CirculationData and applied to the LicensePool separately.

apply(loan, autocommit=True)[source]

Set an appropriate LicensePoolDeliveryMechanism on the given Loan, creating a DeliveryMechanism if necessary.

Parameters:
  • loan – A Loan object.

  • autocommit – Set this to false if you are in the middle of a nested transaction.

Returns:

A LicensePoolDeliveryMechanism if one could be set on the given Loan; None otherwise.

class api.circulation.FulfillmentInfo(collection, data_source_name, identifier_type, identifier, content_link, content_type, content, content_expires)[source]

Bases: CirculationInfo

A record of a technique that can be used right now to fulfill a loan.

property as_response

Bypass the normal process of creating a Flask Response.

Returns:

A Response object, or None if you’re okay with the normal process.

can_cache_manifest = False
class api.circulation.HoldInfo(collection, data_source_name, identifier_type, identifier, start_date, end_date, hold_position, external_identifier=None)[source]

Bases: CirculationInfo

A record of a hold.

Parameters:
  • identifier_type – Ex. Identifier.RBDIGITAL_ID.

  • identifier – Expected to be the unicode string of the isbn, etc.

  • start_date – When the patron made the reservation.

  • end_date – When reserved book is expected to become available. Expected to be passed in date, not unicode format.

  • hold_position – Patron’s place in the hold line. When not available, default to be passed is None, which is equivalent to “first in line”.

class api.circulation.LoanInfo(collection, data_source_name, identifier_type, identifier, start_date, end_date, fulfillment_info=None, external_identifier=None, locked_to=None)[source]

Bases: CirculationInfo

A record of a loan.

api.circulation_exceptions module

exception api.circulation_exceptions.AlreadyCheckedOut(message=None, debug_info=None)[source]

Bases: CannotLoan

The patron can’t put check this book out because they already have it checked out.

status_code = 400
exception api.circulation_exceptions.AlreadyOnHold(message=None, debug_info=None)[source]

Bases: CannotHold

The patron can’t put this book on hold because they already have it on hold.

status_code = 400
exception api.circulation_exceptions.AuthorizationBlocked(message=None, debug_info=None)[source]

Bases: CannotLoan

The patron’s authorization is blocked for some reason other than fines or an expired card.

For instance, the patron has been banned from the library.

as_problem_detail_document(debug=False)[source]

Return a suitable problem detail document.

status_code = 403
exception api.circulation_exceptions.AuthorizationExpired(message=None, debug_info=None)[source]

Bases: CannotLoan

The patron’s authorization has expired.

as_problem_detail_document(debug=False)[source]

Return a suitable problem detail document.

status_code = 403
exception api.circulation_exceptions.AuthorizationFailedException(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 401
exception api.circulation_exceptions.CannotFulfill(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 500
exception api.circulation_exceptions.CannotHold(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 500
exception api.circulation_exceptions.CannotLoan(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 500
exception api.circulation_exceptions.CannotPartiallyFulfill(message=None, debug_info=None)[source]

Bases: CannotFulfill

status_code = 400
exception api.circulation_exceptions.CannotReleaseHold(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 500
exception api.circulation_exceptions.CannotRenew(message=None, debug_info=None)[source]

Bases: CirculationException

The patron can’t renew their loan on this book.

Probably because it’s not available for renewal.

status_code = 400
exception api.circulation_exceptions.CannotReturn(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 500
exception api.circulation_exceptions.CirculationException(message=None, debug_info=None)[source]

Bases: IntegrationException

An exception occured when carrying out a circulation operation.

status_code is the status code that should be returned to the patron.

status_code = 400
exception api.circulation_exceptions.CurrentlyAvailable(message=None, debug_info=None)[source]

Bases: CannotHold

The patron can’t put this book on hold because it’s available now.

status_code = 400
exception api.circulation_exceptions.DeliveryMechanismConflict(message=None, debug_info=None)[source]

Bases: DeliveryMechanismError

The patron specified a delivery mechanism that conflicted with one already set in stone.

exception api.circulation_exceptions.DeliveryMechanismError(message=None, debug_info=None)[source]

Bases: InvalidInputException

status_code = 400

The patron broke the rules about delivery mechanisms.

exception api.circulation_exceptions.DeliveryMechanismMissing(message=None, debug_info=None)[source]

Bases: DeliveryMechanismError

The patron needed to specify a delivery mechanism and didn’t.

exception api.circulation_exceptions.FormatNotAvailable(message=None, debug_info=None)[source]

Bases: CannotFulfill

Our format information for this book was outdated, and it’s no longer available in the requested format.

status_code = 502
exception api.circulation_exceptions.FulfilledOnIncompatiblePlatform(message=None, debug_info=None)[source]

Bases: CannotFulfill

We can’t fulfill the patron’s loan because the loan was already fulfilled on an incompatible platform (i.e. Kindle) in a way that’s exclusive to that platform.

status_code = 451
exception api.circulation_exceptions.InternalServerError(message, debug_message=None)[source]

Bases: IntegrationException

as_problem_detail_document(debug=False)[source]

Return a suitable problem detail document.

status_code = 500
exception api.circulation_exceptions.InvalidInputException(message=None, debug_info=None)[source]

Bases: CirculationException

The patron gave invalid input to the library.

status_code = 400
exception api.circulation_exceptions.LibraryAuthorizationFailedException(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 500
exception api.circulation_exceptions.LibraryInvalidInputException(message=None, debug_info=None)[source]

Bases: InvalidInputException

The library gave invalid input to the book provider.

status_code = 500
exception api.circulation_exceptions.LimitReached(message=None, debug_info=None, library=None)[source]

Bases: CirculationException

The patron cannot carry out an operation because it would push them above some limit set by library policy.

This exception cannot be used on its own. It must be subclassed and the following constants defined:
  • BASE_DOC: A ProblemDetail, used as the basis for conversion of this exception into a

    problem detail document.

  • SETTING_NAME: Then name of the library-specific ConfigurationSetting whose numeric value is the limit that cannot be exceeded.

  • MESSAGE_WITH_LIMIT A string containing the interpolation value “%(limit)s”, which offers a more specific explanation of the limit exceeded.

BASE_DOC = None
MESSAGE_WITH_LIMIT = None
SETTING_NAME = None
as_problem_detail_document(debug=False)[source]

Return a suitable problem detail document.

status_code = 403
exception api.circulation_exceptions.NoAcceptableFormat(message=None, debug_info=None)[source]

Bases: CannotFulfill

We can’t fulfill the patron’s loan because the book is not available in an acceptable format.

status_code = 400
exception api.circulation_exceptions.NoActiveLoan(message=None, debug_info=None)[source]

Bases: CannotFulfill

We can’t fulfill the patron’s loan because they don’t have an active loan.

status_code = 400
exception api.circulation_exceptions.NoAvailableCopies(message=None, debug_info=None)[source]

Bases: CannotLoan

The patron can’t check this book out because all available copies are already checked out.

status_code = 400
exception api.circulation_exceptions.NoLicenses(message=None, debug_info=None)[source]

Bases: NotFoundOnRemote

The library no longer has licenses for this book.

as_problem_detail_document(debug=False)[source]

Return a suitable problem detail document.

exception api.circulation_exceptions.NoOpenAccessDownload(message=None, debug_info=None)[source]

Bases: CirculationException

We expected a book to have an open-access download, but it didn’t.

status_code = 500
exception api.circulation_exceptions.NotCheckedOut(message=None, debug_info=None)[source]

Bases: CannotReturn

The patron can’t return this book because they don’t have it checked out in the first place.

status_code = 400
exception api.circulation_exceptions.NotFoundOnRemote(message=None, debug_info=None)[source]

Bases: CirculationException

We know about this book but the remote site doesn’t seem to.

status_code = 404
exception api.circulation_exceptions.NotOnHold(message=None, debug_info=None)[source]

Bases: CannotReleaseHold

The patron can’t release a hold for this book because they don’t have it on hold in the first place.

status_code = 400
exception api.circulation_exceptions.OutstandingFines(message=None, debug_info=None)[source]

Bases: CannotLoan

The patron has outstanding fines above the limit in the library’s policy.

status_code = 403
exception api.circulation_exceptions.PatronAuthorizationFailedException(message=None, debug_info=None)[source]

Bases: AuthorizationFailedException

status_code = 400
exception api.circulation_exceptions.PatronHoldLimitReached(message=None, debug_info=None, library=None)[source]

Bases: CannotHold, LimitReached

BASE_DOC = <ProblemDetail(uri=http://librarysimplified.org/terms/problem/hold-limit-reached, title=Limit reached., status_code=403, detail=You have reached your hold limit. You cannot place another item on hold until you borrow something or remove a hold., instance=None, debug_message=None
MESSAGE_WITH_LIMIT = l'You have reached your hold limit of %(limit)d. You cannot place another item on hold until you borrow something or remove a hold.'
SETTING_NAME = 'hold_limit'
exception api.circulation_exceptions.PatronLoanLimitReached(message=None, debug_info=None, library=None)[source]

Bases: CannotLoan, LimitReached

BASE_DOC = <ProblemDetail(uri=http://librarysimplified.org/terms/problem/loan-limit-reached, title=Loan limit reached., status_code=403, detail=You have reached your loan limit. You cannot borrow anything further until you return something., instance=None, debug_message=None
MESSAGE_WITH_LIMIT = l'You have reached your loan limit of %(limit)d. You cannot borrow anything further until you return something.'
SETTING_NAME = 'loan_limit'
exception api.circulation_exceptions.PatronNotFoundOnRemote(message=None, debug_info=None)[source]

Bases: NotFoundOnRemote

status_code = 404
exception api.circulation_exceptions.RemoteInitiatedServerError(message, service_name)[source]

Bases: InternalServerError

One of the servers we communicate with had an internal error.

as_problem_detail_document(debug=False)[source]

Return a suitable problem detail document.

status_code = 502
exception api.circulation_exceptions.RemotePatronCreationFailedException(message=None, debug_info=None)[source]

Bases: CirculationException

status_code = 500
exception api.circulation_exceptions.RemoteRefusedReturn(message=None, debug_info=None)[source]

Bases: CannotReturn

The remote refused to count this book as returned.

status_code = 500

api.config module

class api.config.Configuration[source]

Bases: Configuration

ABOUT = 'about'
ADMIN_WEB_HOSTNAMES = 'admin_web_hostnames'
AREA_INPUT_INSTRUCTIONS = l'<ol>Accepted formats:             <li>US zipcode or Canadian FSA</li>             <li>Two-letter US state abbreviation</li>             <li>City, US state abbreviation<i>e.g. "Boston, MA"</i></li>             <li>County, US state abbreviation<i>e.g. "Litchfield County, CT"</i></li>             <li>Canadian province name or two-letter abbreviation</li>             <li>City, Canadian province name/abbreviation<i>e.g. "Stratford, Ontario"/"Stratford, ON"</i></li>         </ol>'
AUTHENTICATION_DOCUMENT_CACHE_TIME = 'authentication_document_cache_time'
BEARER_TOKEN_SIGNING_SECRET = 'bearer_token_signing_secret'
COLOR_SCHEME = 'color_scheme'
CONFIGURATION_CONTACT_EMAIL = 'configuration_contact_email_address'
COPYRIGHT = 'copyright'
COPYRIGHT_DESIGNATED_AGENT_EMAIL = 'copyright_designated_agent_email_address'
COPYRIGHT_DESIGNATED_AGENT_REL = 'http://librarysimplified.org/rel/designated-agent/copyright'
CUSTOM_TOS_HREF = 'tos_href'
CUSTOM_TOS_TEXT = 'tos_text'
DEFAULT_COLOR_SCHEME = 'blue'
DEFAULT_NOTIFICATION_EMAIL_ADDRESS = 'default_notification_email_address'
DEFAULT_OPDS_FORMAT = 'simple_opds_entry'
DEFAULT_TOS_HREF = 'https://librarysimplified.org/simplyetermsofservice2/'
DEFAULT_TOS_TEXT = "Terms of Service for presenting e-reading materials through NYPL's SimplyE mobile app"
DEFAULT_WEB_PRIMARY_COLOR = '#377F8B'
DEFAULT_WEB_SECONDARY_COLOR = '#D53F34'
HELP_EMAIL = 'help-email'
HELP_UNSUBSCRIBE_URI = 'http://librarysimplified.org/rel/email/unsubscribe/options'
HELP_URI = 'help-uri'
HELP_WEB = 'help-web'
HIDDEN_CONTENT_TYPES = 'hidden_content_types'
HOLD_LIMIT = 'hold_limit'
KEY_PAIR = 'key-pair'
LANGUAGE_DESCRIPTION = l'Each value can be either the full name of a language or an <a href="https://www.loc.gov/standards/iso639-2/php/code_list.php" target="_blank">ISO-639-2</a> language code.'
LARGE_COLLECTION_CUTOFF = 10000
LARGE_COLLECTION_LANGUAGES = 'large_collections'
LENDING_POLICY = 'lending'
LIBRARY_DESCRIPTION = 'library_description'
LIBRARY_FOCUS_AREA = 'focus_area'
LIBRARY_SERVICE_AREA = 'service_area'
LIBRARY_SETTINGS = [{'key': 'name', 'label': l'Name', 'description': l'The human-readable name of this library.', 'category': 'Basic Information', 'level': 3, 'required': True}, {'key': 'short_name', 'label': l'Short name', 'description': l'A short name of this library, to use when identifying it in scripts or URLs, e.g. 'NYPL'.', 'category': 'Basic Information', 'level': 3, 'required': True}, {'key': 'website', 'label': l'URL of the library's website', 'description': l'The library's main website, e.g. "https://www.nypl.org/" (not this Circulation Manager's URL).', 'required': True, 'format': 'url', 'level': 3, 'category': 'Basic Information'}, {'key': 'allow_holds', 'label': l'Allow books to be put on hold', 'type': 'select', 'options': [{'key': 'true', 'label': l'Allow holds'}, {'key': 'false', 'label': l'Disable holds'}], 'default': 'true', 'category': 'Loans, Holds, & Fines', 'level': 3}, {'key': 'enabled_entry_points', 'label': l'Enabled entry points', 'description': l'Patrons will see the selected entry points at the top level and in search results. <p>Currently supported audiobook vendors: Bibliotheca, Axis 360', 'type': 'list', 'options': [{'key': 'All', 'label': 'All'}, {'key': 'Book', 'label': 'eBooks'}, {'key': 'Audio', 'label': 'Audiobooks'}], 'default': ['Book'], 'category': 'Lanes & Filters', 'format': 'narrow', 'readOnly': True, 'level': 3}, {'key': 'featured_lane_size', 'label': l'Maximum number of books in the 'featured' lanes', 'type': 'number', 'default': 15, 'category': 'Lanes & Filters', 'level': 1}, {'key': 'minimum_featured_quality', 'label': l'Minimum quality for books that show up in 'featured' lanes', 'description': l'Between 0 and 1.', 'type': 'number', 'max': 1, 'default': 0.65, 'category': 'Lanes & Filters', 'level': 1}, {'key': 'facets_enabled_order', 'label': l'Allow patrons to sort by', 'type': 'list', 'options': [{'key': 'title', 'label': l'Title'}, {'key': 'author', 'label': l'Author'}, {'key': 'added', 'label': l'Recently Added'}, {'key': 'random', 'label': l'Random'}, {'key': 'relevance', 'label': l'Relevance'}], 'default': ['title', 'author', 'added', 'random', 'relevance'], 'category': 'Lanes & Filters', 'paired': 'facets_default_order', 'level': 2}, {'key': 'facets_enabled_available', 'label': l'Allow patrons to filter availability to', 'type': 'list', 'options': [{'key': 'now', 'label': l'Available now'}, {'key': 'all', 'label': l'All'}, {'key': 'always', 'label': l'Yours to keep'}], 'default': ['now', 'all', 'always'], 'category': 'Lanes & Filters', 'paired': 'facets_default_available', 'level': 2}, {'key': 'facets_enabled_collection', 'label': l'Allow patrons to filter collection to', 'type': 'list', 'options': [{'key': 'full', 'label': l'Everything'}, {'key': 'featured', 'label': l'Popular Books'}], 'default': ['full', 'featured'], 'category': 'Lanes & Filters', 'paired': 'facets_default_collection', 'level': 2}, {'key': 'facets_default_order', 'label': l'Default Sort by', 'type': 'select', 'options': [{'key': 'title', 'label': l'Title'}, {'key': 'author', 'label': l'Author'}, {'key': 'added', 'label': l'Recently Added'}, {'key': 'random', 'label': l'Random'}, {'key': 'relevance', 'label': l'Relevance'}], 'default': 'author', 'category': 'Lanes & Filters', 'skip': True}, {'key': 'facets_default_available', 'label': l'Default Availability', 'type': 'select', 'options': [{'key': 'now', 'label': l'Available now'}, {'key': 'all', 'label': l'All'}, {'key': 'always', 'label': l'Yours to keep'}], 'default': 'all', 'category': 'Lanes & Filters', 'skip': True}, {'key': 'facets_default_collection', 'label': l'Default Collection', 'type': 'select', 'options': [{'key': 'full', 'label': l'Everything'}, {'key': 'featured', 'label': l'Popular Books'}], 'default': 'full', 'category': 'Lanes & Filters', 'skip': True}, {'key': 'library_description', 'label': l'A short description of this library', 'description': l'This will be shown to people who aren't sure they've chosen the right library.', 'category': 'Basic Information', 'level': 3}, {'key': 'announcements', 'label': l'Scheduled announcements', 'description': l'Announcements will be displayed to authenticated patrons.', 'category': 'Announcements', 'type': 'announcements', 'level': 1}, {'key': 'help-email', 'label': l'Patron support email address', 'description': l'An email address a patron can use if they need help, e.g. 'simplyehelp@yourlibrary.org'.', 'required': True, 'format': 'email', 'level': 3}, {'key': 'help-web', 'label': l'Patron support web site', 'description': l'A URL for patrons to get help.', 'format': 'url', 'category': 'Patron Support', 'level': 1}, {'key': 'help-uri', 'label': l'Patron support custom integration URI', 'description': l'A custom help integration like Helpstack, e.g. 'helpstack:nypl.desk.com'.', 'category': 'Patron Support', 'level': 3}, {'key': 'http://librarysimplified.org/rel/email/unsubscribe/options', 'label': l'Email (Un)Subscription Management URL', 'description': l'A URL for patrons to manage (or delete) any email subscriptions associated with their account.', 'format': 'url', 'category': 'Patron Support', 'level': 2}, {'key': 'copyright_designated_agent_email_address', 'label': l'Copyright designated agent email', 'description': l'Patrons of this library should use this email address to send a DMCA notification (or other copyright complaint) to the library.<br/>If no value is specified here, the general patron support address will be used.', 'format': 'email', 'category': 'Patron Support', 'level': 2}, {'key': 'configuration_contact_email_address', 'label': l'A point of contact for the organization reponsible for configuring this library', 'description': l'This email address will be shared as part of integrations that you set up through this interface. It will not be shared with the general public. This gives the administrator of the remote integration a way to contact you about problems with this library's use of that integration.<br/>If no value is specified here, the general patron support address will be used.', 'format': 'email', 'category': 'Patron Support', 'level': 2}, {'key': 'default_notification_email_address', 'label': l'Write-only email address for vendor hold notifications', 'description': l'This address must trash all email sent to it. Vendor hold notifications contain sensitive patron information, but <a href="https://confluence.nypl.org/display/SIM/About+Hold+Notifications" target="_blank">cannot be forwarded to patrons</a> because they contain vendor-specific instructions.<br/>The default address will work, but for greater security, set up your own address that trashes all incoming email.', 'default': 'noreply@librarysimplified.org', 'required': True, 'format': 'email', 'level': 3}, {'key': 'color_scheme', 'label': l'Mobile color scheme', 'description': l'This tells mobile applications what color scheme to use when rendering this library's OPDS feed.', 'options': [{'key': 'amber', 'label': l'Amber'}, {'key': 'black', 'label': l'Black'}, {'key': 'blue', 'label': l'Blue'}, {'key': 'bluegray', 'label': l'Blue Gray'}, {'key': 'brown', 'label': l'Brown'}, {'key': 'cyan', 'label': l'Cyan'}, {'key': 'darkorange', 'label': l'Dark Orange'}, {'key': 'darkpurple', 'label': l'Dark Purple'}, {'key': 'green', 'label': l'Green'}, {'key': 'gray', 'label': l'Gray'}, {'key': 'indigo', 'label': l'Indigo'}, {'key': 'lightblue', 'label': l'Light Blue'}, {'key': 'orange', 'label': l'Orange'}, {'key': 'pink', 'label': l'Pink'}, {'key': 'purple', 'label': l'Purple'}, {'key': 'red', 'label': l'Red'}, {'key': 'teal', 'label': l'Teal'}], 'type': 'select', 'default': 'blue', 'category': 'Client Interface Customization', 'level': 2}, {'key': 'web-primary-color', 'label': l'Web primary color', 'description': l'This is the brand primary color for the web application. Must have sufficient contrast with white.', 'type': 'color-picker', 'default': '#377F8B', 'category': 'Client Interface Customization', 'level': 2}, {'key': 'web-secondary-color', 'label': l'Web secondary color', 'description': l'This is the brand secondary color for the web application. Must have sufficient contrast with white.', 'type': 'color-picker', 'default': '#D53F34', 'category': 'Client Interface Customization', 'level': 2}, {'key': 'web-css-file', 'label': l'Custom CSS file for web', 'description': l'Give web applications a CSS file to customize the catalog display.', 'format': 'url', 'category': 'Client Interface Customization', 'level': 3}, {'key': 'web-header-links', 'label': l'Web header links', 'description': l'This gives web applications a list of links to display in the header. Specify labels for each link in the same order under 'Web header labels'.', 'type': 'list', 'category': 'Client Interface Customization', 'level': 2}, {'key': 'web-header-labels', 'label': l'Web header labels', 'description': l'Labels for each link under 'Web header links'.', 'type': 'list', 'category': 'Client Interface Customization', 'level': 2}, {'key': 'logo', 'label': l'Logo image', 'type': 'image', 'description': l'The image must be in GIF, PNG, or JPG format, approximately square, no larger than 135x135 pixels, and look good on a white background.', 'category': 'Client Interface Customization', 'level': 1}, {'key': 'hidden_content_types', 'label': l'Hidden content types', 'type': 'text', 'description': l'A list of content types to hide from all clients, e.g. <code>["application/pdf"]</code>. This can be left blank except to solve specific problems.', 'category': 'Client Interface Customization', 'level': 3}, {'key': 'focus_area', 'label': l'Focus area', 'type': 'list', 'description': l'The library focuses on serving patrons in this geographic area. In most cases this will be a city name like <code>Springfield, OR</code>.', 'category': 'Geographic Areas', 'format': 'geographic', 'instructions': l'<ol>Accepted formats:             <li>US zipcode or Canadian FSA</li>             <li>Two-letter US state abbreviation</li>             <li>City, US state abbreviation<i>e.g. "Boston, MA"</i></li>             <li>County, US state abbreviation<i>e.g. "Litchfield County, CT"</i></li>             <li>Canadian province name or two-letter abbreviation</li>             <li>City, Canadian province name/abbreviation<i>e.g. "Stratford, Ontario"/"Stratford, ON"</i></li>         </ol>', 'capitalize': True, 'level': 1}, {'key': 'service_area', 'label': l'Service area', 'type': 'list', 'description': l'The full geographic area served by this library. In most cases this is the same as the focus area and can be left blank, but it may be a larger area such as a US state (which should be indicated by its abbreviation, like <code>OR</code>).', 'category': 'Geographic Areas', 'format': 'geographic', 'instructions': l'<ol>Accepted formats:             <li>US zipcode or Canadian FSA</li>             <li>Two-letter US state abbreviation</li>             <li>City, US state abbreviation<i>e.g. "Boston, MA"</i></li>             <li>County, US state abbreviation<i>e.g. "Litchfield County, CT"</i></li>             <li>Canadian province name or two-letter abbreviation</li>             <li>City, Canadian province name/abbreviation<i>e.g. "Stratford, Ontario"/"Stratford, ON"</i></li>         </ol>', 'capitalize': True, 'level': 1}, {'key': 'max_outstanding_fines', 'label': l'Maximum amount in fines a patron can have before losing lending privileges', 'type': 'number', 'category': 'Loans, Holds, & Fines', 'level': 1}, {'key': 'loan_limit', 'label': l'Maximum number of books a patron can have on loan at once', 'description': l'(Note: depending on distributor settings, a patron may be able to exceed the limit by checking out books directly from a distributor's app. They may also get a limit exceeded error before they reach these limits if a distributor has a smaller limit.)', 'type': 'number', 'category': 'Loans, Holds, & Fines', 'level': 1}, {'key': 'hold_limit', 'label': l'Maximum number of books a patron can have on hold at once', 'description': l'(Note: depending on distributor settings, a patron may be able to exceed the limit by checking out books directly from a distributor's app. They may also get a limit exceeded error before they reach these limits if a distributor has a smaller limit.)', 'type': 'number', 'category': 'Loans, Holds, & Fines', 'level': 1}, {'key': 'terms-of-service', 'label': l'Terms of Service URL', 'format': 'url', 'category': 'Links', 'level': 1}, {'key': 'privacy-policy', 'label': l'Privacy Policy URL', 'format': 'url', 'category': 'Links', 'level': 1}, {'key': 'copyright', 'label': l'Copyright URL', 'format': 'url', 'category': 'Links', 'level': 2}, {'key': 'about', 'label': l'About URL', 'format': 'url', 'category': 'Links', 'level': 1}, {'key': 'license', 'label': l'License URL', 'format': 'url', 'category': 'Links', 'level': 2}, {'key': 'register', 'label': l'Patron registration URL', 'description': l'A URL where someone who doesn't have a library card yet can sign up for one.', 'format': 'url', 'category': 'Patron Support', 'allowed': ['nypl.card-creator:https://patrons.librarysimplified.org/'], 'level': 1}, {'key': 'large_collections', 'label': l'The primary languages represented in this library's collection', 'type': 'list', 'format': 'language-code', 'description': l'Each value can be either the full name of a language or an <a href="https://www.loc.gov/standards/iso639-2/php/code_list.php" target="_blank">ISO-639-2</a> language code.', 'optional': True, 'category': 'Languages', 'level': 1}, {'key': 'small_collections', 'label': l'Other major languages represented in this library's collection', 'type': 'list', 'format': 'language-code', 'description': l'Each value can be either the full name of a language or an <a href="https://www.loc.gov/standards/iso639-2/php/code_list.php" target="_blank">ISO-639-2</a> language code.', 'optional': True, 'category': 'Languages', 'level': 1}, {'key': 'tiny_collections', 'label': l'Other languages in this library's collection', 'type': 'list', 'format': 'language-code', 'description': l'Each value can be either the full name of a language or an <a href="https://www.loc.gov/standards/iso639-2/php/code_list.php" target="_blank">ISO-639-2</a> language code.', 'optional': True, 'category': 'Languages', 'level': 1}]
LICENSE = 'license'
LOAN_LIMIT = 'loan_limit'
MAX_OUTSTANDING_FINES = 'max_outstanding_fines'
PATRON_WEB_HOSTNAMES = 'patron_web_hostnames'
PRIVACY_POLICY = 'privacy-policy'
REGISTER = 'register'
RESERVATIONS_FEATURE = 'https://librarysimplified.org/rel/policy/reservations'
SECRET_KEY = 'secret_key'
SITEWIDE_SETTINGS = [{'key': 'base_url', 'label': l'Base url of the application', 'required': True, 'format': 'url'}, {'key': 'log_level', 'label': l'Log Level', 'type': 'select', 'options': [{'key': 'DEBUG', 'label': l'Debug'}, {'key': 'INFO', 'label': l'Info'}, {'key': 'WARN', 'label': l'Warn'}, {'key': 'ERROR', 'label': l'Error'}], 'default': 'INFO'}, {'key': 'log_app', 'label': l'Application name', 'description': l'Log messages originating from this application will be tagged with this name. If you run multiple instances, giving each one a different application name will help you determine which instance is having problems.', 'default': 'simplified', 'required': True}, {'key': 'database_log_level', 'label': l'Database Log Level', 'type': 'select', 'options': [{'key': 'DEBUG', 'label': l'Debug'}, {'key': 'INFO', 'label': l'Info'}, {'key': 'WARN', 'label': l'Warn'}, {'key': 'ERROR', 'label': l'Error'}], 'description': l'Database logs are extremely verbose, so unless you're diagnosing a database-related problem, it's a good idea to set a higher log level for database messages.', 'default': 'WARN'}, {'key': 'excluded_audio_data_sources', 'label': l'Excluded audiobook sources', 'description': l'Audiobooks from these data sources will be hidden from the collection, even if they would otherwise show up as available.', 'default': None, 'required': True}, {'key': 'measurement_reaper_enabled', 'label': l'Cleanup old measurement data', 'type': 'select', 'description': l'If this settings is 'true' old book measurement data will be cleaned out of the database. Some sites may want to keep this data for later analysis.', 'options': {'true': 'true', 'false': 'false'}, 'default': 'true'}, {'key': 'bearer_token_signing_secret', 'label': l'Internal signing secret for OAuth and SAML bearer tokens', 'required': True}, {'key': 'secret_key', 'label': l'Internal secret key for admin interface cookies', 'required': True}, {'key': 'patron_web_hostnames', 'label': l'Hostnames for patron web application access', 'type': 'string', 'required': True, 'description': l'Only web applications from these hosts can access this circulation manager. You may set a value which includes a wildcard for subdomains, such as https://*.somedomain.com or https://*.somesub.somedomain.com. Note that such a wildcard will NOT match the root domain alone; that must be included separately.'}, {'key': 'admin_web_hostnames', 'label': l'Hostnames for admin web application access', 'required': True, 'type': 'string', 'description': l'Only admin web applications from these hosts can access this circulation manager. This can be a single hostname (http://catalog.library.org) or a pipe-separated list of hostnames (http://catalog.library.org|https://beta.library.org).  You may set a value which includes a wildcard for subdomains, such as https://*.somedomain.com or https://*.somesub.somedomain.com. Note that such a wildcard will NOT match the root domain alone; that must be included separately.'}, {'key': 'static_file_cache_time', 'label': l'Cache time for static images and JS and CSS files (in seconds)', 'required': True, 'type': 'number'}, {'key': 'authentication_document_cache_time', 'label': l'Cache time for authentication documents (in seconds)', 'required': True, 'type': 'number', 'default': 0}, {'key': 'tos_href', 'label': l'Custom Terms of Service link', 'required': False, 'default': 'https://librarysimplified.org/simplyetermsofservice2/', 'description': l'If your inclusion in the SimplyE mobile app is governed by terms other than the default, put the URL to those terms in this link so that librarians will have access to them. This URL will be used for all libraries on this circulation manager.'}, {'key': 'tos_text', 'label': l'Custom Terms of Service link text', 'required': False, 'default': "Terms of Service for presenting e-reading materials through NYPL's SimplyE mobile app", 'description': l'Custom text for the Terms of Service link in the footer of these administrative interface pages. This is primarily useful if you're not connecting this circulation manager to the SimplyE mobile app. This text will be used for all libraries on this circulation manager.'}]
SMALL_COLLECTION_CUTOFF = 500
SMALL_COLLECTION_LANGUAGES = 'small_collections'
STANDARD_NOREPLY_EMAIL_ADDRESS = 'noreply@librarysimplified.org'
STATIC_FILE_CACHE_TIME = 'static_file_cache_time'
TERMS_OF_SERVICE = 'terms-of-service'
TINY_COLLECTION_LANGUAGES = 'tiny_collections'
WEB_CSS_FILE = 'web-css-file'
WEB_HEADER_LABELS = 'web-header-labels'
WEB_PRIMARY_COLOR = 'web-primary-color'
WEB_SECONDARY_COLOR = 'web-secondary-color'
WSGI_DEBUG_KEY = 'wsgi_debug'
classmethod cipher(key)[source]

Create a Cipher for a public or private key.

This just wraps some hard-to-remember Crypto code.

Parameters:

key – A string containing the key.

Returns:

A Cipher object which will support either encrypt() (public key) or decrypt() (private key).

classmethod classify_holdings(works_by_language)[source]

Divide languages into ‘large’, ‘small’, and ‘tiny’ colletions based on the number of works available for each.

Parameters:

works_by_language – A Counter mapping languages to the number of active works available for that language. The output of Library.estimated_holdings_by_language is a good thing to pass in.

Returns:

a 3-tuple of lists (large, small, tiny).

classmethod configuration_contact_uri(library)[source]
classmethod copyright_designated_agent_uri(library)[source]
classmethod estimate_language_collections_for_library(library)[source]

Guess at appropriate values for the given library for LARGE_COLLECTION_LANGUAGES, SMALL_COLLECTION_LANGUAGES, and TINY_COLLECTION_LANGUAGES. Set configuration values appropriately, overriding any previous values.

classmethod help_uris(library)[source]

Find all the URIs that might help patrons get help from this library.

Yield:

A sequence of 2-tuples (media type, URL)

classmethod key_pair(setting)[source]

Look up a public-private key pair in a ConfigurationSetting.

If the value is missing or incorrect, a new key pair is created and stored.

TODO: This could go into ConfigurationSetting or core Configuration.

Parameters:
  • public_setting – A ConfigurationSetting for the public key.

  • private_setting – A ConfigurationSetting for the private key.

Returns:

A 2-tuple (public key, private key)

classmethod large_collection_languages(library)[source]
classmethod lending_policy()[source]
classmethod load(_db=None)[source]

Load configuration information from the filesystem, and (optionally) from the database.

classmethod max_outstanding_fines(library)[source]
classmethod small_collection_languages(library)[source]
classmethod tiny_collection_languages(library)[source]
classmethod unsubscribe_email_uri(library)[source]
api.config.empty_config()[source]
api.config.temp_config(new_config=None, replacement_classes=None)[source]

api.controller module

class api.controller.AnalyticsController(manager)[source]

Bases: CirculationManagerController

track_event(identifier_type, identifier, event_type)[source]
class api.controller.AnnotationController(manager)[source]

Bases: CirculationManagerController

container(identifier=None, accept_post=True)[source]
container_for_work(identifier_type, identifier)[source]
detail(annotation_id)[source]
class api.controller.CirculationManager(_db, testing=False)[source]

Bases: object

annotator(lane, facets=None, *args, **kwargs)[source]

Create an appropriate OPDS annotator for the given lane.

Parameters:
  • lane – A Lane or WorkList.

  • facets – A faceting object.

  • annotator_class – Instantiate this annotator class if possible. Intended for use in unit tests.

property authentication_for_opds_document

Make sure the current request’s library has an Authentication For OPDS document in the cache, then return the cached version.

If the cache is disabled, a fresh document is created every time.

If the query argument debug is provided and the WSGI_DEBUG_KEY site-wide setting is set to True, the authentication document is annotated with a ‘_debug’ section describing the current WSGI environment. Since this can reveal internal details of deployment, it should only be enabled when diagnosing deployment problems.

cdn_url_for(view, *args, **kwargs)[source]

Generate a URL for a view that (probably) passes through a CDN.

Parameters:
  • view – Name of the view.

  • _facets – The faceting object used to generate the document that’s calling this method. This may change which function is actually used to generate the URL; in particular, it may disable a CDN that would otherwise be used. This is called _facets just in case there’s ever a view that takes ‘facets’ as a real keyword argument.

  • args – Positional arguments to the view function.

  • kwargs – Keyword arguments to the view function.

Retrieve or create a connection to the search interface.

This is created lazily so that a failure to connect only affects feeds that depend on the search engine, not the whole circulation manager.

load_facets_from_request(*args, **kwargs)[source]

Load a faceting object from the incoming request, but also apply some application-specific access restrictions:

  • You can’t use nonstandard caching rules unless you’re an authenticated administrator.

  • You can’t access a WorkList that’s not accessible to you.

load_settings()[source]

Load all necessary configuration settings and external integrations from the database.

This is called once when the CirculationManager is initialized. It may also be called later to reload the site configuration after changes are made in the administrative interface.

log_lanes(lanelist=None, level=0)[source]

Output information about the lane layout.

property public_key_integration_document

Serve a document with the sitewide public key.

reload_settings_if_changed()[source]

If the site configuration has been updated, reload the CirculationManager’s configuration from the database.

setup_adobe_vendor_id(_db, library)[source]

If this Library has an Adobe Vendor ID integration, configure the controller for it.

Returns:

An Authdata object for library, if one could be created.

setup_circulation(library, analytics)[source]

Set up the Circulation object.

setup_configuration_dependent_controllers()[source]

Set up all the controllers that depend on the current site configuration.

This method will be called fresh every time the site configuration changes.

setup_one_time_controllers()[source]

Set up all the controllers that will be used by the web app.

This method will be called only once, no matter how many times the site configuration changes.

Set up a search client.

setup_shared_collection()[source]
property sitewide_key_pair

Look up or create the sitewide public/private key pair.

url_for(view, *args, **kwargs)[source]

Call the url_for function, ensuring that Flask generates an absolute URL.

class api.controller.CirculationManagerController(manager)[source]

Bases: BaseCirculationManagerController

apply_borrowing_policy(patron, license_pool)[source]

Apply the borrowing policy of the patron’s library to the book they’re trying to check out.

This prevents a patron from borrowing an age-inappropriate book or from placing a hold in a library that prohibits holds.

Generally speaking, both of these operations should be prevented before they get to this point; this is an extra layer of protection.

Parameters:
  • patron – A Patron. It’s okay if this turns out to be a ProblemDetail or None due to a problem earlier in the process.

  • license_pool` – The LicensePool the patron is trying to act on.

property circulation

Return the appropriate CirculationAPI for the request Library.

get_patron_circ_objects(object_class, patron, license_pools)[source]
get_patron_hold(patron, license_pools)[source]
get_patron_loan(patron, license_pools)[source]
handle_conditional_request(last_modified=None)[source]

Handle a conditional HTTP request.

Parameters:

last_modified – A datetime representing the time this resource was last modified.

Returns:

a Response, if the incoming request can be handled conditionally. Otherwise, None.

load_lane(lane_identifier)[source]

Turn user input into a Lane object.

load_licensepool(license_pool_id)[source]

Turns user input into a LicensePool

load_licensepooldelivery(pool, mechanism_id)[source]

Turn user input into a LicensePoolDeliveryMechanism object.

load_licensepools(library, identifier_type, identifier)[source]

Turn user input into one or more LicensePool objects.

Parameters:
  • library – The LicensePools must be associated with one of this Library’s Collections.

  • identifier_type – A type of identifier, e.g. “ISBN”

  • identifier – An identifier string, used with identifier_type to look up an Identifier.

load_work(library, identifier_type, identifier)[source]
property search_engine

Return the configured external search engine, or a ProblemDetail if none is configured.

property shared_collection

Return the appropriate SharedCollectionAPI for the request library.

class api.controller.IndexController(manager)[source]

Bases: CirculationManagerController

Redirect the patron to the appropriate feed.

appropriate_index_for_patron_type()[source]
authenticated_patron_root_lane()[source]
authentication_document()[source]

Serve this library’s Authentication For OPDS document.

has_root_lanes()[source]

Does the active library feature root lanes for patrons of certain types?

Returns:

A boolean

public_key_document()[source]

Serves a sitewide public key document

class api.controller.LoanController(manager)[source]

Bases: CirculationManagerController

best_lendable_pool(library, patron, identifier_type, identifier, mechanism_id)[source]

Of the available LicensePools for the given Identifier, return the one that’s the best candidate for loaning out right now.

Returns:

A Loan if this patron already has an active loan, otherwise a LicensePool.

borrow(identifier_type, identifier, mechanism_id=None)[source]

Create a new loan or hold for a book.

Returns:

A Response containing an OPDS entry that includes a link of rel “http://opds-spec.org/acquisition”, which can be used to fetch the book or the license file.

can_fulfill_without_loan(library, patron, pool, lpdm)[source]

Is it acceptable to fulfill the given LicensePoolDeliveryMechanism for the given Patron without creating a Loan first?

This question is usually asked because no Patron has been authenticated, and thus no Loan can be created, but somebody wants a book anyway.

Parameters:
  • library – A Library.

  • patron – A Patron, probably None.

  • lpdm – A LicensePoolDeliveryMechanism.

detail(identifier_type, identifier)[source]
fulfill(license_pool_id, mechanism_id=None, part=None, do_get=None)[source]

Fulfill a book that has already been checked out, or which can be fulfilled with no active loan.

If successful, this will serve the patron a downloadable copy of the book, a key (such as a DRM license file or bearer token) which can be used to get the book, or an OPDS entry containing a link to the book.

Parameters:
  • license_pool_id – Database ID of a LicensePool.

  • mechanism_id – Database ID of a DeliveryMechanism.

  • part – Vendor-specific identifier used when fulfilling a specific part of a book rather than the whole thing (e.g. a single chapter of an audiobook).

revoke(license_pool_id)[source]
sync()[source]

Sync the authenticated patron’s loans and holds with all third-party providers.

Returns:

A Response containing an OPDS feed with up-to-date information.

class api.controller.MARCRecordController(manager)[source]

Bases: CirculationManagerController

DOWNLOAD_TEMPLATE = '\n<html lang="en">\n<head><meta charset="utf8"></head>\n<body>\n%(body)s\n</body>\n</html>'
download_page()[source]
class api.controller.ODLNotificationController(manager)[source]

Bases: CirculationManagerController

Receive notifications from an ODL distributor when the status of a loan changes.

notify(loan_id)[source]
class api.controller.OPDSFeedController(manager)[source]

Bases: CirculationManagerController

crawlable_collection_feed(collection_name)[source]

Build or retrieve a crawlable acquisition feed for the requested collection.

crawlable_library_feed()[source]

Build or retrieve a crawlable acquisition feed for the request library.

crawlable_list_feed(list_name)[source]

Build or retrieve a crawlable, paginated acquisition feed for the named CustomList, sorted by update date.

feed(lane_identifier, feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Build or retrieve a paginated acquisition feed.

Parameters:
  • lane_identifier – An identifier that uniquely identifiers the WorkList whose feed we want.

  • feed_class – A replacement for AcquisitionFeed, for use in tests.

groups(lane_identifier, feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Build or retrieve a grouped acquisition feed.

Parameters:
  • lane_identifier – An identifier that uniquely identifiers the WorkList whose feed we want.

  • feed_class – A replacement for AcquisitionFeed, for use in tests.

navigation(lane_identifier)[source]

Build or retrieve a navigation feed, for clients that do not support groups.

qa_feed(feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Create an OPDS feed containing the information necessary to run a full set of integration tests against this server and the vendors it relies on.

Parameters:

feed_class – Class to substitute for AcquisitionFeed during tests.

qa_series_feed(feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Create an OPDS feed containing books that belong to _some_ series, without regard to _which_ series.

Parameters:

feed_class – Class to substitute for AcquisitionFeed during tests.

search(lane_identifier, feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Search for books.

class api.controller.ProfileController(manager)[source]

Bases: CirculationManagerController

Implement the User Profile Management Protocol.

protocol()[source]

Handle a UPMP request.

class api.controller.RBDFulfillmentProxyController(*args, **kwargs)[source]

Bases: CirculationManagerController

proxy(bearer, api_class=None)[source]
class api.controller.SharedCollectionController(manager)[source]

Bases: CirculationManagerController

Enable this circulation manager to share its collections with libraries on other circulation managers, for collection types that support it.

authenticated_client_from_request()[source]
borrow(collection_name, identifier_type, identifier, hold_id)[source]
fulfill(collection_name, loan_id, mechanism_id, do_get=<bound method HTTP.get_with_timeout of <class 'core.util.http.HTTP'>>)[source]

Fulfill a loan for a given collection, loan ID, and delivery mechanism ID.

Parameters:
  • collection_name – The name of the collection.

  • loan_id – The ID of the loan.

  • mechanism_id – The ID of the delivery mechanism.

  • do_get – An optional function for making HTTP GET requests.

Returns:

The content of the fulfillment with a status code of 200 and the appropriate headers if the loan is successfully fulfilled, or a ProblemDetail object if an error occurs.

hold_info(collection_name, hold_id)[source]
info(collection_name)[source]

Return an OPDS2 catalog-like document with a link to register.

load_collection(collection_name)[source]
loan_info(collection_name, loan_id)[source]
register(collection_name)[source]
revoke_hold(collection_name, hold_id)[source]
revoke_loan(collection_name, loan_id)[source]

Revoke a loan for a given collection and loan ID.

Parameters:
  • collection_name – The name of the collection.

  • loan_id – The ID of the loan.

Returns:

A success response with a status code of 200 if the loan is successfully revoked, or a ProblemDetail object if an error occurs.

class api.controller.StaticFileController(manager)[source]

Bases: CirculationManagerController

static_file(directory, filename)[source]
class api.controller.URNLookupController(manager)[source]

Bases: URNLookupController

work_lookup(route_name)[source]

Build a CirculationManagerAnnotor based on the current library’s top-level WorkList, and use it to generate an OPDS lookup feed.

class api.controller.WorkController(manager)[source]

Bases: CirculationManagerController

contributor(contributor_name, languages, audiences, feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Serve a feed of books written by a particular author

Serve an entry for a single book.

This does not include any loan or hold-specific information for the authenticated patron.

This is different from the /works lookup protocol, in that it returns a single entry while the /works lookup protocol returns a feed containing any number of entries.

recommendations(identifier_type, identifier, novelist_api=None, feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Serve a feed of recommendations related to a given book.

related(identifier_type, identifier, novelist_api=None, feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Serve a groups feed of books related to a given book.

report(identifier_type, identifier)[source]

Report a problem with a book.

series(series_name, languages, audiences, feed_class=<class 'core.opds.AcquisitionFeed'>)[source]

Serve a feed of books in a given series.

api.coverage module

Base classes for CoverageProviders.

The CoverageProviders themselves are in the file corresponding to the service that needs coverage – overdrive.py, metadata_wrangler.py, and so on.

class api.coverage.MockOPDSImportCoverageProvider(collection, *args, **kwargs)[source]

Bases: OPDSImportCoverageProvider

DATA_SOURCE_NAME = 'Library Simplified Open Access Content Server'
SERVICE_NAME = 'Mock Provider'
finalize_license_pool(license_pool)[source]

An OPDS entry was matched with a LicensePool. Do something special to mark the occasion.

By default, nothing happens.

lookup_and_import_batch(batch)[source]

Look up a batch of identifiers and parse the resulting OPDS feed.

This method is overridden by MockOPDSImportCoverageProvider.

queue_import_results(editions, pools, works, messages_by_id)[source]
class api.coverage.OPDSImportCoverageProvider(collection, lookup_client, **kwargs)[source]

Bases: CollectionCoverageProvider

Provide coverage for identifiers by looking them up, in batches, using the Simplified lookup protocol.

DEFAULT_BATCH_SIZE = 25
OPDS_IMPORTER_CLASS

alias of OPDSImporter

property api_method

The method to call to fetch an OPDS feed from the remote server.

create_identifier_mapping(batch)[source]

Map the internal identifiers used for books to the corresponding identifiers used by the lookup client.

By default, no identifier mapping is needed.

finalize_license_pool(pool)[source]

An OPDS entry was matched with a LicensePool. Do something special to mark the occasion.

By default, nothing happens.

import_feed_response(response, id_mapping)[source]

Confirms OPDS feed response and imports feed through the appropriate OPDSImporter subclass.

lookup_and_import_batch(batch)[source]

Look up a batch of identifiers and parse the resulting OPDS feed.

This method is overridden by MockOPDSImportCoverageProvider.

process_batch(batch)[source]

Perform a Simplified lookup and import the resulting OPDS feed.

process_item(identifier)[source]

Handle an individual item (e.g. through ensure_coverage) as a very small batch. Not efficient, but it works.

class api.coverage.ReaperImporter(_db, collection, data_source_name=None, identifier_mapping=None, http_get=None, metadata_client=None, content_modifier=None, map_from_collection=None, mirrors=None)[source]

Bases: OPDSImporter

We are successful if the metadata wrangler acknowledges that an identifier has been removed, and also if the identifier wasn’t in the catalog in the first place.

SUCCESS_STATUS_CODES = [200, 404]
class api.coverage.RegistrarImporter(_db, collection, data_source_name=None, identifier_mapping=None, http_get=None, metadata_client=None, content_modifier=None, map_from_collection=None, mirrors=None)[source]

Bases: OPDSImporter

We are successful whenever the metadata wrangler puts an identifier into the catalog, even if no metadata is immediately available.

SUCCESS_STATUS_CODES = [200, 201, 202]

api.custom_index module

A custom index view customizes a library’s ‘front page’ to serve something other than the default.

This code is DEPRECATED; you probably want a CustomPatronCatalog instead. We’re keeping it around because existing iOS versions of SimplyE need the OPDS navigation feed it generates.

class api.custom_index.COPPAGate(library, integration)[source]

Bases: CustomIndexView

NO_CONTENT = l'Read children's books'
NO_TITLE = l'I'm Under 13'
PROTOCOL = 'COPPA Age Gate'
REQUIREMENT_MET_LANE = 'requirement_met_lane'
REQUIREMENT_NOT_MET_LANE = 'requirement_not_met_lane'
SETTINGS = [{'key': 'requirement_met_lane', 'label': l'ID of lane for patrons who are 13 or older'}, {'key': 'requirement_not_met_lane', 'label': l'ID of lane for patrons who are under 13'}]
URI = 'http://librarysimplified.org/terms/restrictions/coppa'
YES_CONTENT = l'See the full collection'
YES_TITLE = l'I'm 13 or Older'
classmethod gate_tag(restriction, met_url, not_met_url)[source]

Create a simplified:gate tag explaining the boolean option the client is faced with.

classmethod navigation_entry(href, title, content)[source]

Create an <entry> that serves as navigation.

class api.custom_index.CustomIndexView(library, integration)[source]

Bases: object

A custom view that replaces the default OPDS view for a library.

Any subclass of this class must define PROTOCOL and must be passed into a CustomIndexView.register() call after the class definition is complete.

Subclasses of this class are loaded into the CirculationManager, so they should not store any objects obtained from the database without disconnecting them from their session.

BY_PROTOCOL = {'COPPA Age Gate': <class 'api.custom_index.COPPAGate'>}
GOAL = 'custom_index'
classmethod for_library(library)[source]

Find the appropriate CustomIndexView for the given library.

classmethod register(view_class)[source]
classmethod unregister(view_class)[source]

Remove a CustomIndexView from consideration. Only used in tests.

api.custom_patron_catalog module

A custom patron catalog annotates a library’s authentication document to describe an unusual setup.

class api.custom_patron_catalog.COPPAGate(library, integration)[source]

Bases: CustomPatronCatalog

AUTHENTICATION_NO_REL = 'http://librarysimplified.org/terms/rel/authentication/restriction-not-met'
AUTHENTICATION_TYPE = 'http://librarysimplified.org/terms/authentication/gate/coppa'
AUTHENTICATION_YES_REL = 'http://librarysimplified.org/terms/rel/authentication/restriction-met'
PROTOCOL = 'COPPA Age Gate'
REQUIREMENT_MET_LANE = 'requirement_met_lane'
REQUIREMENT_NOT_MET_LANE = 'requirement_not_met_lane'
SETTINGS = [{'key': 'requirement_met_lane', 'label': l'ID of lane for patrons who are 13 or older'}, {'key': 'requirement_not_met_lane', 'label': l'ID of lane for patrons who are under 13'}]
annotate_authentication_document(library, doc, url_for)[source]

Replace the ‘start’ link and add a custom authentication mechanism.

class api.custom_patron_catalog.CustomPatronCatalog(library, integration)[source]

Bases: object

An annotator for a library’s authentication document.

Any subclass of this class must define PROTOCOL and must be passed into a CustomPatronCatalog.register() call after the class definition is complete.

A subclass of this class will be stored in the LibraryAuthenticator. CustomPatronCatalogs should not store any objects obtained from the database without disconnecting them from their session.

BY_PROTOCOL = {'COPPA Age Gate': <class 'api.custom_patron_catalog.COPPAGate'>, 'Custom Root Lane': <class 'api.custom_patron_catalog.CustomRootLane'>}
GOAL = 'custom_patron_catalog'
annotate_authentication_document(library, doc, url_for)[source]

Modify the library’s authentication document.

Parameters:
  • library – A Library

  • doc – A dictionary representing the library’s default authentication document.

  • url_for – An implementation of Flask url_for, used to generate URLs.

Returns:

A dictionary representing the library’s default authentication document. It’s okay to modify doc and return the modified version.

classmethod for_library(library)[source]

Find the appropriate CustomPatronCatalog for the given library.

classmethod register(view_class)[source]

Remove all links with the given relation and replace them with the given link.

Parameters:
  • doc – An authentication document. Will be modified in place.

  • rel – Remove links with this relation.

  • kwargs – Add a new link with these attributes.

Returns:

A modified authentication document.

classmethod unregister(view_class)[source]

Remove a CustomPatronCatalog from consideration. Only used in tests.

class api.custom_patron_catalog.CustomRootLane(library, integration)[source]

Bases: CustomPatronCatalog

Send library patrons to a lane other than the root lane.

LANE = 'lane'
PROTOCOL = 'Custom Root Lane'
SETTINGS = [{'key': 'lane', 'label': l'Send patrons to the lane with this ID.'}]
annotate_authentication_document(library, doc, url_for)[source]

Replace the ‘start’ link with a link to the configured Lane.

api.enki module

class api.enki.BibliographicParser[source]

Bases: object

Parses Enki’s representation of book information into Metadata and CirculationData objects.

LANGUAGE_CODES = {'English': 'eng', 'French': 'fre', 'Spanish': 'spa'}
extract_bibliographic(element)[source]

Extract Metadata and CirculationData from a dictionary of information from Enki.

Returns:

A Metadata with attached CirculationData.

extract_circulation(primary_identifier, availability, formattype)[source]

Turn the ‘availability’ portion of an Enki API response into a CirculationData.

log = <Logger Enki Bibliographic Parser (WARNING)>
process_all(json_data)[source]
class api.enki.EnkiAPI(_db, collection)[source]

Bases: BaseCirculationAPI, HasSelfTests

DESCRIPTION = l'Integrate an Enki collection.'
ENKI = 'Enki'
ENKI_EXTERNAL = 'Enki'
ENKI_ID = 'Enki ID'
ENKI_LIBRARY_ID_KEY = 'enki_library_id'
ERROR_INDICATOR = '<h1>Oops, an error occurred</h1>'
LIBRARY_SETTINGS = [{'key': 'enki_library_id', 'label': l'Library ID', 'required': True}]
NAME = 'Enki'
PRODUCTION_BASE_URL = 'https://enkilibrary.org/API/'
SERVICE_NAME = 'Enki'
SETTINGS = [{'key': 'url', 'label': l'URL', 'default': 'https://enkilibrary.org/API/', 'required': True, 'format': 'url'}]
SET_DELIVERY_MECHANISM_AT = 'fulfill'
adobe_drm = 'application/vnd.adobe.adept+xml'
checkout(patron, pin, licensepool, internal_format)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

property collection
delivery_mechanism_to_internal_format = {('application/epub+zip', None): 'free', ('application/epub+zip', 'application/vnd.adobe.adept+xml'): 'acs'}
enki_library_id(library)[source]

Find the Enki library ID for the given library.

epub = 'application/epub+zip'
external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

fulfill(patron, pin, licensepool, internal_format, **kwargs)[source]

Get the actual resource file to the patron.

Parameters:

kwargs – A container for arguments to fulfill() which are not relevant to this vendor.

Returns:

a FulfillmentInfo object.

get_all_titles(strt=0, qty=10)[source]

Retrieve a single page of items from the Enki collection.

Iterating over the entire collection is very expensive and should only happen during initial data population.

Yield:

A sequence of Metadata objects, each with a CirculationData attached.

get_item(enki_id)[source]

Retrieve bibliographic and availability information for a specific title.

Parameters:

enki_id – An Enki record ID.

Returns:

If the book is in the library’s collection, a Metadata object with attached CirculationData. Otherwise, None.

item_endpoint = 'ItemAPI'
list_endpoint = 'ListAPI'
loan_request(barcode, pin, book_id, enki_library_id)[source]
log = <Logger Enki API (WARNING)>
no_drm = None
parse_fulfill_result(result)[source]
parse_patron_holds(hold_data)[source]
parse_patron_loans(checkout_data)[source]
patron_activity(patron, pin)[source]

Return a patron’s current checkouts and holds.

patron_request(patron, pin, enki_library_id)[source]
place_hold(patron, pin, licensepool, notification_email_address)[source]

Place a book on hold.

Returns:

A HoldInfo object

recent_activity(start, end)[source]

Find circulation events from a certain timeframe that affected loans or holds.

Parameters:

start – A DateTime

Yield:

A sequence of CirculationData objects.

release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Raises:

CannotReleaseHold – If there is an error communicating with the provider, or the provider refuses to release the hold for any reason.

request(url, method='get', extra_headers={}, data=None, params=None, retry_on_timeout=True, **kwargs)[source]

Make an HTTP request to the Enki API.

updated_titles(since)[source]

Find recent changes to book metadata.

NOTE: getUpdateTitles will return a maximum of 1000 items, so in theory this may need to be paginated. This shouldn’t be a problem assuming the monitor is run regularly.

Parameters:

since – A DateTime

Yield:

A sequence of Metadata objects.

user_endpoint = 'UserAPI'
class api.enki.EnkiCollectionReaper(_db, collection, api_class=<class 'api.enki.EnkiAPI'>)[source]

Bases: IdentifierSweepMonitor

Check for books that are in the local collection but have left the Enki collection.

INTERVAL_SECONDS = 14400
PROTOCOL = 'Enki'
SERVICE_NAME = 'Enki Collection Reaper'
process_item(identifier)[source]

Do the work that needs to be done for a given item.

class api.enki.EnkiImport(_db, collection, api_class=<class 'api.enki.EnkiAPI'>, analytics=None)[source]

Bases: CollectionMonitor, TimelineMonitor

Make sure our local collection is up-to-date with the remote Enki collection.

DEFAULT_BATCH_SIZE = 10
DEFAULT_START_TIME = <object object>
FIVE_MINUTES = datetime.timedelta(seconds=300)
INTERVAL_SECONDS = 500
PROTOCOL = 'Enki'
SERVICE_NAME = 'Enki Circulation Monitor'
catch_up_from(start, cutoff, progress)[source]

Find Enki books that changed recently.

Parameters:

start – Find all books that changed since this date.

property collection

Retrieve the Collection object associated with this Monitor.

full_import()[source]

Import the entire Enki collection, page by page.

incremental_import(since)[source]
process_book(bibliographic)[source]

Make the local database reflect the state of the remote Enki collection for the given book.

Parameters:

bibliographic – A Metadata object with attached CirculationData

Returns:

A 2-tuple (LicensePool, Edition). If possible, a presentation-ready Work will be created for the LicensePool.

update_circulation(since)[source]

Process circulation events that happened since since.

Returns:

The total number of circulation events.

class api.enki.MockEnkiAPI(_db, collection=None, *args, **kwargs)[source]

Bases: EnkiAPI

queue_response(status_code, headers={}, content=None)[source]

api.feedbooks module

class api.feedbooks.FeedbooksImportMonitor(_db, collection, import_class, force_reimport=False, **import_class_kwargs)[source]

Bases: OPDSImportMonitor

The same as OPDSImportMonitor, but uses FeedbooksOPDSImporter instead.

PROTOCOL = 'FeedBooks'
data_source(collection)[source]

The data source for all Feedbooks collections is Feedbooks.

opds_url(collection)[source]

Returns the OPDS import URL for the given collection.

This is the base URL plus the language setting.

class api.feedbooks.FeedbooksOPDSImporter(_db, collection, *args, **kwargs)[source]

Bases: OPDSImporter

BASE_OPDS_URL = 'http://www.feedbooks.com/books/recent.atom?lang=%(language)s'
DESCRIPTION = l'Import open-access books from FeedBooks.'
NAME = 'FeedBooks'
REALLY_IMPORT_KEY = 'really_import'
REPLACEMENT_CSS_KEY = 'replacement_css'
SETTINGS = [{'key': 'really_import', 'type': 'select', 'label': l'Really?', 'description': l'Most libraries are better off importing free Feedbooks titles via an OPDS Import integration from NYPL's open-access content server or DPLA's Open Bookshelf. This setting makes sure you didn't create this collection by accident and really want to import directly from Feedbooks.', 'options': [{'key': 'false', 'label': l'Don't actually import directly from Feedbooks.'}, {'key': 'true', 'label': l'I know what I'm doing; import directly from Feedbooks.'}], 'default': 'false'}, {'key': 'external_account_id', 'label': l'Import books in this language', 'description': l'Feedbooks offers separate feeds for different languages. Each one can be made into a separate collection.', 'type': 'select', 'options': [{'key': 'en', 'label': l'English'}, {'key': 'es', 'label': l'Spanish'}, {'key': 'fr', 'label': l'French'}, {'key': 'it', 'label': l'Italian'}, {'key': 'de', 'label': l'German'}], 'default': 'en'}, {'key': 'replacement_css', 'label': l'Replacement stylesheet', 'description': l'If you are mirroring the Feedbooks titles, you may replace the Feedbooks stylesheet with an alternate stylesheet in the mirrored copies. The default value is an accessibility-focused stylesheet produced by the DAISY consortium. If you mirror Feedbooks titles but leave this empty, the Feedbooks titles will be mirrored as-is.', 'default': 'http://www.daisy.org/z3986/2005/dtbook.2005.basic.css'}]
THIRTY_DAYS = datetime.timedelta(days=30)
extract_feed_data(feed, feed_url=None)[source]

Turn an OPDS feed into lists of Metadata and CirculationData objects, with associated messages and next_links.

improve_description(id, metadata)[source]

Improve the description associated with a book, if possible.

This involves fetching an alternate OPDS entry that might contain more detailed descriptions than those available in the main feed.

Turn basic link information into a LinkData object.

FeedBooks puts open-access content behind generic ‘acquisition’ links. We want to treat the EPUBs as open-access links and (at the request of FeedBooks) ignore the other formats.

replace_css(representation)[source]

This function will replace the content of every CSS file listed in an epub’s manifest with the value in self.new_css. The rest of the file is not changed.

classmethod rights_uri_from_entry_tag(entry)[source]

Determine the URI that best encapsulates the rights status of the downloads associated with this book.

classmethod rights_uri_from_feedparser_entry(entry)[source]

(Refuse to) determine the URI that best encapsulates the rights status of the downloads associated with this book.

We cannot answer this question from within feedparser code; we have to wait until we enter elementtree code.

class api.feedbooks.RehostingPolicy[source]

Bases: object

Determining the precise copyright status of the underlying text is not directly useful, because Feedbooks has made derivative works and relicensed under CC-BY-NC. So that’s going to be the license: CC-BY-NC.

Except it’s not that simple. There are two complications.

1. Feedbooks is located in France, and the NYPL/DPLA content servers are hosted in the US. We can’t host a CC-BY-NC book if it’s derived from a work that’s still under US copyright. We must decide whether or not to accept a book in the first place based on the copyright status of the underlying text.

2. Some CC licenses are more restrictive (on the creators of derivative works) than CC-BY-NC. Feedbooks has no authority to relicense these books, so the old licenses need to be preserved.

This class encapsulates the logic necessary to make this decision.

CAN_REHOST_IN_US = {'Attribution (cc by)', 'Attribution Non-Commercial (cc by-nc)', 'Attribution Non-Commercial No Derivatives (cc by-nc-nd)', 'Attribution Non-Commercial Share Alike (cc by-nc-sa)', 'Attribution Share Alike (cc by-sa)', 'This work is available for countries where copyright is Life+50 or in the USA (published before 1923).', 'This work is available for countries where copyright is Life+70 and in the USA.', 'This work was published before 1923 and is in the public domain in the USA only.'}
PUBLIC_DOMAIN_CUTOFF = 1923
RIGHTS_DICT = {'Attribution Non-Commercial No Derivatives (cc by-nc-nd)': 'https://creativecommons.org/licenses/by-nc-nd/4.0', 'Attribution Non-Commercial Share Alike (cc by-nc-sa)': 'https://creativecommons.org/licenses/by-nc-sa/4.0', 'Attribution Share Alike (cc by-sa)': 'https://creativecommons.org/licenses/by-sa/4.0'}
RIGHTS_UNKNOWN = 'Please read the legal notice included in this e-book and/or check the copyright status in your country.'
US_SITES = {'archive.org', 'craphound.com', 'en.wikipedia.org', 'en.wikisource.org', 'futurismic.com', 'gutenberg.org', 'project gutenberg', 'shakespeare.mit.edu'}
classmethod can_rehost_us(rights, source, publication_year)[source]

Can we rehost this book on a US server?

Parameters:
  • rights – What FeedBooks says about the public domain status of the book.

  • source – Where FeedBooks got the book.

  • publication_year – When the text was originally published.

Returns:

True if we can rehost in the US, False if we can’t, None if we’re not sure. The distinction between False and None is only useful when making lists of books that need to have their rights status manually investigated.

classmethod rights_uri(rights, source, publication_year)[source]

api.firstbook module

api.firstbook.AuthenticationProvider

alias of FirstBookAuthenticationAPI

class api.firstbook.FirstBookAuthenticationAPI(library_id, integration, analytics=None, root=None, secret=None)[source]

Bases: BasicAuthenticationProvider

API_PATH = 'rest/V1/serialcode?'
DEFAULT_IDENTIFIER_LABEL = l'Access Code'
DEFAULT_IDENTIFIER_REGULAR_EXPRESSION = '^[A-Za-z0-9@]+$'
DEFAULT_PASSWORD_REGULAR_EXPRESSION = '^[0-9]+$'
DESCRIPTION = l'         An authentication service for Open eBooks that authenticates         using access codes and PINs. (This is the new version as of 8/2022.)'
DISPLAY_NAME = 'First Book (New 8/2022)'
LOGIN_BUTTON_IMAGE = 'FirstBookLoginButton280.png'
NAME = 'First Book (New 8/2022)'
SETTINGS = [{'key': 'url', 'format': 'url', 'label': l'URL', 'required': True}, {'key': 'password', 'label': l'Key', 'required': True}, {'key': 'test_identifier', 'label': l'Test Identifier', 'description': l'A valid identifier that can be used to test that patron authentication is working. An optional Test Password for this identifier can be set in the next section.', 'required': True}, {'key': 'test_password', 'label': l'Test Password', 'description': l'The password for the Test Identifier (above, in previous section).'}, {'key': 'identifier_barcode_format', 'label': l'Patron identifier barcode format', 'description': l'Many libraries render patron identifiers as barcodes on physical library cards. If you specify the barcode format, patrons will be able to scan their library cards with a camera instead of manually typing in their identifiers.', 'type': 'select', 'options': [{'key': 'Codabar', 'label': l'Patron identifiers are are rendered as barcodes in Codabar format'}, {'key': '', 'label': l'Patron identifiers are not rendered as barcodes'}], 'default': '', 'required': True}, {'key': 'identifier_regular_expression', 'label': l'Identifier Regular Expression', 'description': l'A patron's identifier will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'password_regular_expression', 'label': l'Password Regular Expression', 'description': l'A patron's password will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'identifier_keyboard', 'label': l'Keyboard for identifier entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Email address', 'label': l'Email address entry'}, {'key': 'Number pad', 'label': l'Number pad'}], 'default': 'Default', 'required': True}, {'key': 'password_keyboard', 'label': l'Keyboard for password entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Number pad', 'label': l'Number pad'}, {'key': 'No input', 'label': l'Patrons have no password and should not be prompted for one.'}], 'default': 'Default'}, {'key': 'identifier_maximum_length', 'label': l'Maximum identifier length', 'type': 'number'}, {'key': 'password_maximum_length', 'label': l'Maximum password length', 'type': 'number'}, {'key': 'identifier_label', 'label': l'Label for identifier entry'}, {'key': 'password_label', 'label': l'Label for password entry'}]
SUCCESS_MESSAGE = 'Valid Code Pin Pair'
log = <Logger First Book authentication API (WARNING)>
remote_authenticate(username, password)[source]

Does the source of truth approve of these credentials?

Returns:

If the credentials are valid, but nothing more is known about the patron, return True.

If the credentials are valid, _and_ enough information came back in the request to also create a PatronInfo object, you may create that object and return it to save a remote patron lookup later.

If the credentials are invalid, return False or None.

remote_pin_test(barcode, pin)[source]
request(url, header={})[source]

Make an HTTP request.

Defined to be overridden in test mock.

class api.firstbook.MockFirstBookAuthenticationAPI(library, integration, valid={}, bad_connection=False, failure_status_code=None)[source]

Bases: FirstBookAuthenticationAPI

FAILURE = '{"code":404,"message":"Access Code Pin Pair not found"}'
SUCCESS = '"Valid Code Pin Pair"'
request(url, header)[source]

Make an HTTP request.

Defined to be overridden in test mock.

class api.firstbook.MockFirstBookResponse(status_code, content)[source]

Bases: object

api.firstbook2 module

api.firstbook2.AuthenticationProvider

alias of FirstBookAuthenticationAPI

class api.firstbook2.FirstBookAuthenticationAPI(library_id, integration, analytics=None, root=None, secret=None)[source]

Bases: BasicAuthenticationProvider

ALGORITHM = 'HS256'
DEFAULT_IDENTIFIER_LABEL = l'Access Code'
DEFAULT_IDENTIFIER_REGULAR_EXPRESSION = '^[A-Za-z0-9@]+$'
DEFAULT_PASSWORD_REGULAR_EXPRESSION = '^[0-9]+$'
DESCRIPTION = l'         An authentication service for Open eBooks that authenticates         using access codes and PINs. (This is the deprecated version.)'
DISPLAY_NAME = 'First Book (deprecated)'
LOGIN_BUTTON_IMAGE = 'FirstBookLoginButton280.png'
NAME = 'First Book (deprecated)'
SETTINGS = [{'key': 'url', 'format': 'url', 'label': l'URL', 'default': 'https://ebooks.firstbook.org/api/', 'required': True}, {'key': 'password', 'label': l'Key', 'required': True}, {'key': 'test_identifier', 'label': l'Test Identifier', 'description': l'A valid identifier that can be used to test that patron authentication is working. An optional Test Password for this identifier can be set in the next section.', 'required': True}, {'key': 'test_password', 'label': l'Test Password', 'description': l'The password for the Test Identifier (above, in previous section).'}, {'key': 'identifier_barcode_format', 'label': l'Patron identifier barcode format', 'description': l'Many libraries render patron identifiers as barcodes on physical library cards. If you specify the barcode format, patrons will be able to scan their library cards with a camera instead of manually typing in their identifiers.', 'type': 'select', 'options': [{'key': 'Codabar', 'label': l'Patron identifiers are are rendered as barcodes in Codabar format'}, {'key': '', 'label': l'Patron identifiers are not rendered as barcodes'}], 'default': '', 'required': True}, {'key': 'identifier_regular_expression', 'label': l'Identifier Regular Expression', 'description': l'A patron's identifier will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'password_regular_expression', 'label': l'Password Regular Expression', 'description': l'A patron's password will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'identifier_keyboard', 'label': l'Keyboard for identifier entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Email address', 'label': l'Email address entry'}, {'key': 'Number pad', 'label': l'Number pad'}], 'default': 'Default', 'required': True}, {'key': 'password_keyboard', 'label': l'Keyboard for password entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Number pad', 'label': l'Number pad'}, {'key': 'No input', 'label': l'Patrons have no password and should not be prompted for one.'}], 'default': 'Default'}, {'key': 'identifier_maximum_length', 'label': l'Maximum identifier length', 'type': 'number'}, {'key': 'password_maximum_length', 'label': l'Maximum password length', 'type': 'number'}, {'key': 'identifier_label', 'label': l'Label for identifier entry'}, {'key': 'password_label', 'label': l'Label for password entry'}]
SUCCESS_MESSAGE = 'Valid Code Pin Pair'
jwt(barcode, pin)[source]

Create and sign a JWT with the payload expected by the First Book API.

log = <Logger First Book JWT authentication API (WARNING)>
remote_authenticate(username, password)[source]

Does the source of truth approve of these credentials?

Returns:

If the credentials are valid, but nothing more is known about the patron, return True.

If the credentials are valid, _and_ enough information came back in the request to also create a PatronInfo object, you may create that object and return it to save a remote patron lookup later.

If the credentials are invalid, return False or None.

remote_pin_test(barcode, pin)[source]
request(url)[source]

Make an HTTP request.

Defined solely so it can be overridden in the mock.

class api.firstbook2.MockFirstBookAuthenticationAPI(library, integration, valid={}, bad_connection=False, failure_status_code=None)[source]

Bases: FirstBookAuthenticationAPI

FAILURE = '{"code":404,"message":"Access Code Pin Pair not found"}'
SUCCESS = '"Valid Code Pin Pair"'
request(url)[source]

Make an HTTP request.

Defined solely so it can be overridden in the mock.

class api.firstbook2.MockFirstBookResponse(status_code, content)[source]

Bases: object

api.google_analytics_provider module

class api.google_analytics_provider.GoogleAnalyticsProvider(integration, library=None)[source]

Bases: object

DEFAULT_URL = 'http://www.google-analytics.com/collect'
DESCRIPTION = l'How to Configure a Google Analytics Integration'
INSTRUCTIONS = l'<p>In order to track usage statistics, you can configure the Circulation Manager to connect to Google Analytics.</p><p>Create a <a href='https://analytics.google.com/analytics/web/provision/?authuser=0#/provision' rel='noopener' rel='noreferer' target='_blank'>Google Analytics</a> account, or sign into your existing one.</p><p>To capture data from the Library Simplified Circulation Manager in your Google Analytics account, you must set up a property in Google Analytics for Library Simplified.  In your Google Analytics account, on the administration page for the property, go to Custom Definitions > Custom Dimensions, and add the following dimensions, in this order: <ol><li>time</li><li>identifier</li><li>identifier_type</li><li>title</li><li>author</li><li>fiction</li><li>audience</li><li>target_age</li><li>publisher</li><li>language</li><li>genre</li><li>open_access</li><li>distributor</li><li>medium</li><li>library</li></ol></p><p>Each dimension should have the scope set to 'Hit' and the 'Active' box checked.</p><p>Then go to Tracking Info and get the tracking id for the property.  Select your library from the dropdown below, and enter the tracking id into the form.</p>'
LIBRARY_SETTINGS = [{'key': 'tracking_id', 'label': l'Tracking ID', 'required': True}]
NAME = l'Google Analytics'
SETTINGS = [{'key': 'url', 'label': l'URL', 'default': 'http://www.google-analytics.com/collect', 'required': True, 'format': 'url'}]
TRACKING_ID = 'tracking_id'
collect_event(library, license_pool, event_type, time, **kwargs)[source]
post(url, params)[source]
api.google_analytics_provider.Provider

alias of GoogleAnalyticsProvider

api.kansas_patron module

api.kansas_patron.AuthenticationProvider

alias of KansasAuthenticationAPI

class api.kansas_patron.KansasAuthenticationAPI(library_id, integration, analytics=None, base_url=None)[source]

Bases: BasicAuthenticationProvider

DESCRIPTION = l'         An authentication service for the Kansas State Library.         '
DISPLAY_NAME = 'Kansas'
NAME = 'Kansas'
SETTINGS = [{'key': 'url', 'format': 'url', 'label': l'URL', 'default': 'https://ks-kansaslibrary3m.civicplus.com/api/UserDetails', 'required': True}, {'key': 'test_identifier', 'label': l'Test Identifier', 'description': l'A valid identifier that can be used to test that patron authentication is working. An optional Test Password for this identifier can be set in the next section.', 'required': True}, {'key': 'test_password', 'label': l'Test Password', 'description': l'The password for the Test Identifier (above, in previous section).'}, {'key': 'identifier_barcode_format', 'label': l'Patron identifier barcode format', 'description': l'Many libraries render patron identifiers as barcodes on physical library cards. If you specify the barcode format, patrons will be able to scan their library cards with a camera instead of manually typing in their identifiers.', 'type': 'select', 'options': [{'key': 'Codabar', 'label': l'Patron identifiers are are rendered as barcodes in Codabar format'}, {'key': '', 'label': l'Patron identifiers are not rendered as barcodes'}], 'default': '', 'required': True}, {'key': 'identifier_regular_expression', 'label': l'Identifier Regular Expression', 'description': l'A patron's identifier will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'password_regular_expression', 'label': l'Password Regular Expression', 'description': l'A patron's password will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'identifier_keyboard', 'label': l'Keyboard for identifier entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Email address', 'label': l'Email address entry'}, {'key': 'Number pad', 'label': l'Number pad'}], 'default': 'Default', 'required': True}, {'key': 'password_keyboard', 'label': l'Keyboard for password entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Number pad', 'label': l'Number pad'}, {'key': 'No input', 'label': l'Patrons have no password and should not be prompted for one.'}], 'default': 'Default'}, {'key': 'identifier_maximum_length', 'label': l'Maximum identifier length', 'type': 'number'}, {'key': 'password_maximum_length', 'label': l'Maximum password length', 'type': 'number'}, {'key': 'identifier_label', 'label': l'Label for identifier entry'}, {'key': 'password_label', 'label': l'Label for password entry'}]
static create_authorize_request(barcode, pin)[source]
log = <Logger Kansas authentication API (WARNING)>
parse_authorize_response(response)[source]
post_request(data)[source]

Make an HTTP request.

Defined solely so it can be overridden in the mock.

remote_authenticate(username, password)[source]

Does the source of truth approve of these credentials?

Returns:

If the credentials are valid, but nothing more is known about the patron, return True.

If the credentials are valid, _and_ enough information came back in the request to also create a PatronInfo object, you may create that object and return it to save a remote patron lookup later.

If the credentials are invalid, return False or None.

api.lanes module

class api.lanes.ContributorFacets(library, collection, availability, order, order_ascending=None, enabled_facets=None, entrypoint=None, entrypoint_is_default=False, **constructor_kwargs)[source]

Bases: DefaultSortOrderFacets

A list with a contributor restriction is, by default, sorted by title.

DEFAULT_SORT_ORDER = 'title'
class api.lanes.ContributorLane(library, contributor, parent=None, languages=None, audiences=None)[source]

Bases: DynamicLane

A lane of Works written by a particular contributor

CACHED_FEED_TYPE = 'contributor'
MAX_CACHE_AGE = 86400
ROUTE = 'contributor'
modify_search_filter_hook(filter)[source]

A hook method allowing subclasses to modify a Filter object that’s about to find all the works in this WorkList.

This can avoid the need for complex subclasses of Facets.

overview_facets(_db, facets)[source]

Convert a FeaturedFacets to a ContributorFacets suitable for use in a grouped feed.

property url_arguments
class api.lanes.CrawlableCollectionBasedLane[source]

Bases: CrawlableLane

COLLECTION_ROUTE = 'crawlable_collection_feed'
LIBRARY_ROUTE = 'crawlable_library_feed'
MAX_CACHE_AGE = 7200
initialize(library_or_collections)[source]

Initialize with basic data.

This is not a constructor, to avoid conflicts with Lane, an ORM object that subclasses this object but does not use this initialization code.

Parameters:
  • library – Only Works available in this Library will be included in lists.

  • display_name – Name to display for this WorkList in the user interface.

  • genres – Only Works classified under one of these Genres will be included in lists.

  • audiences – Only Works classified under one of these audiences will be included in lists.

  • languages – Only Works in one of these languages will be included in lists.

  • media – Only Works in one of these media will be included in lists.

  • fiction – Only Works with this fiction status will be included in lists.

  • target_age – Only Works targeted at readers in this age range will be included in lists.

  • license_datasource – Only Works with a LicensePool from this DataSource will be included in lists.

  • customlists – Only Works included on one of these CustomLists will be included in lists.

  • list_datasource – Only Works included on a CustomList associated with this DataSource will be included in lists. This overrides any specific CustomLists provided in customlists.

  • list_seen_in_previous_days – Only Works that were added to a matching CustomList within this number of days will be included in lists.

  • children – This WorkList has children, which are also WorkLists.

  • priority – A number indicating where this WorkList should show up in relation to its siblings when it is the child of some other WorkList.

  • entrypoints – A list of EntryPoint classes representing different ways of slicing up this WorkList.

property url_arguments
class api.lanes.CrawlableCustomListBasedLane[source]

Bases: CrawlableLane

A lane that consists of all works in a single CustomList.

ROUTE = 'crawlable_list_feed'
initialize(library, customlist)[source]

Initialize with basic data.

This is not a constructor, to avoid conflicts with Lane, an ORM object that subclasses this object but does not use this initialization code.

Parameters:
  • library – Only Works available in this Library will be included in lists.

  • display_name – Name to display for this WorkList in the user interface.

  • genres – Only Works classified under one of these Genres will be included in lists.

  • audiences – Only Works classified under one of these audiences will be included in lists.

  • languages – Only Works in one of these languages will be included in lists.

  • media – Only Works in one of these media will be included in lists.

  • fiction – Only Works with this fiction status will be included in lists.

  • target_age – Only Works targeted at readers in this age range will be included in lists.

  • license_datasource – Only Works with a LicensePool from this DataSource will be included in lists.

  • customlists – Only Works included on one of these CustomLists will be included in lists.

  • list_datasource – Only Works included on a CustomList associated with this DataSource will be included in lists. This overrides any specific CustomLists provided in customlists.

  • list_seen_in_previous_days – Only Works that were added to a matching CustomList within this number of days will be included in lists.

  • children – This WorkList has children, which are also WorkLists.

  • priority – A number indicating where this WorkList should show up in relation to its siblings when it is the child of some other WorkList.

  • entrypoints – A list of EntryPoint classes representing different ways of slicing up this WorkList.

property url_arguments
uses_customlists = True
class api.lanes.CrawlableFacets(library, collection, availability, order, order_ascending=None, enabled_facets=None, entrypoint=None, entrypoint_is_default=False, **constructor_kwargs)[source]

Bases: Facets

A special Facets class for crawlable feeds.

CACHED_FEED_TYPE = 'crawlable'
SETTINGS = {'available': 'all', 'collection': 'full', 'order': 'last_update'}
classmethod available_facets(config, facet_group_name)[source]

Which facets are enabled for the given facet group?

You can override this to forcible enable or disable facets that might not be enabled in library configuration, but you can’t make up totally new facets.

TODO: This sytem would make more sense if you _could_ make up totally new facets, maybe because each facet was represented as a policy object rather than a key to code implemented elsewhere in this class. Right now this method implies more flexibility than actually exists.

classmethod default_facet(config, facet_group_name)[source]

The default value for the given facet group.

The default value must be one of the values returned by available_facets() above.

class api.lanes.CrawlableLane[source]

Bases: DynamicLane

MAX_CACHE_AGE = 43200
class api.lanes.DatabaseExclusiveWorkList[source]

Bases: DatabaseBackedWorkList

A DatabaseBackedWorkList that can _only_ get Works through the database.

works(*args, **kwargs)[source]

Use a search engine to obtain Work or Work-like objects that belong in this WorkList.

Compare DatabaseBackedWorkList.works_from_database, which uses a database query to obtain the same Work objects.

Parameters:
  • _db – A database connection.

  • facets – A Facets object which may put additional constraints on WorkList membership.

  • pagination – A Pagination object indicating which part of the WorkList the caller is looking at, and/or a limit on the number of works to fetch.

  • kwargs – Different implementations may fetch the list of works from different sources and may need different keyword arguments.

Returns:

A list of Work or Work-like objects, or a database query that generates such a list when executed.

class api.lanes.DynamicLane[source]

Bases: WorkList

A WorkList that’s used to from an OPDS lane, but isn’t a Lane in the database.

class api.lanes.HasSeriesFacets(library, collection, availability, order, order_ascending=None, enabled_facets=None, entrypoint=None, entrypoint_is_default=False, **constructor_kwargs)[source]

Bases: Facets

A faceting object for a feed containg books guaranteed to belong to _some_ series.

modify_search_filter(filter)[source]

Modify the given external_search.Filter object so that it reflects the settings of this Facets object.

This is the Elasticsearch equivalent of apply(). However, the Elasticsearch implementation of (e.g.) the meaning of the different availabilty statuses is kept in Filter.build().

class api.lanes.JackpotFacets(library, collection, availability, order, order_ascending=None, enabled_facets=None, entrypoint=None, entrypoint_is_default=False, **constructor_kwargs)[source]

Bases: Facets

A faceting object for a jackpot feed.

Unlike other faceting objects, AVAILABLE_NOT_NOW is an acceptable option for the availability facet.

classmethod available_facets(config, facet_group_name)[source]

Which facets are enabled for the given facet group?

You can override this to forcible enable or disable facets that might not be enabled in library configuration, but you can’t make up totally new facets.

TODO: This sytem would make more sense if you _could_ make up totally new facets, maybe because each facet was represented as a policy object rather than a key to code implemented elsewhere in this class. Right now this method implies more flexibility than actually exists.

classmethod default_facet(config, facet_group_name)[source]

The default value for the given facet group.

The default value must be one of the values returned by available_facets() above.

class api.lanes.JackpotWorkList(library, facets)[source]

Bases: WorkList

A WorkList guaranteed to, so far as possible, contain the exact selection of books necessary to perform common QA tasks.

This makes it easy to write integration tests that work on real circulation managers and real books.

works(_db, *args, **kwargs)[source]

This worklist never has works of its own.

Only its children have works.

class api.lanes.KnownOverviewFacetsWorkList(facets, *args, **kwargs)[source]

Bases: WorkList

A WorkList whose defining feature is that the Facets object to be used when generating a grouped feed is known in advance.

overview_facets(_db, facets)[source]

Return the faceting object to be used when generating a grouped feed.

Parameters:
  • _db – Ignored – only present for API compatibility.

  • facets – Ignored – only present for API compatibility.

class api.lanes.RecommendationLane(library, work, display_name=None, novelist_api=None, parent=None)[source]

Bases: WorkBasedLane

A lane of recommended Works based on a particular Work

CACHED_FEED_TYPE = 'recommendations'
DISPLAY_NAME = 'Titles recommended by NoveList'
MAX_CACHE_AGE = 86400
ROUTE = 'recommendations'
fetch_recommendations(_db)[source]

Get identifiers of recommendations for this LicensePool

modify_search_filter_hook(filter)[source]

Find Works whose Identifiers include the ISBNs returned by an external recommendation engine.

Parameters:

filter – A Filter object.

overview_facets(_db, facets)[source]

Convert a generic FeaturedFacets to some other faceting object, suitable for showing an overview of this WorkList in a grouped feed.

class api.lanes.RelatedBooksLane(library, work, display_name=None, novelist_api=None)[source]

Bases: WorkBasedLane

A lane of Works all related to a given Work by various criteria.

Each criterion is represented by another WorkBaseLane class:

  • ContributorLane: Works by one of the contributors to this work.

  • SeriesLane: Works in the same series.

  • RecommendationLane: Works provided by a third-party recommendation service.

CACHED_FEED_TYPE = 'related'
DISPLAY_NAME = 'Related Books'
MAX_CACHE_AGE = 86400
ROUTE = 'related_books'
works(_db, *args, **kwargs)[source]

This lane never has works of its own.

Only its sublanes have works.

class api.lanes.SeriesFacets(library, collection, availability, order, order_ascending=None, enabled_facets=None, entrypoint=None, entrypoint_is_default=False, **constructor_kwargs)[source]

Bases: DefaultSortOrderFacets

A list with a series restriction is ordered by series position by default.

DEFAULT_SORT_ORDER = 'series'
class api.lanes.SeriesLane(library, series_name, parent=None, **kwargs)[source]

Bases: DynamicLane

A lane of Works in a particular series.

CACHED_FEED_TYPE = 'series'
MAX_CACHE_AGE = 86400
ROUTE = 'series'
modify_search_filter_hook(filter)[source]

A hook method allowing subclasses to modify a Filter object that’s about to find all the works in this WorkList.

This can avoid the need for complex subclasses of Facets.

overview_facets(_db, facets)[source]

Convert a FeaturedFacets to a SeriesFacets suitable for use in a grouped feed. Our contribution to a grouped feed will be ordered by series position.

property url_arguments
class api.lanes.WorkBasedLane(library, work, display_name=None, children=None, **kwargs)[source]

Bases: DynamicLane

A lane that shows works related to one particular Work.

DISPLAY_NAME = None
ROUTE = None
accessible_to(patron)[source]

In addition to the restrictions imposed by the superclass, a lane based on a specific Work is accessible to a Patron only if the Work itself is age-appropriate for the patron.

Parameters:

patron – A Patron

Returns:

A boolean

append_child(worklist)[source]

Add another Worklist as a child of this one and change its configuration to make sure its results fit in with this lane.

audiences_list_from_source()[source]
property url_arguments
api.lanes.create_default_lanes(_db, library)[source]

Reset the lanes for the given library to the default.

The database will have the following top-level lanes for each large-collection: ‘Adult Fiction’, ‘Adult Nonfiction’, ‘Young Adult Fiction’, ‘Young Adult Nonfiction’, and ‘Children’. Each lane contains additional sublanes. If an NYT integration is configured, there will also be a ‘Best Sellers’ top-level lane.

If there are any small- or tiny-collection languages, the database will also have a top-level lane called ‘World Languages’. The ‘World Languages’ lane will have a sublane for every small- and tiny-collection languages. The small-collection languages will have “Adult Fiction”, “Adult Nonfiction”, and “Children/YA” sublanes; the tiny-collection languages will not have any sublanes.

If run on a Library that already has Lane configuration, this can be an extremely destructive method. All new Lanes will be visible and all Lanes based on CustomLists (but not the CustomLists themselves) will be destroyed.

api.lanes.create_lane_for_small_collection(_db, library, parent, languages, priority=0)[source]

Create a lane (with sublanes) for a small collection based on language, if the language exists in the lookup table.

Parameters:

parent – The parent of the new lane.

api.lanes.create_lane_for_tiny_collection(_db, library, parent, languages, priority=0)[source]

Create a single lane for a tiny collection based on language, if the language exists in the lookup table.

Parameters:

parent – The parent of the new lane.

api.lanes.create_lanes_for_large_collection(_db, library, languages, priority=0)[source]

Ensure that the lanes appropriate to a large collection are all present.

This means:

  • A “%(language)s Adult Fiction” lane containing sublanes for each fiction

    genre.

  • A “%(language)s Adult Nonfiction” lane containing sublanes for

    each nonfiction genre.

  • A “%(language)s YA Fiction” lane containing sublanes for the

    most popular YA fiction genres.

  • A “%(language)s YA Nonfiction” lane containing sublanes for the

    most popular YA fiction genres.

  • A “%(language)s Children and Middle Grade” lane containing

    sublanes for childrens’ books at different age levels.

Parameters:
  • library – Newly created lanes will be associated with this library.

  • languages – Newly created lanes will contain only books in these languages.

Returns:

A list of top-level Lane objects.

TODO: If there are multiple large collections, their top-level lanes do not have distinct display names.

api.lanes.create_world_languages_lane(_db, library, small_languages, tiny_languages, priority=0)[source]

Create a lane called ‘World Languages’ whose sublanes represent the non-large language collections available to this library.

api.lanes.lane_from_genres(_db, library, genres, display_name=None, exclude_genres=None, priority=0, audiences=None, **extra_args)[source]

Turn genre info into a Lane object.

api.lanes.load_lanes(_db, library)[source]

Return a WorkList that reflects the current lane structure of the Library.

If no top-level visible lanes are configured, the WorkList will be configured to show every book in the collection.

If a single top-level Lane is configured, it will returned as the WorkList.

Otherwise, a WorkList containing the visible top-level lanes is returned.

api.local_analytics_exporter module

class api.local_analytics_exporter.LocalAnalyticsExporter[source]

Bases: object

Export large numbers of analytics events in CSV format.

analytics_query(start, end, locations=None, library=None)[source]

Build a database query that fetches rows of analytics data.

This method uses low-level SQLAlchemy code to do all calculations and data conversations in the database. It’s modeled after Work.to_search_documents, which generates a large JSON document entirely in the database.

Returns:

An iterator of results, each of which can be written directly to a CSV file.

export(_db, start, end, locations=None, library=None)[source]

api.marc module

class api.marc.LibraryAnnotator(library)[source]

Bases: Annotator

add_web_client_urls(record, library, identifier, integration=None)[source]
annotate_work_record(work, active_license_pool, edition, identifier, record, integration=None, updated=None)[source]

Add metadata from this work to a MARC record.

Work:

The Work whose record is being annotated.

Active_license_pool:

Of all the LicensePools associated with this Work, the client has expressed interest in this one.

Edition:

The Edition to use when associating bibliographic metadata with this entry.

Identifier:

Of all the Identifiers associated with this Work, the client has expressed interest in this one.

Parameters:

record – A MARCRecord object to be annotated.

value(key, integration)[source]

api.metadata_wrangler module

class api.metadata_wrangler.BaseMetadataWranglerCoverageProvider(collection, lookup_client=None, **kwargs)[source]

Bases: OPDSImportCoverageProvider

Makes sure the metadata wrangler knows about all Identifiers licensed to a Collection.

This has two subclasses: MetadataWranglerCollectionRegistrar (which adds Identifiers from a circulation manager’s catalog to the corresponding catalog on the metadata wrangler) and MetadataWranglerCollectionReaper (which removes Identifiers from the metadata wrangler catalog once they no longer exist in the circulation manager’s catalog).

COVERAGE_COUNTS_FOR_EVERY_COLLECTION = False
DATA_SOURCE_NAME = 'Library Simplified metadata wrangler'
INPUT_IDENTIFIER_TYPES = ['Overdrive ID', 'Bibliotheca ID', 'Axis 360 ID', 'RBdigital ID', 'URI']
create_identifier_mapping(batch)[source]

The metadata wrangler can look up ISBNs and Overdrive identifiers. All other identifier types need to be mapped to ISBNs.

class api.metadata_wrangler.MWAuxiliaryMetadataMonitor(_db, collection, lookup=None, provider=None)[source]

Bases: MetadataWranglerCollectionMonitor

Retrieves and processes requests for needed third-party metadata from the Metadata Wrangler.

The Wrangler will only request metadata if it can’t process an identifier from its own third-party resources. In these cases (e.g. ISBNs from Axis 360 or Bibliotheca), the wrangler will put out a call for metadata that it needs to process the identifier. This monitor answers that call.

DEFAULT_START_TIME = <object object>
SERVICE_NAME = 'Metadata Wrangler Auxiliary Metadata Delivery'
endpoint()[source]
get_identifiers(url=None)[source]

Pulls mapped identifiers from a feed of SimplifiedOPDSMessages.

run_once(progress)[source]

Do the actual work of the Monitor.

Parameters:

progress – A TimestampData representing the work done by the Monitor up to this point.

Returns:

A TimestampData representing how you want the Monitor’s entry in the timestamps table to look like from this point on. NOTE: Modifying the incoming progress and returning it is generally a bad idea, because the incoming progress is full of old data. Instead, return a new TimestampData containing data for only the fields you want to set.

class api.metadata_wrangler.MWCollectionUpdateMonitor(_db, collection, lookup=None)[source]

Bases: MetadataWranglerCollectionMonitor

Retrieves updated metadata from the Metadata Wrangler

DEFAULT_START_TIME = <object object>
SERVICE_NAME = 'Metadata Wrangler Collection Updates'
endpoint(timestamp)[source]
import_one_feed(timestamp, url)[source]
run_once(progress)[source]

Ask the metadata wrangler about titles that have changed since the last time this monitor ran.

Parameters:

progress – A TimestampData representing the span of time covered during the previous run of this monitor.

Returns:

A modified TimestampData.

class api.metadata_wrangler.MetadataUploadCoverageProvider(*args, **kwargs)[source]

Bases: BaseMetadataWranglerCoverageProvider

Provide coverage for identifiers by uploading OPDS metadata to the metadata wrangler.

DATA_SOURCE_NAME = 'Library Simplified Internal Process'
DEFAULT_BATCH_SIZE = 25
OPERATION = 'metadata-upload'
SERVICE_NAME = 'Metadata Upload Coverage Provider'
process_batch(batch)[source]

Create an OPDS feed from a batch and upload it to the metadata client.

class api.metadata_wrangler.MetadataWranglerCollectionMonitor(_db, collection, lookup=None)[source]

Bases: CollectionMonitor

Abstract base CollectionMonitor with helper methods for interactions with the Metadata Wrangler.

assert_authenticated()[source]

Raise an exception unless the client has authentication credentials.

Raising an exception will keep the Monitor timestamp from being updated.

endpoint(*args, **kwargs)[source]
get_response(url=None, **kwargs)[source]
class api.metadata_wrangler.MetadataWranglerCollectionReaper(collection, lookup_client=None, **kwargs)[source]

Bases: BaseMetadataWranglerCoverageProvider

Removes unlicensed identifiers from the remote Metadata Wrangler Collection

OPDS_IMPORTER_CLASS

alias of ReaperImporter

OPERATION = 'reap'
SERVICE_NAME = 'Metadata Wrangler Reaper'
property api_method

The method to call to fetch an OPDS feed from the remote server.

finalize_batch()[source]

Deletes Metadata Wrangler coverage records of reaped Identifiers

This allows Identifiers to be added to the collection again via MetadataWranglerCoverageProvider lookup if a license is repurchased.

items_that_need_coverage(identifiers=None, **kwargs)[source]

Retrieves Identifiers that were imported but are no longer licensed.

class api.metadata_wrangler.MetadataWranglerCollectionRegistrar(collection, lookup_client=None, **kwargs)[source]

Bases: BaseMetadataWranglerCoverageProvider

Register all Identifiers licensed to a Collection with the metadata wrangler.

If OPDS metadata is immediately returned, make use of it. Even if no metadata is returned for an Identifier, mark it as covered.

Once it’s registered, any future updates to the available metadata for a given Identifier will be detected by the MWCollectionUpdateMonitor.

OPDS_IMPORTER_CLASS

alias of RegistrarImporter

OPERATION = 'import'
SERVICE_NAME = 'Metadata Wrangler Collection Registrar'
items_that_need_coverage(identifiers=None, **kwargs)[source]

Retrieves items from the Collection that are not registered with the Metadata Wrangler.

api.millenium_patron module

api.millenium_patron.AuthenticationProvider

alias of MilleniumPatronAPI

class api.millenium_patron.MilleniumPatronAPI(library, integration, analytics=None)[source]

Bases: BasicAuthenticationProvider, XMLParser

ADDRESS_FIELD = 'ADDRESS[pa]'
AUTHENTICATION_MODE = 'auth_mode'
AUTHENTICATION_MODES = ['pin', 'family_name']
BARCODE_FIELD = 'P BARCODE[pb]'
BLOCK_FIELD = 'MBLOCK[p56]'
BLOCK_TYPES = 'block_types'
DEFAULT_CURRENCY = 'USD'
EMAIL_ADDRESS_FIELD = 'EMAIL ADDR[pz]'
ERROR_MESSAGE_FIELD = 'ERRMSG'
EXPIRATION_DATE_FORMAT = '%m-%d-%y'
EXPIRATION_FIELD = 'EXP DATE[p43]'
FAMILY_NAME_AUTHENTICATION_MODE = 'family_name'
FINES_FIELD = 'MONEY OWED[p96]'
HOME_BRANCH_FIELD = 'HOME LIBR[p53]'
HOME_BRANCH_NEIGHBORHOOD_MODE = 'home_branch'
IDENTIFIER_BLACKLIST = 'identifier_blacklist'
LIBRARY_SETTINGS = [{'key': 'http_basic_oauth_enabled', 'label': l'Enable OAuth for HTTP Basic Auth', 'description': l'Enable authentication with bearer tokens generated via basic auth credentials', 'type': 'select', 'options': [{'key': 'false', 'label': l'Disabled'}, {'key': 'true', 'label': l'Enabled'}], 'default': 'false'}, {'key': 'external_type_regular_expression', 'label': l'External Type Regular Expression', 'description': l'Derive a patron's type from their identifier.'}, {'key': 'library_identifier_restriction_type', 'label': l'Library Identifier Restriction Type', 'type': 'select', 'description': l'When multiple libraries share an ILS, a person may be able to authenticate with the ILS but not be considered a patron of <em>this</em> library. This setting contains the rule for determining whether an identifier is valid for this specific library. <p/> If this setting it set to 'No Restriction' then the values for <em>Library Identifier Field</em> and <em>Library Identifier Restriction</em> will not be used.', 'options': [{'key': 'none', 'label': l'No restriction'}, {'key': 'prefix', 'label': l'Prefix Match'}, {'key': 'string', 'label': l'Exact Match'}, {'key': 'regex', 'label': l'Regex Match'}, {'key': 'list', 'label': l'Exact Match, comma separated list'}], 'default': 'none'}, {'key': 'library_identifier_field', 'label': l'Library Identifier Field', 'description': l'This is the field on the patron record that the <em>Library Identifier Restriction Type</em> is applied to. The option 'barcode' matches the users barcode, other values are pulled directly from the patron record for example: 'P TYPE[p47]'. This value is not used if <em>Library Identifier Restriction Type</em> is set to 'No restriction'.'}, {'key': 'library_identifier_restriction', 'label': l'Library Identifier Restriction', 'description': l'This is the restriction applied to the <em>Library Identifier Field</em> using the method chosen in <em>Library Identifier Restriction Type</em>. This value is not used if <em>Library Identifier Restriction Type</em> is set to 'No restriction'.'}, {'key': 'institution_id', 'label': l'Institution ID', 'description': l'A specific identifier for the library or branch, if used in patron authentication'}]
MULTIVALUE_FIELDS = {'NOTE[px]', 'P BARCODE[pb]'}
NAME = 'Millenium'
NEIGHBORHOOD_MODE = 'neighborhood_mode'
NEIGHBORHOOD_MODES = {'disabled', 'home_branch', 'postal_code'}
NO_NEIGHBORHOOD_MODE = 'disabled'
PATRON_TYPE_FIELD = 'P TYPE[p47]'
PERSONAL_NAME_FIELD = 'PATRN NAME[pn]'
PIN_AUTHENTICATION_MODE = 'pin'
POSTAL_CODE_NEIGHBORHOOD_MODE = 'postal_code'
POSTAL_CODE_RES = [re.compile('[^0-9]([0-9]{5})-[0-9]{4}$'), re.compile('[^0-9]([0-9]{5})$'), re.compile('.*[^0-9]([0-9]{5})-[0-9]{4}[^0-9]'), re.compile('.*[^0-9]([0-9]{5})[^0-9]')]
RECORD_NUMBER_FIELD = 'RECORD #[p81]'
SETTINGS = [{'key': 'url', 'format': 'url', 'label': l'URL', 'required': True}, {'key': 'verify_certificate', 'label': l'Certificate Verification', 'type': 'select', 'options': [{'key': 'true', 'label': l'Verify Certificate Normally (Required for production)'}, {'key': 'false', 'label': l'Ignore Certificate Problems (For temporary testing only)'}], 'default': 'true'}, {'key': 'block_types', 'label': l'Block types', 'description': l'Values of MBLOCK[p56] which mean a patron is blocked. By default, any value other than '-' indicates a block.'}, {'key': 'identifier_blacklist', 'label': l'Identifier Blacklist', 'type': 'list', 'description': l'Identifiers containing any of these strings are ignored when finding the 'correct' identifier for a patron's record, even if it means they end up with no identifier at all. If librarians invalidate library cards by adding strings like "EXPIRED" or "INVALID" on to the beginning of the card number, put those strings here so the Circulation Manager knows they do not represent real card numbers.'}, {'key': 'auth_mode', 'label': l'Authentication Mode', 'type': 'select', 'options': [{'key': 'pin', 'label': l'PIN'}, {'key': 'family_name', 'label': l'Family Name'}], 'default': 'pin'}, {'key': 'neighborhood_mode', 'label': l'Patron neighborhood field', 'description': l'It's sometimes possible to guess a patron's neighborhood from their ILS record. You can use this when analyzing circulation activity by neighborhood. If you don't need to do this, it's better for patron privacy to disable this feature.', 'type': 'select', 'options': [{'key': 'disabled', 'label': l'Disable this feature'}, {'key': 'home_branch', 'label': l'Patron's home library branch is their neighborhood.'}, {'key': 'postal_code', 'label': l'Patron's postal code is their neighborhood.'}], 'default': 'disabled'}, {'key': 'test_identifier', 'label': l'Test Identifier', 'description': l'A valid identifier that can be used to test that patron authentication is working. An optional Test Password for this identifier can be set in the next section.', 'required': True}, {'key': 'test_password', 'label': l'Test Password', 'description': l'The password for the Test Identifier (above, in previous section).'}, {'key': 'identifier_barcode_format', 'label': l'Patron identifier barcode format', 'description': l'Many libraries render patron identifiers as barcodes on physical library cards. If you specify the barcode format, patrons will be able to scan their library cards with a camera instead of manually typing in their identifiers.', 'type': 'select', 'options': [{'key': 'Codabar', 'label': l'Patron identifiers are are rendered as barcodes in Codabar format'}, {'key': '', 'label': l'Patron identifiers are not rendered as barcodes'}], 'default': '', 'required': True}, {'key': 'identifier_regular_expression', 'label': l'Identifier Regular Expression', 'description': l'A patron's identifier will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'password_regular_expression', 'label': l'Password Regular Expression', 'description': l'A patron's password will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'identifier_keyboard', 'label': l'Keyboard for identifier entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Email address', 'label': l'Email address entry'}, {'key': 'Number pad', 'label': l'Number pad'}], 'default': 'Default', 'required': True}, {'key': 'password_keyboard', 'label': l'Keyboard for password entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Number pad', 'label': l'Number pad'}, {'key': 'No input', 'label': l'Patrons have no password and should not be prompted for one.'}], 'default': 'Default'}, {'key': 'identifier_maximum_length', 'label': l'Maximum identifier length', 'type': 'number'}, {'key': 'password_maximum_length', 'label': l'Maximum password length', 'type': 'number'}, {'key': 'identifier_label', 'label': l'Label for identifier entry'}, {'key': 'password_label', 'label': l'Label for password entry'}]
USERNAME_FIELD = 'ALT ID[pu]'
VERIFY_CERTIFICATE = 'verify_certificate'
classmethod extract_postal_code(address)[source]

Try to extract a postal code from an address.

classmethod family_name_match(actual_name, supposed_family_name)[source]

Does supposed_family_name match actual_name?

patron_dump_to_patrondata(current_identifier, content)[source]

Convert an HTML patron dump to a PatronData object.

Parameters:
  • current_identifier – Either the authorization identifier the patron just logged in with, or the one currently associated with their Patron record. Keeping track of this ensures we don’t change a patron’s preferred authorization identifier out from under them.

  • content – The HTML document containing the patron dump.

remote_authenticate(username, password)[source]

Does the Millenium Patron API approve of these credentials?

Returns:

False if the credentials are invalid. If they are valid, a PatronData that serves only to indicate which authorization identifier the patron prefers.

request(url, *args, **kwargs)[source]

Actually make an HTTP request. This method exists only so the mock can override it.

setting = {'description': l'A specific identifier for the library or branch, if used in patron authentication', 'key': 'institution_id', 'label': l'Institution ID'}
class api.millenium_patron.MockMilleniumPatronAPI[source]

Bases: MilleniumPatronAPI

This mocks the API on a higher level than the HTTP level.

It is not used in the tests of the MilleniumPatronAPI class. It is used in the Adobe Vendor ID tests but maybe it shouldn’t.

remote_authenticate(barcode, pin)[source]

A barcode that’s 14 digits long is treated as valid, no matter which PIN is used.

That’s so real barcode/PIN combos can be passed through to third parties.

Otherwise, valid test PIN is the first character of the barcode repeated four times.

remote_patron_lookup(patron_or_patrondata)[source]

Ask the remote for information about this patron, and then make sure the patron belongs to the library associated with thie BasicAuthenticationProvider.

the_future = datetime.datetime(2024, 8, 8, 15, 2, 40, 691291, tzinfo=<UTC>)
user1 = <PatronData permanent_id='12345' authorization_identifier='0' username='alice'>
user2 = <PatronData permanent_id='67890' authorization_identifier='5' username='bob'>
users = [<PatronData permanent_id='12345' authorization_identifier='0' username='alice'>, <PatronData permanent_id='67890' authorization_identifier='5' username='bob'>]

api.monitor module

class api.monitor.HoldReaper(*args, **kwargs)[source]

Bases: LoanlikeReaperMonitor

Remove seemingly abandoned holds from the database.

MAX_AGE = 365
MODEL_CLASS

alias of Hold

property where_clause

Find holds that were created a long time ago and either have no end date or have an end date in the past.

The ‘end date’ for a hold is just an estimate, but if the estimate is in the future it’s better to keep the hold around.

class api.monitor.IdlingAnnotationReaper(*args, **kwargs)[source]

Bases: ReaperMonitor

Remove idling annotations for inactive loans.

MAX_AGE = 60
MODEL_CLASS

alias of Annotation

TIMESTAMP_FIELD = 'timestamp'
property where_clause

The annotation must have motivation=IDLING, must be at least 60 days old (meaning there has been no attempt to read the book for 60 days), and must not be associated with one of the patron’s active loans or holds.

class api.monitor.LoanReaper(*args, **kwargs)[source]

Bases: LoanlikeReaperMonitor

Remove expired and abandoned loans from the database.

MAX_AGE = 90
MODEL_CLASS

alias of Loan

property where_clause

Find loans that have either expired, or that were created a long time ago and have no definite end date.

class api.monitor.LoanlikeReaperMonitor(*args, **kwargs)[source]

Bases: ReaperMonitor

SOURCE_OF_TRUTH_PROTOCOLS = ['ODL', 'Shared ODL For Consortia', 'OPDS for Distributors']
property where_clause

We never want to automatically reap loans or holds for situations where the circulation manager is the source of truth. If we delete something we shouldn’t have, we won’t be able to get the ‘real’ information back.

This means loans of open-access content and loans from collections based on a protocol found in SOURCE_OF_TRUTH_PROTOCOLS.

Subclasses will append extra clauses to this filter.

api.novelist module

class api.novelist.MockNoveListAPI(_db, *args, **kwargs)[source]

Bases: NoveListAPI

lookup(identifier)[source]

Requests NoveList metadata for a particular identifier

Parameters:

kwargs – Keyword arguments passed into Representation.post().

Returns:

Metadata object or None

setup_method(*args)[source]
class api.novelist.NoveListAPI(_db, profile, password)[source]

Bases: object

AUTHORIZED_IDENTIFIER = '62521fa1-bdbb-4939-84aa-aee2a52c8d59'
AUTH_PARAMS = '&profile=%(profile)s&password=%(password)s'
COLLECTION_DATA_API = 'http://www.noveListcollectiondata.com/api/collections'
IS_CONFIGURED = None
MAX_REPRESENTATION_AGE = 604800
NAME = l'Novelist API'
NO_ISBN_EQUIVALENCY = 'No clear ISBN equivalency: %r'
PROTOCOL = 'NoveList Select'
QUERY_ENDPOINT = 'https://novselect.ebscohost.com/Data/ContentByQuery?ISBN=%(ISBN)s&ClientIdentifier=%(ClientIdentifier)s&version=%(version)s'
SETTINGS = [{'key': 'username', 'label': l'Profile', 'required': True}, {'key': 'password', 'label': l'Password', 'required': True}]
SITEWIDE = False
classmethod build_query_url(params, include_auth=True)[source]

Builds a unique and url-encoded query endpoint

choose_best_metadata(metadata_objects, identifier)[source]

Chooses the most likely book metadata from a list of Metadata objects

Given several Metadata objects with different NoveList IDs, this method returns the metadata of the ID with the highest representation and a float representing confidence in the result.

create_item_object(object, currentIdentifier, existingItem)[source]

Returns a new item if the current identifier that was processed is not the same as the new object’s ISBN being processed. If the new object’s ISBN matches the current identifier, the previous object’s Author property is updated.

Parameters:
  • object – the current item object to process

  • currentIdentifier – the current identifier to process

  • existingItem – the previously processed item object

Returns:

( current identifier, the existing object if available, a new object if the item wasn’t found before, if the item is ready to the added to the list of books to send )

currentQueryIdentifier = None
classmethod from_config(library)[source]
get_items_from_query(library)[source]

Gets identifiers and its related title, medium, and authors from the database. Keeps track of the current ‘ISBN’ identifier and current item object that is being processed. If the next ISBN being processed is new, the existing one gets added to the list of items. If the ISBN is the same, then we append the Author property since there are multiple contributors.

Returns:

a list of Novelist objects to send

get_recommendations(metadata, recommendations_info)[source]
get_series_information(metadata, series_info, book_info)[source]

Returns metadata object with series info and optimal title key

classmethod is_configured(library)[source]
log = <Logger NoveList API (WARNING)>
lookup(identifier, **kwargs)[source]

Requests NoveList metadata for a particular identifier

Parameters:

kwargs – Keyword arguments passed into Representation.post().

Returns:

Metadata object or None

lookup_equivalent_isbns(identifier)[source]

Finds NoveList data for all ISBNs equivalent to an identifier.

Returns:

Metadata object or None

lookup_info_to_metadata(lookup_representation)[source]

Transforms a NoveList JSON representation into a Metadata object

make_novelist_data_object(items)[source]
medium_to_book_format_type_values = {'Audio': 'Audiobook', 'Book': 'EBook'}
put(url, headers, **kwargs)[source]
put_items_novelist(library)[source]
classmethod review_response(response)[source]

Performs NoveList-specific error review of the request response

classmethod scrubbed_url(params)[source]

Removes authentication details from cached Representation.url

property source
classmethod values(library)[source]
version = '2.2'

api.nyt module

Interface to the New York Times APIs.

class api.nyt.NYTAPI[source]

Bases: object

DATE_FORMAT = '%Y-%m-%d'
TIME_ZONE = tzfile('/usr/share/zoneinfo/America/New_York')
classmethod date_string(d)[source]
classmethod parse_date(d)[source]

Used to parse the publication date of a book.

We don’t know the timezone here, so the date will end up being stored as midnight UTC.

classmethod parse_datetime(d)[source]

Used to parse the publication date of a NYT best-seller list.

We take midnight Eastern time to be the publication time.

class api.nyt.NYTBestSellerAPI(_db, api_key=None, do_get=None, metadata_client=None)[source]

Bases: NYTAPI, HasSelfTests

BASE_URL = 'http://api.nytimes.com/svc/books/v3/lists'
CARDINALITY = 1
GOAL = 'metadata'
HISTORICAL_LIST_MAX_AGE = datetime.timedelta(days=365)
LIST_MAX_AGE = datetime.timedelta(days=1)
LIST_NAMES_URL = 'http://api.nytimes.com/svc/books/v3/lists/names.json'
LIST_OF_LISTS_MAX_AGE = datetime.timedelta(days=1)
LIST_URL = 'http://api.nytimes.com/svc/books/v3/lists.json?list=%s'
NAME = l'NYT Best Seller API'
PROTOCOL = 'New York Times'
SETTINGS = [{'key': 'password', 'label': l'API key', 'required': True}]
SITEWIDE = True
best_seller_list(list_info, date=None)[source]

Create (but don’t update) a NYTBestSellerList object.

classmethod external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

fill_in_history(list)[source]

Update the given list with current and historical data.

classmethod from_config(_db, **kwargs)[source]
list_info(list_name)[source]
list_of_lists(max_age=datetime.timedelta(days=1))[source]
request(path, identifier=None, max_age=datetime.timedelta(days=1))[source]
property source
update(list, date=None, max_age=datetime.timedelta(days=1))[source]

Update the given list with data from the given date.

class api.nyt.NYTBestSellerList(list_info, metadata_client)[source]

Bases: list

property all_dates

Yield a list of estimated dates when new editions of this list were probably published.

property medium

What medium are the books on this list?

Lists like “Audio Fiction” contain audiobooks; all others contain normal books. (TODO: this isn’t quite right; the distinction between ebooks and print books here exists in a way it doesn’t with most other sources of Editions.)

to_customlist(_db)[source]

Turn this NYTBestSeller list into a CustomList object.

update(json_data)[source]

Update the list with information from the given JSON structure.

update_custom_list(custom_list)[source]

Make sure the given CustomList’s CustomListEntries reflect the current state of the NYTBestSeller list.

class api.nyt.NYTBestSellerListTitle(data, medium)[source]

Bases: TitleFromExternalList

api.odilo module

class api.odilo.MockOdiloAPI(_db, collection, *args, **kwargs)[source]

Bases: OdiloAPI

mock_access_token_response(credential, expires_in=- 1)[source]
classmethod mock_collection(_db)[source]
patron_request(patron, pin, *args, **kwargs)[source]

Make an HTTP request on behalf of a patron.

The results are never cached.

queue_response(status_code, headers={}, content=None)[source]
token_post(url, payload, headers={}, **kwargs)[source]

Mock the request for an OAuth token.

class api.odilo.OdiloAPI(_db, collection)[source]

Bases: BaseCirculationAPI, HasSelfTests

ALL_PRODUCTS_ENDPOINT = '/records'
CHECKIN_ENDPOINT = '/checkouts/{checkoutId}/return?patronId={patronId}'
CHECKOUT_ENDPOINT = '/records/{recordId}/checkout'
DESCRIPTION = l'Integrate an Odilo library collection.'
LIBRARY_API_BASE_URL = 'library_api_base_url'
NAME = 'Odilo'
PAGE_SIZE_LIMIT = 200
PATRON_CHECKOUTS_ENDPOINT = '/patrons/{patronId}/checkouts'
PATRON_HOLDS_ENDPOINT = '/patrons/{patronId}/holds'
PLACE_HOLD_ENDPOINT = '/records/{recordId}/hold'
RECORD_AVAILABILITY_ENDPOINT = '/records/{recordId}/availability'
RECORD_METADATA_ENDPOINT = '/records/{recordId}'
RELEASE_HOLD_ENDPOINT = '/holds/{holdId}/cancel'
SETTINGS = [{'key': 'library_api_base_url', 'label': l'Library API base URL', 'description': l'This might look like <code>https://[library].odilo.us/api/v2</code>.', 'required': True, 'format': 'url'}, {'key': 'username', 'label': l'Client Key', 'required': True}, {'key': 'password', 'label': l'Client Secret', 'required': True}]
SET_DELIVERY_MECHANISM_AT = 'borrow'
TOKEN_ENDPOINT = '/token'
check_creds(force_refresh=False)[source]

If the Bearer Token has expired, update it.

checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

checkout(patron, pin, licensepool, internal_format)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Identifier of the book to be checked out is attached to this licensepool.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

property collection
credential_object(refresh)[source]

Look up the Credential object that allows us to use the Odilo API.

delivery_mechanism_to_internal_format = {('application/epub+zip', 'application/vnd.adobe.adept+xml'): 'ACSM_EPUB', ('application/pdf', 'application/vnd.adobe.adept+xml'): 'ACSM_PDF', ('application/vnd.librarysimplified.scorm+zip', None): 'SCORM', ('audio/mpeg', 'Streaming Audio'): 'MP3', ('image/jpeg', None): 'JPG', ('text/html', 'Streaming Text'): 'EBOOK_STREAMING', ('video/mp4', 'Streaming Video'): 'MP4', ('video/x-ms-wmv', 'Streaming Video'): 'WMV'}
error_to_exception = {'CHECKOUT_NOT_FOUND': <class 'api.circulation_exceptions.NotCheckedOut'>, 'ERROR_DATA_NOT_FOUND': <class 'api.circulation_exceptions.NotFoundOnRemote'>, 'LOAN_ALREADY_RESERVED': <class 'api.circulation_exceptions.AlreadyOnHold'>, 'TitleNotCheckedOut': <class 'api.circulation_exceptions.NoActiveLoan'>, 'patronNotFound': <class 'api.circulation_exceptions.PatronNotFoundOnRemote'>}
external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

classmethod extract_date(data, field_name)[source]
fulfill(patron, pin, licensepool, internal_format, **kwargs)[source]

Get the actual resource file to the patron.

Parameters:

kwargs – A container for arguments to fulfill() which are not relevant to this vendor.

Returns:

a FulfillmentInfo object.

get(url, extra_headers={}, exception_on_401=False)[source]

Make an HTTP GET request using the active Bearer Token.

get_availability(record_id)[source]
get_checkout(patron, pin, record_id)[source]

Get the link corresponding to an existing checkout.

get_hold(patron, pin, record_id)[source]
get_metadata(record_id)[source]
get_patron_checkouts(patron, pin)[source]
get_patron_holds(patron, pin)[source]
hold_from_odilo_hold(collection, hold)[source]
loan_info_from_odilo_checkout(collection, checkout)[source]
log = <Logger Odilo API (WARNING)>
patron_activity(patron, pin)[source]

Return a patron’s current checkouts and holds.

patron_request(patron, pin, url, extra_headers={}, data=None, exception_on_401=False, method=None)[source]

Make an HTTP request on behalf of a patron.

The results are never cached.

place_hold(patron, pin, licensepool, notification_email_address)[source]

Place a book on hold.

Returns:

A HoldInfo object

classmethod raise_exception_on_error(data, default_exception_class=None, ignore_exception_codes=None)[source]
refresh_creds(credential)[source]

Fetch a new Bearer Token and update the given Credential object.

release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

property source
token_post(url, payload, headers={}, **kwargs)[source]

Make an HTTP POST request for purposes of getting an OAuth token.

class api.odilo.OdiloBibliographicCoverageProvider(collection, api_class=<class 'api.odilo.OdiloAPI'>, **kwargs)[source]

Bases: BibliographicCoverageProvider

Fill in bibliographic metadata for Odilo records.

This will occasionally fill in some availability information for a single Collection, but we rely on Monitors to keep availability information up to date for all Collections.

DATA_SOURCE_NAME = 'Odilo'
INPUT_IDENTIFIER_TYPES = 'Odilo ID'
PROTOCOL = 'Odilo'
SERVICE_NAME = 'Odilo Bibliographic Coverage Provider'
process_item(record_id, record=None)[source]

Do the work necessary to give coverage to one specific item.

Since this is where the actual work happens, this is not implemented in IdentifierCoverageProvider or WorkCoverageProvider, and must be handled in a subclass.

class api.odilo.OdiloCirculationMonitor(_db, collection, api_class=<class 'api.odilo.OdiloAPI'>)[source]

Bases: CollectionMonitor, TimelineMonitor

Maintain LicensePools for recently changed Odilo titles

DEFAULT_START_TIME = <object object>
INTERVAL_SECONDS = 500
PROTOCOL = 'Odilo'
SERVICE_NAME = 'Odilo Circulation Monitor'
all_ids(modification_date=None)[source]

Get IDs for every book in the system, from modification date if any

catch_up_from(start, cutoff, progress)[source]

Find Odilo books that changed recently.

Progress:

A TimestampData representing the time previously covered by this Monitor.

get_url(limit, modification_date, offset)[source]
class api.odilo.OdiloRepresentationExtractor[source]

Bases: object

Extract useful information from Odilo’s JSON representations.

ACSM = 'ACSM'
ACSM_EPUB = 'ACSM_EPUB'
ACSM_PDF = 'ACSM_PDF'
EBOOK_STREAMING = 'EBOOK_STREAMING'
format_data_for_odilo_format = {'ACSM_EPUB': ('application/epub+zip', 'application/vnd.adobe.adept+xml'), 'ACSM_PDF': ('application/pdf', 'application/vnd.adobe.adept+xml'), 'EBOOK_STREAMING': ('text/html', 'Streaming Text'), 'JPG': ('image/jpeg', None), 'MP3': ('audio/mpeg', 'Streaming Audio'), 'MP4': ('video/mp4', 'Streaming Video'), 'SCORM': ('application/vnd.librarysimplified.scorm+zip', None), 'WMV': ('video/x-ms-wmv', 'Streaming Video')}
log = <Logger OdiloRepresentationExtractor (WARNING)>
odilo_medium_to_simplified_medium = {'ACSM_EPUB': 'Book', 'ACSM_PDF': 'Book', 'EBOOK_STREAMING': 'Book', 'JPG': 'Image', 'MP3': 'Audio', 'MP4': 'Video', 'SCORM': 'Courseware', 'WMV': 'Video'}
classmethod record_info_to_circulation(availability)[source]

Note: The json data passed into this method is from a different file/stream from the json data that goes into the record_info_to_metadata() method.

classmethod record_info_to_metadata(book, availability)[source]

Turn Odilo’s JSON representation of a book into a Metadata object.

Note: The json data passed into this method is from a different file/stream from the json data that goes into the book_info_to_circulation() method.

classmethod set_format(format_received, formats)[source]

api.odl module

class api.odl.MockODLAPI(_db, collection, *args, **kwargs)[source]

Bases: ODLAPI

Mock API for tests that overrides _get and _url_for and tracks requests.

classmethod mock_collection(_db, protocol='ODL')[source]

Create a mock ODL collection to use in tests.

queue_response(status_code, headers={}, content=None)[source]
class api.odl.MockSharedODLAPI(_db, collection, *args, **kwargs)[source]

Bases: SharedODLAPI

Mock API for tests that overrides _get and tracks requests.

classmethod mock_collection(_db)[source]

Create a mock ODL collection to use in tests.

queue_response(status_code, headers={}, content=None)[source]
class api.odl.ODLAPI(_db, collection)[source]

Bases: BaseCirculationAPI, BaseSharedCollectionAPI

ODL (Open Distribution to Libraries) is a specification that allows libraries to manage their own loans and holds. It offers a deeper level of control to the library, but it requires the circulation manager to keep track of individual copies rather than just license pools, and manage its own holds queues.

In addition to circulating books to patrons of a library on the current circulation manager, this API can be used to circulate books to patrons of external libraries. Only one circulation manager per ODL collection should use an ODLAPI - the others should use a SharedODLAPI and configure it to connect to the main circulation manager.

ACTIVE_STATUS = 'active'
CANCELLED_STATUS = 'cancelled'
DESCRIPTION = l'Import books from a distributor that uses ODL (Open Distribution to Libraries).'
EXPIRED_STATUS = 'expired'
LIBRARY_SETTINGS = [{'key': 'ebook_loan_duration', 'label': l'Ebook Loan Duration (in Days)', 'default': 21, 'type': 'number', 'description': l'When a patron uses SimplyE to borrow an ebook from this collection, SimplyE will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.'}]
NAME = 'ODL'
READY_STATUS = 'ready'
RETURNED_STATUS = 'returned'
REVOKED_STATUS = 'revoked'
SETTINGS = [{'key': 'external_account_id', 'label': l'ODL feed URL', 'required': True, 'format': 'url'}, {'key': 'username', 'label': l'Library's API username', 'required': True}, {'key': 'password', 'label': l'Library's API password', 'required': True}, {'key': 'data_source', 'label': l'Data source name', 'required': True}, {'key': 'default_reservation_period', 'label': l'Default Reservation Period (in Days)', 'description': l'The number of days a patron has to check out a book after a hold becomes available.', 'type': 'number', 'default': 3}, {'key': 'external_library_urls', 'label': l'URLs for libraries on other circulation managers that use this collection', 'description': l'A URL should include the library's short name (e.g. https://circulation.librarysimplified.org/NYNYPL/), even if it is the only library on the circulation manager.', 'type': 'list', 'format': 'url'}, {'key': 'ebook_loan_duration', 'label': l'Ebook Loan Duration for libraries on other circulation managers (in Days)', 'default': 21, 'description': l'When a patron from another library borrows an ebook from this collection, the circulation manager will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.', 'type': 'number'}]
SET_DELIVERY_MECHANISM_AT = 'fulfill'
STATUS_VALUES = ['ready', 'active', 'revoked', 'returned', 'cancelled', 'expired']
checkin(patron, pin, licensepool)[source]

Return a loan early.

checkin_from_external_library(client, loan)[source]
checkout(patron, pin, licensepool, internal_format)[source]

Create a new loan.

checkout_to_external_library(client, licensepool, hold=None)[source]
collection(_db)[source]
fulfill(patron, pin, licensepool, internal_format, **kwargs)[source]

Get the actual resource file to the patron.

Parameters:

kwargs – A container for arguments to fulfill() which are not relevant to this vendor.

Returns:

a FulfillmentInfo object.

fulfill_for_external_library(client, loan, mechanism)[source]
get_license_status_document(loan)[source]

Get the License Status Document for a loan.

For a new loan, create a local loan with no external identifier and pass it in to this method.

This will create the remote loan if one doesn’t exist yet. The loan’s internal database id will be used to receive notifications from the distributor when the loan’s status changes.

internal_format(delivery_mechanism)[source]

Each consolidated copy is only available in one format, so we don’t need a mapping to internal formats.

patron_activity(patron, pin)[source]

Look up non-expired loans for this collection in the database.

place_hold(patron, pin, licensepool, notification_email_address)[source]

Create a new hold.

release_hold(patron, pin, licensepool)[source]

Cancel a hold.

release_hold_from_external_library(client, hold)[source]
update_hold_queue(licensepool)[source]
update_loan(loan, status_doc=None)[source]

Check a loan’s status, and if it is no longer active, delete the loan and update its pool’s availability.

class api.odl.ODLExpiredItemsReaper(_db, collection)[source]

Bases: IdentifierSweepMonitor

Responsible for removing expired ODL licenses.

PROTOCOL = 'ODL'
SERVICE_NAME = 'ODL Expired Items Reaper'
process_item(identifier)[source]

Do the work that needs to be done for a given item.

class api.odl.ODLHoldReaper(_db, collection=None, api=None, **kwargs)[source]

Bases: CollectionMonitor

Check for holds that have expired and delete them, and update the holds queues for their pools.

PROTOCOL = 'ODL'
SERVICE_NAME = 'ODL Hold Reaper'
run_once(progress)[source]

Do the actual work of the Monitor.

Parameters:

progress – A TimestampData representing the work done by the Monitor up to this point.

Returns:

A TimestampData representing how you want the Monitor’s entry in the timestamps table to look like from this point on. NOTE: Modifying the incoming progress and returning it is generally a bad idea, because the incoming progress is full of old data. Instead, return a new TimestampData containing data for only the fields you want to set.

class api.odl.ODLImportMonitor(_db, collection, import_class, force_reimport=False, **import_class_kwargs)[source]

Bases: OPDSImportMonitor

Import information from an ODL feed.

PROTOCOL = 'ODL'
SERVICE_NAME = 'ODL Import Monitor'
class api.odl.ODLImporter(_db, collection, data_source_name=None, identifier_mapping=None, http_get=None, metadata_client=None, content_modifier=None, map_from_collection=None, mirrors=None)[source]

Bases: OPDSImporter

Import information and formats from an ODL feed.

The only change from OPDSImporter is that this importer extracts format information from ‘odl:license’ tags.

LICENSE_INFO_DOCUMENT_MEDIA_TYPE = 'application/vnd.odl.info+json'
NAME = 'ODL'
PARSER_CLASS

alias of ODLXMLParser

classmethod parse_license(identifier: str, total_checkouts: Optional[int], concurrent_checkouts: Optional[int], expires: Optional[datetime], checkout_link: Optional[str], odl_status_link: Optional[str], do_get: Callable) Optional[LicenseData][source]

Check the license’s attributes passed as parameters: - if they’re correct, turn them into a LicenseData object - otherwise, return a None

Parameters:
  • identifier – License’s identifier

  • total_checkouts – Total number of checkouts before the license expires

  • concurrent_checkouts – Number of concurrent checkouts allowed for this license

  • expires – Date & time until the license is valid

  • checkout_link – License’s checkout link

  • odl_status_link – License Info Document’s link

  • do_get – Callback performing HTTP GET method

Returns:

LicenseData if all the license’s attributes are correct, None, otherwise

class api.odl.ODLXMLParser[source]

Bases: OPDSXMLParser

NAMESPACES = {'app': 'http://www.w3.org/2007/app', 'atom': 'http://www.w3.org/2005/Atom', 'dc': 'http://purl.org/dc/elements/1.1/', 'dcterms': 'http://purl.org/dc/terms/', 'drm': 'http://librarysimplified.org/terms/drm', 'odl': 'http://opds-spec.org/odl', 'opds': 'http://opds-spec.org/2010/catalog', 'schema': 'http://schema.org/', 'simplified': 'http://librarysimplified.org/terms/'}
class api.odl.SharedODLAPI(_db, collection)[source]

Bases: BaseCirculationAPI

An API for circulation managers to use to connect to an ODL collection that’s shared by another circulation manager.

DESCRIPTION = l'Import books from an ODL collection that's hosted by another circulation manager in the consortium. If this circulation manager will be the main host for the collection, select ODL instead.'
NAME = 'Shared ODL For Consortia'
SETTINGS = [{'key': 'external_account_id', 'label': l'Base URL', 'description': l'The base URL for the collection on the other circulation manager.', 'required': True}, {'key': 'data_source', 'label': l'Data source name', 'required': True}]
SUPPORTS_REGISTRATION = True
SUPPORTS_STAGING = False
checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

checkout(patron, pin, licensepool, internal_format)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

collection(_db)[source]
fulfill(patron, pin, licensepool, internal_format, **kwargs)[source]

Get the actual resource file to the patron.

Parameters:

kwargs – A container for arguments to fulfill() which are not relevant to this vendor.

Returns:

a FulfillmentInfo object.

internal_format(delivery_mechanism)[source]

Each consolidated copy is only available in one format, so we don’t need a mapping to internal formats.

patron_activity(patron, pin)[source]

Return a patron’s current checkouts and holds.

place_hold(patron, pin, licensepool, notification_email_address)[source]

Place a book on hold.

Returns:

A HoldInfo object

release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Raises:

CannotReleaseHold – If there is an error communicating with the provider, or the provider refuses to release the hold for any reason.

class api.odl.SharedODLImportMonitor(_db, collection, import_class, force_reimport=False, **import_class_kwargs)[source]

Bases: OPDSImportMonitor

PROTOCOL = 'Shared ODL For Consortia'
SERVICE_NAME = 'Shared ODL Import Monitor'
opds_url(collection)[source]

Returns the OPDS import URL for the given collection.

By default, this URL is stored as the external account ID, but subclasses may override this.

class api.odl.SharedODLImporter(_db, collection, data_source_name=None, identifier_mapping=None, http_get=None, metadata_client=None, content_modifier=None, map_from_collection=None, mirrors=None)[source]

Bases: OPDSImporter

NAME = 'Shared ODL For Consortia'
classmethod get_fulfill_url(entry, requested_content_type, requested_drm_scheme)[source]

api.onix module

class api.onix.ONIXExtractor[source]

Bases: object

Transform an ONIX file into a list of Metadata objects.

AUDIENCE_TYPES = {'01': 'Adult', '02': 'Children', '03': 'Young Adult', '04': 'Children', '05': 'Adult', '06': 'Adult', '07': 'Adult', '08': 'Adult', '09': 'Adult'}
CONTRIBUTOR_TYPES = {'A01': 'Author', 'A02': 'Author', 'A03': 'Author', 'A04': 'Lyricist', 'A05': 'Lyricist', 'A06': 'Composer', 'A07': 'Illustrator', 'A08': 'Photographer', 'A09': 'Author', 'A10': 'Unknown', 'A11': 'Designer', 'A12': 'Illustrator', 'A13': 'Photographer', 'A14': 'Author', 'A15': 'Introduction Author', 'A16': 'Unknown', 'A17': 'Unknown', 'A18': 'Unknown', 'A19': 'Afterword Author', 'A20': 'Unknown', 'A21': 'Unknown', 'A22': 'Unknown', 'A23': 'Foreword Author', 'A24': 'Introduction Author', 'A25': 'Unknown', 'A26': 'Unknown', 'A27': 'Unknown', 'A29': 'Introduction Author', 'A30': 'Unknown', 'A31': 'Lyricist', 'A32': 'Contributor', 'A33': 'Unknown', 'A34': 'Unknown', 'A35': 'Artist', 'A36': 'Artist', 'A37': 'Unknown', 'A38': 'Unknown', 'A39': 'Unknown', 'A40': 'Artist', 'A41': 'Unknown', 'A42': 'Unknown', 'A43': 'Unknown', 'A44': 'Unknown', 'A45': 'Author', 'A46': 'Artist', 'A47': 'Artist', 'A48': 'Artist', 'A51': 'Unknown', 'A99': 'Unknown', 'B01': 'Editor', 'B02': 'Editor', 'B03': 'Unknown', 'B04': 'Unknown', 'B05': 'Adapter', 'B06': 'Translator', 'B07': 'Unknown', 'B08': 'Translator', 'B09': 'Editor', 'B10': 'Translator', 'B11': 'Editor', 'B12': 'Editor', 'B13': 'Editor', 'B14': 'Editor', 'B15': 'Editor', 'B16': 'Editor', 'B17': 'Editor', 'B18': 'Editor', 'B19': 'Editor', 'B20': 'Editor', 'B21': 'Editor', 'B22': 'Unknown', 'B23': 'Editor', 'B24': 'Editor', 'B25': 'Composer', 'B26': 'Editor', 'B27': 'Unknown', 'B28': 'Unknown', 'B29': 'Editor', 'B30': 'Unknown', 'B31': 'Unknown', 'B99': 'Editor', 'C01': 'Unknown', 'C02': 'Unknown', 'C03': 'Unknown', 'C04': 'Unknown', 'C99': 'Unknown', 'D01': 'Producer', 'D02': 'Director', 'D03': 'Musician', 'D04': 'Unknown', 'D05': 'Director', 'E01': 'Actor', 'E02': 'Performer', 'E03': 'Narrator', 'E04': 'Unknown', 'E05': 'Performer', 'E06': 'Performer', 'E07': 'Narrator', 'E08': 'Performer', 'E09': 'Performer', 'E10': 'Unknown', 'E99': 'Performer', 'F01': 'Photographer', 'F02': 'Editor', 'F99': 'Unknown', 'Z01': 'Unknown', 'Z02': 'Unknown', 'Z99': 'Unknown'}
PRODUCT_CONTENT_TYPES = {'01': 'Audio', '10': 'Book'}
SUBJECT_TYPES = {'01': 'DDC', '03': 'LCC', '04': 'LCSH', '10': 'BISAC', '12': 'BIC'}
classmethod parse(file, data_source_name, default_medium=None)[source]
class api.onix.UsageStatus(value)[source]

Bases: Enum

An enumeration.

LIMITED = '02'
PROHIBITED = '03'
UNLIMITED = '01'
class api.onix.UsageUnit(value)[source]

Bases: Enum

An enumeration.

CHARACTERS = '02'
CONCURRENT_USERS = '07'
COPIES = '01'
DAYS = '09'
DEVICES = '06'
PAGES = '04'
PERCENTAGE = '05'
PERCENTAGE_PER_TIME_PERIOD = '08'
TIMES = '10'
WORDS = '03'

api.opds module

class api.opds.CirculationManagerAnnotator(lane, active_loans_by_work={}, active_holds_by_work={}, active_fulfillments_by_work={}, hidden_content_types=[], test_mode=False)[source]

Bases: Annotator

Generate a number of <link> tags that enumerate all acquisition methods.

Parameters:

direct_fulfillment_delivery_mechanisms – A way to fulfill each LicensePoolDeliveryMechanism in this list will be presented as a link with rel=”http://opds-spec.org/acquisition/open-access”, indicating that it can be downloaded with no intermediate steps such as authentication.

active_licensepool_for(work)[source]

Which license pool would be/has been used to issue a license for this work?

annotate_work_entry(work, active_license_pool, edition, identifier, feed, entry, updated=None)[source]

Make any custom modifications necessary to integrate this OPDS entry into the application’s workflow.

Work:

The Work whose OPDS entry is being annotated.

Active_license_pool:

Of all the LicensePools associated with this Work, the client has expressed interest in this one.

Edition:

The Edition to use when associating bibliographic metadata with this entry. You will probably not need to use this, because bibliographic metadata was associated with the entry when it was created.

Identifier:

Of all the Identifiers associated with this Work, the client has expressed interest in this one.

Parameters:
  • feed – An OPDSFeed – the feed in which this entry will be situated.

  • entry – An lxml Element object, the entry that will be added to the feed.

cdn_url_for(*args, **kwargs)[source]
default_lane_url()[source]
facet_url(facets)[source]
feed_url(lane, facets=None, pagination=None, default_route='feed', extra_kwargs=None)[source]
is_work_entry_solo(work)[source]
Return a boolean value indicating whether the work’s OPDS catalog entry is served by itself,

rather than as a part of the feed.

Parameters:

work (core.model.work.Work) – Work object

Returns:

Boolean value indicating whether the work’s OPDS catalog entry is served by itself, rather than as a part of the feed

Return type:

bool

lane_url(lane)[source]
navigation_url(lane)[source]
rights_attributes(lpdm)[source]

Create a dictionary of tag attributes that explain the rights status of a LicensePoolDeliveryMechanism.

If nothing is known, the dictionary will be empty.

test_url_for(cdn=False, *args, **kwargs)[source]
top_level_title()[source]
url_for(*args, **kwargs)[source]
visible_delivery_mechanisms(licensepool)[source]

Filter the given licensepool’s LicensePoolDeliveryMechanisms to those with content types that are not hidden.

class api.opds.LibraryAnnotator(circulation, lane, library, patron=None, active_loans_by_work={}, active_holds_by_work={}, active_fulfillments_by_work={}, facet_view='feed', test_mode=False, top_level_title='All Books', library_identifies_patrons=True, facets=None)[source]

Bases: CirculationManagerAnnotator

ABOUT = 'about'
COPYRIGHT = 'copyright'
LICENSE = 'license'
PRIVACY_POLICY = 'privacy-policy'
REGISTER = 'register'
TERMS_OF_SERVICE = 'terms-of-service'

Generate one or more <link> tags that can be used to borrow, reserve, or fulfill a book, depending on the state of the book and the current patron.

Parameters:
  • active_license_pool – The LicensePool for which we’re trying to generate <link> tags.

  • active_loan – A Loan object representing the current patron’s existing loan for this title, if any.

  • active_hold – A Hold object representing the current patron’s existing hold on this title, if any.

  • active_fulfillment – A LicensePoolDeliveryMechanism object representing the mechanism, if any, which the patron has chosen to fulfill this work.

  • feed – The OPDSFeed that will eventually contain these <link> tags.

  • identifier – The Identifier of the title for which we’re trying to generate <link> tags.

  • direct_fulfillment_delivery_mechanisms – A list of LicensePoolDeliveryMechanisms for the given LicensePool that should have fulfillment-type <link> tags generated for them, even if this method wouldn’t normally think that makes sense.

  • mock_api – A mock object to stand in for the API to the vendor who provided this LicensePool. If this is not provided, a live API for that vendor will be used.

Create a <link> tag that points to the circulation manager’s Authentication for OPDS document for the current library.

Find all the <author> tags and add a link to each one that points to the author’s other works.

add_patron(feed_obj)[source]
adobe_id_tags(patron_identifier)[source]

Construct tags using the DRM Extensions for OPDS standard that explain how to get an Adobe ID for this patron, and how to manage their list of device IDs. :param delivery_mechanism: A DeliveryMechanism :return: If Adobe Vendor ID delegation is configured, a list containing a <drm:licensor> tag. If not, an empty list.

annotate_feed(feed, lane)[source]

Make any custom modifications necessary to integrate this OPDS feed into the application’s workflow.

annotate_work_entry(work, active_license_pool, edition, identifier, feed, entry)[source]

Make any custom modifications necessary to integrate this OPDS entry into the application’s workflow.

Work:

The Work whose OPDS entry is being annotated.

Active_license_pool:

Of all the LicensePools associated with this Work, the client has expressed interest in this one.

Edition:

The Edition to use when associating bibliographic metadata with this entry. You will probably not need to use this, because bibliographic metadata was associated with the entry when it was created.

Identifier:

Of all the Identifiers associated with this Work, the client has expressed interest in this one.

Parameters:
  • feed – An OPDSFeed – the feed in which this entry will be situated.

  • entry – An lxml Element object, the entry that will be added to the feed.

default_lane_url(facets=None)[source]
drm_device_registration_tags(license_pool, active_loan, delivery_mechanism)[source]

Construct OPDS Extensions for DRM tags that explain how to register a device with the DRM server that manages this loan. :param delivery_mechanism: A DeliveryMechanism

feed_url(lane, facets=None, pagination=None, default_route='feed')[source]

Create a new fulfillment link.

This link may include tags from the OPDS Extensions for DRM.

group_uri(work, license_pool, identifier)[source]

The URI to be associated with this Work when making it part of a grouped feed.

By default, this does nothing. See circulation/LibraryAnnotator for a subclass that does something.

Returns:

A 2-tuple (URI, title)

groups_url(lane, facets=None)[source]
lane_url(lane, facets=None)[source]
language_and_audience_key_from_work(work)[source]

Generate a permanent link a client can follow for information about this entry, and only this entry.

Note that permalink is distinct from the Atom <id>, which is always the identifier’s URN.

Returns:

A 2-tuple (URL, media type). If a single value is returned, the media type will be presumed to be that of an OPDS entry.

classmethod related_books_available(work, library)[source]
Returns:

bool asserting whether related books might exist for a particular Work

search_url(lane, query, pagination, facets=None)[source]
top_level_title()[source]
class api.opds.LibraryLoanAndHoldAnnotator(circulation, lane, library, patron=None, active_loans_by_work={}, active_holds_by_work={}, active_fulfillments_by_work={}, facet_view='feed', test_mode=False, top_level_title='All Books', library_identifies_patrons=True, facets=None)[source]

Bases: LibraryAnnotator

classmethod active_loans_for(circulation, patron, test_mode=False, **response_kwargs)[source]
annotate_feed(feed, lane)[source]

Annotate the feed with top-level DRM device registration tags and a link to the User Profile Management Protocol endpoint.

drm_device_registration_feed_tags(patron)[source]

Return tags that provide information on DRM device deregistration independent of any particular loan. These tags will go under the <feed> tag.

This allows us to deregister an Adobe ID, in preparation for logout, even if there is no active loan that requires one.

classmethod single_item_feed(circulation, item, fulfillment=None, test_mode=False, feed_class=<class 'core.opds.AcquisitionFeed'>, **response_kwargs)[source]

Construct a response containing a single OPDS entry representing an active loan or hold.

Parameters:
  • circulation – A CirculationAPI

  • item – A Loan, Hold, or LicensePool if first two are missing.

  • fulfillment – A FulfillmentInfo representing the format in which an active loan should be fulfilled.

  • test_mode – Passed along to the constructor for this annotator class.

  • feed_class – A drop-in replacement for AcquisitionFeed, for use in tests.

  • response_kwargs – Extra keyword arguments to be passed into the OPDSEntryResponse constructor.

Returns:

An OPDSEntryResponse

Create a <link> tag that points to the circulation manager’s User Profile Management Protocol endpoint for the current patron.

class api.opds.SharedCollectionAnnotator(collection, lane, active_loans_by_work={}, active_holds_by_work={}, active_fulfillments_by_work={}, test_mode=False)[source]

Bases: CirculationManagerAnnotator

Generate a number of <link> tags that enumerate all acquisition methods.

default_lane_url()[source]
feed_url(lane, facets=None, pagination=None, default_route='feed')[source]

Create a new fulfillment link.

lane_url(lane)[source]
top_level_title()[source]
class api.opds.SharedCollectionLoanAndHoldAnnotator(collection, lane, active_loans_by_work={}, active_holds_by_work={}, active_fulfillments_by_work={}, test_mode=False)[source]

Bases: SharedCollectionAnnotator

classmethod single_item_feed(collection, item, fulfillment=None, test_mode=False, feed_class=<class 'core.opds.AcquisitionFeed'>, **response_kwargs)[source]

Create an OPDS entry representing a single loan or hold.

TODO: This and LibraryLoanAndHoldAnnotator.single_item_feed can potentially be refactored. The main obstacle is different routes and arguments for ‘loan info’ and ‘hold info’.

Returns:

An OPDSEntryResponse

api.opds_for_distributors module

class api.opds_for_distributors.MockOPDSForDistributorsAPI(_db, collection, *args, **kwargs)[source]

Bases: OPDSForDistributorsAPI

classmethod mock_collection(_db)[source]

Create a mock OPDS For Distributors collection to use in tests.

queue_response(status_code, headers={}, content=None)[source]
class api.opds_for_distributors.OPDSForDistributorsAPI(_db, collection)[source]

Bases: BaseCirculationAPI, HasSelfTests

DESCRIPTION = l'Import books from a distributor that requires authentication to get the OPDS feed and download books.'
NAME = 'OPDS for Distributors'
SETTINGS = [{'key': 'external_account_id', 'label': l'URL', 'required': True, 'format': 'url'}, {'key': 'data_source', 'label': l'Data source name', 'required': True}, {'key': 'default_audience', 'label': l'Default audience', 'description': l'If the vendor does not specify the target audience for their books, assume the books have this target audience.', 'type': 'select', 'format': 'narrow', 'options': [{'key': '', 'label': l'No default audience'}, {'key': 'Adult', 'label': 'Adult'}, {'key': 'Adults Only', 'label': 'Adults Only'}, {'key': 'All Ages', 'label': 'All Ages'}, {'key': 'Children', 'label': 'Children'}, {'key': 'Research', 'label': 'Research'}, {'key': 'Young Adult', 'label': 'Young Adult'}], 'default': '', 'required': False, 'readOnly': True}, {'key': 'username', 'label': l'Library's username or access key', 'required': True}, {'key': 'password', 'label': l'Library's password or secret key', 'required': True}]
SUPPORTED_MEDIA_TYPES = ['application/epub+zip', 'application/pdf', 'application/audiobook+json']
can_fulfill_without_loan(patron, licensepool, lpdm)[source]

Since OPDS For Distributors delivers books to the library rather than creating loans, any book can be fulfilled without identifying the patron, assuming the library’s policies allow it.

Just to be safe, though, we require that the DeliveryMechanism’s drm_scheme be either ‘no DRM’ or ‘bearer token’, since other DRM schemes require identifying a patron.

checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

checkout(patron, pin, licensepool, internal_format)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

delivery_mechanism_to_internal_format = {('application/audiobook+json', 'application/vnd.librarysimplified.bearer-token+json'): 'application/audiobook+json', ('application/epub+zip', 'application/vnd.librarysimplified.bearer-token+json'): 'application/epub+zip', ('application/pdf', 'application/vnd.librarysimplified.bearer-token+json'): 'application/pdf'}
external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

fulfill(patron, pin, licensepool, internal_format, **kwargs)[source]

Retrieve a bearer token that can be used to download the book.

Parameters:

kwargs – A container for arguments to fulfill() which are not relevant to this vendor.

Returns:

a FulfillmentInfo object.

patron_activity(patron, pin)[source]

Return a patron’s current checkouts and holds.

class api.opds_for_distributors.OPDSForDistributorsImportMonitor(_db, collection, import_class, **kwargs)[source]

Bases: OPDSImportMonitor

Monitor an OPDS feed that requires or allows authentication, such as Biblioboard or Plympton.

PROTOCOL = 'OPDS for Distributors'
class api.opds_for_distributors.OPDSForDistributorsImporter(_db, collection, data_source_name=None, identifier_mapping=None, http_get=None, metadata_client=None, content_modifier=None, map_from_collection=None, mirrors=None)[source]

Bases: OPDSImporter

NAME = 'OPDS for Distributors'
update_work_for_edition(*args, **kwargs)[source]

After importing a LicensePool, set its availability appropriately. Books imported through OPDS For Distributors are not open-access, but a library that can perform this import has a license for the title and can distribute unlimited copies.

class api.opds_for_distributors.OPDSForDistributorsReaperMonitor(_db, collection, import_class, **kwargs)[source]

Bases: OPDSForDistributorsImportMonitor

This is an unusual import monitor that crawls the entire OPDS feed and keeps track of every identifier it sees, to find out if anything has been removed from the collection.

feed_contains_new_data(feed)[source]

Does the given feed contain any entries that haven’t been imported yet?

import_one_feed(feed)[source]

Import every book mentioned in an OPDS feed.

run_once(progress)[source]

Check to see if any identifiers we know about are no longer present on the remote. If there are any, remove them.

Parameters:

progress – A TimestampData, ignored.

api.overdrive module

class api.overdrive.MockOverdriveAPI(_db, collection, *args, **kwargs)[source]

Bases: MockOverdriveAPI, OverdriveAPI

collection_token = 'fake token'
library_data = '{"id":1810,"name":"My Public Library (MA)","type":"Library","collectionToken":"1a09d9203","links":{"self":{"href":"http://api.overdrive.com/v1/libraries/1810","type":"application/vnd.overdrive.api+json"},"products":{"href":"http://api.overdrive.com/v1/collections/1a09d9203/products","type":"application/vnd.overdrive.api+json"},"dlrHomepage":{"href":"http://ebooks.nypl.org","type":"text/html"}},"formats":[{"id":"audiobook-wma","name":"OverDrive WMA Audiobook"},{"id":"ebook-pdf-adobe","name":"Adobe PDF eBook"},{"id":"ebook-mediado","name":"MediaDo eBook"},{"id":"ebook-epub-adobe","name":"Adobe EPUB eBook"},{"id":"ebook-kindle","name":"Kindle Book"},{"id":"audiobook-mp3","name":"OverDrive MP3 Audiobook"},{"id":"ebook-pdf-open","name":"Open PDF eBook"},{"id":"ebook-overdrive","name":"OverDrive Read"},{"id":"video-streaming","name":"Streaming Video"},{"id":"ebook-epub-open","name":"Open EPUB eBook"}]}'
patron_request(patron, pin, *args, **kwargs)[source]

Make an HTTP request on behalf of a patron.

The results are never cached.

token_data = '{"access_token":"foo","token_type":"bearer","expires_in":3600,"scope":"LIB META AVAIL SRCH"}'
class api.overdrive.MockOverdriveResponse(status_code, headers, content)[source]

Bases: object

json()[source]
class api.overdrive.NewTitlesOverdriveCollectionMonitor(_db, collection, api_class=<class 'api.overdrive.OverdriveAPI'>, analytics_class=<class 'core.analytics.Analytics'>)[source]

Bases: OverdriveCirculationMonitor

Monitor the Overdrive collection for newly added titles.

This catches any new titles that slipped through the cracks of the RecentOverdriveCollectionMonitor.

DEFAULT_START_TIME = <object object>
OVERLAP = datetime.timedelta(days=7)
SERVICE_NAME = 'Overdrive New Title Monitor'
recently_changed_ids(start, cutoff)[source]

Ignore the dates and return all IDs.

should_stop(start, api_description, is_changed)[source]
class api.overdrive.OverdriveAPI(_db, collection)[source]

Bases: OverdriveAPI, BaseCirculationAPI, HasSelfTests, OverdriveAPIConstants

CHILD_SETTINGS = [{'key': 'external_account_id', 'label': l'Library ID', 'required': True}]
DEFAULT_ERROR_URL = 'http://librarysimplified.org/'
DESCRIPTION = l'Integrate an Overdrive collection. For an Overdrive Advantage collection, select the consortium's Overdrive collection as the parent.'
ERROR_MESSAGE_TO_EXCEPTION = {'PatronHasExceededCheckoutLimit': <class 'api.circulation_exceptions.PatronLoanLimitReached'>, 'PatronHasExceededCheckoutLimit_ForCPC': <class 'api.circulation_exceptions.PatronLoanLimitReached'>}
LIBRARY_SETTINGS = [{'key': 'ils_name', 'label': l'ILS Name', 'default': 'default', 'description': l'When multiple libraries share an Overdrive account, Overdrive uses a setting called 'ILS Name' to determine which ILS to check when validating a given patron.'}, {'key': 'ebook_loan_duration', 'label': l'Default Loan Period (in Days)', 'default': 21, 'type': 'number', 'description': l'Until it hears otherwise from the distributor, this server will assume that any given loan for this library from this collection will last this number of days. This number is usually a negotiated value between the library and the distributor. This only affects estimates&mdash;it cannot affect the actual length of loans.'}]
LOCK_IN_FORMATS = ['ebook-epub-open', 'ebook-epub-adobe', 'ebook-pdf-adobe', 'ebook-pdf-open']
NAME = 'Overdrive'
SETTINGS = [{'key': 'external_account_id', 'label': l'Library ID', 'required': True}, {'key': 'website_id', 'label': l'Website ID', 'required': True}, {'key': 'username', 'label': l'Client Key', 'required': True}, {'key': 'password', 'label': l'Client Secret', 'required': True}, {'key': 'server_nickname', 'label': l'Server family', 'description': l'Unless you hear otherwise from Overdrive, your integration should use their production servers.', 'type': 'select', 'options': [{'label': l'Production', 'key': 'production'}, {'label': l'Testing', 'key': 'testing'}], 'default': 'production'}]
SET_DELIVERY_MECHANISM_AT = 'fulfill'
adobe_drm = 'application/vnd.adobe.adept+xml'
checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

checkout(patron, pin, licensepool, internal_format)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Identifier of the book to be checked out is attached to this licensepool.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

circulation_lookup(book)[source]
default_notification_email_address(patron, pin)[source]

Find the email address this patron wants to use for hold notifications.

Returns:

The email address Overdrive has on record for this patron’s hold notifications, or None if there is no such address.

delivery_mechanism_to_internal_format = {('application/epub+zip', None): 'ebook-epub-open', ('application/epub+zip', 'application/vnd.adobe.adept+xml'): 'ebook-epub-adobe', ('application/pdf', None): 'ebook-pdf-open', ('application/pdf', 'application/vnd.adobe.adept+xml'): 'ebook-pdf-adobe', ('Streaming Text', 'Streaming'): 'ebook-overdrive', ('Streaming Audio', 'Streaming'): 'audiobook-overdrive', ('application/vnd.overdrive.circulation.api+json;profile=audiobook', 'Libby DRM'): 'audiobook-overdrive-manifest'}
epub = 'application/epub+zip'
error_to_exception = {'TitleNotCheckedOut': <class 'api.circulation_exceptions.NoActiveLoan'>}
external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

classmethod extract_data_from_checkout_response(checkout_response_json, format_type, error_url)[source]
classmethod extract_data_from_hold_response(hold_response_json)[source]

Extract a download link from the given format descriptor.

Parameters:
  • format – A JSON document describing a specific format in which Overdrive makes a book available.

  • error_url – Value to interpolate for the {errorpageurl} URI template value. This is ignored if you’re fetching a manifest; instead, the ‘errorpageurl’ variable is removed entirely.

  • fetch_manifest – If this is true, the download link will be modified to a URL that an authorized mobile client can use to fetch a manifest file.

classmethod extract_expiration_date(data)[source]
fill_out_form(**values)[source]
fulfill(patron, pin, licensepool, internal_format, **kwargs)[source]

Get the actual resource file to the patron.

Parameters:

kwargs – A container for arguments to fulfill() which are not relevant to this vendor.

Returns:

a FulfillmentInfo object.

Extract a download link from the given response.

Parameters:
  • checkout_response – A JSON document describing a checkout-type response from the Overdrive API.

  • format_type – The internal (Overdrive-facing) format type that should be retrieved. ‘x-manifest’ format types are treated as a variant of the ‘x’ format type – Overdrive doesn’t recognise ‘x-manifest’ and uses ‘x’ for delivery of both streaming content and manifests.

  • error_url – Value to interpolate for the {errorpageurl} URI template value. This is ignored if you’re fetching a manifest; instead, the ‘errorpageurl’ variable is removed entirely.

Get the link to the ACSM or manifest for an existing loan.

get_hold(patron, pin, overdrive_id)[source]
get_loan(patron, pin, overdrive_id)[source]
get_loans(patron, pin)[source]

Get a JSON structure describing all of a patron’s outstanding loans.

get_patron_checkouts(patron, pin)[source]
get_patron_credential(patron, pin)[source]

Create an OAuth token for the given patron.

get_patron_holds(patron, pin)[source]
get_patron_information(patron, pin)[source]
libby_drm = 'Libby DRM'
lock_in_format(patron, pin, overdrive_id, format_type)[source]

Convert an Overdrive Read or Overdrive Listen link template to a direct-download link for the manifest.

This means removing any templated arguments for Overdrive Read authentication URL and error URL; and adding a value for the ‘contentfile’ argument.

Parameters:

link – An Overdrive Read or Overdrive Listen template link.

no_drm = None
overdrive_audiobook_manifest = 'application/vnd.overdrive.circulation.api+json;profile=audiobook'
patron_activity(patron, pin)[source]

Return a patron’s current checkouts and holds.

patron_request(patron, pin, url, extra_headers={}, data=None, exception_on_401=False, method=None)[source]

Make an HTTP request on behalf of a patron.

The results are never cached.

pdf = 'application/pdf'
perform_early_return(patron, pin, loan, http_get=None)[source]

Ask Overdrive for a loanEarlyReturnURL for the given loan and try to hit that URL.

Parameters:
  • patron – A Patron

  • pin – Authorization PIN for the patron

  • loan – A Loan object corresponding to the title on loan.

  • http_get – You may pass in a mock of HTTP.get_with_timeout for use in tests.

place_hold(patron, pin, licensepool, notification_email_address)[source]

Place a book on hold.

Returns:

A HoldData object, if a hold was successfully placed, or the book was already on hold.

Raise:

A CirculationException explaining why no hold could be placed.

classmethod process_checkout_data(checkout, collection)[source]

Convert one checkout from Overdrive’s list of checkouts into a LoanInfo object.

Returns:

A LoanInfo object if the book can be fulfilled by the default Library Simplified client, and None otherwise.

process_place_hold_response(response, patron, pin, licensepool)[source]

Process the response to a HOLDS_ENDPOINT request.

Returns:

A HoldData object, if a hold was successfully placed, or the book was already on hold.

Raise:

A CirculationException explaining why no hold could be placed.

raise_exception_on_error(data, custom_error_to_exception={})[source]
refresh_patron_access_token(credential, patron, pin)[source]

Request an OAuth bearer token that allows us to act on behalf of a specific patron.

Documentation: https://developer.overdrive.com/apis/patron-auth

release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Raises:

CannotReleaseHold – If there is an error communicating with Overdrive, or Overdrive refuses to release the hold for any reason.

scope_string(library)[source]

Create the Overdrive scope string for the given library.

This is used when setting up Patron Authentication, and when generating the X-Overdrive-Scope header used by SimplyE to set up its own Patron Authentication.

streaming_audio = 'Streaming Audio'
streaming_drm = 'Streaming'
streaming_text = 'Streaming Text'
update_availability(licensepool)[source]

Update availability information for a book.

update_formats(licensepool)[source]

Update the format information for a single book.

Incidentally updates the metadata, just in case Overdrive has changed it.

update_licensepool(book_id)[source]

Update availability information for a single book.

If the book has never been seen before, a new LicensePool will be created for the book.

The book’s LicensePool will be updated with current circulation information. Bibliographic coverage will be ensured for the Overdrive Identifier, and a Work will be created for the LicensePool and set as presentation-ready.

update_licensepool_with_book_info(book, license_pool, is_new_pool)[source]

Update a book’s LicensePool with information from a JSON representation of its circulation info.

Then, create an Edition and make sure it has bibliographic coverage. If the new Edition is the only candidate for the pool’s presentation_edition, promote it to presentation status.

class api.overdrive.OverdriveAPIConstants[source]

Bases: object

MANIFEST_INTERNAL_FORMATS = {'audiobook-overdrive-manifest', 'ebook-overdrive-manifest'}
STREAMING_FORMATS = ['ebook-overdrive', 'audiobook-overdrive']
class api.overdrive.OverdriveAdvantageAccountListScript(_db=None)[source]

Bases: Script

explain_advantage_collection(collection)[source]

Explain a single Overdrive Advantage collection.

explain_main_collection(collection)[source]

Explain an Overdrive collection and all of its Advantage collections.

run()[source]

Explain every Overdrive collection and, for each one, all of its Advantage collections.

class api.overdrive.OverdriveCirculationMonitor(_db, collection, api_class=<class 'api.overdrive.OverdriveAPI'>, analytics_class=<class 'core.analytics.Analytics'>)[source]

Bases: CollectionMonitor, TimelineMonitor

Maintain LicensePools for recently changed Overdrive titles. Create basic Editions for any new LicensePools that show up.

OVERLAP = datetime.timedelta(seconds=60)
PROTOCOL = 'Overdrive'
SERVICE_NAME = 'Overdrive Circulation Monitor'
catch_up_from(start, cutoff, progress)[source]

Find Overdrive books that changed recently.

Progress:

A TimestampData representing the time previously covered by this Monitor.

recently_changed_ids(start, cutoff)[source]
class api.overdrive.OverdriveCollectionReaper(_db, collection, api_class=<class 'api.overdrive.OverdriveAPI'>)[source]

Bases: IdentifierSweepMonitor

Check for books that are in the local collection but have left our Overdrive collection.

PROTOCOL = 'Overdrive'
SERVICE_NAME = 'Overdrive Collection Reaper'
process_item(identifier)[source]

Do the work that needs to be done for a given item.

class api.overdrive.OverdriveFormatSweep(_db, collection, api_class=<class 'api.overdrive.OverdriveAPI'>)[source]

Bases: IdentifierSweepMonitor

Check the current formats of every Overdrive book in our collection.

DEFAULT_BATCH_SIZE = 25
PROTOCOL = 'Overdrive'
SERVICE_NAME = 'Overdrive Format Sweep'
process_item(identifier)[source]

Do the work that needs to be done for a given item.

class api.overdrive.OverdriveManifestFulfillmentInfo(collection, content_link, overdrive_identifier, scope_string)[source]

Bases: FulfillmentInfo

property as_response

Bypass the normal process of creating a Flask Response.

Returns:

A Response object, or None if you’re okay with the normal process.

class api.overdrive.RecentOverdriveCollectionMonitor(*args, **kwargs)[source]

Bases: OverdriveCirculationMonitor

Monitor recently changed books in the Overdrive collection.

MAXIMUM_CONSECUTIVE_UNCHANGED_BOOKS = 100
SERVICE_NAME = 'Reverse Chronological Overdrive Collection Monitor'
should_stop(start, api_description, is_changed)[source]

api.problem_details module

api.rbdigital module

class api.rbdigital.AudiobookManifest(content_dict, fulfill_part_url, **kwargs)[source]

Bases: AudiobookManifest

A standard AudiobookManifest derived from an RBdigital audiobook manifest.

classmethod best_cover(images=[])[source]
import_metadata(rbdigital_field, standard_field=None, transform=None)[source]

Map a field in an RBdigital manifest to the corresponding standard manifest field.

import_spine(file_data, proxy_url)[source]

Import an RBdigital spine item as a Web Publication Manifest spine item.

Parameters:
  • file_data – A dictionary of information about this spine item, obtained from RBdigital.

  • proxy_url – A URL generated by the circulation manager (as opposed to being generated by RBdigital) for fulfilling this spine item as an audio file (as opposed to a JSON document that links to an audio file).

class api.rbdigital.MockRBDigitalAPI(_db, collection, base_path=None, **kwargs)[source]

Bases: RBDigitalAPI

property collection

We can store the actual Collection object with a mock API, so there’s no need to store the ID and do lookups.

get_data(filename)[source]
classmethod mock_collection(_db)[source]
populate_all_catalog()[source]

Set up to use the smaller test catalog file, and then call the real populate_all_catalog. Used to test import on non-test permanent database.

queue_response(status_code, headers={}, content=None)[source]

Allows smoother faster creation of unit tests by letting us live-test as we write.

exception api.rbdigital.RBDProxyException[source]

Bases: Exception

class api.rbdigital.RBDigitalAPI(_db, collection)[source]

Bases: BaseCirculationAPI, HasSelfTests

API_VERSION = 'v1'
BASE_SETTINGS = []
BEARER_TOKEN_PROPERTY = 'bearer'
CACHED_IDENTIFIER_PROPERTY = 'patronId'
CREDENTIAL_TYPES = {'bearer': {'label': 'Patron Bearer Token', 'lifetime': 84600}, 'patronId': {'label': 'Identifier Received From Remote Service', 'lifetime': None}}
DATE_FORMAT = '%Y-%m-%d'
DEFAULT_LOAN_DURATION = 7
EXPIRATION_DATE_FORMAT = '%Y-%m-%d'
LIBRARY_SETTINGS = [{'key': 'audio_loan_duration', 'label': l'Audiobook Loan Duration (in Days)', 'default': 7, 'type': 'number', 'description': l'When a patron uses SimplyE to borrow an audiobook from this collection, SimplyE will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.'}, {'key': 'ebook_loan_duration', 'label': l'Ebook Loan Duration (in Days)', 'default': 7, 'type': 'number', 'description': l'When a patron uses SimplyE to borrow an ebook from this collection, SimplyE will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.'}]
NAME = 'RBdigital'
PRODUCTION_BASE_URL = 'https://api.rbdigital.com/'
PROXY_BEARER_GRACE_PERIOD = 1800
QA_BASE_URL = 'http://api.rbdigitalstage.com/'
RESPONSE_VERBOSITY = {0: 'basic', 1: 'compact', 2: 'complete', 3: 'extended', 4: 'hypermedia'}
SERVER_NICKNAMES = {'production': 'https://api.rbdigital.com/', 'qa': 'http://api.rbdigitalstage.com/'}
SETTINGS = [{'key': 'password', 'label': l'Basic Token', 'required': True}, {'key': 'external_account_id', 'label': l'Library ID (numeric)', 'required': True, 'type': 'number'}, {'key': 'url', 'label': l'URL', 'default': 'https://api.rbdigital.com/', 'required': True, 'format': 'url'}]
align_dates_to_available_snapshots(from_date=None, to_date=None)[source]

Given specified begin and end dates for a delta, return the best dates from those available.

Note: It might be useful to raise an exception or log a message if either of the “best” dates is too distant from the associated specified date.

The endpoint utilized returns a JSON array of “snapshot” objects (nb: tenantId is the library ID):
Example snapshot format:

“tenantId”: 525, “asOf”: “2020-04-07”, “eBookCount”: 1630, “eAudioCount”: 13414, “totalCount”: 15044

:return Best available begin and end dates.

property authorization_headers
cache_patron_bearer_token(patron, value)[source]
checkin(patron, pin, licensepool)[source]

Allow a patron to return an ebook or audio before its due date.

Parameters:
  • patron – a Patron object for the patron who wants to return the book.

  • pin – The patron’s password (not used).

  • licensepool – The Identifier of the book to be checked out is attached to this licensepool.

:return True on success, raises circulation exceptions on failure.

checkout(patron, pin, licensepool, internal_format)[source]

Associate an eBook or eAudio with a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s password (not used).

  • licensepool – The Identifier of the book to be checked out is attached to this licensepool.

  • internal_format – Represents the patron’s desired book format. Ignored for now.

:return LoanInfo on success, None on failure.

circulate_item(patron_id, item_id, hold=False, return_item=False, days=None)[source]

Borrow or return a catalog item. :param patron_id RBDigital internal id :param item_id isbn :return A dictionary of information on the transaction or error status and message Calling methods are expected to use this dictionary to create XxxInfo objects.

property collection
classmethod create_identifier_strings(identifiers)[source]
create_patron(library, authorization_identifier, email_address, bearer_token_handler=None)[source]

Ask RBdigital to create a new patron record.

Parameters:
  • library – Library for the patron that needs a new RBdigital account. This has no necessary connection to the ‘library_id’ associated with the RBDigitalAPI, since multiple circulation manager libraries may share an RBdigital account.

  • authorization_identifier – The identifier the patron uses to authenticate with their library.

  • email_address – The email address, if any, which the patron has shared with their library.

:return The internal RBdigital identifier for this patron.

property default_circulation_replacement_policy
dummy_email_address(library, authorization_identifier)[source]

The fake email address to send to RBdigital when creating an account for the given patron.

Parameters:
  • library – A Library.

  • authorization_identifier – A patron’s authorization identifier.

Returns:

An email address unique to this patron which will bounce or reject all mail sent to it.

dummy_patron_identifier(authorization_identifier)[source]

Add six random alphanumeric characters to the end of the given authorization_identifier.

Returns:

A random identifier based on the input identifier.

external_integration(_db)[source]

Locate the ExternalIntegration associated with this object. The status of the self-tests will be stored as a ConfigurationSetting on this ExternalIntegration.

By default, there is no way to get from an object to its ExternalIntegration, and self-test status will not be stored.

fetch_patron_bearer_token(patron)[source]

Obtain a patron bearer token for an RBdigital Patron.

A patron bearer token for an account within an RBdigital collection can be obtained with the patron’s RBdigital userId for that collection. (An initial bearer token also can also be captured when an RBdigital account is first created, but that is not applicable here.)

We don’t cache userId’s locally, but can retrieve them with the account’s `username. (This usually has the same value as the patron’s barcode/authorization_identifier; but, because of the Barcode+6 technique used to create accounts for patrons who don’t have a registered email address, this is not always the case, so we cannot rely on it.) So, we obtain the username by looking it up using the patronId, a property that we cache locally.

So, the process, in summary:
  • Get patronId from cache or RBdigital,

  • Fetch username using patronId.

  • Fetch userId using username.

  • Obtain bearer token using userId.

Parameters:

patron – A Patron.

Returns:

A bearer token associated with the patron.

fulfill(patron, pin, licensepool, internal_format, part=None, fulfill_part_url=None)[source]

Get an actual resource file to the patron. This may represent the entire book or only one part of it.

Parameters:
  • part – When the patron wants to fulfill a specific part of the book, rather than the title as a whole, this will be set to a string representation of the numeric position of the desired part.

  • fulfill_part_url – When the book can be fulfilled in parts, this function will take a part number and generate the URL to fulfill that specific part.

:return a FulfillmentInfo object.

Gets a list of ebook and eaudio items this library has access to, that are currently available to lend. Uses the “availability” facet of the search function. An alternative to self.get_availability_info(). Calls paged search until done. Uses minimal verbosity for result set.

Note: Some libraries can see other libraries’ catalogs, even if the patron cannot checkout the items. The library ownership information is in the “interest” fields of the response.

:return A dictionary representation of the response, containing catalog count and ebook item - interest pairs.

get_all_catalog()[source]

Gets the entire RBDigital catalog for a particular library.

Note: This call taxes RBDigital’s servers, and is to be performed sparingly. The results are returned unpaged.

Also, the endpoint returns about as much metadata per item as the media/{isbn} endpoint does. If want more metadata, perform a search.

:return A list of dictionaries representation of the response.

static get_credential_by_token(_db, data_source, credential_type, token)[source]
get_delta(from_date=None, to_date=None, verbosity=None)[source]

Gets the changes to the library’s catalog.

:return A dictionary listing items added/removed/modified in the collection.

get_ebook_availability_info(media_type='ebook')[source]

Gets a list of ebook items this library has access to, through the “availability” endpoint. The response at this endpoint is laconic – just enough fields per item to identify the item and declare it either available to lend or not.

:param media_type ‘eBook’/’eAudio’

:return A list of dictionary items, each item giving “yes/no” answer on a book’s current availability to lend.
Example of returned item format:

“timeStamp”: “2016-10-07T16:11:52.5887333Z” “isbn”: “9781420128567” “mediaType”: “eBook” “availability”: false “titleId”: 39764

get_metadata_by_isbn(identifier)[source]

Gets metadata, s.a. publisher, date published, genres, etc for the eBook or eAudio item passed, using isbn to search on. If isbn is not found, the response we get from RBDigital is an error message, and we throw an error.

:return the json dictionary of the response object

get_patron_checkouts(patron_id, fulfill_part_url=None, request_fulfillment=None, fulfillment_proxy=None)[source]

Gets the books and audio the patron currently has checked out. Obtains fulfillment info for each item – the way to fulfill a book is to get this list of possibilities first, and then call individual fulfillment endpoints on the individual items.

:param patron_id RBDigital internal id for the patron.

Parameters:

fulfill_part_url – A function that generates circulation manager fulfillment URLs for individual parts of a book.

get_patron_holds(patron_id)[source]

:param patron_id RBDigital internal id for the patron.

internal_format(delivery_mechanism)[source]

We don’t need to do any mapping between delivery mechanisms and internal formats, because each title is only available in one format.

log = <Logger RBDigital Patron API (WARNING)>
my_audiobook_setting = {'default': 7, 'description': l'When a patron uses SimplyE to borrow an audiobook from this collection, SimplyE will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.', 'key': 'audio_loan_duration', 'label': l'Audiobook Loan Duration (in Days)', 'type': 'number'}
my_ebook_setting = {'default': 7, 'description': l'When a patron uses SimplyE to borrow an ebook from this collection, SimplyE will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.', 'key': 'ebook_loan_duration', 'label': l'Ebook Loan Duration (in Days)', 'type': 'number'}
patron_activity(patron, pin)[source]

Get a patron’s current checkouts and holds.

Parameters:
  • patron – a Patron object for the patron who wants to return the book.

  • pin – The patron’s password (not used).

patron_bearer_token(patron)[source]
patron_credential(kind, patron, value=None)[source]

Provide the credential of the given type for the given Patron, either from the cache or by retrieving it from the remote service.

The behavior is as follows:
  • If a value is specified, we’ll cache it.

  • If no value is specified and no cached credential is present and unexpired, then we’ll retrieve a value from the remote service and cache it.

  • The cached value will be returned.

Parameters:
  • patron – A Patron.

  • kind – The type of credential.

  • value – An optional value for the credential, which, if provided, will replace replace the value in the cache.

Returns:

The credential value for type type for the patron.

patron_fulfillment_request(patron, url, reauthorize=True)[source]

Make a fulfillment request on behalf of a patron, using the a bearer token either previously cached or newly retrieved on behalf of the patron.

If the reauthorize parameter is set to True, then if the request fails with status code 401 (invalid bearer token), then we will attempt to obtain a new bearer token for the patron and repeat the request.

Parameters:
  • patron – A Patron.

  • url – URL for a resource.

  • reauthorize – (Optional) Boolean indicating whether to reauthorize the patron bearer token if we receive status code 401.

Returns:

The request response.

patron_remote_identifier(patron)[source]

Locate the identifier for the given Patron’s account on the RBdigital side, creating a new RBdigital account if necessary.

The identifier is cached in a persistent Credential object.

The logic is complicated and spread out over multiple methods, so here it is all in one place:

If an already-cached identifier is present, we use it.

Otherwise, we look up the patron’s barcode on RBdigital to try to find their existing RBdigital account.

If we find an existing RBdigital account, we cache the identifier associated with that account.

Otherwise, we need to create an RBdigital account for this patron:

If the ILS provides access to the patron’s email address, we create an account using the patron’s actual barcode and email address. This will let them use the ‘recover password’ feature if they want to use the RBdigital web site.

If the ILS does not provide access to the patron’s email address, we create an account using the patron’s actual barcode but with six random characters appended. This will let the patron create a new RBdigital account using their actual barcode, if they want to use the web site.

Parameters:

patron – A Patron.

Returns:

The identifier associated with the patron’s (possibly newly created) RBdigital account. This is an RBdigital-internal identifier with no connection to any identifier used by the patron, the circulation manager, and the ILS.

patron_remote_identifier_lookup(remote_identifier)[source]

Look up a patron’s RBdigital account based on an identifier associated with their circulation manager account.

Parameters:

remote_identifier – Depending on the context, this may be the patron’s actual barcode, or a random string _based_ on their barcode.

Returns:

The internal RBdigital patron ID for the given identifier, or None if there is no corresponding RBdigital account.

place_hold(patron, pin, licensepool, notification_email_address)[source]

Place a book on hold.

Note: If the requested book is available for checkout, RBDigital will respond with a “success” to the hold request. Then, at the next database clean-up sweep, RBDigital will automatically convert the hold record to a checkout record.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s password (not used).

  • licensepool – The Identifier of the book to be checked out is attached to this licensepool.

  • internal_format – Represents the patron’s desired book format. Ignored for now.

Returns:

A HoldInfo object on success, None on failure

populate_all_catalog()[source]

Call get_all_catalog to get all of library’s book info from RBDigital. Create Work, Edition, LicensePool objects in our database.

populate_delta(months=1, today=None)[source]

Call get_delta for the last month to get all of the library’s book info changes from RBDigital. Update Work, Edition, LicensePool objects in our database.

Parameters:

today – A date to use instead of the current date, for use in tests.

queue_response(status_code, headers={}, content=None)[source]

Allows smoother faster creation of unit tests by letting us live-test as we write.

reauthorize_patron_bearer_token(patron)[source]
release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Parameters:
  • patron – a Patron object for the patron who wants to return the book.

  • pin – The patron’s password (not used).

  • licensepool – The Identifier of the book to be checked out is attached to this licensepool.

:return True on success, raises circulation exceptions on failure.

request(url, method='get', extra_headers={}, data=None, params=None, verbosity='complete')[source]

Make an HTTP request.

search(mediatype='ebook', genres=[], audience=None, availability=None, author=None, title=None, page_size=100, page_index=None, verbosity=None)[source]

Form a rest-ful search query, send to RBDigital, and obtain the results.

Parameters:
  • mediatype – Facet to limit results by media type. Options are: “eAudio”, “eBook”.

  • genres – The books found lie at intersection of genres passed.

  • audience – Facet to limit results by target age group. Options include (there may be more): “adult”, “beginning-reader”, “childrens”, “young-adult”.

  • availability – Facet to limit results by copies left. Options are “available”, “unavailable”, or None

  • author – Full name to search on.

  • title – Book title to search on.

  • page_index – Used for paginated result sets. Zero-based.

  • verbosity – “basic” returns smaller number of response json lines than “complete”, etc..

:return the response object

property source
update_availability(licensepool)[source]

Update the availability information for a single LicensePool. Part of the CirculationAPI interface. Inactive for now, because we’d have to request and go through all availabilities from RBDigital just to pick the one licensepool we want.

update_licensepool_for_identifier(isbn, availability, medium, policy=None)[source]

Update availability information for a single book.

If the book has never been seen before, a new LicensePool will be created for the book.

The book’s LicensePool will be updated with current approximate circulation information (we can tell if it’s available, but not how many copies). Bibliographic coverage will be ensured for the RBDigital Identifier. Work will be created for the LicensePool and set as presentation-ready.

:param isbn the identifier RBDigital uses :param availability boolean denoting if book can be lent to patrons :param medium: The name RBDigital uses for the book’s medium.

validate_item(licensepool)[source]

Are we performing operations on a book that exists and can be uniquely identified?

validate_response(response, message, action='')[source]

RBDigital tries to communicate statuses and errors through http codes. Malformed url requests will throw a 500, non-existent ids will get a 404, trying an action like checkout on a patron/item combo that’s blocked (like if the item is already checked out, for example) will get a 409, etc.. Further details are usually elaborated on in the “message” field of the response.

:param response http response object :message RBDigital puts error explanation into ‘message’ field in response dictionary

class api.rbdigital.RBDigitalBibliographicCoverageProvider(collection, api_class=<class 'api.rbdigital.RBDigitalAPI'>, api_class_kwargs={}, **kwargs)[source]

Bases: BibliographicCoverageProvider

Fill in bibliographic metadata for RBDigital records.

DATA_SOURCE_NAME = 'RBdigital'
DEFAULT_BATCH_SIZE = 25
INPUT_IDENTIFIER_TYPES = 'RBdigital ID'
PROTOCOL = 'RBdigital'
SERVICE_NAME = 'RBDigital Bibliographic Coverage Provider'
process_item(identifier)[source]

RBDigital availability information is served separately from the book’s metadata. Furthermore, the metadata returned by the “book by isbn” request is less comprehensive than the data returned by the “search titles/genres/etc.” endpoint.

This method hits the “by isbn” endpoint and updates the bibliographic metadata returned by it.

update_metadata(catalog_item, identifier=None)[source]

Creates db objects corresponding to the book info passed in.

Note: It is expected that CoverageProvider.handle_success, which is responsible for setting the work to be presentation-ready is handled in the calling code.

:catalog_item - JSON representation of the book’s metadata, coming from RBDigital. :return CoverageFailure or a database object (Work, Identifier, etc.)

class api.rbdigital.RBDigitalCirculationMonitor(_db, collection, batch_size=None, api_class=<class 'api.rbdigital.RBDigitalAPI'>, api_class_kwargs={})[source]

Bases: CollectionMonitor

Maintain LicensePools for RBDigital titles.

Bibliographic data isn’t inserted into new LicensePools until we hear from the metadata wrangler.

DEFAULT_BATCH_SIZE = 50
DEFAULT_START_TIME = datetime.datetime(1970, 1, 1, 0, 0, tzinfo=<UTC>)
PROTOCOL = 'RBdigital'
SERVICE_NAME = 'RBDigital CirculationMonitor'
process_availability(media_type='eBook')[source]
run_once(progress)[source]

Update the availability information of all titles in the RBdigital collection.

Parameters:

progress – A TimestampData, ignored.

Returns:

A TimestampData describing what was accomplished.

class api.rbdigital.RBDigitalDeltaMonitor(_db, collection, api_class=<class 'api.rbdigital.RBDigitalAPI'>, api_class_kwargs={})[source]

Bases: RBDigitalSyncMonitor

SERVICE_NAME = 'RBDigital Delta Sync'
invoke()[source]
class api.rbdigital.RBDigitalFulfillmentProxy(patron, api, for_part=None)[source]

Bases: object

make_request(url)[source]
proxied_manifest(manifest)[source]
classmethod proxy(_db, bearer, url, api_class=None)[source]
class api.rbdigital.RBDigitalImportMonitor(_db, collection, api_class=<class 'api.rbdigital.RBDigitalAPI'>, api_class_kwargs={})[source]

Bases: RBDigitalSyncMonitor

SERVICE_NAME = 'RBDigital Full Import'
invoke()[source]
class api.rbdigital.RBDigitalRepresentationExtractor[source]

Bases: object

Extract useful information from RBDigital’s JSON representations.

DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
DATE_FORMAT = '%Y-%m-%d'
classmethod isbn_info_to_metadata(book, include_bibliographic=True, include_formats=True)[source]

Turn RBDigital’s JSON representation of a book into a Metadata object. Assumes the JSON is in the format that comes from the media/{isbn} endpoint.

TODO: Use the seriesTotal field.

:param book a json response-derived dictionary of book attributes

log = <Logger RBDigital representation extractor (WARNING)>
rbdigital_medium_to_simplified_medium = {'eAudio': 'Audio', 'eBook': 'Book'}
class api.rbdigital.RBDigitalSyncMonitor(_db, collection, api_class=<class 'api.rbdigital.RBDigitalAPI'>, api_class_kwargs={})[source]

Bases: CollectionMonitor

PROTOCOL = 'RBdigital'
invoke()[source]
run_once(progress)[source]

Find books in the RBdigital collection that changed recently.

Parameters:

progress – A TimestampData, ignored.

Returns:

A TimestampData describing what was accomplished.

class api.rbdigital.RBFulfillmentInfo(fulfill_part_url, request_fulfillment, *args, **kwargs)[source]

Bases: APIAwareFulfillmentInfo

An RBdigital-specific FulfillmentInfo implementation.

We use these instead of real FulfillmentInfo objects because generating a FulfillmentInfo object may require an extra HTTP request, and there’s often no need to make that request.

do_fetch()[source]

Actually make the API request.

When implemented, this method must set values for some or all of _content_link, _content_type, _content, and _content_expires.

fetch_access_document(url)[source]

Retrieve an access document from RBdigital and process it.

An access document is a small JSON document containing a link to the URL we actually want to deliver.

fulfill_part(part)[source]

Fulfill a specific part of this book.

This will navigate the access document and find a link to the actual MP3 file so that a client doesn’t know how to parse access documents.

Returns:

A FulfillmentInfo if the part could be fulfilled; a ProblemDetail otherwise.

classmethod process_access_document(access_document)[source]

Process the intermediary document served by RBdigital to tell you how to actually download a file.

api.registry module

class api.registry.LibraryRegistrationScript(_db=None)[source]

Bases: LibraryInputScript

Register local libraries with a remote library registry.

GOAL = 'discovery'
PROTOCOL = 'OPDS Registration'
classmethod arg_parser(_db)[source]
do_run(cmd_args=None, in_unit_test=False)[source]
process_library(registration, stage, url_for)[source]

Push one Library’s registration to the given RemoteRegistry.

class api.registry.Registration(registry, library)[source]

Bases: object

A library’s registration for a particular registry.

The registration does not correspond to one specific data model object – it’s a relationship between a Library and an ExternalIntegration, and a set of ConfigurationSettings that configure the relationship between the two.

FAILURE_STATUS = 'failure'
LIBRARY_REGISTRATION_STAGE = 'library-registration-stage'
LIBRARY_REGISTRATION_STATUS = 'library-registration-status'
LIBRARY_REGISTRATION_WEB_CLIENT = 'library-registration-web-client'
PRODUCTION_STAGE = 'production'
SUCCESS_STATUS = 'success'
TESTING_STAGE = 'testing'
VALID_REGISTRATION_STAGES = ['testing', 'production']
push(stage, url_for, catalog_url=None, do_get=<bound method HTTP.debuggable_get of <class 'core.util.http.HTTP'>>, do_post=<bound method HTTP.debuggable_post of <class 'core.util.http.HTTP'>>)[source]

Attempt to register a library with a RemoteRegistry.

NOTE: This method is designed to be used in a controller. Other callers may use this method, but they must be able to render a ProblemDetail when there’s a failure.

NOTE: The application server must be running when this method is called, because part of the OPDS Directory Registration Protocol is the remote server retrieving the library’s Authentication For OPDS document.

Parameters:
  • stage – Either TESTING_STAGE or PRODUCTION_STAGE

  • url_for – Flask url_for() or equivalent, used to generate URLs for the application server.

  • do_get – Mockable method to make a GET request.

  • do_post – Mockable method to make a POST request.

Returns:

A ProblemDetail if there was a problem; otherwise True.

setting(key, default_value=None)[source]

Find or create a ConfigurationSetting that configures this relationship between library and registry.

Parameters:

key – Name of the ConfigurationSetting.

Returns:

A 2-tuple (ConfigurationSetting, is_new)

class api.registry.RemoteRegistry(integration)[source]

Bases: object

A circulation manager’s view of a remote service that supports the OPDS Directory Registration Protocol:

https://github.com/NYPL-Simplified/Simplified/wiki/OPDS-Directory-Registration-Protocol

In practical terms, this may be a library registry (which has DISCOVERY_GOAL and wants to help patrons find their libraries) or it may be a shared ODL collection (which has LICENSE_GOAL).

DEFAULT_LIBRARY_REGISTRY_URL = 'https://libraryregistry.librarysimplified.org/'
OPDS_1_PREFIX = 'application/atom+xml;profile=opds-catalog'
OPDS_2_TYPE = 'application/opds+json'
fetch_catalog(catalog_url=None, do_get=<bound method HTTP.debuggable_get of <class 'core.util.http.HTTP'>>)[source]

Fetch the root catalog for this RemoteRegistry.

Returns:

A ProblemDetail if there’s a problem communicating with the service or parsing the catalog; otherwise a 2-tuple (registration URL, Adobe vendor ID).

fetch_registration_document(do_get=<bound method HTTP.debuggable_get of <class 'core.util.http.HTTP'>>)[source]

Fetch a discovery service’s registration document and extract useful information from it.

Returns:

A ProblemDetail if there’s a problem accessing the service; otherwise, a 2-tuple (terms_of_service_link, terms_of_service_html), containing information about the Terms of Service that govern a circulation manager’s registration with the discovery service.

classmethod for_integration_id(_db, integration_id, goal)[source]

Find a LibraryRegistry object configured by the given ExternalIntegration ID.

Parameters:

goal – The ExternalIntegration’s .goal must be this goal.

classmethod for_protocol_and_goal(_db, protocol, goal)[source]

Find all LibraryRegistry objects with the given protocol and goal.

classmethod for_protocol_goal_and_url(_db, protocol, goal, url)[source]

Get a LibraryRegistry for the given protocol, goal, and URL. Create the corresponding ExternalIntegration if necessary.

property registrations

Find all of this site’s successful registrations with this RemoteRegistry.

Yield:

A sequence of Registration objects.

api.selftest module

class api.selftest.HasCollectionSelfTests[source]

Bases: HasSelfTests

Extra tests to verify the integrity of imported collections of books.

This is a mixin method that requires that self.collection point to the Collection to be tested.

class api.selftest.HasSelfTests[source]

Bases: HasSelfTests

Circulation-specific enhancements for HasSelfTests.

Circulation self-tests frequently need to test the ability to act on behalf of a specific patron.

default_patrons(collection)[source]

Find a usable default Patron for each of the libraries associated with the given Collection.

Yield:

A sequence of (Library, Patron, password) 3-tuples. Yields (SelfTestFailure, None, None) if the Collection is not associated with any libraries, if a library does not have a default patron configured, or if there is an exception acquiring a library’s default patron.

class api.selftest.RunSelfTestsScript(_db=None, output=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>)[source]

Bases: LibraryInputScript

Run the self-tests for every collection in the given library where that’s possible.

do_run(*args, **kwargs)[source]
process_result(result)[source]

Process a single TestResult object.

test_collection(collection, api_map, extra_args=None)[source]

api.shared_collection module

class api.shared_collection.BaseSharedCollectionAPI[source]

Bases: object

APIs that permit external circulation managers to access their collections should extend this class.

EXTERNAL_LIBRARY_URLS = 'external_library_urls'
SETTINGS = [{'key': 'external_library_urls', 'label': l'URLs for libraries on other circulation managers that use this collection', 'description': l'A URL should include the library's short name (e.g. https://circulation.librarysimplified.org/NYNYPL/), even if it is the only library on the circulation manager.', 'type': 'list', 'format': 'url'}, {'key': 'ebook_loan_duration', 'label': l'Ebook Loan Duration for libraries on other circulation managers (in Days)', 'default': 21, 'description': l'When a patron from another library borrows an ebook from this collection, the circulation manager will ask for a loan that lasts this number of days. This must be equal to or less than the maximum loan duration negotiated with the distributor.', 'type': 'number'}]
checkin_from_external_library(client, loan)[source]
checkout_to_external_library(client, pool, hold=None)[source]
fulfill_for_external_library(client, loan, mechanism)[source]
release_hold_from_external_library(client, hold)[source]
class api.shared_collection.SharedCollectionAPI(_db, api_map=None)[source]

Bases: object

Logic for circulating books to patrons of libraries on other circulation managers. This can be used for something like ODL where the circulation manager is responsible for managing loans and holds rather than the distributor, or potentially for inter-library loans for other collection types.

api(collection)[source]

Find the API to use for the given collection, and raise an exception if there isn’t one.

api_for_licensepool(pool)[source]

Find the API to use for the given license pool.

borrow(collection, client, pool, hold=None)[source]
check_client_authorization(collection, client)[source]

Verify that an IntegrationClient is whitelisted for access to the collection.

property default_api_map

When you see a Collection that implements protocol X, instantiate API class Y to handle that collection.

fulfill(collection, client, loan, mechanism)[source]
register(collection, auth_document_url, do_get=<bound method HTTP.get_with_timeout of <class 'core.util.http.HTTP'>>)[source]

Register a library on an external circulation manager for access to this collection. The library’s auth document url must be whitelisted in the collection’s settings.

revoke_hold(collection, client, hold)[source]
revoke_loan(collection, client, loan)[source]

api.simple_authentication module

api.simple_authentication.AuthenticationProvider

alias of SimpleAuthenticationProvider

class api.simple_authentication.SimpleAuthenticationProvider(library, integration, analytics=None)[source]

Bases: BasicAuthenticationProvider

An authentication provider that authenticates a single patron.

This serves only one purpose: to set up a working circulation manager before connecting it to an ILS.

ADDITIONAL_TEST_IDENTIFIERS = 'additional_test_identifiers'
DESCRIPTION = l'         An internal authentication service that authenticates a single patron.         This is useful for testing a circulation manager before connecting         it to an ILS.'
NAME = 'Simple Authentication Provider'
SETTINGS = [{'key': 'test_identifier', 'label': l'Test Identifier', 'description': l'A valid identifier that can be used to test that patron authentication is working.', 'required': True}, {'key': 'test_password', 'label': l'Test Password', 'description': l'The password for the Test Identifier.', 'required': True}, {'key': 'identifier_barcode_format', 'label': l'Patron identifier barcode format', 'description': l'Many libraries render patron identifiers as barcodes on physical library cards. If you specify the barcode format, patrons will be able to scan their library cards with a camera instead of manually typing in their identifiers.', 'type': 'select', 'options': [{'key': 'Codabar', 'label': l'Patron identifiers are are rendered as barcodes in Codabar format'}, {'key': '', 'label': l'Patron identifiers are not rendered as barcodes'}], 'default': '', 'required': True}, {'key': 'identifier_regular_expression', 'label': l'Identifier Regular Expression', 'description': l'A patron's identifier will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'password_regular_expression', 'label': l'Password Regular Expression', 'description': l'A patron's password will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'identifier_keyboard', 'label': l'Keyboard for identifier entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Email address', 'label': l'Email address entry'}, {'key': 'Number pad', 'label': l'Number pad'}], 'default': 'Default', 'required': True}, {'key': 'password_keyboard', 'label': l'Keyboard for password entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Number pad', 'label': l'Number pad'}, {'key': 'No input', 'label': l'Patrons have no password and should not be prompted for one.'}], 'default': 'Default'}, {'key': 'identifier_maximum_length', 'label': l'Maximum identifier length', 'type': 'number'}, {'key': 'password_maximum_length', 'label': l'Maximum password length', 'type': 'number'}, {'key': 'identifier_label', 'label': l'Label for identifier entry'}, {'key': 'password_label', 'label': l'Label for password entry'}, {'key': 'additional_test_identifiers', 'label': l'Additional test identifiers', 'type': 'list', 'description': l'Identifiers for additional patrons to use in testing. The identifiers will all use the same test password as the first identifier.'}, {'key': 'neighborhood', 'label': l'Test neighborhood', 'description': l'For analytics purposes, all patrons will be 'from' this neighborhood.'}]
TEST_NEIGHBORHOOD = 'neighborhood'
basic_settings = [{'key': 'test_identifier', 'label': l'Test Identifier', 'description': l'A valid identifier that can be used to test that patron authentication is working.', 'required': True}, {'key': 'test_password', 'label': l'Test Password', 'description': l'The password for the Test Identifier.', 'required': True}, {'key': 'identifier_barcode_format', 'label': l'Patron identifier barcode format', 'description': l'Many libraries render patron identifiers as barcodes on physical library cards. If you specify the barcode format, patrons will be able to scan their library cards with a camera instead of manually typing in their identifiers.', 'type': 'select', 'options': [{'key': 'Codabar', 'label': l'Patron identifiers are are rendered as barcodes in Codabar format'}, {'key': '', 'label': l'Patron identifiers are not rendered as barcodes'}], 'default': '', 'required': True}, {'key': 'identifier_regular_expression', 'label': l'Identifier Regular Expression', 'description': l'A patron's identifier will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'password_regular_expression', 'label': l'Password Regular Expression', 'description': l'A patron's password will be immediately rejected if it doesn't match this regular expression.'}, {'key': 'identifier_keyboard', 'label': l'Keyboard for identifier entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Email address', 'label': l'Email address entry'}, {'key': 'Number pad', 'label': l'Number pad'}], 'default': 'Default', 'required': True}, {'key': 'password_keyboard', 'label': l'Keyboard for password entry', 'type': 'select', 'options': [{'key': 'Default', 'label': l'System default'}, {'key': 'Number pad', 'label': l'Number pad'}, {'key': 'No input', 'label': l'Patrons have no password and should not be prompted for one.'}], 'default': 'Default'}, {'key': 'identifier_maximum_length', 'label': l'Maximum identifier length', 'type': 'number'}, {'key': 'password_maximum_length', 'label': l'Maximum password length', 'type': 'number'}, {'key': 'identifier_label', 'label': l'Label for identifier entry'}, {'key': 'password_label', 'label': l'Label for password entry'}]
classmethod generate_patrondata(authorization_identifier, neighborhood=None)[source]
i = 10
remote_authenticate(username, password)[source]

Fake ‘remote’ authentication.

s = {'description': l'The password for the Test Identifier.', 'key': 'test_password', 'label': l'Test Password', 'required': True}
setting = {'key': 'password_label', 'label': l'Label for password entry'}
valid_patron(username, password)[source]

Is this patron associated with the given password in the given dictionary?

api.testing module

class api.testing.AnnouncementTest[source]

Bases: object

A test that needs to create announcements.

a_week_ago = '2024-07-22'
active = {'content': 'A sample announcement.', 'finish': '2024-07-30', 'id': 'active', 'start': '2024-07-29'}
expired = {'content': 'A sample announcement.', 'finish': '2024-07-28', 'id': 'expired', 'start': '2024-07-22'}
format = '%Y-%m-%d'
forthcoming = {'content': 'A sample announcement.', 'finish': '2024-08-05', 'id': 'forthcoming', 'start': '2024-07-30'}
in_a_week = '2024-08-05'
today = '2024-07-29'
tomorrow = '2024-07-30'
yesterday = '2024-07-28'
class api.testing.MockCirculationAPI(*args, **kwargs)[source]

Bases: CirculationAPI

add_remote_hold(*args, **kwargs)[source]
add_remote_loan(*args, **kwargs)[source]
api_for_license_pool(licensepool)[source]

Find the API to use for the given license pool.

local_holds(patron)[source]
local_loans(patron)[source]
patron_activity(patron, pin)[source]

Return a 3-tuple (loans, holds, completeness).

queue_checkin(licensepool, response)[source]
queue_checkout(licensepool, response)[source]
queue_fulfill(licensepool, response)[source]
queue_hold(licensepool, response)[source]
queue_release_hold(licensepool, response)[source]
class api.testing.MockRemoteAPI(set_delivery_mechanism_at, can_revoke_hold_when_reserved)[source]

Bases: BaseCirculationAPI

checkin(patron, pin, licensepool)[source]

Return a book early.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

checkout(patron_obj, patron_password, licensepool, delivery_mechanism)[source]

Check out a book on behalf of a patron.

Parameters:
  • patron – a Patron object for the patron who wants to check out the book.

  • pin – The patron’s alleged password.

  • licensepool – Contains lending info as well as link to parent Identifier.

  • internal_format – Represents the patron’s desired book format.

Returns:

a LoanInfo object.

fulfill(patron, pin, licensepool, internal_format=None, part=None, fulfill_part_url=None)[source]

Get the actual resource file to the patron.

Implementations are encouraged to define **kwargs as a container for vendor-specific arguments, so that they don’t have to change as new arguments are added.

Parameters:
  • internal_format – A vendor-specific name indicating the format requested by the patron.

  • part – A vendor-specific identifier indicating that the patron wants to fulfill one specific part of the book (e.g. one chapter of an audiobook), not the whole thing.

  • fulfill_part_url – A function that takes one argument (a vendor-specific part identifier) and returns the URL to use when fulfilling that part.

Returns:

a FulfillmentInfo object.

internal_format(delivery_mechanism)[source]

Look up the internal format for this delivery mechanism or raise an exception.

Parameters:

delivery_mechanism – A LicensePoolDeliveryMechanism

place_hold(patron, pin, licensepool, hold_notification_email=None)[source]

Place a book on hold.

Returns:

A HoldInfo object

queue_checkin(response)[source]
queue_checkout(response)[source]
queue_fulfill(response)[source]
queue_hold(response)[source]
queue_release_hold(response)[source]
release_hold(patron, pin, licensepool)[source]

Release a patron’s hold on a book.

Raises:

CannotReleaseHold – If there is an error communicating with the provider, or the provider refuses to release the hold for any reason.

update_availability(licensepool)[source]

Simply record the fact that update_availability was called.

update_loan(loan, status_doc)[source]
class api.testing.MockSharedCollectionAPI(*args, **kwargs)[source]

Bases: SharedCollectionAPI

borrow(collection, client, pool, hold=None)[source]
fulfill(patron, pin, licensepool, internal_format=None, part=None, fulfill_part_url=None)[source]
queue_borrow(response)[source]
queue_fulfill(response)[source]
queue_register(response)[source]
queue_revoke_hold(response)[source]
queue_revoke_loan(response)[source]
register(collection, url)[source]

Register a library on an external circulation manager for access to this collection. The library’s auth document url must be whitelisted in the collection’s settings.

revoke_hold(collection, client, hold)[source]
revoke_loan(collection, client, loan)[source]
class api.testing.MonitorTest[source]

Bases: DatabaseTest

property ts

Make the timestamp used by run() when calling run_once().

This makes it easier to test run_once() in isolation.

class api.testing.VendorIDTest[source]

Bases: DatabaseTest

A DatabaseTest that knows how to set up an Adobe Vendor ID integration.

TEST_NODE_VALUE = 114740953091845
TEST_VENDOR_ID = 'vendor id'
initialize_adobe(vendor_id_library, short_token_libraries=[])[source]

Initialize an Adobe Vendor ID integration and a Short Client Token integration with a number of libraries.

Parameters:
  • vendor_id_library – The Library that should have an Adobe Vendor ID integration.

  • short_token_libraries – The Libraries that should have a Short Client Token integration.

api.web_publication_manifest module

Vendor-specific variants of the standard Web Publication Manifest classes.

class api.web_publication_manifest.FindawayManifest(license_pool, accountId=None, checkoutId=None, fulfillmentId=None, licenseId=None, sessionKey=None, spine_items=[])[source]

Bases: AudiobookManifest

FINDAWAY_EXTENSION_CONTEXT = 'http://librarysimplified.org/terms/third-parties/findaway.com/'
MEDIA_TYPE = 'application/vnd.librarysimplified.findaway.license+json'
class api.web_publication_manifest.SpineItem(title, duration, part, sequence, media_type='audio/mpeg')[source]

Bases: object

Metadata about a piece of playable audio from an audiobook.

classmethod sort_key(o)[source]

Used to sort a list of SpineItem objects in reading order.

Module contents