Skip to main content

Django Dynamic Settings

Build status PyPI PyPI - Django version PyPI - Python version PyPI - License

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

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:

  1. Code-defined defaults via Django setting:
DYNAMIC_SETTINGS_ENCRYPTED_FIELDS = ["password", "api_key", "token"]
  1. Runtime via the /encrypted_keys/ REST API or admin tooling. Admins POST/DELETE entries against the EncryptedKey endpoint 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 key is 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_KEY via SHA256 → urlsafe base64. Zero setup required.
  • Optional override: set DYNAMIC_SETTINGS_ENCRYPTION_KEY to a stable 32-byte urlsafe base64 Fernet key. Recommended if SECRET_KEY is 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 EncryptedKey row is created and all existing matching Setting data 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 EncryptedKey table. Any row whose field_name is not in DYNAMIC_SETTINGS_ENCRYPTED_FIELDS is removed and its data decrypted on the next run — including rows created through the POST /encrypted_keys API. 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-run to 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 Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dj_dynamic_settings-0.3.1-py3-none-any.whl (20.7 kB view details)

Uploaded Python 3

File details

Details for the file dj_dynamic_settings-0.3.1-py3-none-any.whl.

File metadata

File hashes

Hashes for dj_dynamic_settings-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9983745217a06cf75e3e9bf30b519880cebab1bd975719ccdfdd37087c164812
MD5 8968bf018eda6d4bfd48a3deac5e4db4
BLAKE2b-256 d8946679c34bb9ef3195f76073b0efceac2e96a01062ab58f9f3441d6bba9c5f

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.1 This release

1 file

0.3.0

1 file

0.2.10

2 files

0.2.9

2 files

0.2.8

2 files

0.2.7

2 files

0.2.6

2 files

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

0.0.1

2 files

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