drf-subscription
A generic subscription app for django.
Features
- Recurrence — RFC 5545 recurrence rules powered by
python-dateutil
rrule(daily, weekly, monthly, yearly, intervals, counts, until, month/week day filters) with deterministic, non-mutatingoccurrences(). - Generic relations — subscribe any model, attach resources to any model, snapshots stored as JSON.
- Execution engine — run callbacks and/or signed webhooks when an
occurrence is active: signal accelerator plus a periodic dispatch
(
subscription_dispatchmanagement command or Celery beat), with retries and full audit records (ResourceExecution). - Webhooks — signed HMAC-SHA256 POSTs with configurable headers, timeout and host allowlist.
- REST API — CRUD endpoints, filters,
run/pause/resume/historyactions, pagination and configurable permissions.
See docs/index.md for the full documentation.
Usage example
Say your SaaS bills premium users on the first day of every month.
You already have a User model; this is all you need to schedule the
billing callback while the subscription is active:
# billing/subscriptions.py
from datetime import timedelta
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from subscription.models import (
Subscription, SubscriptionLine, SubscriptionEvent, Resource,
)
def subscribe_premium(user):
user_ct = ContentType.objects.get_for_model(user)
now = timezone.now()
subscription = Subscription.objects.create(
name=f'Premium - {user.username}',
active=True,
content_type=user_ct,
object_pk=str(user.pk),
)
line = SubscriptionLine.objects.create(
subscription=subscription,
start=now,
end=now + timedelta(days=365), # one-year plan
)
# First day of every month, one day long.
event = SubscriptionEvent.objects.create(
subscription_line=line,
start=now,
end=now + timedelta(days=1),
frequency='MONTHLY',
by_month_day=[1],
)
Resource.objects.create(
subscription_event=event,
content_type=user_ct,
object_pk=str(user.pk),
callback='billing.tasks.charge_user',
)
The callback receives the resource; the user is the generic relation
and payload holds the field snapshot:
# billing/tasks.py
def charge_user(sender, instance, **kwargs):
user = instance.content_object # the subscribed User
fields = instance.payload # stored field values
... # charge the card, send the invoice
To notify an external service instead, drop the callback and point
the resource at a webhook:
Resource.objects.create(
subscription_event=event,
content_type=user_ct,
object_pk=str(user.pk),
webhook_url='https://billing.example.com/hooks/premium/',
webhook_secret='shared-secret', # HMAC-SHA256 signature
)
Finally, run the periodic dispatch (cron or Celery beat, see
docs/index.md); it executes each resource once per
occurrence and keeps an audit trail in ResourceExecution:
python manage.py subscription_dispatch
Installation
Dependencies are managed exclusively with uv:
uv sync
REST API
The app ships a DRF router exposing these endpoints (admin-only by default):
/api/subscriptions//api/subscription-lines//api/subscription-events//api/resources/
Versioning
This project follows Semantic Versioning. The version is
kept in subscription/__init__.py (__version__) and read by pyproject.toml.
To bump it:
uv run python scripts/bump_version.py patch # or minor / major
uv run python scripts/bump_version.py patch --tag # and create the git tag vX.Y.Z
Release
Releases are a two-step process handled by two GitHub Actions workflows:
- Bump version (Actions → Bump version → Run workflow): choose
patch,minorormajor. It bumps the version, updates the changelog, commits, tags and pushes. - Publish (Actions → Publish → Run workflow): builds the package, publishes it to PyPI and creates the GitHub Release for the current version.
Alternatively, you can do it locally — bumping and pushing triggers the Publish workflow automatically:
uv run python scripts/bump_version.py patch --commit --tag --push
PyPI publishing setup
Publication uses Trusted Publishing (recommended) or an API token.
Trusted Publishing (recommended)
- On PyPI, go to your project → Settings → Publishing.
- Click Add a new trusted publisher and fill in:
- Owner:
gmork2 - Repository:
drf-subscription - Workflow name:
publish.yml - Environment name: leave blank
- Owner:
- Save.
Only publish.yml publishes to PyPI, so a single trusted publisher is enough.
API token (alternative)
Create an API token on PyPI and add it as a GitHub Actions secret named
PYPI_TOKEN. If set, it takes precedence over trusted publishing.
Metadata
Release files for drf-subscription 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| drf_subscription-0.2.0.tar.gz | 62.7 kB | Without provenance | Details for drf_subscription-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| drf_subscription-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for drf_subscription-0.2.0-py3-none-any.whl |
Total release size: 122.1 kB
Release files / drf_subscription-0.2.0.tar.gz
| Download URL | drf_subscription-0.2.0.tar.gz |
|---|---|
| Size | 62.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7c7949721c4d61ed6e24bc78ae0b67ec15dc2a61a85486273b86cf3e0d4efc32
|
|
BLAKE2b-256 checksum How to use checksums |
c0e523396db3483467cce996e7d44b5404ee4553ea6e97f90de872f2b7716f24
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / drf_subscription-0.2.0-py3-none-any.whl
| Download URL | drf_subscription-0.2.0-py3-none-any.whl |
|---|---|
| Size | 59.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
036624760d6708f206840fbc7ca0af186329a5e198e495940fe9b9653b9f1017
|
|
BLAKE2b-256 checksum How to use checksums |
6a2a226a6971152577b03760af64f6da37fac1d04ba7b211f2fe98be2db81824
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for drf-subscription 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.