Skip to main content

A Magento-style system configuration app for Django

Project description

django-sysconfig

PyPI version Python versions CI Release License Demo

A Magento-style system configuration app for Django. Define typed, structured configuration fields in code, store their values in the database, and manage everything through a built-in admin UI — without touching settings.py.


Table of Contents


Features

  • Typed fields — string, integer, decimal, boolean, select, textarea, and encrypted secret types
  • Code-driven schema — configuration structure lives in sysconfig.py files; only values are stored in the database
  • Dot-notation accessorconfig.get("myapp.general.site_name") returns the correct Python type automatically
  • Caching — values are cached via Django's cache framework and invalidated on every write
  • Encryption at rest — secret fields use Fernet (AES-128-CBC + HMAC), key derived from SECRET_KEY
  • 20 built-in validators — email, URL, IP, hostname, range, regex, slug, JSON, port, and more
  • Auto-discoverysysconfig.py files are found and loaded automatically on Django startup
  • Admin UI — built-in staff-only views for browsing and editing configuration per app and section
  • on_save callbacks — react to value changes with custom logic (cache busting, webhooks, etc.)

Requirements

  • Python ≥ 3.11
  • Django ≥ 4.2
  • cryptography ≥ 41.0

Installation

pip install django-sysconfig

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "django_sysconfig",
]

Run migrations:

python manage.py migrate

Optionally, wire up the admin UI:

# urls.py
from django.urls import include, path

urlpatterns = [
    # Must come BEFORE path("admin/", ...) so Django matches it first
    path("admin/config/", include("django_sysconfig.urls")),
    path("admin/", admin.site.urls),
]

Quick Start

1. Define your config schema in myapp/sysconfig.py:

from django_sysconfig.registry import register_config, Section, Field
from django_sysconfig.frontend_models import (
    StringFrontendModel,
    IntegerFrontendModel,
    BooleanFrontendModel,
)
from django_sysconfig.validators import NotEmptyValidator, RangeValidator

@register_config("myapp")
class MyAppConfig:
    class General(Section):
        label = "General Settings"
        sort_order = 10

        site_name = Field(
            StringFrontendModel,
            label="Site Name",
            comment="The public-facing name of the site.",
            default="My App",
            validators=[NotEmptyValidator()],
        )

        max_items = Field(
            IntegerFrontendModel,
            label="Max Items Per User",
            default=100,
            validators=[RangeValidator(min_value=1, max_value=10_000)],
        )

        maintenance_mode = Field(
            BooleanFrontendModel,
            label="Maintenance Mode",
            default=False,
        )

2. Read values anywhere in your project:

from django_sysconfig.accessor import config

site_name = config.get("myapp.general.site_name")       # "My App"
max_items = config.get("myapp.general.max_items")       # 100
maintenance = config.get("myapp.general.maintenance_mode")  # False

Defining Configuration

Create a sysconfig.py file inside any installed Django app. Decorate a class with @register_config("<app_label>") and nest Section subclasses containing Field instances.

from django_sysconfig.registry import register_config, Section, Field
from django_sysconfig.frontend_models import StringFrontendModel, SelectFrontendModel
from django_sysconfig.validators import NotEmptyValidator, ChoiceValidator

@register_config("notifications")
class NotificationsConfig:
    class Email(Section):
        label = "Email Settings"
        sort_order = 10

        sender_address = Field(
            StringFrontendModel,
            label="Sender Address",
            default="no-reply@example.com",
            validators=[NotEmptyValidator()],
        )

        format = Field(
            SelectFrontendModel,
            label="Email Format",
            default="html",
            choices=[("html", "HTML"), ("text", "Plain Text")],
            validators=[ChoiceValidator(["html", "text"])],
        )

    class Sms(Section):
        label = "SMS Settings"
        sort_order = 20

        enabled = Field(
            BooleanFrontendModel,
            label="Enable SMS",
            default=False,
        )

Field options

Parameter Type Description
frontend_model type[BaseFrontendModel] The field type class (required)
label str Human-readable label shown in the admin UI
comment str Help text shown below the input; HTML is allowed
default Any Default value when no DB record exists
sort_order int Display order within the section (lower = first)
validators list[BaseValidator] Validators run before saving
on_save Callable Callback invoked after a value is saved
**kwargs Extra args passed to the frontend model (e.g., choices)

