Source code for thorn.decorators

"""Webhook decorators."""
from __future__ import absolute_import, unicode_literals
import inspect
from itertools import chain
from six import iteritems as items, itervalues as values
from .utils.compat import bytes_if_py2


[docs]def webhook_model(*args, **kwargs): # type: (*Any, **Any) -> Any """Decorate model to send webhooks based on changes to that model. Keyword Arguments: on_create (~thorn.Event): Event to dispatch whenever an instance of this model is created (``post_save``). on_change (~thorn.Event): Event to dispatch whenever an instance of this model is changed (``post_save``). on_delete (~thorn.Event): Event to dispatch whenever an instance of this model is deleted (``post_delete``). on_$event (~thorn.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 :class:`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})``: .. code-block:: python @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: .. code-block:: python @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}) """ def _augment_model(model): # type: (type) -> type try: Webhooks = model.webhooks except AttributeError: Webhooks, attrs, handlers = None, {}, {} else: if not inspect.isclass(Webhooks): raise TypeError( 'Model.webhooks is not a class, but {0!r}'.format( type(Webhooks))) # get all the attributes from the webhooks class description. attrs = dict(Webhooks.__dict__) # extract all the `on_*` event handlers. handlers = {k: v for k, v in items(attrs) if k.startswith('on_')} # preserve non-event handler attributes. attrs = {k: v for k, v in items(attrs) if k not in handlers} if Webhooks is None or not issubclass(Webhooks, WebhookCapable): # Model.webhooks should inherit from WebhookCapable. # Note that we keep this attribute for introspection purposes # only (kind of like Model._meta), it doesn't implement # any functionality at this point. Webhooks = type( bytes_if_py2('webhooks'), (WebhookCapable,), attrs) model_webhooks = Webhooks(**dict(handlers, **kwargs)) return model_webhooks.contribute_to_model(model) if len(args) == 1: if callable(args[0]): return _augment_model(*args) raise TypeError('argument 1 to @webhook_model() must be a callable') if args: raise TypeError( '@webhook_model() takes exactly 1 argument ({0} given)'.format( sum([len(args), len(kwargs)]))) return _augment_model
[docs]class WebhookCapable(object): """Implementation of model.webhooks. The decorator sets model.webhooks to be an instance of this type. """ reverse = None # type: model_reverser sender_field = None # type: str events = None # type: Mapping[str, Event] def __init__(self, on_create=None, on_change=None, on_delete=None, reverse=None, sender_field=None, **kwargs): # type: (Event, Event, Event, model_reverser, str, **Any) -> None self.events = {} if reverse is not None: self.reverse = reverse if sender_field is not None: self.sender_field = sender_field self.update_events( {k: v for k, v in items(kwargs) if k.startswith('on_')}, on_create=on_create and on_create.dispatches_on_create(), on_change=on_change and on_change.dispatches_on_change(), on_delete=on_delete and on_delete.dispatches_on_delete(), )
[docs] def update_events(self, events, **kwargs): # type: (Mapping[str, Event], **Any) -> None self.events.update(self.connect_events(events, **kwargs))
[docs] def connect_events(self, events, **kwargs): # type: (Mapping[str, Event], **Any) -> Mapping[str, Event] return { k: self.contribute_to_event(v) for k, v in chain(items(events), items(kwargs)) }
[docs] def contribute_to_event(self, event): # type: (Event) -> Event if event: if event.reverse is None: event.reverse = self.reverse event.sender_field = self.sender_field return event
[docs] def contribute_to_model(self, model): # type: (type) -> type [event.connect_model(model) for event in values(self.events) if event] model.webhooks = self model.webhook_events = self # XXX remove for Thorn 2.0 return model
[docs] def payload(self, instance): return self.delegate_to_model(instance, 'webhook_payload')
[docs] def headers(self, instance): return self.delegate_to_model(instance, 'webhook_headers')
[docs] def delegate_to_model(self, instance, meth, *args, **kwargs): # type: (Model, str, *Any, **Any) -> Any fun = getattr(instance, meth, None) if fun is not None: return fun(*args, **kwargs)
def __getitem__(self, key): # type: (str) -> Any return self.events[key] def __setitem__(self, key, value): # type: (str, Any) -> None self.events[key] = value def __delitem__(self, key): # type: (str) -> None del self.events[key]