thorn.request — Thorn 1.3.0 documentation

# Source code for thorn.request

    """

        thorn.webhook.request
        =====================

        Webhook HTTP requests.

    """
    from __future__ import absolute_import, unicode_literals

    import thorn
    import requests

    from celery import uuid
    from celery.utils import cached_property
    from requests.exceptions import ConnectionError, Timeout
    from requests.packages.urllib3.util.url import parse_url
    from vine import maybe_promise, promise
    from vine.abstract import Thenable, ThenableProxy

    from ._state import app_or_default
    from .utils.compat import bytes_if_py2, restore_from_keys
    from .utils.log import get_logger
    from .validators import deserialize_validator, serialize_validator

    __all__ = ['Request']

    DEFAULT_USER_AGENT = 'Mozilla/5.0 (compatible; thorn/{0}; {1})'.format(
        thorn.__version__, requests.utils.default_user_agent(),
    )

    REQUEST_REPR = """\
    <{0}: {1.event} -> {1.subscriber.url} sender={1.sender!r}>\
    """

    logger = get_logger(__name__)

    @Thenable.register
    class Request(ThenableProxy):
        """Webhook HTTP request

        :param event: Name of event.
        :param data: Event payload.
        :param sender: Sender of event (or :const:`None`).
        :param subscriber: Subscriber to dispatch the request for.

        :keyword on_success: Optional callback called if the HTTP request
            succeeds.  Must take single argument: ``request``.
        :keyword on_timeout: Optional callback called if the HTTP request
            times out. Must have signature: ``(request, exc)``.
        :keyword on_error: Optional callback called if the HTTP request
            fails.  Must have signature: ``(request, exc)``.
        :keyword headers: Additional HTTP headers to send with the request.
        :keyword user_agent: Set custom HTTP user agent.
        :keyword recipient_validators: List of serialized recipient validators.
        :keyword allow_keepalive: Allow reusing session for this HTTP request.
            Enabled by default.

        :keyword retry: Retry in the event of timeout/failure?
            Disabled by default.
        :keyword retry_max: Maximum number of times to retry before giving up.
            Default is 3.
        :keyword retry_delay: Delay between retries in seconds int/float.
            Default is 60 seconds.

        """

        app = None

        Session = requests.Session

        #: Holds the response after the HTTP request is performed.
        response = None

        #: Tuple of exceptions considered a connection error.
        connection_errors = (ConnectionError,)

        #: Tuple of exceptions considered a timeout error.
        timeout_errors = (Timeout,)

        #: HTTP User-Agent header.
        user_agent = DEFAULT_USER_AGENT

        def __init__(self, event, data, sender, subscriber,
                     id=None, on_success=None, on_error=None,
                     timeout=None, on_timeout=None,
                     retry=None, retry_max=None, retry_delay=None,
                     headers=None, user_agent=None, app=None,
                     recipient_validators=None,
                     allow_keepalive=True):
            self.app = app_or_default(app or self.app)
            self.id = id or uuid()
            self.event = event
            self.data = data
            self.sender = sender
            self.subscriber = subscriber
            self.timeout = timeout
            self.on_success = on_success
            self.on_timeout = maybe_promise(on_timeout)
            self.on_error = on_error
            self.retry = self.app.settings.THORN_RETRY if retry is None else retry
            self.retry_max = (
                self.app.settings.THORN_RETRY_MAX
                if retry_max is None else retry_max)
            self.retry_delay = (
                self.app.settings.THORN_RETRY_DELAY
                if retry_delay is None else retry_delay)
            if recipient_validators is None:
                recipient_validators = self.app.settings.THORN_RECIPIENT_VALIDATORS
            self.allow_keepalive = allow_keepalive
            self._recipient_validators = recipient_validators
            self.response = None
            self._headers = headers
            self._set_promise_target(promise(
                args=(self,), callback=self.on_success, on_error=self.on_error,
            ))
            if user_agent:
                self.user_agent = user_agent

        def validate_recipient(self, url):
            return [validate(url) for validate in self.recipient_validators]

        def sign_request(self, subscriber, data):
            return subscriber.sign(data)

        def dispatch(self, session=None, propagate=False, close_session=False):
            if self.cancelled:
                return
            self.validate_recipient(self.subscriber.url)
            if session is None or not self.allow_keepalive:
                session, close_session = self.Session(), True
            try:
                self.response = session.post(
                    url=self.subscriber.url,
                    data=self.data,
                    timeout=self.timeout,
                    headers=self.annotate_headers({
                        'Hook-HMAC': self.sign_request(self.subscriber, self.data),
                        'Hook-Subscription': str(self.subscriber.uuid),
                    }),
                )
            except self.timeout_errors as exc:
                self.handle_timeout_error(exc, propagate=propagate)
            except self.connection_errors as exc:
                self.handle_connection_error(exc, propagate=propagate)
            else:
                self._p()
            finally:
                if close_session and session is not None:
                    session.close()
            return self

        def handle_timeout_error(self, exc, propagate=False):
            logger.info('Timed out while dispatching webhook request: %r',
                        exc, exc_info=1, extra={'data': self.as_dict()})
            if self.on_timeout:
                return self.on_timeout(self, exc)
            return self._p.throw(exc, propagate=propagate)

        def handle_connection_error(self, exc, propagate=False):
            logger.error('Error dispatching webhook request: %r',
                         exc, exc_info=1, extra={'data': self.as_dict()})
            self._p.throw(exc, propagate=propagate)

        def as_dict(self):
            """Return a dictionary representation of this request
            suitable for serialization."""
            return {
                'id': self.id,
                'event': self.event,
                'sender': self.sender,
                'subscriber': self.subscriber.as_dict(),
                'data': self.data,
                'timeout': self.timeout,
                'retry': self.retry,
                'retry_max': self.retry_max,
                'retry_delay': self.retry_delay,
                'recipient_validators': self._serialize_validators(
                    self._recipient_validators,
                ),
                'allow_keepalive': self.allow_keepalive,
            }

        def annotate_headers(self, extra_headers):
            return dict(self.headers, **extra_headers)

        def _serialize_validators(self, validators):
            return [serialize_validator(v) for v in validators]

        def __repr__(self):
            return bytes_if_py2(REQUEST_REPR.format(type(self).__name__, self))

        def __reduce__(self):
            return restore_from_keys, (type(self), (), self.__reduce_keys__())

        def __reduce_keys__(self):
            return dict(
                self.as_dict(),
                headers=self._headers,
                user_agent=self.user_agent,
            )

        @cached_property
        def headers(self):
            return dict(self.default_headers, **self._headers or {})

        @property
        def default_headers(self):
            return {
                'Content-Type': self.subscriber.content_type,
                'User-Agent': self.user_agent,
                'Hook-Event': self.event,
                'Hook-Delivery': self.id,
            }

        @cached_property
        def urlident(self):
            """Used to order HTTP requests by URL."""
            url = parse_url(self.subscriber.url)
            return url.host, url.port or 80, url.scheme or 'http'

        @property
        def value(self):
            return self.response  # here for Thenable-compatiblity.

        @cached_property
        def recipient_validators(self):
            return [
                deserialize_validator(v) for v in self._recipient_validators
            ]