pico-ioc 1.4.0__py3-none-any.whl → 2.0.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.
- pico_ioc/__init__.py +91 -45
- pico_ioc/_version.py +1 -1
- pico_ioc/aop.py +247 -0
- pico_ioc/api.py +791 -207
- pico_ioc/config_runtime.py +289 -0
- pico_ioc/constants.py +10 -0
- pico_ioc/container.py +289 -189
- pico_ioc/event_bus.py +224 -0
- pico_ioc/exceptions.py +66 -0
- pico_ioc/factory.py +48 -0
- pico_ioc/locator.py +53 -0
- pico_ioc/scope.py +106 -40
- pico_ioc-2.0.0.dist-info/METADATA +230 -0
- pico_ioc-2.0.0.dist-info/RECORD +17 -0
- pico_ioc/_state.py +0 -75
- pico_ioc/builder.py +0 -294
- pico_ioc/config.py +0 -332
- pico_ioc/decorators.py +0 -158
- pico_ioc/interceptors.py +0 -56
- pico_ioc/plugins.py +0 -28
- pico_ioc/policy.py +0 -245
- pico_ioc/proxy.py +0 -129
- pico_ioc/public_api.py +0 -76
- pico_ioc/resolver.py +0 -132
- pico_ioc/scanner.py +0 -203
- pico_ioc/utils.py +0 -25
- pico_ioc-1.4.0.dist-info/METADATA +0 -241
- pico_ioc-1.4.0.dist-info/RECORD +0 -22
- {pico_ioc-1.4.0.dist-info → pico_ioc-2.0.0.dist-info}/WHEEL +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {pico_ioc-1.4.0.dist-info → pico_ioc-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# src/pico_ioc/config_runtime.py
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import hashlib
|
|
6
|
+
from dataclasses import is_dataclass, fields
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple, Union, get_args, get_origin, Annotated
|
|
9
|
+
from .exceptions import ConfigurationError
|
|
10
|
+
from .constants import PICO_META
|
|
11
|
+
|
|
12
|
+
class Discriminator:
|
|
13
|
+
def __init__(self, name: str):
|
|
14
|
+
self.name = name
|
|
15
|
+
|
|
16
|
+
class TreeSource:
|
|
17
|
+
def get_tree(self) -> Mapping[str, Any]:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
class DictSource(TreeSource):
|
|
21
|
+
def __init__(self, data: Mapping[str, Any]):
|
|
22
|
+
self._data = data
|
|
23
|
+
def get_tree(self) -> Mapping[str, Any]:
|
|
24
|
+
return self._data
|
|
25
|
+
|
|
26
|
+
class JsonTreeSource(TreeSource):
|
|
27
|
+
def __init__(self, path: str):
|
|
28
|
+
self._path = path
|
|
29
|
+
def get_tree(self) -> Mapping[str, Any]:
|
|
30
|
+
try:
|
|
31
|
+
with open(self._path, "r", encoding="utf-8") as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
raise ConfigurationError(f"Failed to load JSON config: {e}")
|
|
35
|
+
|
|
36
|
+
class YamlTreeSource(TreeSource):
|
|
37
|
+
def __init__(self, path: str):
|
|
38
|
+
self._path = path
|
|
39
|
+
def get_tree(self) -> Mapping[str, Any]:
|
|
40
|
+
try:
|
|
41
|
+
import yaml
|
|
42
|
+
except Exception:
|
|
43
|
+
raise ConfigurationError("PyYAML not installed")
|
|
44
|
+
try:
|
|
45
|
+
with open(self._path, "r", encoding="utf-8") as f:
|
|
46
|
+
data = yaml.safe_load(f) or {}
|
|
47
|
+
return data
|
|
48
|
+
except Exception as e:
|
|
49
|
+
raise ConfigurationError(f"Failed to load YAML config: {e}")
|
|
50
|
+
|
|
51
|
+
def _deep_merge(a: Any, b: Any) -> Any:
|
|
52
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
53
|
+
out = dict(a)
|
|
54
|
+
for k, v in b.items():
|
|
55
|
+
if k in out:
|
|
56
|
+
out = dict(out)
|
|
57
|
+
out[k] = _deep_merge(out[k], v)
|
|
58
|
+
else:
|
|
59
|
+
out[k] = v
|
|
60
|
+
return out
|
|
61
|
+
return b
|
|
62
|
+
|
|
63
|
+
def _walk_path(root: Any, path: str) -> Any:
|
|
64
|
+
cur = root
|
|
65
|
+
for part in path.split("."):
|
|
66
|
+
if isinstance(cur, dict) and part in cur:
|
|
67
|
+
cur = cur[part]
|
|
68
|
+
else:
|
|
69
|
+
raise ConfigurationError(f"Invalid ref path: {path}")
|
|
70
|
+
return cur
|
|
71
|
+
|
|
72
|
+
_env_pat = re.compile(r"\$\{ENV:([A-Za-z_][A-Za-z0-9_]*)\}")
|
|
73
|
+
_ref_pat = re.compile(r"\$\{ref:([A-Za-z0-9_.]+)\}")
|
|
74
|
+
|
|
75
|
+
def _interpolate_string(s: str, root: Any) -> str:
|
|
76
|
+
def repl_env(m):
|
|
77
|
+
v = os.environ.get(m.group(1))
|
|
78
|
+
if v is None:
|
|
79
|
+
raise ConfigurationError(f"Missing ENV var {m.group(1)}")
|
|
80
|
+
return v
|
|
81
|
+
def repl_ref(m):
|
|
82
|
+
v = _walk_path(root, m.group(1))
|
|
83
|
+
if isinstance(v, (dict, list)):
|
|
84
|
+
raise ConfigurationError("Cannot interpolate non-scalar ref")
|
|
85
|
+
return str(v)
|
|
86
|
+
s = _env_pat.sub(repl_env, s)
|
|
87
|
+
s = _ref_pat.sub(repl_ref, s)
|
|
88
|
+
return s
|
|
89
|
+
|
|
90
|
+
def _resolve_refs(node: Any, root: Any) -> Any:
|
|
91
|
+
if isinstance(node, dict):
|
|
92
|
+
if "$ref" in node and len(node) == 1:
|
|
93
|
+
return _resolve_refs(_walk_path(root, node["$ref"]), root)
|
|
94
|
+
return {k: _resolve_refs(v, root) for k, v in node.items()}
|
|
95
|
+
if isinstance(node, list):
|
|
96
|
+
return [_resolve_refs(x, root) for x in node]
|
|
97
|
+
if isinstance(node, str):
|
|
98
|
+
return _interpolate_string(node, root)
|
|
99
|
+
return node
|
|
100
|
+
|
|
101
|
+
def canonicalize(node: Any) -> bytes:
|
|
102
|
+
return json.dumps(node, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
103
|
+
|
|
104
|
+
class ConfigResolver:
|
|
105
|
+
def __init__(self, sources: Tuple[TreeSource, ...]):
|
|
106
|
+
self._sources = tuple(sources)
|
|
107
|
+
self._tree: Optional[Mapping[str, Any]] = None
|
|
108
|
+
def tree(self) -> Mapping[str, Any]:
|
|
109
|
+
if self._tree is None:
|
|
110
|
+
acc: Mapping[str, Any] = {}
|
|
111
|
+
for s in self._sources:
|
|
112
|
+
acc = _deep_merge(acc, s.get_tree())
|
|
113
|
+
acc = _resolve_refs(acc, acc)
|
|
114
|
+
self._tree = acc
|
|
115
|
+
return self._tree
|
|
116
|
+
def subtree(self, prefix: Optional[str]) -> Any:
|
|
117
|
+
t = self.tree()
|
|
118
|
+
if not prefix:
|
|
119
|
+
return t
|
|
120
|
+
cur = t
|
|
121
|
+
for part in prefix.split("."):
|
|
122
|
+
if isinstance(cur, dict) and part in cur:
|
|
123
|
+
cur = cur[part]
|
|
124
|
+
else:
|
|
125
|
+
raise ConfigurationError(f"Missing config prefix: {prefix}")
|
|
126
|
+
return cur
|
|
127
|
+
|
|
128
|
+
class TypeAdapterRegistry:
|
|
129
|
+
def __init__(self):
|
|
130
|
+
self._adapters: Dict[type, Any] = {}
|
|
131
|
+
def register(self, t: type, fn):
|
|
132
|
+
self._adapters[t] = fn
|
|
133
|
+
def get(self, t: type):
|
|
134
|
+
return self._adapters.get(t)
|
|
135
|
+
|
|
136
|
+
class ObjectGraphBuilder:
|
|
137
|
+
def __init__(self, resolver: ConfigResolver, registry: TypeAdapterRegistry):
|
|
138
|
+
self._resolver = resolver
|
|
139
|
+
self._registry = registry
|
|
140
|
+
def build_from_prefix(self, target_type: type, prefix: Optional[str]) -> Any:
|
|
141
|
+
node = self._resolver.subtree(prefix)
|
|
142
|
+
inst = self._build(node, target_type, ("$root" if not prefix else prefix,))
|
|
143
|
+
try:
|
|
144
|
+
h = hashlib.sha256(canonicalize(node)).hexdigest()
|
|
145
|
+
m = getattr(inst, PICO_META, None)
|
|
146
|
+
if m is None:
|
|
147
|
+
setattr(inst, PICO_META, {"config_hash": h, "config_prefix": prefix, "config_source_order": tuple(type(s).__name__ for s in self._resolver._sources)})
|
|
148
|
+
else:
|
|
149
|
+
m.update({"config_hash": h, "config_prefix": prefix, "config_source_order": tuple(type(s).__name__ for s in self._resolver._sources)})
|
|
150
|
+
except Exception:
|
|
151
|
+
pass
|
|
152
|
+
return inst
|
|
153
|
+
def _build(self, node: Any, t: Any, path: Tuple[str, ...]) -> Any:
|
|
154
|
+
if t is Any or t is object:
|
|
155
|
+
return node
|
|
156
|
+
adapter = self._registry.get(t) if isinstance(t, type) else None
|
|
157
|
+
if adapter:
|
|
158
|
+
return adapter(node)
|
|
159
|
+
org = get_origin(t)
|
|
160
|
+
if org is Annotated:
|
|
161
|
+
base, meta = self._split_annotated(t)
|
|
162
|
+
return self._build_discriminated(node, base, meta, path)
|
|
163
|
+
if org in (list, List):
|
|
164
|
+
elem_t = get_args(t)[0] if get_args(t) else Any
|
|
165
|
+
if not isinstance(node, list):
|
|
166
|
+
raise ConfigurationError(f"Expected list at {'.'.join(path)}")
|
|
167
|
+
return [self._build(x, elem_t, path + (str(i),)) for i, x in enumerate(node)]
|
|
168
|
+
if org in (dict, Dict, Mapping):
|
|
169
|
+
args = get_args(t)
|
|
170
|
+
kt = args[0] if args else str
|
|
171
|
+
vt = args[1] if len(args) > 1 else Any
|
|
172
|
+
if kt not in (str, Any, object):
|
|
173
|
+
raise ConfigurationError(f"Only dicts with string keys supported at {'.'.join(path)}")
|
|
174
|
+
if not isinstance(node, dict):
|
|
175
|
+
raise ConfigurationError(f"Expected dict at {'.'.join(path)}")
|
|
176
|
+
return {k: self._build(v, vt, path + (k,)) for k, v in node.items()}
|
|
177
|
+
if org is Union:
|
|
178
|
+
args = [a for a in get_args(t)]
|
|
179
|
+
if not isinstance(node, dict):
|
|
180
|
+
for cand in args:
|
|
181
|
+
try:
|
|
182
|
+
return self._build(node, cand, path)
|
|
183
|
+
except Exception:
|
|
184
|
+
continue
|
|
185
|
+
raise ConfigurationError(f"No union match at {'.'.join(path)}")
|
|
186
|
+
if "$type" in node:
|
|
187
|
+
tn = str(node["$type"])
|
|
188
|
+
for cand in args:
|
|
189
|
+
if isinstance(cand, type) and getattr(cand, "__name__", "") == tn:
|
|
190
|
+
cleaned = {k: v for k, v in node.items() if k != "$type"}
|
|
191
|
+
return self._build(cleaned, cand, path)
|
|
192
|
+
raise ConfigurationError(f"Discriminator $type did not match at {'.'.join(path)}")
|
|
193
|
+
for cand in args:
|
|
194
|
+
try:
|
|
195
|
+
return self._build(node, cand, path)
|
|
196
|
+
except Exception:
|
|
197
|
+
continue
|
|
198
|
+
raise ConfigurationError(f"No union match at {'.'.join(path)}")
|
|
199
|
+
if isinstance(t, type) and issubclass(t, Enum):
|
|
200
|
+
if isinstance(node, str):
|
|
201
|
+
try:
|
|
202
|
+
return t[node]
|
|
203
|
+
except Exception:
|
|
204
|
+
for e in t:
|
|
205
|
+
if str(e.value) == node:
|
|
206
|
+
return e
|
|
207
|
+
raise ConfigurationError(f"Invalid enum at {'.'.join(path)}")
|
|
208
|
+
if isinstance(t, type) and is_dataclass(t):
|
|
209
|
+
if not isinstance(node, dict):
|
|
210
|
+
raise ConfigurationError(f"Expected object at {'.'.join(path)}")
|
|
211
|
+
known = {f.name for f in fields(t)}
|
|
212
|
+
extra = [k for k in node.keys() if k not in known]
|
|
213
|
+
if extra:
|
|
214
|
+
raise ConfigurationError(f"Unknown keys {extra} at {'.'.join(path)}")
|
|
215
|
+
vals: Dict[str, Any] = {}
|
|
216
|
+
for f in fields(t):
|
|
217
|
+
if f.name in node:
|
|
218
|
+
vals[f.name] = self._build(node[f.name], f.type, path + (f.name,))
|
|
219
|
+
else:
|
|
220
|
+
continue
|
|
221
|
+
return t(**vals)
|
|
222
|
+
if isinstance(t, type):
|
|
223
|
+
if t in (str, int, float, bool):
|
|
224
|
+
return self._coerce_prim(node, t, path)
|
|
225
|
+
if hasattr(t, "__init__"):
|
|
226
|
+
if not isinstance(node, dict):
|
|
227
|
+
raise ConfigurationError(f"Expected object for ctor at {'.'.join(path)}")
|
|
228
|
+
kwargs: Dict[str, Any] = {}
|
|
229
|
+
import inspect
|
|
230
|
+
sig = inspect.signature(t.__init__)
|
|
231
|
+
for name, p in sig.parameters.items():
|
|
232
|
+
if name in ("self", "cls"):
|
|
233
|
+
continue
|
|
234
|
+
if name in node:
|
|
235
|
+
kwargs[name] = self._build(node[name], p.annotation if p.annotation is not inspect._empty else Any, path + (name,))
|
|
236
|
+
return t(**kwargs)
|
|
237
|
+
return node
|
|
238
|
+
def _split_annotated(self, t: Any) -> Tuple[Any, Tuple[Any, ...]]:
|
|
239
|
+
args = get_args(t)
|
|
240
|
+
base = args[0] if args else Any
|
|
241
|
+
metas = tuple(args[1:]) if len(args) > 1 else ()
|
|
242
|
+
return base, metas
|
|
243
|
+
def _build_discriminated(self, node: Any, base: Any, metas: Tuple[Any, ...], path: Tuple[str, ...]) -> Any:
|
|
244
|
+
disc_name = None
|
|
245
|
+
for m in metas:
|
|
246
|
+
if isinstance(m, Discriminator):
|
|
247
|
+
disc_name = m.name
|
|
248
|
+
break
|
|
249
|
+
if disc_name and isinstance(node, dict) and disc_name in node:
|
|
250
|
+
if get_origin(base) is Union:
|
|
251
|
+
tn = str(node[disc_name])
|
|
252
|
+
for cand in get_args(base):
|
|
253
|
+
if isinstance(cand, type) and getattr(cand, "__name__", "") == tn:
|
|
254
|
+
cleaned = {k: v for k, v in node.items() if k != disc_name}
|
|
255
|
+
return self._build(cleaned, cand, path)
|
|
256
|
+
raise ConfigurationError(f"Discriminator {disc_name} did not match at {'.'.join(path)}")
|
|
257
|
+
return self._build(node, base, path)
|
|
258
|
+
def _coerce_prim(self, node: Any, t: type, path: Tuple[str, ...]) -> Any:
|
|
259
|
+
if t is str:
|
|
260
|
+
if isinstance(node, str):
|
|
261
|
+
return node
|
|
262
|
+
return str(node)
|
|
263
|
+
if t is int:
|
|
264
|
+
if isinstance(node, int):
|
|
265
|
+
return node
|
|
266
|
+
if isinstance(node, str) and node.strip().isdigit() or (isinstance(node, str) and node.strip().startswith("-") and node.strip()[1:].isdigit()):
|
|
267
|
+
return int(node)
|
|
268
|
+
raise ConfigurationError(f"Expected int at {'.'.join(path)}")
|
|
269
|
+
if t is float:
|
|
270
|
+
if isinstance(node, (int, float)):
|
|
271
|
+
return float(node)
|
|
272
|
+
if isinstance(node, str):
|
|
273
|
+
try:
|
|
274
|
+
return float(node)
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
raise ConfigurationError(f"Expected float at {'.'.join(path)}")
|
|
278
|
+
if t is bool:
|
|
279
|
+
if isinstance(node, bool):
|
|
280
|
+
return node
|
|
281
|
+
if isinstance(node, str):
|
|
282
|
+
s = node.strip().lower()
|
|
283
|
+
if s in ("1", "true", "yes", "on", "y", "t"):
|
|
284
|
+
return True
|
|
285
|
+
if s in ("0", "false", "no", "off", "n", "f"):
|
|
286
|
+
return False
|
|
287
|
+
raise ConfigurationError(f"Expected bool at {'.'.join(path)}")
|
|
288
|
+
return node
|
|
289
|
+
|