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. Applied by the API and the admin.
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. Both forms print a warning per group or catalogue rule the stored values break; the exit code does not change for that.
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] [--defaults [--created-at ISO8601]] [--schema] Writes the latest snapshot document as JSON to a file or to standard output. --defaults writes the version-zero document from the catalogue in code, --schema writes the JSON Schema of documents for that catalogue; both need no database. --created-at fixes the timestamp of the defaults document so repeated runs produce the same file.
tunables_import FILE --actor NAME [--reason TEXT] [--strict] [--replace] 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. --replace also resets every override the document does not name, so the document becomes the complete state.
tunables_protect_history [--remove] [--database ALIAS] Installs PostgreSQL triggers that reject UPDATE, DELETE and TRUNCATE on the history tables.
tunables_publish [VERSION] [--publisher PATH] Sends a stored snapshot, the latest by default, to the configured publishers again and records the outcome. Fails when a publisher fails.

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.

Each publisher's outcome is recorded: the version it last received, when, and the last error if any. GET status/ shows this per publisher, so a gap between the current version and a publisher's last version is visible. manage.py tunables_publish [VERSION] [--publisher PATH] sends a stored snapshot, the latest by default, to all or one publisher again, and fails with the publisher's error when it does not succeed.

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 definitions/ every tunable with its type, default and texts
GET groups/{group}/schema/, GET schema/ JSON Schema and UI schema per group
GET snapshots/schema/ JSON Schema of snapshot documents for this catalogue
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
GET diff/?from=&to= per-key changes between two versions
GET status/ sync state, current version and catalogue hashes, for probes
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.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 django-tunables 0.2.0
File Size Uploaded Provenance Actions
django_tunables-0.2.0.tar.gz 68.6 kB Without provenance Details for django_tunables-0.2.0.tar.gz

Built distribution (wheel)

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

Total release size: 120.1 kB

Release files / django_tunables-0.2.0.tar.gz

Download URL django_tunables-0.2.0.tar.gz
Size 68.6 kB
Tags Source
SHA-256 checksum
How to use checksums
efb2ed19d220e1bd2e6abe398e192b6e0a6f706e5f9b8ff42994a68ebf32fd05
BLAKE2b-256 checksum
How to use checksums
98fa72e9d30a21a4a136ccb39c39d85bba1a91a8a5ed1dd8aa11d5dd052ab860
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.2.0-py3-none-any.whl

Download URL django_tunables-0.2.0-py3-none-any.whl
Size 51.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
39c9aa7de55fb510ffbc2ffeb68e6c8558d2ba0c961f9a21bd52303b0bfaba04
BLAKE2b-256 checksum
How to use checksums
8b3749e1e8694a0c03f0bc661b4063f0875870496c742932848ce94dba3c510a
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.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