Skip to main content

drf-subscription

License: GPL v3 CI

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-mutating occurrences().
  • 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_dispatch management 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/ history actions, 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:

  1. Bump version (Actions → Bump version → Run workflow): choose patch, minor or major. It bumps the version, updates the changelog, commits, tags and pushes.
  2. 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.

  1. On PyPI, go to your project → Settings → Publishing.
  2. Click Add a new trusted publisher and fill in:
    • Owner: gmork2
    • Repository: drf-subscription
    • Workflow name: publish.yml
    • Environment name: leave blank
  3. 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)

Source distribution for drf-subscription 0.2.0
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)

Table of built distributions (wheels) for drf-subscription 0.2.0
File Interpreter ABI Platform Actions
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page