Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: CI

on:
pull_request:
branches: [ main ]
push:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11']
django-version: ['4.0', '4.1', '4.2']

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true

- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root

- name: Install project
run: poetry install --no-interaction

- name: Install specific Django version
run: poetry add django~=${{ matrix.django-version }}

- name: Run tests
run: make test

- name: Upload coverage reports
uses: codecov/codecov-action@v3
if: matrix.python-version == '3.10' && matrix.django-version == '4.2'
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella

lint:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true

- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v3
with:
path: .venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}

- name: Install dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: poetry install --no-interaction --no-root

- name: Install project
run: poetry install --no-interaction

- name: Run linting
run: make lint
22 changes: 1 addition & 21 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,6 @@

## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:

# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# .idea/shelf

# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml

# Gradle:
# .idea/gradle.xml
# .idea/libraries

# Mongo Explorer plugin:
# .idea/mongoSettings.xml

## File-based project format:
*.ipr
Expand Down Expand Up @@ -118,3 +97,4 @@ target/

#Test Coverage
tests/reports/
/pytest_report.xml
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## ISC License

Copyright (c) 2021 AM-Flow b.v.
Copyright (c) 2021-2025 AM-Flow b.v.

Copyright (c) 2016 Zapier Inc.

Expand Down
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
PATH := $(PATH):$(HOME)/.local/bin
SHELL := env PATH=$(PATH) /bin/bash

.PHONY: build format test lint

build:
poetry install

format:
poetry run ruff check . --select I --fix
poetry run ruff format .

lint:
poetry run ruff check . --diff
poetry run ruff format . --check --diff

test:
poetry run pytest tests/
101 changes: 52 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

drf-hooks is a fork of [Zapier's django-rest-hooks](https://github.com/zapier/django-rest-hooks), which is unfortunately not maintained anymore.

drf-hooks adds closer DRF integration by allowing you to specify DRF serializers to use for each model rather than requiring a `serialize_hook()` method on your models. It also allows hooks to specify custom headers to be added in the hook request (for instance for authentication).


## What are 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 notificatied in real-time by our
any combination of event and URLs, then get notified in real-time by our
bundled threaded callback mechanism.

The best part is: by reusing Django's great signals framework, this library is
The best part is: by reusing Django's signals framework, this library is
dead simple. Here's how to get started:

1. Add `'drf_hooks'` to installed apps in settings.py.
Expand All @@ -34,48 +32,24 @@ 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).

### Changelog

#### Version 0.1.0:

- Forked from zapier rest hooks
- Support for DRF serializers
- Custom hook header support


### Development

Running the tests for Django REST Hooks is very easy, just:
### Requirements

```
git clone https://github.com/am-flow/drf-hooks && cd drf-hooks
```
* Python 3.9+
* Django 3.1+
* Django REST framework 3.11+

Next, you'll want to make a virtual environment (we recommend using virtualenvwrapper
but you could skip this we suppose) and then install dependencies:

```
mkvirtualenv drf-hooks
pip install -r requirements.txt
```

Now you can run the tests!
### Installing & Configuring

```
python runtests.py
pip install drf-hooks
```

### Requirements

* Python 3.7+ (tested on 3.7)
* Django 3.1+ (tested on 3.1)

### Installing & Configuring

We recommend pip to install drf-hooks:
or

```
pip install drf-hooks
poetry add drf-hooks
```

Next, you'll need to add `drf_hooks` to `INSTALLED_APPS` and configure
Expand Down Expand Up @@ -106,6 +80,9 @@ HOOK_SERIALIZERS = {


### bookstore/models.py ###
from django.db import models
from rest_framework import serializers


class Book(models.Model):
# NOTE: it is important to have a user property
Expand Down Expand Up @@ -134,7 +111,7 @@ class Book(models.Model):

### bookstore/serializers.py ###

class BookSerializer(serializers.ModelSerializer)
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
Expand All @@ -144,19 +121,19 @@ For the simplest experience, you'll just piggyback off the standard ORM which wi
handle the basic `created`, `updated` and `deleted` signals & events:

```python
>>> from django.contrib.auth.models import User
>>> from drf_hooks.models import Hook
>>> jrrtolkien = User.objects.create(username='jrrtolkien')
>>> hook = Hook(user=jrrtolkien,
from django.contrib.auth.models import User
from drf_hooks.models import Hook
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...
>>> from bookstore.models import Book
>>> book = Book(user=jrrtolkien,
hook.save() # creates the hook and stores it for later...
from bookstore.models import Book
book = Book(user=jrrtolkien,
title='The Two Towers',
pages=327,
fiction=True)
>>> book.save() # fires off 'bookstore.Book.created' hook automatically
book.save() # fires off 'bookstore.Book.created' hook automatically
...
```

Expand Down Expand Up @@ -184,15 +161,18 @@ triggered anyways.

```python
...
>>> 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 automatically
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 automatically
```

You can also fire custom events with an arbitrary payload:

```python
import datetime
from django.contrib.auth.models import User

from drf_hooks.signals import raw_hook_event

user = User.objects.get(id=123)
Expand Down Expand Up @@ -342,3 +322,26 @@ class CeleryHook(AbstractHook):

We also don't handle retries or cleanup. Generally, if you get a `410` or
a bunch of `4xx` or `5xx`, you should delete the Hook and let the user know.


### Development

#### Running tests

Clone the repo:

```
git clone https://github.com/am-flow/drf-hooks && cd drf-hooks
```

Install dependencies:

```
make build
```

Run tests:

```
make tests
```
16 changes: 10 additions & 6 deletions drf_hooks/admin.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
from django.contrib import admin
from django.conf import settings
from django import forms
from django.conf import settings
from django.contrib import admin

from .models import get_hook_model


class HookForm(forms.ModelForm):
"""
Model form to handle registered events, asuring
Model form to handle registered events, assuring
only events declared on HOOK_EVENTS settings
can be registered.
"""

class Meta:
model = get_hook_model()
fields = ['user', 'target', 'event', 'headers']
fields = ["user", "target", "event", "headers"]

def __init__(self, *args, **kwargs):
super(HookForm, self).__init__(*args, **kwargs)
self.fields['event'] = forms.ChoiceField(choices=self.get_admin_events())
self.fields["event"] = forms.ChoiceField(choices=self.get_admin_events())

@classmethod
def get_admin_events(cls):
return [(x, x) for x in settings.HOOK_EVENTS]


class HookAdmin(admin.ModelAdmin):
list_display = [f.name for f in get_hook_model()._meta.fields]
raw_id_fields = ['user', ]
raw_id_fields = [
"user",
]
form = HookForm


Expand Down
Loading