appsettings2 2.0.1__tar.gz → 2.0.7__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.
- {appsettings2-2.0.1/src/appsettings2.egg-info → appsettings2-2.0.7}/PKG-INFO +7 -8
- {appsettings2-2.0.1 → appsettings2-2.0.7}/README.md +5 -5
- {appsettings2-2.0.1 → appsettings2-2.0.7}/pyproject.toml +3 -4
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/Configuration.py +69 -23
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/__init__.py +2 -2
- {appsettings2-2.0.1 → appsettings2-2.0.7/src/appsettings2.egg-info}/PKG-INFO +7 -8
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2.egg-info/requires.txt +1 -1
- {appsettings2-2.0.1 → appsettings2-2.0.7}/LICENSE +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/setup.cfg +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/ConfigurationBuilder.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/ConfigurationException.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/helpers.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/CommandLineConfigurationProvider.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/ConfigurationProvider.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/EnvironmentConfigurationProvider.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/JsonConfigurationProvider.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/TomlConfigurationProvider.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/YamlConfigurationProvider.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/__init__.py +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/py.typed +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2.egg-info/SOURCES.txt +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2.egg-info/dependency_links.txt +0 -0
- {appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: appsettings2
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.7
|
|
4
4
|
Summary: Unifies configuration sources into a Configuration object that can be bound to complex types, or accessed directly for configuration data.
|
|
5
5
|
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
6
|
License: MIT License
|
|
@@ -32,7 +32,6 @@ Keywords: appsettings,configuration
|
|
|
32
32
|
Classifier: Intended Audience :: Developers
|
|
33
33
|
Classifier: Operating System :: OS Independent
|
|
34
34
|
Classifier: Programming Language :: Python
|
|
35
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
36
35
|
Requires-Python: >=3.11
|
|
37
36
|
Description-Content-Type: text/markdown
|
|
38
37
|
License-File: LICENSE
|
|
@@ -43,7 +42,7 @@ Requires-Dist: flake8; extra == "dev"
|
|
|
43
42
|
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
44
43
|
Requires-Dist: mypy; extra == "dev"
|
|
45
44
|
Requires-Dist: pep8-naming; extra == "dev"
|
|
46
|
-
Requires-Dist: punit>=1.4.
|
|
45
|
+
Requires-Dist: punit>=1.4.9; extra == "dev"
|
|
47
46
|
Requires-Dist: twine; extra == "dev"
|
|
48
47
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
49
48
|
Dynamic: license-file
|
|
@@ -132,12 +131,12 @@ json = """{
|
|
|
132
131
|
|
|
133
132
|
class ConnStrs:
|
|
134
133
|
"""An ugly class name to demonstrate the class name does not matter."""
|
|
135
|
-
SampleDB:str
|
|
134
|
+
SampleDB: str
|
|
136
135
|
|
|
137
136
|
class AppSettings:
|
|
138
|
-
ConnectionStrings:ConnStrs
|
|
139
|
-
EnableSwagger:bool
|
|
140
|
-
MaxBatchSize:int
|
|
137
|
+
ConnectionStrings: ConnStrs
|
|
138
|
+
EnableSwagger: bool
|
|
139
|
+
MaxBatchSize: int
|
|
141
140
|
|
|
142
141
|
configuration = ConfigurationBuilder()\
|
|
143
142
|
.add_provider(JsonConfigurationProvider(json=json))
|
|
@@ -170,7 +169,7 @@ You can transform a `Configuration` instance into a dictionary (as a copy.) If y
|
|
|
170
169
|
|
|
171
170
|
```python
|
|
172
171
|
configuration = builder.build()
|
|
173
|
-
d = configuration.
|
|
172
|
+
d = configuration.to_dict()
|
|
174
173
|
print(d)
|
|
175
174
|
```
|
|
176
175
|
|
|
@@ -82,12 +82,12 @@ json = """{
|
|
|
82
82
|
|
|
83
83
|
class ConnStrs:
|
|
84
84
|
"""An ugly class name to demonstrate the class name does not matter."""
|
|
85
|
-
SampleDB:str
|
|
85
|
+
SampleDB: str
|
|
86
86
|
|
|
87
87
|
class AppSettings:
|
|
88
|
-
ConnectionStrings:ConnStrs
|
|
89
|
-
EnableSwagger:bool
|
|
90
|
-
MaxBatchSize:int
|
|
88
|
+
ConnectionStrings: ConnStrs
|
|
89
|
+
EnableSwagger: bool
|
|
90
|
+
MaxBatchSize: int
|
|
91
91
|
|
|
92
92
|
configuration = ConfigurationBuilder()\
|
|
93
93
|
.add_provider(JsonConfigurationProvider(json=json))
|
|
@@ -120,7 +120,7 @@ You can transform a `Configuration` instance into a dictionary (as a copy.) If y
|
|
|
120
120
|
|
|
121
121
|
```python
|
|
122
122
|
configuration = builder.build()
|
|
123
|
-
d = configuration.
|
|
123
|
+
d = configuration.to_dict()
|
|
124
124
|
print(d)
|
|
125
125
|
```
|
|
126
126
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "appsettings2"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.7"
|
|
4
4
|
description = "Unifies configuration sources into a Configuration object that can be bound to complex types, or accessed directly for configuration data."
|
|
5
5
|
keywords = ["appsettings", "configuration"]
|
|
6
6
|
authors = [
|
|
@@ -11,8 +11,7 @@ license = {file = "LICENSE"}
|
|
|
11
11
|
classifiers = [
|
|
12
12
|
"Intended Audience :: Developers",
|
|
13
13
|
"Operating System :: OS Independent",
|
|
14
|
-
"Programming Language :: Python"
|
|
15
|
-
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
14
|
+
"Programming Language :: Python"
|
|
16
15
|
]
|
|
17
16
|
requires-python = ">=3.11"
|
|
18
17
|
dependencies = [
|
|
@@ -26,7 +25,7 @@ dev = [
|
|
|
26
25
|
"flake8-pyproject",
|
|
27
26
|
"mypy",
|
|
28
27
|
"pep8-naming",
|
|
29
|
-
"punit (>=1.4.
|
|
28
|
+
"punit (>=1.4.9)",
|
|
30
29
|
"twine",
|
|
31
30
|
"types-PyYAML"
|
|
32
31
|
]
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# SPDX-FileCopyrightText: © 2025 Shaun Wilson
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
5
6
|
import json
|
|
7
|
+
import logging
|
|
6
8
|
import re
|
|
7
9
|
import types
|
|
8
|
-
import
|
|
9
|
-
from typing import Any, Iterator, TYPE_CHECKING
|
|
10
|
+
from types import NoneType
|
|
11
|
+
from typing import Any, Iterator, Union, TYPE_CHECKING, get_args, get_origin, get_type_hints
|
|
10
12
|
import unicodedata
|
|
11
13
|
|
|
12
14
|
from .ConfigurationException import ConfigurationException
|
|
@@ -15,7 +17,7 @@ from .ConfigurationException import ConfigurationException
|
|
|
15
17
|
class Configuration:
|
|
16
18
|
"""The :py:class:`~appsettings2.Configuration` class is how applications access configuration data populated by :py:class:`~appsettings2.providers.ConfigurationProvider` objects. It exposes configuration data through dynamic object attributes as well as a dictionary-like interface."""
|
|
17
19
|
|
|
18
|
-
__key_scrub_re: re.Pattern | None
|
|
20
|
+
__key_scrub_re: re.Pattern[str] | None
|
|
19
21
|
__keys: dict[str, str]
|
|
20
22
|
__normalize: bool
|
|
21
23
|
|
|
@@ -29,8 +31,11 @@ class Configuration:
|
|
|
29
31
|
self.__keys = {}
|
|
30
32
|
self.__logger = logging.getLogger('appsettings2')
|
|
31
33
|
self.__normalize = normalize
|
|
32
|
-
self.__key_scrub_re =
|
|
33
|
-
|
|
34
|
+
self.__key_scrub_re = (
|
|
35
|
+
None
|
|
36
|
+
if not scrubkeys
|
|
37
|
+
else re.compile(r'[^A-Za-z0-9_]', re.IGNORECASE | re.UNICODE)
|
|
38
|
+
)
|
|
34
39
|
|
|
35
40
|
if TYPE_CHECKING:
|
|
36
41
|
|
|
@@ -38,7 +43,6 @@ class Configuration:
|
|
|
38
43
|
"""Tell type checkers that accessing any attribute returns any type."""
|
|
39
44
|
...
|
|
40
45
|
|
|
41
|
-
# This tells mypy that setting any attribute is allowed
|
|
42
46
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
43
47
|
"""Tell type checkers that setting any attribute to any value is allowed."""
|
|
44
48
|
...
|
|
@@ -79,21 +83,27 @@ class Configuration:
|
|
|
79
83
|
"""Get the number of configuration keys."""
|
|
80
84
|
return len(self.__keys)
|
|
81
85
|
|
|
82
|
-
def
|
|
86
|
+
def __deunionize(self, t: type) -> type:
|
|
87
|
+
if get_origin(t) is Union:
|
|
88
|
+
t = [e for e in get_args(t) if e is not NoneType][0]
|
|
89
|
+
return t
|
|
90
|
+
|
|
91
|
+
def __recursive_bind(self, target: object, source: Configuration | dict[str, Any]) -> Any:
|
|
83
92
|
if target is None:
|
|
84
93
|
return None
|
|
85
94
|
if hasattr(target, '__class__'):
|
|
86
|
-
target_type_hints =
|
|
87
|
-
getattr(target, '__class__')
|
|
95
|
+
target_type_hints = get_type_hints(
|
|
96
|
+
getattr(target, '__class__')
|
|
97
|
+
)
|
|
88
98
|
else:
|
|
89
|
-
target_type_hints =
|
|
99
|
+
target_type_hints = get_type_hints(target)
|
|
90
100
|
names = set(dir(target) | target_type_hints.keys())
|
|
91
101
|
for aname in names:
|
|
92
102
|
if aname.startswith('_'):
|
|
93
103
|
continue
|
|
94
104
|
lval = None if not hasattr(
|
|
95
105
|
target, aname) else getattr(target, aname)
|
|
96
|
-
if isinstance(lval, types.FunctionType
|
|
106
|
+
if isinstance(lval, (types.FunctionType, types.MethodType)):
|
|
97
107
|
continue
|
|
98
108
|
ahint = target_type_hints.get(aname)
|
|
99
109
|
rval = source.get(aname)
|
|
@@ -120,8 +130,8 @@ class Configuration:
|
|
|
120
130
|
# non-list values cannot be merged
|
|
121
131
|
raise Exception(
|
|
122
132
|
f'Cannot bind `None` to attribute `{aname}`')
|
|
123
|
-
phints =
|
|
124
|
-
if phints is None
|
|
133
|
+
phints = get_type_hints(getattr(prop, 'fget'))
|
|
134
|
+
if phints is None:
|
|
125
135
|
# NOTE: can't get hints from getter, can't bind
|
|
126
136
|
continue
|
|
127
137
|
ahint = phints.get('return')
|
|
@@ -129,6 +139,23 @@ class Configuration:
|
|
|
129
139
|
# NOTE: fget hint missing return spec, can't bind
|
|
130
140
|
continue
|
|
131
141
|
if rval is None:
|
|
142
|
+
if get_origin(ahint) is dict:
|
|
143
|
+
value_type = get_args(ahint)[1]
|
|
144
|
+
if isinstance(source, Configuration):
|
|
145
|
+
lval = dict[str, Any]()
|
|
146
|
+
for dk, dv in source.items():
|
|
147
|
+
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
148
|
+
setattr(target, aname, lval)
|
|
149
|
+
continue
|
|
150
|
+
elif isinstance(source, dict):
|
|
151
|
+
if value_type in (str, int, float, bool, type(None)):
|
|
152
|
+
setattr(target, aname, source)
|
|
153
|
+
else:
|
|
154
|
+
lval = dict[str, Any]()
|
|
155
|
+
for dk, dv in source.items():
|
|
156
|
+
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
157
|
+
setattr(target, aname, lval)
|
|
158
|
+
continue
|
|
132
159
|
setattr(target, aname, None)
|
|
133
160
|
elif ahint is float:
|
|
134
161
|
setattr(target, aname, float(rval))
|
|
@@ -137,23 +164,42 @@ class Configuration:
|
|
|
137
164
|
elif ahint is str:
|
|
138
165
|
setattr(target, aname, str(rval))
|
|
139
166
|
elif isinstance(rval, Configuration):
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
167
|
+
if get_origin(ahint) is dict:
|
|
168
|
+
value_type = get_args(ahint)[1]
|
|
169
|
+
if value_type in (str, int, float, bool, type(None)):
|
|
170
|
+
lval = rval.to_dict()
|
|
171
|
+
setattr(target, aname, lval)
|
|
172
|
+
else:
|
|
173
|
+
lval = dict[str, Any]()
|
|
174
|
+
for dk, dv in rval.items():
|
|
175
|
+
lval[dk] = self.__recursive_bind_type(value_type, dv)
|
|
176
|
+
setattr(target, aname, lval)
|
|
143
177
|
else:
|
|
144
178
|
if lval is None:
|
|
179
|
+
ahint = self.__deunionize(ahint)
|
|
145
180
|
lval = ahint()
|
|
146
181
|
setattr(target, aname, lval)
|
|
147
182
|
self.__recursive_bind(lval, rval)
|
|
148
|
-
elif
|
|
183
|
+
elif get_origin(ahint) is list:
|
|
149
184
|
element_type = ahint.__args__[0]
|
|
150
185
|
if lval is None:
|
|
186
|
+
ahint = self.__deunionize(ahint)
|
|
151
187
|
lval = ahint()
|
|
152
188
|
setattr(target, aname, lval)
|
|
153
189
|
for e in rval:
|
|
154
190
|
lval.append(
|
|
155
191
|
self.__recursive_bind_type(element_type, e)
|
|
156
192
|
)
|
|
193
|
+
elif get_origin(ahint) is set:
|
|
194
|
+
element_type = ahint.__args__[0]
|
|
195
|
+
if lval is None:
|
|
196
|
+
ahint = self.__deunionize(ahint)
|
|
197
|
+
lval = ahint()
|
|
198
|
+
setattr(target, aname, lval)
|
|
199
|
+
for e in rval:
|
|
200
|
+
lval.add(
|
|
201
|
+
self.__recursive_bind_type(element_type, e)
|
|
202
|
+
)
|
|
157
203
|
else:
|
|
158
204
|
setattr(target, aname, rval)
|
|
159
205
|
return target
|
|
@@ -167,7 +213,7 @@ class Configuration:
|
|
|
167
213
|
return int(source)
|
|
168
214
|
elif element_type is str:
|
|
169
215
|
return str(source)
|
|
170
|
-
elif isinstance(source, Configuration
|
|
216
|
+
elif isinstance(source, (Configuration, dict)):
|
|
171
217
|
v = element_type()
|
|
172
218
|
return self.__recursive_bind(v, source)
|
|
173
219
|
else:
|
|
@@ -183,7 +229,7 @@ class Configuration:
|
|
|
183
229
|
'NFKC',
|
|
184
230
|
key))
|
|
185
231
|
|
|
186
|
-
def __scrub_uc(self, m: re.Match) -> str:
|
|
232
|
+
def __scrub_uc(self, m: re.Match[str]) -> str:
|
|
187
233
|
match unicodedata.category(m[0]):
|
|
188
234
|
case 'Lu' | 'Ll' | 'Lt' | 'Lm' | 'Lo' | 'Nl' | 'Mn' | 'Mc' | 'Nd' | 'Pc':
|
|
189
235
|
return m[0]
|
|
@@ -230,7 +276,7 @@ class Configuration:
|
|
|
230
276
|
delattr(self, t[1])
|
|
231
277
|
|
|
232
278
|
@staticmethod
|
|
233
|
-
def from_dict(source: dict, normalize: bool = False, scrubkeys: bool = False) -> 'Configuration':
|
|
279
|
+
def from_dict(source: dict[str, Any], normalize: bool = False, scrubkeys: bool = False) -> 'Configuration':
|
|
234
280
|
"""
|
|
235
281
|
Construct a :py:class:`~appsettings2.Configuration` instance from the supplied dictionary `source`.
|
|
236
282
|
|
|
@@ -359,12 +405,12 @@ class Configuration:
|
|
|
359
405
|
for k in self.__keys.values():
|
|
360
406
|
v = getattr(self, self.__scrub_key(k))
|
|
361
407
|
if isinstance(v, Configuration):
|
|
362
|
-
result[k] = v.
|
|
408
|
+
result[k] = v.to_dict()
|
|
363
409
|
elif issubclass(type(v), list):
|
|
364
410
|
tmp = []
|
|
365
411
|
for e in v:
|
|
366
412
|
if isinstance(e, Configuration):
|
|
367
|
-
tmp.append(e.
|
|
413
|
+
tmp.append(e.to_dict())
|
|
368
414
|
else:
|
|
369
415
|
tmp.append(e)
|
|
370
416
|
result[k] = tmp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: appsettings2
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.7
|
|
4
4
|
Summary: Unifies configuration sources into a Configuration object that can be bound to complex types, or accessed directly for configuration data.
|
|
5
5
|
Author-email: Shaun Wilson <mrshaunwilson@msn.com>
|
|
6
6
|
License: MIT License
|
|
@@ -32,7 +32,6 @@ Keywords: appsettings,configuration
|
|
|
32
32
|
Classifier: Intended Audience :: Developers
|
|
33
33
|
Classifier: Operating System :: OS Independent
|
|
34
34
|
Classifier: Programming Language :: Python
|
|
35
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
36
35
|
Requires-Python: >=3.11
|
|
37
36
|
Description-Content-Type: text/markdown
|
|
38
37
|
License-File: LICENSE
|
|
@@ -43,7 +42,7 @@ Requires-Dist: flake8; extra == "dev"
|
|
|
43
42
|
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
44
43
|
Requires-Dist: mypy; extra == "dev"
|
|
45
44
|
Requires-Dist: pep8-naming; extra == "dev"
|
|
46
|
-
Requires-Dist: punit>=1.4.
|
|
45
|
+
Requires-Dist: punit>=1.4.9; extra == "dev"
|
|
47
46
|
Requires-Dist: twine; extra == "dev"
|
|
48
47
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
49
48
|
Dynamic: license-file
|
|
@@ -132,12 +131,12 @@ json = """{
|
|
|
132
131
|
|
|
133
132
|
class ConnStrs:
|
|
134
133
|
"""An ugly class name to demonstrate the class name does not matter."""
|
|
135
|
-
SampleDB:str
|
|
134
|
+
SampleDB: str
|
|
136
135
|
|
|
137
136
|
class AppSettings:
|
|
138
|
-
ConnectionStrings:ConnStrs
|
|
139
|
-
EnableSwagger:bool
|
|
140
|
-
MaxBatchSize:int
|
|
137
|
+
ConnectionStrings: ConnStrs
|
|
138
|
+
EnableSwagger: bool
|
|
139
|
+
MaxBatchSize: int
|
|
141
140
|
|
|
142
141
|
configuration = ConfigurationBuilder()\
|
|
143
142
|
.add_provider(JsonConfigurationProvider(json=json))
|
|
@@ -170,7 +169,7 @@ You can transform a `Configuration` instance into a dictionary (as a copy.) If y
|
|
|
170
169
|
|
|
171
170
|
```python
|
|
172
171
|
configuration = builder.build()
|
|
173
|
-
d = configuration.
|
|
172
|
+
d = configuration.to_dict()
|
|
174
173
|
print(d)
|
|
175
174
|
```
|
|
176
175
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/ConfigurationProvider.py
RENAMED
|
File without changes
|
|
File without changes
|
{appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/JsonConfigurationProvider.py
RENAMED
|
File without changes
|
{appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/TomlConfigurationProvider.py
RENAMED
|
File without changes
|
{appsettings2-2.0.1 → appsettings2-2.0.7}/src/appsettings2/providers/YamlConfigurationProvider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|