aspyx 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.
Potentially problematic release.
This version of aspyx might be problematic. Click here for more details.
- aspyx/di/__init__.py +29 -0
- aspyx/di/aop/__init__.py +11 -0
- aspyx/di/aop/aop.py +532 -0
- aspyx/di/configuration/__init__.py +8 -0
- aspyx/di/configuration/configuration.py +190 -0
- aspyx/di/di.py +1055 -0
- aspyx/reflection/__init__.py +9 -0
- aspyx/reflection/proxy.py +58 -0
- aspyx/reflection/reflection.py +134 -0
- aspyx-0.1.0.dist-info/METADATA +451 -0
- aspyx-0.1.0.dist-info/RECORD +14 -0
- aspyx-0.1.0.dist-info/WHEEL +5 -0
- aspyx-0.1.0.dist-info/licenses/LICENSE +21 -0
- aspyx-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional, Type, TypeVar
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
from aspyx.di import injectable, Environment, CallableProcessor, LifecycleCallable, Lifecycle, environment
|
|
9
|
+
from aspyx.reflection import Decorators, DecoratorDescriptor, TypeDescriptor
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
class ConfigurationException(Exception):
|
|
14
|
+
"""
|
|
15
|
+
Exception raised for errors in the configuration logic.
|
|
16
|
+
"""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@injectable()
|
|
20
|
+
class ConfigurationManager:
|
|
21
|
+
"""
|
|
22
|
+
ConfigurationManager is responsible for managing different configuration sources by merging the different values
|
|
23
|
+
and offering a uniform api.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__slots__ = ["sources", "_data", "coercions"]
|
|
27
|
+
|
|
28
|
+
# constructor
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.sources = []
|
|
32
|
+
self._data = dict()
|
|
33
|
+
self.coercions = {
|
|
34
|
+
int: int,
|
|
35
|
+
float: float,
|
|
36
|
+
bool: lambda v: str(v).lower() in ("1", "true", "yes", "on"),
|
|
37
|
+
str: str,
|
|
38
|
+
# Add more types as needed
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# internal
|
|
42
|
+
|
|
43
|
+
def _register(self, source: ConfigurationSource):
|
|
44
|
+
self.sources.append(source)
|
|
45
|
+
self.load_source(source)
|
|
46
|
+
|
|
47
|
+
# public
|
|
48
|
+
|
|
49
|
+
def load_source(self, source: ConfigurationSource):
|
|
50
|
+
def merge_dicts(a: dict, b: dict) -> dict:
|
|
51
|
+
result = a.copy()
|
|
52
|
+
for key, b_val in b.items():
|
|
53
|
+
if key in result:
|
|
54
|
+
a_val = result[key]
|
|
55
|
+
if isinstance(a_val, dict) and isinstance(b_val, dict):
|
|
56
|
+
result[key] = merge_dicts(a_val, b_val) # Recurse
|
|
57
|
+
else:
|
|
58
|
+
result[key] = b_val # Overwrite
|
|
59
|
+
else:
|
|
60
|
+
result[key] = b_val
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
self._data = merge_dicts(self._data, source.load())
|
|
64
|
+
|
|
65
|
+
def get(self, path: str, type: Type[T], default : Optional[T]=None) -> T:
|
|
66
|
+
"""
|
|
67
|
+
Get a configuration value by path and type, with optional coercion.
|
|
68
|
+
Arguments:
|
|
69
|
+
path (str): The path to the configuration value, e.g. "database.host".
|
|
70
|
+
type (Type[T]): The expected type.
|
|
71
|
+
default (Optional[T]): The default value to return if the path is not found.
|
|
72
|
+
Returns:
|
|
73
|
+
T: The configuration value coerced to the specified type, or the default value if not found.
|
|
74
|
+
"""
|
|
75
|
+
def resolve_value(path: str, default=None) -> T:
|
|
76
|
+
keys = path.split(".")
|
|
77
|
+
current = self._data
|
|
78
|
+
for key in keys:
|
|
79
|
+
if not isinstance(current, dict) or key not in current:
|
|
80
|
+
return default
|
|
81
|
+
current = current[key]
|
|
82
|
+
|
|
83
|
+
return current
|
|
84
|
+
|
|
85
|
+
v = resolve_value(path, default)
|
|
86
|
+
|
|
87
|
+
if isinstance(v, type):
|
|
88
|
+
return v
|
|
89
|
+
|
|
90
|
+
if type in self.coercions:
|
|
91
|
+
try:
|
|
92
|
+
return self.coercions[type](v)
|
|
93
|
+
except Exception:
|
|
94
|
+
raise ConfigurationException(f"error during coercion to {type}")
|
|
95
|
+
else:
|
|
96
|
+
raise ConfigurationException(f"unknown coercion to {type}")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ConfigurationSource(ABC):
|
|
100
|
+
"""
|
|
101
|
+
A configuration source is a provider of configuration data.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
__slots__ = []
|
|
105
|
+
|
|
106
|
+
def __init__(self, manager: ConfigurationManager):
|
|
107
|
+
manager._register(self)
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
@abstractmethod
|
|
111
|
+
def load(self) -> dict:
|
|
112
|
+
"""
|
|
113
|
+
return the configuration values of this source as a dictionary."""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@injectable()
|
|
117
|
+
class EnvConfigurationSource(ConfigurationSource):
|
|
118
|
+
"""
|
|
119
|
+
EnvConfigurationSource loads all environment variables.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
__slots__ = []
|
|
123
|
+
|
|
124
|
+
# constructor
|
|
125
|
+
|
|
126
|
+
def __init__(self, manager: ConfigurationManager):
|
|
127
|
+
super().__init__(manager)
|
|
128
|
+
|
|
129
|
+
load_dotenv()
|
|
130
|
+
|
|
131
|
+
# implement
|
|
132
|
+
|
|
133
|
+
def load(self) -> dict:
|
|
134
|
+
def merge_dicts(a, b):
|
|
135
|
+
"""Recursively merges b into a"""
|
|
136
|
+
for key, value in b.items():
|
|
137
|
+
if isinstance(value, dict) and key in a and isinstance(a[key], dict):
|
|
138
|
+
merge_dicts(a[key], value)
|
|
139
|
+
else:
|
|
140
|
+
a[key] = value
|
|
141
|
+
return a
|
|
142
|
+
|
|
143
|
+
def explode_key(key, value):
|
|
144
|
+
"""Explodes keys with '.' or '/' into nested dictionaries"""
|
|
145
|
+
parts = key.replace('/', '.').split('.')
|
|
146
|
+
d = current = {}
|
|
147
|
+
for part in parts[:-1]:
|
|
148
|
+
current[part] = {}
|
|
149
|
+
current = current[part]
|
|
150
|
+
current[parts[-1]] = value
|
|
151
|
+
return d
|
|
152
|
+
|
|
153
|
+
exploded = {}
|
|
154
|
+
|
|
155
|
+
for key, value in os.environ.items():
|
|
156
|
+
if '.' in key or '/' in key:
|
|
157
|
+
partial = explode_key(key, value)
|
|
158
|
+
merge_dicts(exploded, partial)
|
|
159
|
+
else:
|
|
160
|
+
exploded[key] = value
|
|
161
|
+
|
|
162
|
+
return exploded
|
|
163
|
+
|
|
164
|
+
# decorator
|
|
165
|
+
|
|
166
|
+
def value(key: str, default=None):
|
|
167
|
+
"""
|
|
168
|
+
Decorator to inject a configuration value into a method.
|
|
169
|
+
|
|
170
|
+
Arguments:
|
|
171
|
+
key (str): The configuration key to inject.
|
|
172
|
+
default: The default value to use if the key is not found.
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
def decorator(func):
|
|
176
|
+
Decorators.add(func, value, key, default)
|
|
177
|
+
|
|
178
|
+
return func
|
|
179
|
+
|
|
180
|
+
return decorator
|
|
181
|
+
|
|
182
|
+
@injectable()
|
|
183
|
+
class ConfigurationLifecycleCallable(LifecycleCallable):
|
|
184
|
+
def __init__(self, processor: CallableProcessor, manager: ConfigurationManager):
|
|
185
|
+
super().__init__(value, processor, Lifecycle.ON_INIT)
|
|
186
|
+
|
|
187
|
+
self.manager = manager
|
|
188
|
+
|
|
189
|
+
def args(self, decorator: DecoratorDescriptor, method: TypeDescriptor.MethodDescriptor, environment: Environment):
|
|
190
|
+
return [self.manager.get(decorator.args[0], method.paramTypes[0], decorator.args[1])]
|