This release is a pre-release and may not be stable for production use.
Django Dynamic Settings
Django Dynamic Settings allows you to create & use dynamic settings backed by a database.
Installation
Installation using pip:
pip install dj-dynamic-settings
dj_dynamic_settings app has to be added to INSTALLED_APPS and migrate command has to be run.
INSTALLED_APPS = (
# other apps here...
"dj_dynamic_settings",
)
dj_dynamic_settings.urls must be included to a desired url path.
urlpatterns = [
...,
url(r"^api/v1/", include("dj_dynamic_settings.urls")),
]
Setting class must be defined & registered. Please make sure that this class' module runs whenever the application runs.
from dj_dynamic_settings.registry import BaseSetting, registry
from dj_dynamic_settings.validators import TypeValidator
@registry.register
class FeatureActive(BaseSetting):
key = "FEATURE_ACTIVE"
validators = [TypeValidator(bool)]
default = False
description = "Flag for Feature X"
Create Setting instance using view.
import requests
requests.post(
url="https://your-app.com/api/v1/dynamic_settings/",
headers={
"Authorization": "Token <secret-login-token>",
},
json={
"key": "FEATURE_ACTIVE",
"value": True,
"is_active": True,
}
)
Access this setting as in django.conf.settings
from dj_dynamic_settings.conf import settings
settings.FEATURE_ACTIVE # True
Caching and cross-server invalidation
A value read through dj_dynamic_settings.conf.settings is served from the
first layer that has it:
- In-process holder — a dict held on the
Settingssingleton. No I/O. - Shared cache — the Django cache backend named by
CACHE_ALIAS. - Database — the
Settingrow, or the registered class'default.
Saving or deleting a Setting drops its shared cache entry and increments a
global version counter kept in the same cache. Other processes read that counter
periodically; when it has moved they discard their holder, so a change made on
one server reaches the rest without a restart.
| Setting | Default | Description |
|---|---|---|
DYNAMIC_SETTINGS_USE_CACHE |
True |
Use the shared cache and the in-process holder. When False, every read goes to the database, nothing is held, and no invalidation is needed. |
DYNAMIC_SETTINGS_CACHE_ALIAS |
"default" |
Which entry of Django's CACHES to use. |
DYNAMIC_SETTINGS_CACHE_TIMEOUT |
3600 |
TTL in seconds of a cached setting value. |
DYNAMIC_SETTINGS_CACHE_KEY_PREFIX |
"dynamic-settings" |
Prefix of every cache key. Two projects sharing one cache backend must not share this value. |
DYNAMIC_SETTINGS_VERSION_CHECK_ENABLED |
True |
Invalidate the holder from the global version counter. |
DYNAMIC_SETTINGS_VERSION_CHECK_INTERVAL |
30 |
Seconds between two version checks in one process. |
DYNAMIC_SETTINGS_VERSION_KEY |
"global_version" |
Cache key suffix of the version counter. |
DYNAMIC_SETTINGS_LOCAL_VERSION_CACHE_ENABLED |
True |
Share the observed version between the processes of one machine through a file, to cut cache round-trips. |
DYNAMIC_SETTINGS_LOCAL_VERSION_CACHE_PATH |
"/tmp/dynamic_settings" |
Directory of that file. Must be writable by the application user. |
DYNAMIC_SETTINGS_LOCAL_VERSION_CACHE_TTL |
30 |
Seconds a version read from that file is trusted before the shared cache is consulted again. |
How stale a read can be
Propagation is not immediate, and the bound depends on where the change was made:
| Change made | Usually visible elsewhere within |
|---|---|
| Same machine | VERSION_CHECK_INTERVAL (default 30s) |
| Another machine, local version cache on | VERSION_CHECK_INTERVAL + LOCAL_VERSION_CACHE_TTL (default 60s) |
| Another machine, local version cache off | VERSION_CHECK_INTERVAL (default 30s) |
The two terms are independent: a process consults the version at most once per
VERSION_CHECK_INTERVAL, and the version it then reads from the local file may
itself have been written up to LOCAL_VERSION_CACHE_TTL ago. A write publishes
the new version to its own machine's file immediately, which is why the local
term drops out for changes made there. These are the ordinary bounds rather than
hard guarantees: a process that happens to read the file while another is
refreshing it keeps the previous version for one further interval.
If the cache backend is unreachable the check is skipped and the process keeps serving the values it already holds, rather than failing every settings read.
Do not set
VERSION_CHECK_ENABLEDtoFalseon a multi-process deployment. With the check off, a process only clears its holder for a write it performed itself, so every other worker serves its held values until it restarts. It is safe only where a single process performs all writes and all reads.
Local version cache
The local cache is an optimisation, never a source of truth. It holds one small
binary record — the version counter and its expiry — in
LOCAL_VERSION_CACHE_PATH, read through mmap and written under an exclusive
flock, which requires a POSIX platform. Processes that share that filesystem
read the file instead of the shared cache for LOCAL_VERSION_CACHE_TTL seconds,
turning their version checks into one cache round-trip per machine. The file
name carries a digest of CACHE_KEY_PREFIX, so two projects sharing a directory
do not overwrite each other's counter.
A record that is torn, expired or unreadable is rejected — the record carries a checksum — and the version is read from the shared cache instead. An unwritable path degrades the same way, so the local cache can be lost or disabled at any time without affecting correctness.
Create / Update Triggers
To fire a callback method when a specific setting value updated or created, you can implement post_save_actions in BaseSetting inherited class
Following example shows how to implement post_save_actions method.
The callback method will be called with following kwargs:
key=instance.key
value=instance.value
created=created # is create operation
Note: post_save_actions returns an array, so you can add multiple callback methods. These callback methods will be called synchronously.
class PostUpdateTestConfiguration(BaseSetting):
key = "X_FEATURE_POST_UPDATE"
validators = [...]
@classmethod
def post_save_actions(cls):
return [
on_data_updated,
]
def on_data_updated(*args, **kwargs):
pass
Testing Tools
override_settings()
You can override a setting for a test method or test class.
from dj_dynamic_settings.utils import override_settings
from django.test import TestCase
@override_settings(SOME_SETTING="some_setting")
class FeatureTestCase(TestCase):
@override_settings(SOME_OTHER_SETTING="SOME_OTHER_SETTING")
def test_feature(self):
# Some stuff
pass
def test_feature_x(self):
with override_settings(SOME_OTHER_SETTING="SOME_OTHER_SETTING"):
# Some stuff
pass
Selective Field-Level Encryption
dj-dynamic-settings supports selective Fernet encryption of sensitive field values. Configuration is two-pronged:
- Code-defined defaults via Django setting:
DYNAMIC_SETTINGS_ENCRYPTED_FIELDS = ["password", "api_key", "token"]
- Runtime via the
/encrypted_keys/REST API or admin tooling. Admins POST/DELETE entries against theEncryptedKeyendpoint to add/remove encrypted fields without a code deploy.
Both sources are merged via a frozenset union — duplicates dedup automatically.
Setting value JSON content is scanned recursively:
- If the Setting's own
keyis in the active field-name set, the entire value is encrypted (scalar case). - Inside dicts (at any nesting depth), keys matching the active set have their values encrypted.
- Inside lists, each element is recursed.
Decryption is transparent — your code reads settings.X_FOO and gets plaintext, no changes required:
from dj_dynamic_settings.conf import settings
settings.X_PAYMENT_GATEWAY # returns plaintext, decrypted on the fly
Cache (Django cache framework) stores ciphertext only; decryption happens at return time. API GET responses replace encrypted fields with "***ENCRYPTED***" (configurable via DYNAMIC_SETTINGS_ENCRYPTION_MASK).
Writes treat the mask string as a sentinel: when a PATCH/PUT echoes the mask back for a field (e.g. the client copied a GET response and changed only one field), the stored encrypted value is preserved instead of re-encrypting the mask text. A mask sent for a path that has no stored encrypted value (including on create) is rejected with a validation error. Consequently the mask string itself cannot be stored as a literal value; override DYNAMIC_SETTINGS_ENCRYPTION_MASK if that literal is ever needed.
Encryption key:
- Default: derived from Django's
SECRET_KEYvia SHA256 → urlsafe base64. Zero setup required. - Optional override: set
DYNAMIC_SETTINGS_ENCRYPTION_KEYto a stable 32-byte urlsafe base64 Fernet key. Recommended ifSECRET_KEYis ever rotated.
When an EncryptedKey row is created or deleted through the API, all existing Settings are scanned and re-encrypted or decrypted symmetrically, atomically within the request. See Encrypting existing data below for the non-API path and the immutability of field_name.
Encrypting existing data
The DYNAMIC_SETTINGS_ENCRYPTED_FIELDS setting is the source of truth for which
fields are encrypted at rest. Reconcile the EncryptedKey table to match it with:
python manage.py sync_encrypted_fields # apply the reconcile
python manage.py sync_encrypted_fields --dry-run # print the plan, write nothing
The command mirrors the table to the list, in one transaction:
- A field newly added to the list → an
EncryptedKeyrow is created and all existing matchingSettingdata is encrypted. - A field removed from the list → existing data is decrypted at rest and the row is deleted (a WARNING is logged for each decrypted field).
Run it once per deploy, after migrate. On Akinon Cloud, add it to the
akinon.json release script:
"release": "python manage.py migrate --no-input && python manage.py sync_encrypted_fields"
The list is authoritative for the entire
EncryptedKeytable. Any row whosefield_nameis not inDYNAMIC_SETTINGS_ENCRYPTED_FIELDSis removed and its data decrypted on the next run — including rows created through thePOST /encrypted_keysAPI. A project that runs this command should manage encrypted fields exclusively through the settings list, not the API.
Removing a field decrypts data at rest. An accidental edit to the list will decrypt sensitive values on the next deploy. Use
--dry-runto preview, and review list changes carefully.
field_name is immutable on an existing EncryptedKey (a PATCH/PUT changing it
returns HTTP 400).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dj_dynamic_settings-0.4.0rc2.tar.gz.
File metadata
- Download URL: dj_dynamic_settings-0.4.0rc2.tar.gz
- Upload date:
- Size: 46.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/4.0.2 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2edb7477327a41530031bd719e89c35dd15b7d58f2b837e00d4b293e4988d783
|
|
| MD5 |
20c818c7d3ac84a796421e94dcc8aa6b
|
|
| BLAKE2b-256 |
ebc1f0ac8ae0eec453bffc5800121983cb18951d6459d6e89bc5ee201a7813eb
|
File details
Details for the file dj_dynamic_settings-0.4.0rc2-py3-none-any.whl.
File metadata
- Download URL: dj_dynamic_settings-0.4.0rc2-py3-none-any.whl
- Upload date:
- Size: 26.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/4.0.2 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca873803e7d6565fff6c4f3c82e167d0ae79d339bfbc472ef588073b00521a71
|
|
| MD5 |
2b5ac63fe5add7d4a0ed00e6617036e8
|
|
| BLAKE2b-256 |
21a2713f0952629d7755213330288fcc7dab5e53e048f2fab7b545fac3b6392e
|