Package: django-rest-webhooks
This is a fork ofhttps://github.com/selfcommunity/rest-hooksthat is
a fork ofhttps://github.com/zapier/django-rest-hooksthat is not supported anymore.What are Django REST Hooks?REST Hooks are fancier versions of webhooks. Traditional webhooks are usually
managed manually by the user, but REST Hooks are not! They encourage RESTful
access to the hooks (or subscriptions) themselves. Add one, two or 15 hooks for
any combination of event and URLs, then get notification in real-time by our
bundled threaded callback mechanism.The best part is: by reusing Django's great signals framework, this library is
dead simple. Here's how to get started:Add'django_rest_webhooks'to installed apps in settings.py.Define yourHOOK_EVENTSin settings.py.Start sending hooks!Using ourbuilt-in actions, zero work is required to supportanybasiccreated,updated, anddeletedactions across any Django model. We also allow forcustom actions(IE: beyondCRUD) to be simply defined and triggered
for any model, as well as truly custom events that let you send arbitrary
payloads.By default, this library will just POST Django's JSON serialization of a model,
but you can alternatively provide aserialize_hookmethod to customize payloads.Please note:this package does not implement any UI/API code, it only
provides a handy framework or reference implementation for which to build upon.
If you want to make a Django form or API resource, you'll need to do that yourself
(though we've provided some example bits of code below).ChangelogVersion 0.0.1:First release of the project after fork fromhttps://github.com/selfcommunity/rest-hooksVersion 0.0.2:Remove some Python 2.7 supportVersion 0.0.3:Update signals importDevelopmentRunning the tests for Django REST Hooks is very easy, just:git clone https://github.com/leanrank/django-rest-webhooks && cd django-rest-hooksNext, you'll want to make a virtual environment (we recommend using virtualenvwrapper
but you could skip this we suppose) and then install dependencies:mkvirtualenv django-rest-webhooks
pip install -r devrequirements.txtNow you can run the tests!python runtests.pyRequirementsPython 3 (tested on 3.3, 3.4, 3.6, 3.8)Django 1.8+ (tested on 1.8, 1.9, 1.10, 1.11, 2.0, 2.2, 3.0)Installing & ConfiguringWe recommend pip to install Django REST Hooks:pip install django-rest-webhooksNext, you'll need to adddjango_rest_webhooks.apps.RestHooksConfigtoINSTALLED_APPSand configure
yourHOOK_EVENTSsetting:### config.py ###INSTALLED_APPS=(# other apps here..."django_rest_webhooks",)HOOK_EVENTS={# 'any.event.name': 'App.Model.Action' (created/updated/deleted)"book.added":"bookstore.Book.created","book.changed":"bookstore.Book.updated+","book.removed":"bookstore.Book.deleted",# and custom events, no extra meta data needed"book.read":"bookstore.Book.read","user.logged_in":None}### bookstore/models.py ###classBook(models.Model):# NOTE: it is important to have a user property# as we use it to help find and trigger each Hook# which is specific to users. If you want a Hook to# be triggered for all users, add '+' to built-in Hooks# or pass user_override=False for custom_hook eventsuser=models.ForeignKey("auth.User",on_delete=models.CASCADE)# maybe user is off a related object, so try...# user = property(lambda self: self.intermediary.user)title=models.CharField(max_length=128)pages=models.PositiveIntegerField()fiction=models.BooleanField()# ... other fields here ...defserialize_hook(self,hook):# optional, there are serialization defaults# we recommend always sending the Hook# metadata along for the ride as wellreturn{"hook":hook.dict(),"data":{"id":self.id,"title":self.title,"pages":self.pages,"fiction":self.fiction,# ... other fields here ...}}defmark_as_read(self):# models can also have custom defined eventsfromdjango_rest_webhooks.signalsimporthook_eventhook_event.send(sender=self.__class__,action="read",instance=self# the Book object)For the simplest experience, you'll just piggyback off the standard ORM which will
handle the basiccreated,updatedanddeletedsignals & events:>>>fromdjango.contrib.auth.modelsimportUser>>>fromdjango_rest_webhooks.modelsimportHook>>>jrrtolkien=User.objects.create(username="jrrtolkien")>>>hook=Hook(user=jrrtolkien,event="book.added",target="http://example.com/target.php")>>>hook.save()# creates the hook and stores it for later...>>>frombookstore.modelsimportBook>>>book=Book(user=jrrtolkien,title="The Two Towers",pages=327,fiction=True)>>>book.save()# fires off 'bookstore.Book.created' hook automatically...NOTE: If you try to register an invalid event hook (not listed on HOOK_EVENTS in settings.py)
you will get aValidationError.Now that the book has been created,http://example.com/target.phpwill get:POST http://example.com/target.php \
    -H Content-Type: application/json \
    -d '{"hook": {
           "id":      123,
           "event":   "book.added",
           "target":  "http://example.com/target.php"},
         "data": {
           "title":   "The Two Towers",
           "pages":   327,
           "fiction": true}}'You can continue the example, triggering two more hooks in a similar method. However,
