django-dataclassconf 0.1.0__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_dataclassconf-0.1.0/LICENSE +21 -0
- django_dataclassconf-0.1.0/PKG-INFO +151 -0
- django_dataclassconf-0.1.0/README.md +125 -0
- django_dataclassconf-0.1.0/pyproject.toml +50 -0
- django_dataclassconf-0.1.0/setup.cfg +4 -0
- django_dataclassconf-0.1.0/src/django_dataclassconf/__init__.py +9 -0
- django_dataclassconf-0.1.0/src/django_dataclassconf/conf.py +92 -0
- django_dataclassconf-0.1.0/src/django_dataclassconf.egg-info/PKG-INFO +151 -0
- django_dataclassconf-0.1.0/src/django_dataclassconf.egg-info/SOURCES.txt +10 -0
- django_dataclassconf-0.1.0/src/django_dataclassconf.egg-info/dependency_links.txt +1 -0
- django_dataclassconf-0.1.0/src/django_dataclassconf.egg-info/requires.txt +2 -0
- django_dataclassconf-0.1.0/src/django_dataclassconf.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felix
|
|
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,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django_dataclassconf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple Django package setting loader that utilizes dataclasses for type hinting and type checking
|
|
5
|
+
Author-email: Felix Leo Flores <floresfelixleo.fleurs96@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Jxst-Felix/django-dataclassconf
|
|
8
|
+
Project-URL: Issues, https://github.com/Jxst-Felix/django-dataclassconf/issues
|
|
9
|
+
Keywords: django,settings,configuration,dataclasses,type-hints
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: Django<6.0,>=4.2
|
|
24
|
+
Requires-Dist: dacite>=1.8.0
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
<!-- Add badges here later when I have github actions, pypi link, and all -->
|
|
28
|
+
|
|
29
|
+
# Django DataclassConf
|
|
30
|
+
|
|
31
|
+
A simple Django package setting loader that utilizes dataclasses for type hinting and type checking.
|
|
32
|
+
|
|
33
|
+
Bring modern Python typing, robust validation, and full IDE autocomplete to your Django configurations.
|
|
34
|
+
|
|
35
|
+
`django-dataclassconf` allows you to bind your Django settings cleanly to structured standard Python `dataclasses`. Powered by [`dacite`](https://github.com/konradhalas/dacite), it automatically catches dynamic setting updates while ensuring your configuration layer stays type-safe and isolated.
|
|
36
|
+
|
|
37
|
+
## But Why?
|
|
38
|
+
|
|
39
|
+
* **Fail-Fast Validation:** Catch bad configuration types or missing values instantly during server startup or container deployment rather than hitting silent runtime crashes mid-request.
|
|
40
|
+
|
|
41
|
+
* **Full IDE Autocomplete:** Say goodbye to blind `getattr(settings, "MY_SETTING")` calls. Enjoy full hovering type definitions and autocompletion in VS Code, PyCharm, and MyPy.
|
|
42
|
+
|
|
43
|
+
* **Dual Format Normalization:** Merges flat environment styles (`PREFIX_TIMEOUT = 30`) and structured dictionary blocks (`PREFIX = {"TIMEOUT": 30}`) into a single unified object seamlessly.
|
|
44
|
+
|
|
45
|
+
* **Test-Safe Isolation:** Fully supports Django's test suite cycles. When settings are overridden dynamically in unit tests, your dataclasses mutate cleanly in-place and revert automatically.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### 1. Define Your Configuration Dataclass
|
|
50
|
+
|
|
51
|
+
Create a file named `config.py` (or any name you prefer tbh) within your application or package.
|
|
52
|
+
Inherit from `BaseConfig` and define your variables.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from dataclasses import dataclass, field
|
|
56
|
+
from django_dataclassconf.config import BaseConfig, config_loader
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class MyPackageConfig(BaseConfig):
|
|
60
|
+
DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
|
|
61
|
+
MAX_FILE_SIZE: int = 10000
|
|
62
|
+
INDEXER_CLASS: str = 'myapp.utils.DocumentIndexer'
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def _prefix(self) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Define the config's prefix here, return blank string if it doesn't
|
|
68
|
+
have a prefix such as when we write a configuration dataclass that
|
|
69
|
+
will hold the `DEBUG` setting
|
|
70
|
+
"""
|
|
71
|
+
return 'MY_PACKAGE'
|
|
72
|
+
|
|
73
|
+
package_config = MyPackageConfig()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Subscribe to the Configuration Loader
|
|
77
|
+
|
|
78
|
+
For your dataclass to grab configuration data from Django's `settings.py` on startup and capture updates during tests, subscribe your instance into `config_loader` inside your app's initialization hook:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# my_app/apps.py
|
|
82
|
+
class MyAppConfig(AppConfig):
|
|
83
|
+
name = 'my_app'
|
|
84
|
+
|
|
85
|
+
def ready(self):
|
|
86
|
+
from django_dataclassconf.config import config_loader
|
|
87
|
+
from .config import package_config
|
|
88
|
+
|
|
89
|
+
config_loader.subscribe(package_config)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Access Your Settings Anywhere
|
|
93
|
+
|
|
94
|
+
Core Practice would be to import your configuration instance directly instead of using the global `django.conf.settings` object to harness the full type safety and IDE autocomplete.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# my_app/views.py
|
|
98
|
+
from django.http import HttpResponse
|
|
99
|
+
from my_app.config import package_config
|
|
100
|
+
|
|
101
|
+
def my_view(request):
|
|
102
|
+
...
|
|
103
|
+
# Your IDE now natively autocompletes these fields
|
|
104
|
+
if file_size > package_config.MAX_FILE_SIZE:
|
|
105
|
+
return HttpResponse(
|
|
106
|
+
{'detail': 'File size has exceeded the maximum size limit!'},
|
|
107
|
+
status = 400
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Extras
|
|
112
|
+
|
|
113
|
+
Your Configuration Dataclass can also be a nested dataclasses, you only need to inherit `BaseConfig` to the root configuration dataclass.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from dataclasses import dataclass, field
|
|
117
|
+
from django_dataclassconf.config import BaseConfig, config_loader
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class DocumentPreview:
|
|
121
|
+
preview_page_count: int = 10
|
|
122
|
+
strip_cover_page: bool = False
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class MyPackageConfig(BaseConfig):
|
|
126
|
+
DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
|
|
127
|
+
PREVIEW: DocumentPreview = field(default_factory=DocumentPreview)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def _prefix(self) -> str:
|
|
131
|
+
return 'MY_PACKAGE'
|
|
132
|
+
|
|
133
|
+
configuration = MyPackageConfig()
|
|
134
|
+
config_loader.subscribe(configuration)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
And it will look like this in `settings.py`
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
MY_PACKAGE = {
|
|
141
|
+
'DOCUMENTS_ROOT_PATH': '/custom/path/',
|
|
142
|
+
'PREVIEW': {
|
|
143
|
+
'preview_page_count': 12,
|
|
144
|
+
'strip_cover_page': True
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<!-- Add badges here later when I have github actions, pypi link, and all -->
|
|
2
|
+
|
|
3
|
+
# Django DataclassConf
|
|
4
|
+
|
|
5
|
+
A simple Django package setting loader that utilizes dataclasses for type hinting and type checking.
|
|
6
|
+
|
|
7
|
+
Bring modern Python typing, robust validation, and full IDE autocomplete to your Django configurations.
|
|
8
|
+
|
|
9
|
+
`django-dataclassconf` allows you to bind your Django settings cleanly to structured standard Python `dataclasses`. Powered by [`dacite`](https://github.com/konradhalas/dacite), it automatically catches dynamic setting updates while ensuring your configuration layer stays type-safe and isolated.
|
|
10
|
+
|
|
11
|
+
## But Why?
|
|
12
|
+
|
|
13
|
+
* **Fail-Fast Validation:** Catch bad configuration types or missing values instantly during server startup or container deployment rather than hitting silent runtime crashes mid-request.
|
|
14
|
+
|
|
15
|
+
* **Full IDE Autocomplete:** Say goodbye to blind `getattr(settings, "MY_SETTING")` calls. Enjoy full hovering type definitions and autocompletion in VS Code, PyCharm, and MyPy.
|
|
16
|
+
|
|
17
|
+
* **Dual Format Normalization:** Merges flat environment styles (`PREFIX_TIMEOUT = 30`) and structured dictionary blocks (`PREFIX = {"TIMEOUT": 30}`) into a single unified object seamlessly.
|
|
18
|
+
|
|
19
|
+
* **Test-Safe Isolation:** Fully supports Django's test suite cycles. When settings are overridden dynamically in unit tests, your dataclasses mutate cleanly in-place and revert automatically.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### 1. Define Your Configuration Dataclass
|
|
24
|
+
|
|
25
|
+
Create a file named `config.py` (or any name you prefer tbh) within your application or package.
|
|
26
|
+
Inherit from `BaseConfig` and define your variables.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from django_dataclassconf.config import BaseConfig, config_loader
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class MyPackageConfig(BaseConfig):
|
|
34
|
+
DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
|
|
35
|
+
MAX_FILE_SIZE: int = 10000
|
|
36
|
+
INDEXER_CLASS: str = 'myapp.utils.DocumentIndexer'
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def _prefix(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Define the config's prefix here, return blank string if it doesn't
|
|
42
|
+
have a prefix such as when we write a configuration dataclass that
|
|
43
|
+
will hold the `DEBUG` setting
|
|
44
|
+
"""
|
|
45
|
+
return 'MY_PACKAGE'
|
|
46
|
+
|
|
47
|
+
package_config = MyPackageConfig()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Subscribe to the Configuration Loader
|
|
51
|
+
|
|
52
|
+
For your dataclass to grab configuration data from Django's `settings.py` on startup and capture updates during tests, subscribe your instance into `config_loader` inside your app's initialization hook:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# my_app/apps.py
|
|
56
|
+
class MyAppConfig(AppConfig):
|
|
57
|
+
name = 'my_app'
|
|
58
|
+
|
|
59
|
+
def ready(self):
|
|
60
|
+
from django_dataclassconf.config import config_loader
|
|
61
|
+
from .config import package_config
|
|
62
|
+
|
|
63
|
+
config_loader.subscribe(package_config)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 3. Access Your Settings Anywhere
|
|
67
|
+
|
|
68
|
+
Core Practice would be to import your configuration instance directly instead of using the global `django.conf.settings` object to harness the full type safety and IDE autocomplete.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# my_app/views.py
|
|
72
|
+
from django.http import HttpResponse
|
|
73
|
+
from my_app.config import package_config
|
|
74
|
+
|
|
75
|
+
def my_view(request):
|
|
76
|
+
...
|
|
77
|
+
# Your IDE now natively autocompletes these fields
|
|
78
|
+
if file_size > package_config.MAX_FILE_SIZE:
|
|
79
|
+
return HttpResponse(
|
|
80
|
+
{'detail': 'File size has exceeded the maximum size limit!'},
|
|
81
|
+
status = 400
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Extras
|
|
86
|
+
|
|
87
|
+
Your Configuration Dataclass can also be a nested dataclasses, you only need to inherit `BaseConfig` to the root configuration dataclass.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from dataclasses import dataclass, field
|
|
91
|
+
from django_dataclassconf.config import BaseConfig, config_loader
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class DocumentPreview:
|
|
95
|
+
preview_page_count: int = 10
|
|
96
|
+
strip_cover_page: bool = False
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class MyPackageConfig(BaseConfig):
|
|
100
|
+
DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
|
|
101
|
+
PREVIEW: DocumentPreview = field(default_factory=DocumentPreview)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def _prefix(self) -> str:
|
|
105
|
+
return 'MY_PACKAGE'
|
|
106
|
+
|
|
107
|
+
configuration = MyPackageConfig()
|
|
108
|
+
config_loader.subscribe(configuration)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
And it will look like this in `settings.py`
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
MY_PACKAGE = {
|
|
115
|
+
'DOCUMENTS_ROOT_PATH': '/custom/path/',
|
|
116
|
+
'PREVIEW': {
|
|
117
|
+
'preview_page_count': 12,
|
|
118
|
+
'strip_cover_page': True
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django_dataclassconf"
|
|
3
|
+
description = "A simple Django package setting loader that utilizes dataclasses for type hinting and type checking"
|
|
4
|
+
authors = [
|
|
5
|
+
{name = "Felix Leo Flores", email = "floresfelixleo.fleurs96@gmail.com"}
|
|
6
|
+
]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICEN[CS]E*"]
|
|
10
|
+
keywords = [
|
|
11
|
+
"django", "settings",
|
|
12
|
+
"configuration",
|
|
13
|
+
"dataclasses",
|
|
14
|
+
"type-hints",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Framework :: Django",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.9"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"Django>=4.2,<6.0",
|
|
31
|
+
"dacite>=1.8.0",
|
|
32
|
+
]
|
|
33
|
+
dynamic = ["version"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Repository = "https://github.com/Jxst-Felix/django-dataclassconf"
|
|
37
|
+
Issues = "https://github.com/Jxst-Felix/django-dataclassconf/issues"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["setuptools>=64", "wheel"]
|
|
41
|
+
build-backend = "setuptools.build_meta"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.package-dir]
|
|
47
|
+
"" = "src"
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.dynamic]
|
|
50
|
+
version = { attr = "django_dataclassconf.__version__" }
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Implements an Observer pattern for when settings have changed"""
|
|
2
|
+
from django.test.signals import setting_changed
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from dacite import from_dict
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseConfig(ABC):
|
|
12
|
+
"""Base class that updates a dataclass to use them as config classes"""
|
|
13
|
+
@property
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def _prefix(self) -> str: ...
|
|
16
|
+
|
|
17
|
+
def update(self, config_data: typing.Dict[str, typing.Any]):
|
|
18
|
+
previous_data = asdict(self)
|
|
19
|
+
new_data = {**previous_data, **config_data}
|
|
20
|
+
validated_instance = from_dict(data_class = self.__class__, data = new_data)
|
|
21
|
+
vars(self).update(vars(validated_instance))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PackageConfigLoader:
|
|
25
|
+
"""Loads config into multiple dataclasses"""
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self._dataclass_configs: typing.List[BaseConfig] = []
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def prefixes(self) -> typing.List[str]:
|
|
31
|
+
"""Provides the list of prefixes available in config loader, excludes empty prefixes"""
|
|
32
|
+
return [
|
|
33
|
+
config._prefix
|
|
34
|
+
for config in self._dataclass_configs
|
|
35
|
+
if config._prefix
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def subscribe(self, config: BaseConfig):
|
|
39
|
+
if config not in self._dataclass_configs:
|
|
40
|
+
self._dataclass_configs.append(config)
|
|
41
|
+
self.refresh_config(config, self._settings_as_dict())
|
|
42
|
+
|
|
43
|
+
def refresh_config(
|
|
44
|
+
self, config: BaseConfig,
|
|
45
|
+
config_data: typing.Dict[str, typing.Any]
|
|
46
|
+
):
|
|
47
|
+
"""Refreshes a single specific configuration instance from raw settings"""
|
|
48
|
+
prefix = config._prefix
|
|
49
|
+
if not prefix:
|
|
50
|
+
config.update(config_data)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
prefix_ = f'{prefix}_'
|
|
54
|
+
flat_config = {
|
|
55
|
+
k.removeprefix(prefix_): v
|
|
56
|
+
for k, v in config_data.items()
|
|
57
|
+
if k.startswith(prefix_)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
config_dict = config_data.get(prefix, {})
|
|
61
|
+
if isinstance(config_dict, config.__class__):
|
|
62
|
+
config_dict = asdict(config_dict)
|
|
63
|
+
|
|
64
|
+
elif not isinstance(config_dict, dict):
|
|
65
|
+
config_dict = {}
|
|
66
|
+
|
|
67
|
+
config.update({**config_dict, **flat_config})
|
|
68
|
+
|
|
69
|
+
def refresh_all(self, setting: str):
|
|
70
|
+
config_data = None
|
|
71
|
+
for config in self._dataclass_configs:
|
|
72
|
+
prefix = config._prefix
|
|
73
|
+
prefix_ = f'{prefix}_'
|
|
74
|
+
|
|
75
|
+
if not prefix or setting == prefix or setting.startswith(prefix_):
|
|
76
|
+
if config_data is None:
|
|
77
|
+
config_data = self._settings_as_dict()
|
|
78
|
+
self.refresh_config(config, config_data)
|
|
79
|
+
|
|
80
|
+
def _settings_as_dict(self) -> typing.Dict[str, typing.Any]:
|
|
81
|
+
return {
|
|
82
|
+
k: getattr(settings, k)
|
|
83
|
+
for k in dir(settings)
|
|
84
|
+
if k.isupper()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
config_loader = PackageConfigLoader()
|
|
89
|
+
|
|
90
|
+
def refresh_settings(sender, setting, value, enter, **kwargs):
|
|
91
|
+
config_loader.refresh_all(setting)
|
|
92
|
+
setting_changed.connect(refresh_settings)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django_dataclassconf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple Django package setting loader that utilizes dataclasses for type hinting and type checking
|
|
5
|
+
Author-email: Felix Leo Flores <floresfelixleo.fleurs96@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/Jxst-Felix/django-dataclassconf
|
|
8
|
+
Project-URL: Issues, https://github.com/Jxst-Felix/django-dataclassconf/issues
|
|
9
|
+
Keywords: django,settings,configuration,dataclasses,type-hints
|
|
10
|
+
Classifier: Framework :: Django
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: Django<6.0,>=4.2
|
|
24
|
+
Requires-Dist: dacite>=1.8.0
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
<!-- Add badges here later when I have github actions, pypi link, and all -->
|
|
28
|
+
|
|
29
|
+
# Django DataclassConf
|
|
30
|
+
|
|
31
|
+
A simple Django package setting loader that utilizes dataclasses for type hinting and type checking.
|
|
32
|
+
|
|
33
|
+
Bring modern Python typing, robust validation, and full IDE autocomplete to your Django configurations.
|
|
34
|
+
|
|
35
|
+
`django-dataclassconf` allows you to bind your Django settings cleanly to structured standard Python `dataclasses`. Powered by [`dacite`](https://github.com/konradhalas/dacite), it automatically catches dynamic setting updates while ensuring your configuration layer stays type-safe and isolated.
|
|
36
|
+
|
|
37
|
+
## But Why?
|
|
38
|
+
|
|
39
|
+
* **Fail-Fast Validation:** Catch bad configuration types or missing values instantly during server startup or container deployment rather than hitting silent runtime crashes mid-request.
|
|
40
|
+
|
|
41
|
+
* **Full IDE Autocomplete:** Say goodbye to blind `getattr(settings, "MY_SETTING")` calls. Enjoy full hovering type definitions and autocompletion in VS Code, PyCharm, and MyPy.
|
|
42
|
+
|
|
43
|
+
* **Dual Format Normalization:** Merges flat environment styles (`PREFIX_TIMEOUT = 30`) and structured dictionary blocks (`PREFIX = {"TIMEOUT": 30}`) into a single unified object seamlessly.
|
|
44
|
+
|
|
45
|
+
* **Test-Safe Isolation:** Fully supports Django's test suite cycles. When settings are overridden dynamically in unit tests, your dataclasses mutate cleanly in-place and revert automatically.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### 1. Define Your Configuration Dataclass
|
|
50
|
+
|
|
51
|
+
Create a file named `config.py` (or any name you prefer tbh) within your application or package.
|
|
52
|
+
Inherit from `BaseConfig` and define your variables.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from dataclasses import dataclass, field
|
|
56
|
+
from django_dataclassconf.config import BaseConfig, config_loader
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class MyPackageConfig(BaseConfig):
|
|
60
|
+
DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
|
|
61
|
+
MAX_FILE_SIZE: int = 10000
|
|
62
|
+
INDEXER_CLASS: str = 'myapp.utils.DocumentIndexer'
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def _prefix(self) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Define the config's prefix here, return blank string if it doesn't
|
|
68
|
+
have a prefix such as when we write a configuration dataclass that
|
|
69
|
+
will hold the `DEBUG` setting
|
|
70
|
+
"""
|
|
71
|
+
return 'MY_PACKAGE'
|
|
72
|
+
|
|
73
|
+
package_config = MyPackageConfig()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. Subscribe to the Configuration Loader
|
|
77
|
+
|
|
78
|
+
For your dataclass to grab configuration data from Django's `settings.py` on startup and capture updates during tests, subscribe your instance into `config_loader` inside your app's initialization hook:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# my_app/apps.py
|
|
82
|
+
class MyAppConfig(AppConfig):
|
|
83
|
+
name = 'my_app'
|
|
84
|
+
|
|
85
|
+
def ready(self):
|
|
86
|
+
from django_dataclassconf.config import config_loader
|
|
87
|
+
from .config import package_config
|
|
88
|
+
|
|
89
|
+
config_loader.subscribe(package_config)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Access Your Settings Anywhere
|
|
93
|
+
|
|
94
|
+
Core Practice would be to import your configuration instance directly instead of using the global `django.conf.settings` object to harness the full type safety and IDE autocomplete.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# my_app/views.py
|
|
98
|
+
from django.http import HttpResponse
|
|
99
|
+
from my_app.config import package_config
|
|
100
|
+
|
|
101
|
+
def my_view(request):
|
|
102
|
+
...
|
|
103
|
+
# Your IDE now natively autocompletes these fields
|
|
104
|
+
if file_size > package_config.MAX_FILE_SIZE:
|
|
105
|
+
return HttpResponse(
|
|
106
|
+
{'detail': 'File size has exceeded the maximum size limit!'},
|
|
107
|
+
status = 400
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Extras
|
|
112
|
+
|
|
113
|
+
Your Configuration Dataclass can also be a nested dataclasses, you only need to inherit `BaseConfig` to the root configuration dataclass.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from dataclasses import dataclass, field
|
|
117
|
+
from django_dataclassconf.config import BaseConfig, config_loader
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class DocumentPreview:
|
|
121
|
+
preview_page_count: int = 10
|
|
122
|
+
strip_cover_page: bool = False
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class MyPackageConfig(BaseConfig):
|
|
126
|
+
DOCUMENTS_ROOT_PATH: str = '/the/default/path/'
|
|
127
|
+
PREVIEW: DocumentPreview = field(default_factory=DocumentPreview)
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def _prefix(self) -> str:
|
|
131
|
+
return 'MY_PACKAGE'
|
|
132
|
+
|
|
133
|
+
configuration = MyPackageConfig()
|
|
134
|
+
config_loader.subscribe(configuration)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
And it will look like this in `settings.py`
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
MY_PACKAGE = {
|
|
141
|
+
'DOCUMENTS_ROOT_PATH': '/custom/path/',
|
|
142
|
+
'PREVIEW': {
|
|
143
|
+
'preview_page_count': 12,
|
|
144
|
+
'strip_cover_page': True
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/django_dataclassconf/__init__.py
|
|
5
|
+
src/django_dataclassconf/conf.py
|
|
6
|
+
src/django_dataclassconf.egg-info/PKG-INFO
|
|
7
|
+
src/django_dataclassconf.egg-info/SOURCES.txt
|
|
8
|
+
src/django_dataclassconf.egg-info/dependency_links.txt
|
|
9
|
+
src/django_dataclassconf.egg-info/requires.txt
|
|
10
|
+
src/django_dataclassconf.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
django_dataclassconf
|