thorn

Python Webhook and Event Framework.

class thorn.Thorn(dispatcher=None, set_as_current=True)[source]

Thorn application.

Dispatcher[source]
Event[source]
ModelEvent[source]
Request[source]
Settings[source]
Subscriber
Subscribers
autodetect_env(apply=<operator.methodcaller object>)[source]
config
disable_buffer(owner=None)[source]

Disable buffering.

Raises:
  • BufferNotEmpty – if there are still items in the
  • buffer when disabling.
dispatcher[source]
dispatchers = {u'celery': u'thorn.dispatch.celery:Dispatcher', u'default': u'thorn.dispatch.base:Dispatcher', u'disabled': u'thorn.dispatch.disabled:Dispatcher'}
enable_buffer(owner=None)[source]

Start buffering up events instead of dispatching them directly.

Note

User will be responsible for flushing the buffer via flush_buffer(), say periodically or at the end of a web request.

env[source]
environments = set([u'thorn.environment.django:DjangoEnv'])
event_cls = u'thorn.events:Event'
flush_buffer(owner=None)[source]

Flush events accumulated while buffering active.

Note

This will force send any buffered events, but the mechanics of how this happens is up to the dispatching backend:

  • default

    Sends buffered events one by one.

  • celery

    Sends a single message containing all buffered events, a worker will then pick that up and execute the web requests.

hmac_sign[source]
model_event_cls = u'thorn.events:ModelEvent'
model_reverser[source]
on_commit
request_cls = u'thorn.request:Request'
reverse
set_current()[source]
set_default()[source]
settings[source]
settings_cls = u'thorn.conf:Settings'
signals
subclass_with_self(Class, name=None, attribute=u'app', reverse=None, keep_reduce=False, **kw)[source]

Subclass an app-compatible class.

App-compatible means the class has an ‘app’ attribute providing the default app, e.g.: class Foo(object): app = None.

Parameters:

Class (Any) – The class to subclass.

Keyword Arguments:
 
  • name (str) – Custom name for the target subclass.
  • attribute (str) – Name of the attribute holding the app. Default is "app".
  • reverse (str) – Reverse path to this object used for pickling purposes. E.g. for app.AsyncResult use "AsyncResult".
  • keep_reduce (bool) – If enabled a custom __reduce__ implementation will not be provided.
webhook_model[source]
class thorn.Event(name, timeout=None, dispatcher=None, retry=None, retry_max=None, retry_delay=None, app=None, recipient_validators=None, subscribers=None, request_data=None, allow_keepalive=None, **kwargs)[source]

Webhook Event.

Parameters:

name (str) – Name of this event. Namespaces can be dot-separated, and if so subscribers can glob-match based on the parts in the name, e.g. "order.created".

Keyword Arguments:
 
  • timeout (float) – Default request timeout for this event.
  • retry (bool) – Enable/disable retries when dispatching this event fails Disabled by default.
  • retry_max (int) – Max number of retries (3 by default).
  • retry_delay (float) – Delay between retries (60 seconds by default).
  • recipient_validators (Sequence) –

    List of functions validating the recipient URL string. Functions must raise an error if the URL is blocked. Default is to only allow HTTP and HTTPS, with respective reserved ports 80 and 443, and to block internal IP networks, and can be changed using the THORN_RECIPIENT_VALIDATORS setting:

    recipient_validators=[
        thorn.validators.block_internal_ips(),
        thorn.validators.ensure_protocol('http', 'https'),
        thorn.validators.ensure_port(80, 443),
    ]
    
  • subscribers – Additional subscribers, as a list of URLs, subscriber model objects, or callback functions returning these
  • request_data – Optional mapping of extra data to inject into event payloads,
  • allow_keepalive – Flag to disable HTTP connection keepalive for this event only. Keepalive is enabled by default.

Warning

block_internal_ips() will only test for reserved internal networks, and not private networks with a public IP address. You can block those using block_cidr_network.

allow_keepalive = True
app = None
dispatcher
prepare_payload(data)[source]
prepare_recipient_validators(validators)[source]

Prepare recipient validator list (instance-wide).

Note

This value will be cached

Return v

prepared_recipient_validators[source]
recipient_validators = None
send(data, sender=None, on_success=None, on_error=None, timeout=None, on_timeout=None)[source]

Send event to all subscribers.

Parameters:

data (Any) – Event payload (must be json serializable).

Keyword Arguments:
 
  • sender (Any) – Optional event sender, as a User instance.
  • context (Dict) – Extra context to pass to subscriber callbacks.
  • timeout (float) – Specify custom HTTP request timeout overriding the THORN_EVENT_TIMEOUT setting.
  • on_success (Callable) – Callback called for each HTTP request if the request succeeds. Must take single Request argument.
  • on_timeout (Callable) – Callback called for each HTTP request if the request times out. Takes two arguments: a Request, and the time out exception instance.
  • on_error (Callable) – Callback called for each HTTP request if the request fails. Takes two arguments: a Request argument, and the error exception instance.