Section options

Attribute Type Description
label str Section heading shown in the admin UI
sort_order int Display order among sections (lower = first)

Field Types

Class Python type Description
StringFrontendModel str Single-line text input
TextareaFrontendModel str Multi-line text area
IntegerFrontendModel int Integer number input
DecimalFrontendModel Decimal Decimal number input (accepts step kwarg)
BooleanFrontendModel bool Checkbox
SelectFrontendModel str Dropdown select (requires choices kwarg)
SecretFrontendModel str Password input — encrypted at rest (see Encryption)

Select field choices format:

Field(
    SelectFrontendModel,
    label="Environment",
    default="production",
    choices=[
        ("development", "Development"),
        ("staging", "Staging"),
        ("production", "Production"),
    ],
)

Decimal field with custom step:

Field(
    DecimalFrontendModel,
    label="Tax Rate",
    default=Decimal("0.20"),
    step="0.001",   # passed through to the HTML input
)

Reading and Writing Values

All paths use dot notation with exactly three parts: app_label.section.field.

from django_sysconfig.accessor import config

# --- Reading ---

# Returns the typed value; falls back to the field's default if not set in DB
config.get("myapp.general.site_name")           # str
config.get("myapp.general.max_items")           # int
config.get("myapp.general.maintenance_mode")    # bool

# Supply a fallback for unknown/unregistered paths (no exception raised)
config.get("myapp.general.unknown", default=42)

# All values for an entire app  →  {section: {field: value, ...}, ...}
config.all("myapp")

# All values for one section  →  {field: value, ...}
config.section("myapp.general")

# Check if a path is registered in code
config.exists("myapp.general.site_name")        # True / False

# Check if a value has been explicitly saved to the database
config.is_set("myapp.general.site_name")        # True / False

# --- Writing ---

config.set("myapp.general.site_name", "Acme Corp")
config.set("myapp.general.max_items", 500)
config.set("myapp.general.maintenance_mode", True)

# Save multiple values atomically
config.set_many({
    "myapp.general.site_name": "Acme Corp",
    "myapp.general.max_items": 500,
})

Exceptions

Exception Raised when
InvalidPathError Path does not have exactly three dot-separated parts
AppNotFoundError No config is registered for the given app label
FieldNotFoundError The field does not exist in the registered schema
ConfigValueError A value cannot be serialized for the given field type

All inherit from ConfigError.


Validators

Import validators from django_sysconfig.validators and pass them as a list to the validators parameter on a Field.

from django_sysconfig.validators import NotEmptyValidator, EmailValidator, RangeValidator

Field(StringFrontendModel, validators=[NotEmptyValidator(), EmailValidator()])

All validators accept an optional message argument to override the default error message.

Presence

Validator Description
NotEmptyValidator() Value must not be None, empty string, empty list, or empty dict. Alias: Required
NotBlankValidator() String value must not be whitespace-only (None is allowed)

String length

Validator Description
MinLengthValidator(min_length) String must be at least min_length characters
MaxLengthValidator(max_length) String must be at most max_length characters

Pattern

Validator Description
RegexValidator(pattern, flags=0, inverse=False) Value must match (or not match, if inverse=True) the given regex pattern
SlugValidator() Value must contain only letters, digits, hyphens, and underscores
JsonValidator() Value must be a valid JSON string

Numeric

Validator Description
RangeValidator(min_value=None, max_value=None) Number must fall within the given range (both bounds inclusive, either optional)
PositiveValidator() Number must be greater than zero
NonNegativeValidator() Number must be zero or greater
PortValidator() Integer must be a valid port number (1–65535)

Network / format

Validator Description
EmailValidator() Must be a valid email address
UrlValidator(schemes=None) Must be a valid URL; schemes defaults to ["http", "https", "ftp"]
IPv4Validator() Must be a valid IPv4 address
IPv6Validator() Must be a valid IPv6 address
IPAddressValidator(version=None) Must be a valid IP address; version can be 4, 6, or None (both)
HostnameValidator() Must be a valid RFC 1123 hostname
DomainValidator() Must be a valid domain name (max 253 characters)

Other

