Skip to main content

django-tunables

Runtime-tunable parameters for Django with a full audit trail. The host project declares a catalogue of parameters in Python: keys, types, defaults, constraints, grouping. Operators change values through the Django admin or a REST API. Every change is an atomic, versioned change set, and every version produces a snapshot: one JSON document holding the complete effective parameter set, ready for other processes to read from the database or a file. Each group can also be described as JSON Schema plus a UI schema, so an external client can render its own editing forms.

It is not a replacement for Django settings, and it does not patch django.conf.settings. It has no per-user, per-tenant or per-environment scopes; one database holds one catalogue with one effective value per key. Scheduled changes, nullable values, and notifications beyond the publisher hook are out of scope.

How it differs from django-constance and django-dynamic-preferences

Both of those store dynamic settings in the database and edit them in the admin. django-tunables adds change sets with actor and reason, an append-only history, a snapshot per version that other processes can read without importing the package, and a schema endpoint that describes each group to external clients. Two ideas were borrowed: the type to form field mapping comes from constance, and giving each type its own form field, serializer and validator comes from dynamic-preferences.

Requirements

Python 3.12 or newer, Django 5.2 or newer, Django REST Framework 3.15 or newer. PostgreSQL 14 or newer for the optional database-level history protection. SQLite works for everything else.

pip install django-tunables

Quickstart

Add the apps and point the setting at your catalogue. In settings.py:

INSTALLED_APPS = [
    # ...
    "rest_framework",
    "tunables",
]

TUNABLES = {"CATALOGUE": "myproject.tunables_catalogue.catalogue"}

Declare the catalogue. In myproject/tunables_catalogue.py:

from tunables import Boolean, Catalogue, Enum, Float, Group, List, Tunable

pricing = Group(
    "pricing",
    title="Pricing",
    order=1,
    tunables=[
        Tunable("vat_rate", Float(min=0.0, max=1.0), 0.24, title="VAT rate"),
        Tunable("free_shipping_over", Float(min=0.0), 50.0, title="Free shipping over", unit="EUR"),
        Tunable("currencies", List(Enum(["EUR", "USD", "GBP"]), min_items=1, unique=True), ["EUR"]),
        Tunable("allow_backorders", Boolean(), False),
    ],
)

thermostat = Group(
    "thermostat",
    title="Thermostat",
    order=2,
    tunables=[
        Tunable("target_c", Float(min=5.0, max=30.0), 21.0, title="Target temperature", unit="°C"),
        Tunable("mode", Enum(["auto", "heat", "cool", "off"]), "auto"),
    ],
)

catalogue = Catalogue([pricing, thermostat])

Mount the API next to the admin. In myproject/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/tunables/", include("tunables.api.urls")),
]

Create the tables and the first snapshot:

python manage.py migrate
python manage.py tunables_sync

Read the effective values:

curl -s http://localhost:8000/api/tunables/values/

Change one:

curl -s -X POST http://localhost:8000/api/tunables/changesets/ \
  -H "Content-Type: application/json" \
  -H "X-Tunables-Actor: alice" \
  -d '{"changes": [{"key": "pricing.vat_rate", "value": 0.2}], "reason": "autumn rate"}'

The response is the new change set with version 1. tunables_show prints the effective values with an override marker, and the admin's Tunables page offers the same edit as a form. Run tunables_sync after every deployment, after migrate, so the database mirror follows the catalogue in code.

Settings

All settings live in one dictionary, TUNABLES. Only CATALOGUE is required.

Key Default Meaning
CATALOGUE required Dotted path to a Catalogue instance or to a zero-argument callable returning one.
ENVIRONMENT "" Written into every snapshot document as environment.
PUBLISHERS [] Dotted paths of publisher classes, instantiated once with no arguments.
FILE_PUBLISHER_PATH None Target file of tunables.publishers.FilePublisher.
PAGE_SIZE 50 Page size of the change set list endpoint.
API_AUTHENTICATION_CLASSES None DRF authentication classes for the API. None uses the host's DRF defaults.
API_PERMISSION_CLASSES None DRF permission classes for the API. None uses the host's DRF defaults.
ACTOR_RESOLVER tunables.api.actors.default_actor_resolver Callable turning a request into an Actor.
EDITABLE_GROUPS None Callable returning the group names a request may write, or None for all.
ACTOR_HEADER X-Tunables-Actor Header naming the actor of an unauthenticated request.
CLIENT_HEADER X-Tunables-Client Header naming the client program.
REQUEST_ID_HEADER X-Request-ID Header whose value is recorded as the change set's request id.
REASON_HEADER X-Tunables-Reason Header carrying the reason for a PATCH of group values.

Management commands

