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.
Files changed (34) hide show
  1. django_sysconfig-0.0.1/.gitignore +59 -0
  2. django_sysconfig-0.0.1/LICENSE +21 -0
  3. django_sysconfig-0.0.1/PKG-INFO +486 -0
  4. django_sysconfig-0.0.1/README.md +449 -0
  5. django_sysconfig-0.0.1/django_sysconfig/__init__.py +35 -0
  6. django_sysconfig-0.0.1/django_sysconfig/accessor.py +368 -0
  7. django_sysconfig-0.0.1/django_sysconfig/admin.py +36 -0
  8. django_sysconfig-0.0.1/django_sysconfig/apps.py +12 -0
  9. django_sysconfig-0.0.1/django_sysconfig/cache.py +45 -0
  10. django_sysconfig-0.0.1/django_sysconfig/encryption.py +108 -0
  11. django_sysconfig-0.0.1/django_sysconfig/exceptions.py +78 -0
  12. django_sysconfig-0.0.1/django_sysconfig/frontend_models.py +319 -0
  13. django_sysconfig-0.0.1/django_sysconfig/helpers.py +34 -0
  14. django_sysconfig-0.0.1/django_sysconfig/migrations/0001_initial.py +55 -0
  15. django_sysconfig-0.0.1/django_sysconfig/migrations/__init__.py +0 -0
  16. django_sysconfig-0.0.1/django_sysconfig/models.py +34 -0
  17. django_sysconfig-0.0.1/django_sysconfig/registry.py +302 -0
  18. django_sysconfig-0.0.1/django_sysconfig/templates/admin/index.html +70 -0
  19. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/app_config.html +530 -0
  20. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/app_list.html +91 -0
  21. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/boolean.html +36 -0
  22. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/decimal.html +23 -0
  23. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/integer.html +23 -0
  24. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/secret.html +49 -0
  25. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/select.html +24 -0
  26. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/string.html +22 -0
  27. django_sysconfig-0.0.1/django_sysconfig/templates/django_sysconfig/frontend_models/textarea.html +20 -0
  28. django_sysconfig-0.0.1/django_sysconfig/urls.py +10 -0
  29. django_sysconfig-0.0.1/django_sysconfig/validators.py +601 -0
  30. django_sysconfig-0.0.1/django_sysconfig/views.py +216 -0
  31. django_sysconfig-0.0.1/pyproject.toml +99 -0
  32. django_sysconfig-0.0.1/tests/__init__.py +0 -0
  33. django_sysconfig-0.0.1/tests/settings.py +38 -0
  34. 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