subscribers
class thorn.ModelEvent(name, *args, **kwargs)[source]

Event related to model changes.

This event type follows a specific payload format:

{"event": "(str)event_name",
 "ref": "(URL)model_location",
 "sender": "(User pk)optional_sender",
 "data": {"event specific data": "value"}}
Parameters:

name (str) – Name of event.

Keyword Arguments:
 
  • reverse (Callable) – A function that takes a model instance and returns the canonical URL for that resource.
  • sender_field (str) – Field used as a sender for events, e.g. "account.user", will use instance.account.user.
  • signal_honors_transaction (bool) –

    If enabled the webhook dispatch will be tied to any current database transaction: webhook is sent on transaction commit, and ignored if the transaction rolls back.

    Default is True (taken from the
    THORN_SIGNAL_HONORS_TRANSACTION setting), but

    requires Django 1.9 or greater. Earlier Django versions will execute the dispatch immediately.

    New in version 1.5.

  • propagate_errors (bool) –

    If enabled errors will propagate up to the caller (even when called by signal).

    Disabled by default.

    New in version 1.5.

  • signal_dispatcher (signal_dispatcher) – Custom signal_dispatcher used to connect this event to a model signal.
  • $field__$op (Any) –

    Optional filter arguments to filter the model instances to dispatch for. These keyword arguments can be defined just like the arguments to a Django query set, the only difference being that you have to specify an operator for every field: this means last_name="jerry" does not work, and you have to use last_name__eq="jerry" instead.

    See Q for more information.

See also

In addition the same arguments as Event is supported.

connect_model(model)[source]
dispatches_on_change()[source]
dispatches_on_create()[source]
dispatches_on_delete()[source]
dispatches_on_m2m_add(related_field)[source]
dispatches_on_m2m_clear(related_field)[source]
dispatches_on_m2m_remove(related_field)[source]
get_absolute_url(instance)[source]
instance_data(instance)[source]

Get event data from instance.webhooks.payload().

instance_headers(instance)[source]

Get event headers from instance.webhooks.headers().

instance_sender(instance)[source]

Get event sender from model instance.

on_signal(instance, **kwargs)[source]
send(instance, data=None, sender=None, **kwargs)[source]

Send event for model instance.

Keyword Arguments:
 data (Any) – Event specific data.

See also

Event.send() for more arguments supported.

send_from_instance(instance, context={}, **kwargs)[source]
should_dispatch(instance, **kwargs)[source]
signal_dispatcher
signal_honors_transaction[source]
to_message(data, instance=None, sender=None, ref=None)[source]
class thorn.Q(*args, **kwargs)[source]

Object query node.

This class works like django.db.models.Q, but is used for filtering regular Python objects instead of database rows.

Examples

>>> # Match object with `last_name` attribute set to "Costanza":
>>> Q(last_name__eq="Costanza")
>>> # Match object with `author.last_name` attribute set to "Benes":
>>> Q(author__last_name__eq="Benes")
>>> # You are not allowed to specify any key without an operator,
>>> # even when the following would be fine using Django`s Q objects:
>>> Q(author__last_name="Benes")   # <-- ERROR, will raise ValueError
>>> # Attributes can be nested arbitrarily deep:
>>> Q(a__b__c__d__e__f__g__x__gt=3.03)
>>> # The special `*__eq=True` means "match any *true-ish* value":
>>> Q(author__account__is_staff__eq=True)
>>> # Similarly the `*__eq=False` means "match any *false-y*" value":
>>> Q(author__account__is_disabled=False)
Returns:
to match an object with the given predicates,
call the return value with the object to match: Q(x__eq==808)(obj).
Return type:Callable
apply_op(getter, op, rhs, obj, *args)[source]
apply_trans_op(getter, op, rhs, obj)[source]
branches = {False: <built-in function truth>, True: <built-in function not_>}

If the node is negated (~a / a.negate()), branch will be True, and we reverse the query into a not a one.

compile(fields)[source]
compile_node(field)[source]

Compile node into a cached function that performs the match.

Returns:taking the object to match.
Return type:Callable
compile_op(lhs, rhs, opcode)[source]
gate
gates = {u'AND': <built-in function all>, u'OR': <built-in function any>}

The gate decides the boolean operator of this tree node. A node can either be OR (a | b), or an AND note (a & b). - Default is AND.

