django-dataclassconf 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,9 @@
1
+ __version__ = '0.1.0'
2
+
3
+ from .conf import BaseConfig, config_loader
4
+
5
+ __all__ = [
6
+ "BaseConfig",
7
+ "config_loader",
8
+ "__version__",
9
+ ]
@@ -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,7 @@
1
+ django_dataclassconf/__init__.py,sha256=a4YqKSapptC8fQBnf9muBvnjyqiBj-YBI2VA3uERSgQ,149
2
+ django_dataclassconf/conf.py,sha256=rrAMcUmu7NZCcZusM9G3ina0eMRTbkTC7la_6JdVkiE,3079
3
+ django_dataclassconf-0.1.0.dist-info/licenses/LICENSE,sha256=3J2rBqh6X_suoDNzlioYNHzH04qbE-xgJsZEgFsYuds,1083
4
+ django_dataclassconf-0.1.0.dist-info/METADATA,sha256=w9kUHCPaHkHONpw48IwFvpjR53XBGC69xQighflqSz0,5596
5
+ django_dataclassconf-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ django_dataclassconf-0.1.0.dist-info/top_level.txt,sha256=NA-D720DXTcA4lzsLB2aC4359J-Xw3xPh5Zt1A-r1Ww,21
7
+ django_dataclassconf-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ django_dataclassconf