Beispiel #1
0
    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
            )
Beispiel #2
0
    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)
Beispiel #3
0
 def __init__(self,
              library,
              integration,
              analytics=None,
              patron=None,
              patrondata=None,
              *args,
              **kwargs):
     raise RemoteIntegrationException("Mock", "Mock exploded.")
Beispiel #4
0
    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
        )
Beispiel #5
0
    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)
Beispiel #6
0
    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))
Beispiel #7
0
    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
            )
Beispiel #8
0
    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
Beispiel #9
0
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