Validator Description
ChoiceValidator(choices) Value must be one of the items in choices
PathValidator(must_be_absolute=False) Value must look like a valid file path; optionally require an absolute path

Running validators manually

from django_sysconfig.validators import validate_value, NotEmptyValidator, EmailValidator

errors = validate_value(
    "not-an-email",
    [NotEmptyValidator(), EmailValidator()],
    field_label="Sender Address",
)
# ["Sender Address: Enter a valid email address."]

on_save Callback

Attach a callback to any field to react when its value changes. The callback receives the full dot-notation path, the new value, and the old value.

def on_maintenance_mode_change(path: str, new_value: bool, old_value: bool) -> None:
    if new_value and not old_value:
        # Notify ops team, clear caches, etc.
        pass

maintenance_mode = Field(
    BooleanFrontendModel,
    label="Maintenance Mode",
    default=False,
    on_save=on_maintenance_mode_change,
)

The callback is fired after the value has been successfully written to the database and the cache has been updated.


Encryption

Fields using SecretFrontendModel are encrypted at rest. Values are encrypted with Fernet (AES-128-CBC + HMAC-SHA256) using a key derived from Django's SECRET_KEY via SHA-256.

  • The encrypted value is stored as a Fernet token in the database.
  • The admin UI always masks the value — it is never displayed.
  • Values are decrypted transparently when read via config.get(...).
  • Rotating SECRET_KEY will make existing encrypted values unreadable; re-save them after rotation.
from django_sysconfig.frontend_models import SecretFrontendModel

api_key = Field(
    SecretFrontendModel,
    label="API Key",
    comment="Your third-party API key. Stored encrypted.",
)

Admin UI

The admin UI is a pair of staff-only class-based views:

URL View Description
/admin/config/ ConfigAppListView Lists all apps that have registered configuration
/admin/config/<app_label>/ ConfigAppDetailView Renders and saves all fields for an app

The Django admin index page is extended with a banner linking to the config UI.

Both views require the user to be a staff member (is_staff=True).


How It Works

  1. Discovery — On startup, AppConfig.ready() calls autodiscover_modules("sysconfig"), which imports sysconfig.py from every installed app.
  2. Registration@register_config("app_label") registers the class with the global ConfigRegistry. For every field that has a default, a ConfigValue database row is created via get_or_create (existing values are never overwritten).
  3. Readingconfig.get("app.section.field") checks the cache first. On a miss, it queries the database. The raw string is deserialized by the field's FrontendModel into the correct Python type (int, bool, Decimal, etc.).
  4. Writingconfig.set(...) serializes the value, writes it to the database, invalidates the cache entry, and then calls the on_save callback if one is defined.
  5. Caching — The cache layer wraps Django's standard cache framework. Entries have no expiry and are invalidated explicitly on every write.

Contributing

See CONTRIBUTING.md for how to set up your local development environment, run tests, and submit a pull request.


License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_sysconfig-0.2.0.tar.gz (33.5 kB view details)

Uploaded Source

Built Distribution

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

django_sysconfig-0.2.0-py3-none-any.whl (39.4 kB view details)

Uploaded Python 3

File details

Details for the file django_sysconfig-0.2.0.tar.gz.

File metadata

  • Download URL: django_sysconfig-0.2.0.tar.gz
  • Upload date:
  • Size: 33.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_sysconfig-0.2.0.tar.gz
Algorithm Hash digest
SHA256 dd19733130625e5f849e2f557e01edce1cf52409cc51a8715d4dbddaafca9327
MD5 0326a57de9ac85ec4efe1d81294dcdd3
BLAKE2b-256 50e1717dacdd1a98fd015c6be615e90b1114395beb58b74b4c9551510b58c86e

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_sysconfig-0.2.0.tar.gz:

Publisher: release.yml on krishnamodepalli/django-sysconfig

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_sysconfig-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_sysconfig-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cf9080a3eae4f0dc052b403d730b6456563b3a55068e97bd41b7dc45c9f4205d
MD5 04ef9694c5d0976df957b1464bcc0515
BLAKE2b-256 d0b3fe0b6b5be56ea3434a6d015e9badd72aed813b21087311488aa4a6678423

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_sysconfig-0.2.0-py3-none-any.whl:

Publisher: release.yml on krishnamodepalli/django-sysconfig

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page