operators = {u'contains': <built-in function contains>, u'endswith': <function endswith at 0x7fa7f0727c08>, u'eq': <built-in function eq>, u'gt': <built-in function gt>, u'gte': <built-in function ge>, u'in': <function reversed at 0x7fa7f06c1398>, u'is': <built-in function is_>, u'is_not': <built-in function is_not>, u'lt': <built-in function lt>, u'lte': <built-in function le>, u'ne': <built-in function ne>, u'not': <function <lambda> at 0x7fa7f06c1938>, u'not_in': <function reversed at 0x7fa7f06c1578>, u'now_contains': <function compare at 0x7fa7f06c18c0>, u'now_endswith': <function compare at 0x7fa7f06c1b90>, u'now_eq': <function compare at 0x7fa7f06c1050>, u'now_gt': <function compare at 0x7fa7f06c1140>, u'now_gte': <function compare at 0x7fa7f06c1230>, u'now_in': <function compare at 0x7fa7f06c1320>, u'now_is': <function compare at 0x7fa7f06c16e0>, u'now_is_not': <function compare at 0x7fa7f06c17d0>, u'now_lt': <function compare at 0x7fa7f06c11b8>, u'now_lte': <function compare at 0x7fa7f06c12a8>, u'now_ne': <function compare at 0x7fa7f06c10c8>, u'now_not_in': <function compare at 0x7fa7f06c1500>, u'now_startswith': <function compare at 0x7fa7f06c1aa0>, u'startswith': <function startswith at 0x7fa7f0727b90>, u'true': <function <lambda> at 0x7fa7f06c19b0>}

Mapping of opcode to binary operator functionf(a, b). Operators may return any true-ish or false-y value.

prepare_opcode(O, rhs)[source]
prepare_statement(lhs, rhs)[source]
stack[source]
class thorn.model_reverser(view_name, *args, **kwargs)[source]

Describes how to get the canonical URL for a model instance.

Examples

>>> # This:
>>> model_reverser('article-detail', uuid='uuid')
>>> # for an article instance will generate the URL by calling:
>>> reverse('article_detail', kwargs={'uuid': instance.uuid})
>>> # And this:
>>> model_reverser('article-detail', 'category.name', uuid='uuid')
>>> # for an article instance will generate the URL by calling:
>>> reverse('article-detail',
...         args=[instance.category.name],
...         kwargs={'uuid': instance.uuid},
... )
thorn.webhook_model(*args, **kwargs)[source]

Decorate model to send webhooks based on changes to that model.

Keyword Arguments:
 
  • on_create (Event) – Event to dispatch whenever an instance of this model is created (post_save).
  • on_change (Event) – Event to dispatch whenever an instance of this model is changed (post_save).
  • on_delete (Event) – Event to dispatch whenever an instance of this model is deleted (post_delete).
  • on_$event (Event) – Additional user defined events.,
  • sender_field (str) –

    Default field used as a sender for events, e.g. "account.user", will use instance.account.user.

    Individual events can override the sender field user.

  • reverse (Callable) –

    A thorn.reverse.model_reverser instance (or any callable taking an model instance as argument), that describes how to get the URL for an instance of this model.

    Individual events can override the reverser used.

    Note: On Django you can instead define a get_absolute_url method on the Model.

Examples

Simple article model, where the URL reference is retrieved by reverse('article-detail', kwargs={'uuid': article.uuid}):

@webhook_model
class Article(models.Model):
    uuid = models.UUIDField()

    class webhooks:
        on_create = ModelEvent('article.created')
        on_change = ModelEvent('article.changed')
        on_delete = ModelEvent('article.removed')
        on_deactivate = ModelEvent(
            'article.deactivate', deactivated__eq=True,
        )

    @models.permalink
    def get_absolute_url(self):
        return ('blog:article-detail', None, {'uuid': self.uuid})

The URL may not actually exist after deletion, so maybe we want to point the reference to something else in that special case, like a category that can be reversed by doing reverse('category-detail', args=[article.category.name]).

We can do that by having the on_delete event override the method used to get the absolute url (reverser), for that event only:

@webhook_model
class Article(model.Model):
    uuid = models.UUIDField()
    category = models.ForeignKey('category')

    class webhooks:
        on_create = ModelEvent('article.created')
        on_change = ModelEvent('article.changed')
        on_delete = ModelEvent(
            'article.removed',
            reverse=model_reverser(
                'category:detail', 'category.name'),
        )
        on_hipri_delete = ModelEvent(
            'article.internal_delete', priority__gte=30.0,
        ).dispatches_on_delete()

    @models.permalink
    def get_absolute_url(self):
        return ('blog:article-detail', None, {'uuid': self.uuid})
class thorn.buffer_events(flush_freq=None, flush_timeout=None, app=None)[source]

Context that enables event buffering.

The buffer will be flushed at context exit, or when the buffer is flushed explicitly:

with buffer_events() as buffer:
    ...
    buffer.flush()  # <-- flush here.
# <-- # implicit flush here.
flush()[source]
maybe_flush()[source]
should_flush()[source]