A Magento-style system configuration app for Django
Project description
django-sysconfig
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
- Requirements
- Installation
- Quick Start
- Defining Configuration
- Field Types
- Reading and Writing Values
- Validators
- on_save Callback
- Encryption
- Admin UI
- How It Works
- License
Features
- Typed fields — string, integer, decimal, boolean, select, textarea, and encrypted secret types
- Code-driven schema — configuration structure lives in
sysconfig.pyfiles; only values are stored in the database - Dot-notation accessor —
config.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-discovery —
sysconfig.pyfiles 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_KEYwill 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
- Discovery — On startup,
AppConfig.ready()callsautodiscover_modules("sysconfig"), which importssysconfig.pyfrom every installed app. - Registration —
@register_config("app_label")registers the class with the globalConfigRegistry. For every field that has a default, aConfigValuedatabase row is created viaget_or_create(existing values are never overwritten). - Reading —
config.get("app.section.field")checks the cache first. On a miss, it queries the database. The raw string is deserialized by the field'sFrontendModelinto the correct Python type (int, bool, Decimal, etc.). - Writing —
config.set(...)serializes the value, writes it to the database, invalidates the cache entry, and then calls theon_savecallback if one is defined. - 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
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dd19733130625e5f849e2f557e01edce1cf52409cc51a8715d4dbddaafca9327
|
|
| MD5 |
0326a57de9ac85ec4efe1d81294dcdd3
|
|
| BLAKE2b-256 |
50e1717dacdd1a98fd015c6be615e90b1114395beb58b74b4c9551510b58c86e
|
Provenance
The following attestation bundles were made for django_sysconfig-0.2.0.tar.gz:
Publisher:
release.yml on krishnamodepalli/django-sysconfig
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_sysconfig-0.2.0.tar.gz -
Subject digest:
dd19733130625e5f849e2f557e01edce1cf52409cc51a8715d4dbddaafca9327 - Sigstore transparency entry: 1107765818
- Sigstore integration time:
-
Permalink:
krishnamodepalli/django-sysconfig@8403581cd239d9c12cf0b4337d2a2ef9543419ce -
Branch / Tag:
refs/heads/master - Owner: https://github.com/krishnamodepalli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8403581cd239d9c12cf0b4337d2a2ef9543419ce -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_sysconfig-0.2.0-py3-none-any.whl.
File metadata
- Download URL: django_sysconfig-0.2.0-py3-none-any.whl
- Upload date:
- Size: 39.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf9080a3eae4f0dc052b403d730b6456563b3a55068e97bd41b7dc45c9f4205d
|
|
| MD5 |
04ef9694c5d0976df957b1464bcc0515
|
|
| BLAKE2b-256 |
d0b3fe0b6b5be56ea3434a6d015e9badd72aed813b21087311488aa4a6678423
|
Provenance
The following attestation bundles were made for django_sysconfig-0.2.0-py3-none-any.whl:
Publisher:
release.yml on krishnamodepalli/django-sysconfig
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_sysconfig-0.2.0-py3-none-any.whl -
Subject digest:
cf9080a3eae4f0dc052b403d730b6456563b3a55068e97bd41b7dc45c9f4205d - Sigstore transparency entry: 1107765820
- Sigstore integration time:
-
Permalink:
krishnamodepalli/django-sysconfig@8403581cd239d9c12cf0b4337d2a2ef9543419ce -
Branch / Tag:
refs/heads/master - Owner: https://github.com/krishnamodepalli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8403581cd239d9c12cf0b4337d2a2ef9543419ce -
Trigger Event:
push
-
Statement type: