def __init__(self, library, integration, analytics=None, client=None, connect=True): """An object capable of communicating with a SIP server. :param server: Hostname of the SIP server. :param port: The port number to connect to on the SIP server. :param login_user_id: SIP field CN; the user ID to use when initiating a SIP session, if necessary. This is _not_ a patron identifier (SIP field AA); it identifies the SC creating the SIP session. SIP2 defines SC as "...any library automation device dealing with patrons or library materials." :param login_password: Sip field CO; the password to use when initiating a SIP session, if necessary. :param location_code: SIP field CP; the location code to use when initiating a SIP session. A location code supposedly refers to the physical location of a self-checkout machine within a library system. Some libraries require a special location code to be provided when authenticating patrons; others may require the circulation manager to be treated as its own special 'location'. :param field_separator: The field delimiter (see "Variable-length fields" in the SIP2 spec). If no value is specified, the default (the pipe character) will be used. :param client: A drop-in replacement for the SIPClient object. Only intended for use during testing. :param connect: If this is false, the generated SIPClient will not attempt to connect to the server. Only intended for use during testing. """ super(SIP2AuthenticationProvider, self).__init__( library, integration, analytics ) try: server = None if client: if callable(client): client = client() else: server = integration.url port = integration.setting(self.PORT).int_value login_user_id = integration.username login_password = integration.password location_code = integration.setting(self.LOCATION_CODE).value field_separator = integration.setting( self.FIELD_SEPARATOR).value or '|' client = SIPClient( target_server=server, target_port=port, login_user_id=login_user_id, login_password=login_password, location_code=location_code, separator=field_separator, connect=connect ) except IOError, e: raise RemoteIntegrationException( server or 'unknown server', e.message )
def patron_information(self, username, password): try: if self.client: sip = self.client else: sip = SIPClient(target_server=self.server, target_port=self.port, login_user_id=self.login_user_id, login_password=self.login_password, location_code=self.location_code, institution_id=self.institution_id, separator=self.field_separator, use_ssl=self.use_ssl, ssl_cert=self.ssl_cert, ssl_key=self.ssl_key, dialect=self.dialect) sip.connect() sip.login() info = sip.patron_information(username, password) sip.end_session(username, password) sip.disconnect() return info except IOError, e: raise RemoteIntegrationException(self.server or 'unknown server', e.message)
def __init__(self, library, integration, analytics=None, patron=None, patrondata=None, *args, **kwargs): raise RemoteIntegrationException("Mock", "Mock exploded.")
def test_with_service_name(self): """You don't have to provide a URL when creating a RemoteIntegrationException; you can just provide the service name. """ exc = RemoteIntegrationException( "Unreliable Service", "I just can't handle your request right now." ) # Since only the service name is provided, there are no details to # elide in the non-debug version of a problem detail document. debug_detail = exc.document_detail(debug=True) other_detail = exc.document_detail(debug=False) assert debug_detail == other_detail assert ( "The server tried to access Unreliable Service but the third-party service experienced an error." == debug_detail )
def remote_authenticate(self, username, password): """Authenticate a patron with the SIP2 server. :param username: The patron's username/barcode/card number/authorization identifier. :param password: The patron's password/pin/access code. """ try: info = self.client.patron_information(username, password) except IOError, e: raise RemoteIntegrationException( self.client.target_server or 'unknown server', e.message)
def patron_information(self, username, password): try: sip = self._client sip.connect() sip.login() info = sip.patron_information(username, password) sip.end_session(username, password) sip.disconnect() return info except IOError as e: raise RemoteIntegrationException(self.server or "unknown server", str(e))
def remote_authenticate(self, username, password): """Authenticate a patron with the SIP2 server. :param username: The patron's username/barcode/card number/authorization identifier. :param password: The patron's password/pin/access code. """ if not self.collects_password: # Even if we were somehow given a password, we won't be # passing it on. password = None try: info = self.client.patron_information(username, password) except IOError, e: raise RemoteIntegrationException( self.client.target_server or 'unknown server', e.message )
def request( self, url, method="get", extra_headers={}, data=None, params=None, retry_on_timeout=True, **kwargs ): """Make an HTTP request to the Enki API.""" headers = dict(extra_headers) response = None try: response = self._request( method, url, headers=headers, data=data, params=params, **kwargs ) except RequestTimedOut as e: if not retry_on_timeout: raise e self.log.info("Request to %s timed out once. Trying a second time.", url) return self.request( url, method, extra_headers, data, params, retry_on_timeout=False, **kwargs ) # Look for the error indicator and raise # RemoteIntegrationException if it appears. if response.content and self.ERROR_INDICATOR in response.content.decode( "utf-8" ): raise RemoteIntegrationException(url, "An unknown error occured") return response
class EnkiAPI(BaseCirculationAPI, HasSelfTests): PRODUCTION_BASE_URL = "https://enkilibrary.org/API/" ENKI_LIBRARY_ID_KEY = u'enki_library_id' DESCRIPTION = _("Integrate an Enki collection.") SETTINGS = [ { "key": ExternalIntegration.URL, "label": _("URL"), "default": PRODUCTION_BASE_URL, "required": True, "format": "url" }, ] + BaseCirculationAPI.SETTINGS LIBRARY_SETTINGS = [ { "key": ENKI_LIBRARY_ID_KEY, "label": _("Library ID"), "required": True }, ] list_endpoint = "ListAPI" item_endpoint = "ItemAPI" user_endpoint = "UserAPI" NAME = u"Enki" ENKI = NAME ENKI_EXTERNAL = NAME ENKI_ID = u"Enki ID" # Create a lookup table between common DeliveryMechanism identifiers # and Enki format types. epub = Representation.EPUB_MEDIA_TYPE adobe_drm = DeliveryMechanism.ADOBE_DRM no_drm = DeliveryMechanism.NO_DRM delivery_mechanism_to_internal_format = { (epub, no_drm): 'free', (epub, adobe_drm): 'acs', } # Enki API serves all responses with a 200 error code and a # text/html Content-Type. However, there's a string that # reliably shows up in error pages which is unlikely to show up # in normal API operation. ERROR_INDICATOR = '<h1>Oops, an error occurred</h1>' SET_DELIVERY_MECHANISM_AT = BaseCirculationAPI.FULFILL_STEP SERVICE_NAME = "Enki" log = logging.getLogger("Enki API") def __init__(self, _db, collection): self._db = _db if collection.protocol != self.ENKI: raise ValueError( "Collection protocol is %s, but passed into EnkiAPI!" % collection.protocol ) self.collection_id = collection.id self.base_url = collection.external_integration.url or self.PRODUCTION_BASE_URL def external_integration(self, _db): return self.collection.external_integration def enki_library_id(self, library): """Find the Enki library ID for the given library.""" _db = Session.object_session(library) return ConfigurationSetting.for_library_and_externalintegration( _db, self.ENKI_LIBRARY_ID_KEY, library, self.external_integration(_db) ).value @property def collection(self): return Collection.by_id(self._db, id=self.collection_id) def _run_self_tests(self, _db): now = datetime.datetime.utcnow() def count_loans_and_holds(): """Count recent circulation events that affected loans or holds. """ one_hour_ago = now - datetime.timedelta(hours=1) count = len(list(self.recent_activity(since=one_hour_ago))) return "%s circulation events in the last hour" % count yield self.run_test( "Counting recent circulation changes.", count_loans_and_holds ) def count_title_changes(): """Count changes to title metadata (usually because of new titles). """ one_day_ago = now - datetime.timedelta(hours=24) return "%s titles added/updated in the last day" % ( len(list(self.updated_titles(since=one_day_ago))) ) yield self.run_test( "Counting recent collection changes.", count_title_changes, ) for result in self.default_patrons(self.collection): if isinstance(result, SelfTestResult): yield result continue library, patron, pin = result task = "Checking patron activity, using test patron for library %s" % library.name def count_loans_and_holds(patron, pin): activity = list(self.patron_activity(patron, pin)) return "Total loans and holds: %s" % len(activity) yield self.run_test( task, count_loans_and_holds, patron, pin ) def request(self, url, method='get', extra_headers={}, data=None, params=None, retry_on_timeout=True, **kwargs): """Make an HTTP request to the Enki API.""" headers = dict(extra_headers) response = None try: response = self._request( method, url, headers=headers, data=data, params=params, **kwargs ) except RequestTimedOut, e: if not retry_on_timeout: raise e self.log.info( "Request to %s timed out once. Trying a second time.", url ) return self.request( url, method, extra_headers, data, params, retry_on_timeout=False, **kwargs ) # Look for the error indicator and raise # RemoteIntegrationException if it appears. if response.content and self.ERROR_INDICATOR in response.content: raise RemoteIntegrationException(url, "An unknown error occured") return response