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
[docs]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
[docs] def validate_recipient(self, url): return [validate(url) for validate in self.recipient_validators]
[docs] def sign_request(self, subscriber, data): return subscriber.sign(data)
[docs] 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
[docs] 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)
[docs] 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)
[docs] 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, }
[docs] 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
[docs] 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
[docs] 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
[docs] def recipient_validators(self): return [ deserialize_validator(v) for v in self._recipient_validators ]