Command Does
tunables_sync [--check] Mirrors the catalogue into the database, creates the state row and snapshot 0 on a fresh database, and writes a system version when the catalogue structure changed. --check exits 1 when the stored catalogue version differs from the code, without writing. Idempotent.
tunables_show [--group NAME] [--json] Prints the effective values of the latest snapshot, one key per line, marking overrides. --json prints the snapshot document.
tunables_export [--output FILE] Writes the latest snapshot document as JSON to a file or to standard output.
tunables_import FILE --actor NAME [--reason TEXT] [--strict] Applies the values of a snapshot document as one change set with source: import. Unknown keys are skipped with a warning, or rejected with --strict.
tunables_protect_history [--remove] [--database ALIAS] Installs PostgreSQL triggers that reject UPDATE, DELETE and TRUNCATE on the history tables.

tunables_sync --check compares the catalogue hash only. A change to a title or description makes the mirror stale without changing the hash; the next tunables_sync updates the mirror without writing a new version.

Publishers and the signal

The snapshot row in the database is always written. In addition, every committed snapshot is sent to the tunables.signals.snapshot_published signal, with the Snapshot instance as snapshot, and then to each publisher named in PUBLISHERS. A publisher is any object with a publish(snapshot) method. An exception inside publish is logged under the tunables.publishers logger and does not affect the write. Both the signal and the publishers run after the transaction commits.

tunables.publishers.FilePublisher writes the document to FILE_PUBLISHER_PATH, using a temporary file in the same directory and a rename, so readers never see a partial file:

TUNABLES = {
    "CATALOGUE": "myproject.tunables_catalogue.catalogue",
    "PUBLISHERS": ["tunables.publishers.FilePublisher"],
    "FILE_PUBLISHER_PATH": "/var/lib/myproject/tunables.json",
}

Reading snapshots from other processes

A reader polls tunables_state.current_version, and when it changes fetches tunables_snapshot.document for that version. The document format, the JSON Schema shipped with the package, and the table contract are in docs/snapshot-format.md.

API

Method and path Purpose
GET groups/, GET groups/{group}/ groups and their definitions
GET groups/{group}/schema/, GET schema/ JSON Schema and UI schema
GET values/, GET groups/{group}/values/ effective values, with ETag
GET changesets/, GET changesets/{version}/ history, paginated and filterable
GET snapshots/latest/, GET snapshots/{version}/, GET export/ snapshot documents
POST changesets/, POST validate/ apply or dry-run a list of changes
PATCH groups/{group}/values/ form-shaped write of one group
POST rollback/, POST import/ restore a version, import a document

Writes accept If-Match for optimistic concurrency and every response carries X-Tunables-Version. Errors are RFC 9457 problem documents. The full reference, including the problem types and error codes, is in docs/api.md.

Admin

The Tunables page in the admin lists the groups and edits one group per form with a reason field and a reset box per override. The change set history is read only and has a rollback action. See docs/admin.md.

A screenshot of the group edit form will be added here.

Documentation

Development

uv sync --all-extras
uv run pytest
uv run pytest --postgres    # the whole suite on a PostgreSQL testcontainer, needs Docker
uv run ruff check . && uv run mypy src
scripts/quickstart_check.sh    # installs the built wheel into a fresh project and runs the quickstart

License

MIT. See LICENSE.

Metadata

Release files for django-tunables 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-tunables 0.1.1
File Size Uploaded Provenance Actions
django_tunables-0.1.1.tar.gz 55.8 kB Without provenance Details for django_tunables-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for django-tunables 0.1.1
File Interpreter ABI Platform Actions
django_tunables-0.1.1-py3-none-any.whl Python 3 none any Details for django_tunables-0.1.1-py3-none-any.whl

Total release size: 100.0 kB

Release files / django_tunables-0.1.1.tar.gz

Download URL django_tunables-0.1.1.tar.gz
Size 55.8 kB
Tags Source
SHA-256 checksum
How to use checksums
3300451c8cab91717b8b16e2182a459dca3b5a6dcbf09f2a58afedafb189e862
BLAKE2b-256 checksum
How to use checksums
d0156cbfa64c5ebef7e7b8254f710cd252cb6197c15c470760c8d263aa9ef44e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / django_tunables-0.1.1-py3-none-any.whl

Download URL django_tunables-0.1.1-py3-none-any.whl
Size 44.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
73ce65547644a9a0e3387492f221df5bc06c6f1921dc7b8cbe916550eff2e4c1
BLAKE2b-256 checksum
How to use checksums
986dc34a33d9f25c2dc6b73133743f46efbb0d35c9de77970cfdc42c8d71f483
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.16 {"installer":{"name":"uv","version":"0.12.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for django-tunables 0.1.1

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