since we have no hooks set up for'book.changed'or'book.removed', they wouldn't get
triggered anyways....>>>book.title+=": Deluxe Edition">>>book.pages=352>>>book.save()# would fire off 'bookstore.Book.updated' hook automatically>>>book.delete()# would fire off 'bookstore.Book.deleted' hook automaticallyYou can also fire custom events with an arbitrary payload:fromdjango_rest_webhooks.signalsimportraw_hook_eventuser=User.objects.get(id=123)raw_hook_event.send(sender=None,event_name="user.logged_in",payload={"username":user.username,"email":user.email,"when":datetime.datetime.now().isoformat()},user=user# required: used to filter Hooks)How does it work?Django has a stellarsignals framework, all
REST Hooks does is register to receive allpost_save(created/updated) andpost_delete(deleted)
signals. It then filters them down by:WhichApp.Model.Actionactually have an event registered insettings.HOOK_EVENTS.After it verifies that a matching event exists, it searches for matching Hooks via the ORM.Any Hooks that are found for the User/event combination get sent a payload via POST.How would you interact with it in the real world?Let's imagine for a second that you've plugged REST Hooks into your API.
One could definitely provide a user interface to create hooks themselves via
a standard browser & HTML based CRUD interface, but the real magic is when
the Hook resource is part of an API.The basic target functionality is:POSThttp://your-app.com/api/hooks?username=me&api_key=abcdef\-HContent-Type:application/json\-d'{"target":    "http://example.com/target.php","event":     "book.added"}'Now, whenever a Book is created (either via an ORM, a Django form, admin, etc...),http://example.com/target.phpwill get:POSThttp://example.com/target.php\-HContent-Type:application/json\-d'{"hook": {"id":      123,"event":   "book.added","target":  "http://example.com/target.php"},"data": {"title":   "Structure and Interpretation of Computer Programs","pages":   657,"fiction": false}}'It is important to note that REST Hooks will handle all of this hook
callback logic for you automatically.But you can stop it anytime you like with a simple:DELETE http://your-app.com/api/hooks/123?username=me&api_key=abcdefIf you already have a REST API, this should be relatively straightforward,
but if not, Tastypie is a great choice.Some referenceTastypieorDjango REST framework: + REST Hook code is below.Tastypie### resources.py ###fromtastypie.resourcesimportModelResourcefromtastypie.authenticationimportApiKeyAuthenticationfromtastypie.authorizationimportAuthorizationfromdjango_rest_webhooks.modelsimportHookclassHookResource(ModelResource):defobj_create(self,bundle,request=None,**kwargs):returnsuper(HookResource,self).obj_create(bundle,request,user=request.user)defapply_authorization_limits(self,request,object_list):returnobject_list.filter(user=request.user)classMeta:resource_name="hooks"queryset=Hook.objects.all()authentication=ApiKeyAuthentication()authorization=Authorization()allowed_methods=["get","post","delete"]fields=["event","target"]### urls.py ###fromtastypie.apiimportApiv1_api=Api(api_name="v1")v1_api.register(HookResource())urlpatterns=patterns("",(r"^api/",include(v1_api.urls)),)Django REST framework (3.+)### serializers.py ###fromdjango.confimportsettingsfromrest_frameworkimportserializers,exceptionsfromdjango_rest_webhooks.modelsimportHookclassHookSerializer(serializers.ModelSerializer):defvalidate_event(self,event):ifeventnotinsettings.HOOK_EVENTS:err_msg=f"Unexpected event{event}"raiseexceptions.ValidationError(detail=err_msg,code=400)returneventclassMeta:model=Hookfields="__all__"read_only_fields=("user",)### views.py ###fromrest_frameworkimportviewsetsfromdjango_rest_webhooks.modelsimportHookfrom.serializersimportHookSerializerclassHookViewSet(viewsets.ModelViewSet):"""Retrieve, create, update or destroy webhooks."""queryset=Hook.objects.all()model=Hookserializer_class=HookSerializerdefperform_create(self,serializer):serializer.save(user=self.request.user)### urls.py ###fromrest_frameworkimportroutersfrom.importviewsrouter=routers.SimpleRouter(trailing_slash=False)router.register(r"webhooks",views.HookViewSet,"webhook")urlpatterns=router.urlsSome gotchas:Instead of doing blocking HTTP requests inside of signals, we've opted
for a simple Threading pool that should handle the majority of use cases.However, if you use Celery, we'dreallyrecommend using a simple task
to handle this instead of threads. A quick example:### config.py ###HOOK_DELIVERER="path.to.tasks.deliver_hook_wrapper"### tasks.py ###fromceleryimportTaskimportjsonimportrequestsclassDeliverHook(Task):max_retries=5defrun(self,target,payload,instance_id=None,hook_id=None,**kwargs):"""target:     the url to receive the payload.payload:    a python primitive data structureinstance_id:   a possibly None "trigger" instance IDhook_id:       the ID of defining Hook object"""try:response=requests.post(url=target,data=json.dumps(payload),headers={"Content-Type":"application/json"})ifresponse.status_code>=500:response.raise_for_status()exceptrequests.ConnectionError:delay_in_seconds=2**self.request.retriesself.retry(countdown=delay_in_seconds)defdeliver_hook_wrapper(target,payload,instance,hook):# instance is None if using custom event, not built-inifinstanceisnotNone:instance_id=instance.idelse:instance_id=None# pass ID's not objects because using pickle for objects is a bad thingkwargs=dict(target=target,payload=payload,instance_id=instance_id,hook_id=hook.id)DeliverHook.apply_async(kwargs=kwargs)We also don't handle retries or cleanup. Generally, if you get a410or
a bunch of4xxor5xx, you should delete the Hook and let the user know.Extend the Hook model:The defaultHookmodel fields can be extended using theAbstractHookmodel.
For example, to add ais_activefield on your hooks:### config.py ###HOOK_CUSTOM_MODEL="path.to.models.CustomHook"### models.py ###fromdjango.dbimportmodelsfromdjango_rest_webhooks.modelsimportAbstractHookclassCustomHook(AbstractHook):is_active=models.BooleanField(default=True)The extendedCustomHookmodel can be combined with a theHOOK_FINDERsetting
for advanced QuerySet filtering.### config.py ###HOOK_FINDER="path.to.find_and_fire_hook"### utils.py ###from.modelsimportCustomHookdeffind_and_fire_hook(event_name,instance,**kwargs):filters={"event":event_name,"is_active":True,}hooks=CustomHook.objects.filter(**filters)forhookinhooks:hook.deliver_hook(instance)