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.

@@ -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])]