django-sysconfig 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_sysconfig-0.0.1/.gitignore +59 -0
- django_sysconfig-0.0.1/LICENSE +21 -0
- django_sysconfig-0.0.1/PKG-INFO +486 -0
- django_sysconfig-0.0.1/README.md +449 -0
- django_sysconfig-0.0.1/django_sysconfig/__init__.py +35 -0
- django_sysconfig-0.0.1/django_sysconfig/accessor.py +368 -0
- django_sysconfig-0.0.1/django_sysconfig/admin.py +36 -0
- django_sysconfig-0.0.1/django_sysconfig/apps.py +12 -0
- django_sysconfig-0.0.1/django_sysconfig/cache.py +45 -0
- django_sysconfig-0.0.1/django_sysconfig/encryption.py +108 -0
- django_sysconfig-0.0.1/django_sysconfig/exceptions.py +78 -0
- django_sysconfig-0.0.1/django_sysconfig/frontend_models.py +319 -0
- django_sysconfig-0.0.1/django_sysconfig/helpers.py +34 -0
- django_sysconfig-0.0.1/django_sysconfig/migrations/0001_initial.py +55 -0
- django_sysconfig-0.0.1/django_sysconfig/migrations/__init__.py +0 -0
- django_sysconfig-0.0.1/django_sysconfig/models.py +34 -0
- django_sysconfig-0.0.1/django_sysconfig/registry.py +302 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/admin/index.html +70 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/app_config.html +530 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/app_list.html +91 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/boolean.html +36 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/decimal.html +23 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/integer.html +23 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/secret.html +49 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/select.html +24 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/string.html +22 -0
- django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/textarea.html +20 -0
- django_sysconfig-0.0.1/django_sysconfig/urls.py +10 -0
- django_sysconfig-0.0.1/django_sysconfig/validators.py +601 -0
- django_sysconfig-0.0.1/django_sysconfig/views.py +216 -0
- django_sysconfig-0.0.1/pyproject.toml +99 -0
- django_sysconfig-0.0.1/tests/__init__.py +0 -0
- django_sysconfig-0.0.1/tests/settings.py +38 -0
- django_sysconfig-0.0.1/tests/test_validators.py +709 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.pyo
|
|
6
|
+
*.pyd
|
|
7
|
+
|
|
8
|
+
# Distribution / packaging
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.egg
|
|
13
|
+
.eggs/
|
|
14
|
+
|
|
15
|
+
# Virtual environments
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
env/
|
|
19
|
+
|
|
20
|
+
# Local development settings (never commit)
|
|
21
|
+
settings_local.py
|
|
22
|
+
local_settings.py
|
|
23
|
+
|
|
24
|
+
# Testing
|
|
25
|
+
.pytest_cache/
|
|
26
|
+
.coverage
|
|
27
|
+
htmlcov/
|
|
28
|
+
.tox/
|
|
29
|
+
|
|
30
|
+
# Editors
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
|
|
36
|
+
# OS
|
|
37
|
+
.DS_Store
|
|
38
|
+
Thumbs.db
|
|
39
|
+
|
|
40
|
+
# Django
|
|
41
|
+
*.sqlite3
|
|
42
|
+
db.sqlite3
|
|
43
|
+
media/
|
|
44
|
+
staticfiles/
|
|
45
|
+
|
|
46
|
+
# Env files
|
|
47
|
+
.env
|
|
48
|
+
.env.*
|
|
49
|
+
!.env.example
|
|
50
|
+
|
|
51
|
+
# Ruff / Black
|
|
52
|
+
.ruff_cache/
|
|
53
|
+
.mypy_cache/
|
|
54
|
+
|
|
55
|
+
# Copilot AI context (session notes, discussions — not for version control)
|
|
56
|
+
.copilot/
|
|
57
|
+
|
|
58
|
+
# Agent instructions (local-only, not for version control)
|
|
59
|
+
AGENTS.md
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-sysconfig
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A Magento-style system configuration app for Django
|
|
5
|
+
Project-URL: Homepage, https://github.com/krishnamodepalli/django-sysconfig
|
|
6
|
+
Project-URL: Repository, https://github.com/krishnamodepalli/django-sysconfig
|
|
7
|
+
Project-URL: Issues, https://github.com/krishnamodepalli/django-sysconfig/issues
|
|
8
|
+
Author: Krishna Modepalli
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: admin,configuration,django,settings
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Framework :: Django
|
|
15
|
+
Classifier: Framework :: Django :: 4.2
|
|
16
|
+
Classifier: Framework :: Django :: 5.0
|
|
17
|
+
Classifier: Framework :: Django :: 5.1
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Requires-Dist: cryptography>=41.0
|
|
29
|
+
Requires-Dist: django>=4.2
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black>=24.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-django>=4.8; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# django-sysconfig
|
|
39
|
+
|
|
40
|
+
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`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [Features](#features)
|
|
47
|
+
- [Requirements](#requirements)
|
|
48
|
+
- [Installation](#installation)
|
|
49
|
+
- [Quick Start](#quick-start)
|
|
50
|
+
- [Defining Configuration](#defining-configuration)
|
|
51
|
+
- [Field options](#field-options)
|
|
52
|
+
- [Section options](#section-options)
|
|
53
|
+
- [Field Types](#field-types)
|
|
54
|
+
- [Reading and Writing Values](#reading-and-writing-values)
|
|
55
|
+
- [Validators](#validators)
|
|
56
|
+
- [on\_save Callback](#on_save-callback)
|
|
57
|
+
- [Encryption](#encryption)
|
|
58
|
+
- [Admin UI](#admin-ui)
|
|
59
|
+
- [How It Works](#how-it-works)
|
|
60
|
+
- [License](#license)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- **Typed fields** — string, integer, decimal, boolean, select, textarea, and encrypted secret types
|
|
67
|
+
- **Code-driven schema** — configuration structure lives in `sysconfig.py` files; only values are stored in the database
|
|
68
|
+
- **Dot-notation accessor** — `config.get("myapp.general.site_name")` returns the correct Python type automatically
|
|
69
|
+
- **Caching** — values are cached via Django's cache framework and invalidated on every write
|
|
70
|
+
- **Encryption at rest** — secret fields use Fernet (AES-128-CBC + HMAC), key derived from `SECRET_KEY`
|
|
71
|
+
- **20 built-in validators** — email, URL, IP, hostname, range, regex, slug, JSON, port, and more
|
|
72
|
+
- **Auto-discovery** — `sysconfig.py` files are found and loaded automatically on Django startup
|
|
73
|
+
- **Admin UI** — built-in staff-only views for browsing and editing configuration per app and section
|
|
74
|
+
- **on\_save callbacks** — react to value changes with custom logic (cache busting, webhooks, etc.)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Requirements
|
|
79
|
+
|
|
80
|
+
- Python ≥ 3.11
|
|
81
|
+
- Django ≥ 4.2
|
|
82
|
+
- `cryptography` ≥ 41.0
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Installation
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install django-sysconfig
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Add to `INSTALLED_APPS`:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
INSTALLED_APPS = [
|
|
96
|
+
...
|
|
97
|
+
"django_sysconfig",
|
|
98
|
+
]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Run migrations:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
python manage.py migrate
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Optionally, wire up the admin UI:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# urls.py
|
|
111
|
+
from django.urls import include, path
|
|
112
|
+
|
|
113
|
+
urlpatterns = [
|
|
114
|
+
# Must come BEFORE path("admin/", ...) so Django matches it first
|
|
115
|
+
path("admin/config/", include("django_sysconfig.urls")),
|
|
116
|
+
path("admin/", admin.site.urls),
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Quick Start
|
|
123
|
+
|
|
124
|
+
**1. Define your config schema** in `myapp/sysconfig.py`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from django_sysconfig.registry import register_config, Section, Field
|
|
128
|
+
from django_sysconfig.frontend_models import (
|
|
129
|
+
StringFrontendModel,
|
|
130
|
+
IntegerFrontendModel,
|
|
131
|
+
BooleanFrontendModel,
|
|
132
|
+
)
|
|
133
|
+
from django_sysconfig.validators import NotEmptyValidator, RangeValidator
|
|
134
|
+
|
|
135
|
+
@register_config("myapp")
|
|
136
|
+
class MyAppConfig:
|
|
137
|
+
class General(Section):
|
|
138
|
+
label = "General Settings"
|
|
139
|
+
sort_order = 10
|
|
140
|
+
|
|
141
|
+
site_name = Field(
|
|
142
|
+
StringFrontendModel,
|
|
143
|
+
label="Site Name",
|
|
144
|
+
comment="The public-facing name of the site.",
|
|
145
|
+
default="My App",
|
|
146
|
+
validators=[NotEmptyValidator()],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
max_items = Field(
|
|
150
|
+
IntegerFrontendModel,
|
|
151
|
+
label="Max Items Per User",
|
|
152
|
+
default=100,
|
|
153
|
+
validators=[RangeValidator(min_value=1, max_value=10_000)],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
maintenance_mode = Field(
|
|
157
|
+
BooleanFrontendModel,
|
|
158
|
+
label="Maintenance Mode",
|
|
159
|
+
default=False,
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**2. Read values anywhere in your project:**
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from django_sysconfig.accessor import config
|
|
167
|
+
|
|
168
|
+
site_name = config.get("myapp.general.site_name") # "My App"
|
|
169
|
+
max_items = config.get("myapp.general.max_items") # 100
|
|
170
|
+
maintenance = config.get("myapp.general.maintenance_mode") # False
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Defining Configuration
|
|
176
|
+
|
|
177
|
+
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.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from django_sysconfig.registry import register_config, Section, Field
|
|
181
|
+
from django_sysconfig.frontend_models import StringFrontendModel, SelectFrontendModel
|
|
182
|
+
from django_sysconfig.validators import NotEmptyValidator, ChoiceValidator
|
|
183
|
+
|
|
184
|
+
@register_config("notifications")
|
|
185
|
+
class NotificationsConfig:
|
|
186
|
+
class Email(Section):
|
|
187
|
+
label = "Email Settings"
|
|
188
|
+
sort_order = 10
|
|
189
|
+
|
|
190
|
+
sender_address = Field(
|
|
191
|
+
StringFrontendModel,
|
|
192
|
+
label="Sender Address",
|
|
193
|
+
default="no-reply@example.com",
|
|
194
|
+
validators=[NotEmptyValidator()],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
format = Field(
|
|
198
|
+
SelectFrontendModel,
|
|
199
|
+
label="Email Format",
|
|
200
|
+
default="html",
|
|
201
|
+
choices=[("html", "HTML"), ("text", "Plain Text")],
|
|
202
|
+
validators=[ChoiceValidator(["html", "text"])],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
class Sms(Section):
|
|
206
|
+
label = "SMS Settings"
|
|
207
|
+
sort_order = 20
|
|
208
|
+
|
|
209
|
+
enabled = Field(
|
|
210
|
+
BooleanFrontendModel,
|
|
211
|
+
label="Enable SMS",
|
|
212
|
+
default=False,
|
|
213
|
+
)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Field options
|
|
217
|
+
|
|
218
|
+
| Parameter | Type | Description |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| `frontend_model` | `type[BaseFrontendModel]` | The field type class (required) |
|
|
221
|
+
| `label` | `str` | Human-readable label shown in the admin UI |
|
|
222
|
+
| `comment` | `str` | Help text shown below the input; HTML is allowed |
|
|
223
|
+
| `default` | `Any` | Default value when no DB record exists |
|
|
224
|
+
| `sort_order` | `int` | Display order within the section (lower = first) |
|
|
225
|
+
| `validators` | `list[BaseValidator]` | Validators run before saving |
|
|
226
|
+
| `on_save` | `Callable` | Callback invoked after a value is saved |
|
|
227
|
+
| `**kwargs` | | Extra args passed to the frontend model (e.g., `choices`) |
|
|
228
|
+
|
|
229
|
+
### Section options
|
|
230
|
+
|
|
231
|
+
| Attribute | Type | Description |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `label` | `str` | Section heading shown in the admin UI |
|
|
234
|
+
| `sort_order` | `int` | Display order among sections (lower = first) |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Field Types
|
|
239
|
+
|
|
240
|
+
| Class | Python type | Description |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `StringFrontendModel` | `str` | Single-line text input |
|
|
243
|
+
| `TextareaFrontendModel` | `str` | Multi-line text area |
|
|
244
|
+
| `IntegerFrontendModel` | `int` | Integer number input |
|
|
245
|
+
| `DecimalFrontendModel` | `Decimal` | Decimal number input (accepts `step` kwarg) |
|
|
246
|
+
| `BooleanFrontendModel` | `bool` | Checkbox |
|
|
247
|
+
| `SelectFrontendModel` | `str` | Dropdown select (requires `choices` kwarg) |
|
|
248
|
+
| `SecretFrontendModel` | `str` | Password input — encrypted at rest (see [Encryption](#encryption)) |
|
|
249
|
+
|
|
250
|
+
**Select field choices format:**
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
Field(
|
|
254
|
+
SelectFrontendModel,
|
|
255
|
+
label="Environment",
|
|
256
|
+
default="production",
|
|
257
|
+
choices=[
|
|
258
|
+
("development", "Development"),
|
|
259
|
+
("staging", "Staging"),
|
|
260
|
+
("production", "Production"),
|
|
261
|
+
],
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Decimal field with custom step:**
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
Field(
|
|
269
|
+
DecimalFrontendModel,
|
|
270
|
+
label="Tax Rate",
|
|
271
|
+
default=Decimal("0.20"),
|
|
272
|
+
step="0.001", # passed through to the HTML input
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Reading and Writing Values
|
|
279
|
+
|
|
280
|
+
All paths use dot notation with exactly three parts: `app_label.section.field`.
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from django_sysconfig.accessor import config
|
|
284
|
+
|
|
285
|
+
# --- Reading ---
|
|
286
|
+
|
|
287
|
+
# Returns the typed value; falls back to the field's default if not set in DB
|
|
288
|
+
config.get("myapp.general.site_name") # str
|
|
289
|
+
config.get("myapp.general.max_items") # int
|
|
290
|
+
config.get("myapp.general.maintenance_mode") # bool
|
|
291
|
+
|
|
292
|
+
# Supply a fallback for unknown/unregistered paths (no exception raised)
|
|
293
|
+
config.get("myapp.general.unknown", default=42)
|
|
294
|
+
|
|
295
|
+
# All values for an entire app → {section: {field: value, ...}, ...}
|
|
296
|
+
config.all("myapp")
|
|
297
|
+
|
|
298
|
+
# All values for one section → {field: value, ...}
|
|
299
|
+
config.section("myapp.general")
|
|
300
|
+
|
|
301
|
+
# Check if a path is registered in code
|
|
302
|
+
config.exists("myapp.general.site_name") # True / False
|
|
303
|
+
|
|
304
|
+
# Check if a value has been explicitly saved to the database
|
|
305
|
+
config.is_set("myapp.general.site_name") # True / False
|
|
306
|
+
|
|
307
|
+
# --- Writing ---
|
|
308
|
+
|
|
309
|
+
config.set("myapp.general.site_name", "Acme Corp")
|
|
310
|
+
config.set("myapp.general.max_items", 500)
|
|
311
|
+
config.set("myapp.general.maintenance_mode", True)
|
|
312
|
+
|
|
313
|
+
# Save multiple values atomically
|
|
314
|
+
config.set_many({
|
|
315
|
+
"myapp.general.site_name": "Acme Corp",
|
|
316
|
+
"myapp.general.max_items": 500,
|
|
317
|
+
})
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Exceptions
|
|
321
|
+
|
|
322
|
+
| Exception | Raised when |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `InvalidPathError` | Path does not have exactly three dot-separated parts |
|
|
325
|
+
| `AppNotFoundError` | No config is registered for the given app label |
|
|
326
|
+
| `FieldNotFoundError` | The field does not exist in the registered schema |
|
|
327
|
+
| `ConfigValueError` | A value cannot be serialized for the given field type |
|
|
328
|
+
|
|
329
|
+
All inherit from `ConfigError`.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Validators
|
|
334
|
+
|
|
335
|
+
Import validators from `django_sysconfig.validators` and pass them as a list to the `validators` parameter on a `Field`.
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
from django_sysconfig.validators import NotEmptyValidator, EmailValidator, RangeValidator
|
|
339
|
+
|
|
340
|
+
Field(StringFrontendModel, validators=[NotEmptyValidator(), EmailValidator()])
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
All validators accept an optional `message` argument to override the default error message.
|
|
344
|
+
|
|
345
|
+
### Presence
|
|
346
|
+
|
|
347
|
+
| Validator | Description |
|
|
348
|
+
|---|---|
|
|
349
|
+
| `NotEmptyValidator()` | Value must not be `None`, empty string, empty list, or empty dict. Alias: `Required` |
|
|
350
|
+
| `NotBlankValidator()` | String value must not be whitespace-only (`None` is allowed) |
|
|
351
|
+
|
|
352
|
+
### String length
|
|
353
|
+
|
|
354
|
+
| Validator | Description |
|
|
355
|
+
|---|---|
|
|
356
|
+
| `MinLengthValidator(min_length)` | String must be at least `min_length` characters |
|
|
357
|
+
| `MaxLengthValidator(max_length)` | String must be at most `max_length` characters |
|
|
358
|
+
|
|
359
|
+
### Pattern
|
|
360
|
+
|
|
361
|
+
| Validator | Description |
|
|
362
|
+
|---|---|
|
|
363
|
+
| `RegexValidator(pattern, flags=0, inverse=False)` | Value must match (or not match, if `inverse=True`) the given regex pattern |
|
|
364
|
+
| `SlugValidator()` | Value must contain only letters, digits, hyphens, and underscores |
|
|
365
|
+
| `JsonValidator()` | Value must be a valid JSON string |
|
|
366
|
+
|
|
367
|
+
### Numeric
|
|
368
|
+
|
|
369
|
+
| Validator | Description |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `RangeValidator(min_value=None, max_value=None)` | Number must fall within the given range (both bounds inclusive, either optional) |
|
|
372
|
+
| `PositiveValidator()` | Number must be greater than zero |
|
|
373
|
+
| `NonNegativeValidator()` | Number must be zero or greater |
|
|
374
|
+
| `PortValidator()` | Integer must be a valid port number (1–65535) |
|
|
375
|
+
|
|
376
|
+
### Network / format
|
|
377
|
+
|
|
378
|
+
| Validator | Description |
|
|
379
|
+
|---|---|
|
|
380
|
+
| `EmailValidator()` | Must be a valid email address |
|
|
381
|
+
| `UrlValidator(schemes=None)` | Must be a valid URL; `schemes` defaults to `["http", "https", "ftp"]` |
|
|
382
|
+
| `IPv4Validator()` | Must be a valid IPv4 address |
|
|
383
|
+
| `IPv6Validator()` | Must be a valid IPv6 address |
|
|
384
|
+
| `IPAddressValidator(version=None)` | Must be a valid IP address; `version` can be `4`, `6`, or `None` (both) |
|
|
385
|
+
| `HostnameValidator()` | Must be a valid RFC 1123 hostname |
|
|
386
|
+
| `DomainValidator()` | Must be a valid domain name (max 253 characters) |
|
|
387
|
+
|
|
388
|
+
### Other
|
|
389
|
+
|
|
390
|
+
| Validator | Description |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `ChoiceValidator(choices)` | Value must be one of the items in `choices` |
|
|
393
|
+
| `PathValidator(must_be_absolute=False)` | Value must look like a valid file path; optionally require an absolute path |
|
|
394
|
+
|
|
395
|
+
### Running validators manually
|
|
396
|
+
|
|
397
|
+
```python
|
|
398
|
+
from django_sysconfig.validators import validate_value, NotEmptyValidator, EmailValidator
|
|
399
|
+
|
|
400
|
+
errors = validate_value(
|
|
401
|
+
"not-an-email",
|
|
402
|
+
[NotEmptyValidator(), EmailValidator()],
|
|
403
|
+
field_label="Sender Address",
|
|
404
|
+
)
|
|
405
|
+
# ["Sender Address: Enter a valid email address."]
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## on\_save Callback
|
|
411
|
+
|
|
412
|
+
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.
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
def on_maintenance_mode_change(path: str, new_value: bool, old_value: bool) -> None:
|
|
416
|
+
if new_value and not old_value:
|
|
417
|
+
# Notify ops team, clear caches, etc.
|
|
418
|
+
pass
|
|
419
|
+
|
|
420
|
+
maintenance_mode = Field(
|
|
421
|
+
BooleanFrontendModel,
|
|
422
|
+
label="Maintenance Mode",
|
|
423
|
+
default=False,
|
|
424
|
+
on_save=on_maintenance_mode_change,
|
|
425
|
+
)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The callback is fired **after** the value has been successfully written to the database and the cache has been updated.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## Encryption
|
|
433
|
+
|
|
434
|
+
Fields using `SecretFrontendModel` are **encrypted at rest**. Values are encrypted with [Fernet](https://cryptography.io/en/latest/fernet/) (AES-128-CBC + HMAC-SHA256) using a key derived from Django's `SECRET_KEY` via SHA-256.
|
|
435
|
+
|
|
436
|
+
- The encrypted value is stored as a Fernet token in the database.
|
|
437
|
+
- The admin UI always masks the value — it is never displayed.
|
|
438
|
+
- Values are decrypted transparently when read via `config.get(...)`.
|
|
439
|
+
- Rotating `SECRET_KEY` will make existing encrypted values unreadable; re-save them after rotation.
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
from django_sysconfig.frontend_models import SecretFrontendModel
|
|
443
|
+
|
|
444
|
+
api_key = Field(
|
|
445
|
+
SecretFrontendModel,
|
|
446
|
+
label="API Key",
|
|
447
|
+
comment="Your third-party API key. Stored encrypted.",
|
|
448
|
+
)
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Admin UI
|
|
454
|
+
|
|
455
|
+
The admin UI is a pair of staff-only class-based views:
|
|
456
|
+
|
|
457
|
+
| URL | View | Description |
|
|
458
|
+
|---|---|---|
|
|
459
|
+
| `/admin/config/` | `ConfigAppListView` | Lists all apps that have registered configuration |
|
|
460
|
+
| `/admin/config/<app_label>/` | `ConfigAppDetailView` | Renders and saves all fields for an app |
|
|
461
|
+
|
|
462
|
+
The Django admin index page is extended with a banner linking to the config UI.
|
|
463
|
+
|
|
464
|
+
Both views require the user to be a **staff member** (`is_staff=True`).
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## How It Works
|
|
469
|
+
|
|
470
|
+
1. **Discovery** — On startup, `AppConfig.ready()` calls `autodiscover_modules("sysconfig")`, which imports `sysconfig.py` from every installed app.
|
|
471
|
+
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).
|
|
472
|
+
3. **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's `FrontendModel` into the correct Python type (int, bool, Decimal, etc.).
|
|
473
|
+
4. **Writing** — `config.set(...)` serializes the value, writes it to the database, invalidates the cache entry, and then calls the `on_save` callback if one is defined.
|
|
474
|
+
5. **Caching** — The cache layer wraps Django's standard cache framework. Entries have no expiry and are invalidated explicitly on every write.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Contributing
|
|
479
|
+
|
|
480
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up your local development environment, run tests, and submit a pull request.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## License
|
|
485
|
+
|
|
486
|
+
MIT
|