rewire 0.1.0__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.
- rewire-0.1.0/LICENSE +21 -0
- rewire-0.1.0/PKG-INFO +76 -0
- rewire-0.1.0/README.md +30 -0
- rewire-0.1.0/pyproject.toml +30 -0
- rewire-0.1.0/rewire/__init__.py +1 -0
- rewire-0.1.0/rewire/classproperty.py +10 -0
- rewire-0.1.0/rewire/config.py +417 -0
- rewire-0.1.0/rewire/context.py +79 -0
- rewire-0.1.0/rewire/dependencies.py +363 -0
- rewire-0.1.0/rewire/lifecycle.py +165 -0
- rewire-0.1.0/rewire/loader.py +42 -0
- rewire-0.1.0/rewire/log.py +175 -0
- rewire-0.1.0/rewire/plugins.py +187 -0
- rewire-0.1.0/rewire/space.py +115 -0
- rewire-0.1.0/rewire/store.py +31 -0
- rewire-0.1.0/rewire.egg-info/PKG-INFO +76 -0
- rewire-0.1.0/rewire.egg-info/SOURCES.txt +19 -0
- rewire-0.1.0/rewire.egg-info/dependency_links.txt +1 -0
- rewire-0.1.0/rewire.egg-info/requires.txt +11 -0
- rewire-0.1.0/rewire.egg-info/top_level.txt +1 -0
- rewire-0.1.0/setup.cfg +4 -0
rewire-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 rewirepy
|
|
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.
|
rewire-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: rewire
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Streamlined Type-Based Dependency Injection Framework
|
|
5
|
+
Author-email: Ivan Vozhakov <gou177@bk.ru>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 rewirepy
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/rewirepy/example-fastapi
|
|
29
|
+
Keywords: dependency injection,configuration managment
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Classifier: Programming Language :: Python
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Requires-Python: >=3.12
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: pydantic>=2.0.0
|
|
37
|
+
Requires-Dist: anyio>=4.0.0
|
|
38
|
+
Requires-Dist: PyYAML>=6.0.1
|
|
39
|
+
Requires-Dist: loguru<=1.0.0
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: black; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest; extra == "dev"
|
|
43
|
+
Requires-Dist: trio; extra == "dev"
|
|
44
|
+
Requires-Dist: pytest-anyio; extra == "dev"
|
|
45
|
+
Requires-Dist: graphviz; extra == "dev"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Rewire: A Streamlined Type-Based Dependency Injection Framework for Focused Development
|
|
49
|
+
|
|
50
|
+
Rewire is a user-friendly framework dedicated to type-based dependency injection, allowing developers to focus on writing code rather than dealing with boilerplate.
|
|
51
|
+
|
|
52
|
+
## Key Features and Benefits
|
|
53
|
+
|
|
54
|
+
### Dependency Injection
|
|
55
|
+
|
|
56
|
+
Employ Rewire's advanced type-based dependency injection system to automatically manage the instantiation, configuration, and resolution of components in your application. This lets you concentrate on
|
|
57
|
+
developing functionality without being bogged down by dependency management.
|
|
58
|
+
|
|
59
|
+
### Configuration Injection (YAML & Python Functions)
|
|
60
|
+
|
|
61
|
+
Easily load configurations from a YAML file using Rewire's versatile configuration injection mechanism, which supports Python function extensions. Set up complex applications with ease and consistency
|
|
62
|
+
while maintaining flexibility.
|
|
63
|
+
|
|
64
|
+
### Automatic File Import
|
|
65
|
+
|
|
66
|
+
Automatically import required files based on defined configurations within your Rewire-powered application. Keep your project organized by trusting Rewire to handle file imports, promoting code
|
|
67
|
+
reusability and minimizing potential errors.
|
|
68
|
+
|
|
69
|
+
### Lifecycle Manager
|
|
70
|
+
|
|
71
|
+
Easily manage the lifecycles of components with Rewire's robust start/stop manager. Control the initialization and disposal of resources efficiently, ensuring optimal resource usage.
|
|
72
|
+
|
|
73
|
+
### Extensibility & Hackability
|
|
74
|
+
|
|
75
|
+
Rewire is designed to be both extensible and hackable, enabling developers to tailor the framework to their specific needs. Customize components, add new features, or integrate Rewire with other
|
|
76
|
+
libraries as desired, making it a powerful choice for any project.
|
rewire-0.1.0/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
# Rewire: A Streamlined Type-Based Dependency Injection Framework for Focused Development
|
|
3
|
+
|
|
4
|
+
Rewire is a user-friendly framework dedicated to type-based dependency injection, allowing developers to focus on writing code rather than dealing with boilerplate.
|
|
5
|
+
|
|
6
|
+
## Key Features and Benefits
|
|
7
|
+
|
|
8
|
+
### Dependency Injection
|
|
9
|
+
|
|
10
|
+
Employ Rewire's advanced type-based dependency injection system to automatically manage the instantiation, configuration, and resolution of components in your application. This lets you concentrate on
|
|
11
|
+
developing functionality without being bogged down by dependency management.
|
|
12
|
+
|
|
13
|
+
### Configuration Injection (YAML & Python Functions)
|
|
14
|
+
|
|
15
|
+
Easily load configurations from a YAML file using Rewire's versatile configuration injection mechanism, which supports Python function extensions. Set up complex applications with ease and consistency
|
|
16
|
+
while maintaining flexibility.
|
|
17
|
+
|
|
18
|
+
### Automatic File Import
|
|
19
|
+
|
|
20
|
+
Automatically import required files based on defined configurations within your Rewire-powered application. Keep your project organized by trusting Rewire to handle file imports, promoting code
|
|
21
|
+
reusability and minimizing potential errors.
|
|
22
|
+
|
|
23
|
+
### Lifecycle Manager
|
|
24
|
+
|
|
25
|
+
Easily manage the lifecycles of components with Rewire's robust start/stop manager. Control the initialization and disposal of resources efficiently, ensuring optimal resource usage.
|
|
26
|
+
|
|
27
|
+
### Extensibility & Hackability
|
|
28
|
+
|
|
29
|
+
Rewire is designed to be both extensible and hackable, enabling developers to tailor the framework to their specific needs. Customize components, add new features, or integrate Rewire with other
|
|
30
|
+
libraries as desired, making it a powerful choice for any project.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rewire"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Streamlined Type-Based Dependency Injection Framework"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Ivan Vozhakov", email = "gou177@bk.ru" }]
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["dependency injection", "configuration managment"]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"pydantic >= 2.0.0",
|
|
20
|
+
"anyio >= 4.0.0",
|
|
21
|
+
"PyYAML >= 6.0.1",
|
|
22
|
+
"loguru <= 1.0.0",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.12"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["black", "pytest", "trio", "pytest-anyio", "graphviz"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/rewirepy/example-fastapi"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from . import context, dependencies, plugins, config, log, lifecycle
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from functools import lru_cache
|
|
3
|
+
import json
|
|
4
|
+
from os import getenv
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import (
|
|
8
|
+
Annotated,
|
|
9
|
+
Any,
|
|
10
|
+
Callable,
|
|
11
|
+
ClassVar,
|
|
12
|
+
Dict,
|
|
13
|
+
Literal,
|
|
14
|
+
Optional,
|
|
15
|
+
Self,
|
|
16
|
+
Type,
|
|
17
|
+
cast,
|
|
18
|
+
overload,
|
|
19
|
+
)
|
|
20
|
+
from loguru import logger
|
|
21
|
+
from pydantic import BaseModel, Field, TypeAdapter
|
|
22
|
+
import yaml
|
|
23
|
+
from rewire.classproperty import classproperty
|
|
24
|
+
from rewire.context import use_context_value
|
|
25
|
+
import inspect
|
|
26
|
+
from rewire.dependencies import Dependency, TypeRef
|
|
27
|
+
from rewire.space import Module
|
|
28
|
+
|
|
29
|
+
from rewire.store import SimpleStore
|
|
30
|
+
|
|
31
|
+
CONFIG_FILE = getenv("CONFIG_FILE", "./config.yaml")
|
|
32
|
+
|
|
33
|
+
UNSET = object()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
cwdContext = ContextVar("rewire.config.cwdConfig", default="./")
|
|
37
|
+
fileContext = ContextVar("rewire.config.fileContext", default=CONFIG_FILE)
|
|
38
|
+
rootContext = ContextVar("rewire.config.rootContext")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConfigLoader(yaml.SafeLoader):
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EnvRequired(Exception):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PyCode(BaseModel):
|
|
50
|
+
code: str
|
|
51
|
+
cwd: str = Field(default_factory=cwdContext.get)
|
|
52
|
+
file: str = Field(default_factory=fileContext.get)
|
|
53
|
+
|
|
54
|
+
def execute(self):
|
|
55
|
+
return eval(
|
|
56
|
+
self.code,
|
|
57
|
+
{"this": rootContext.get(), "self": rootContext.get()},
|
|
58
|
+
self.functions(),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def functions(self):
|
|
62
|
+
return {"include": self.include, "getenv": getenv}
|
|
63
|
+
|
|
64
|
+
def include(self, file: str):
|
|
65
|
+
with (
|
|
66
|
+
use_context_value(cwdContext, self.cwd),
|
|
67
|
+
use_context_value(fileContext, self.file),
|
|
68
|
+
):
|
|
69
|
+
return load_yaml(file)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class PyExecCode(PyCode):
|
|
73
|
+
def execute(self):
|
|
74
|
+
container = SimpleStore[Callable]()
|
|
75
|
+
|
|
76
|
+
exec(
|
|
77
|
+
"def __pyexec():\n"
|
|
78
|
+
+ "\n".join(f" {x}" for x in self.code.splitlines())
|
|
79
|
+
+ "\n__container.set(__pyexec)",
|
|
80
|
+
{
|
|
81
|
+
"this": rootContext.get(),
|
|
82
|
+
"self": rootContext.get(),
|
|
83
|
+
"__container": container,
|
|
84
|
+
},
|
|
85
|
+
self.functions(),
|
|
86
|
+
)
|
|
87
|
+
fn = container.get()
|
|
88
|
+
assert fn
|
|
89
|
+
return fn()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EvalDict(dict):
|
|
93
|
+
__pydantic_validator__ = None
|
|
94
|
+
|
|
95
|
+
def __getitem__(self, __k):
|
|
96
|
+
if __k not in self:
|
|
97
|
+
raise AttributeError(__k)
|
|
98
|
+
value = super().__getitem__(__k)
|
|
99
|
+
|
|
100
|
+
if isinstance(value, PyCode):
|
|
101
|
+
return value.execute()
|
|
102
|
+
|
|
103
|
+
if isinstance(value, dict):
|
|
104
|
+
return type(self)(value)
|
|
105
|
+
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
def __getattribute__(self, __name: str) -> Any:
|
|
109
|
+
try:
|
|
110
|
+
return super().__getattribute__(__name)
|
|
111
|
+
except AttributeError:
|
|
112
|
+
return self.__getitem__(__name) # type: ignore
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load_yaml_env(loader, node: yaml.ScalarNode):
|
|
116
|
+
value = getenv(*node.value.split(":", 1))
|
|
117
|
+
if value is None:
|
|
118
|
+
raise EnvRequired(f'env variable {node.value.split(":", 1)[0]!r} required')
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load_yaml_include(loader, node: yaml.ScalarNode):
|
|
123
|
+
return load_yaml(node.value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_yaml_py(load, node: yaml.ScalarNode):
|
|
127
|
+
return PyCode(code=node.value)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def load_yaml_pyexec(load, node: yaml.ScalarNode):
|
|
131
|
+
return PyExecCode(code=node.value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load_yaml_yaml(load, node: yaml.ScalarNode):
|
|
135
|
+
return prepare_yaml(yaml.load(node.value, ConfigLoader))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
ConfigLoader.add_constructor("!env", load_yaml_env)
|
|
139
|
+
ConfigLoader.add_constructor("!include", load_yaml_include)
|
|
140
|
+
ConfigLoader.add_constructor("!py", load_yaml_py)
|
|
141
|
+
ConfigLoader.add_constructor("!pyexec", load_yaml_pyexec)
|
|
142
|
+
ConfigLoader.add_constructor("!yaml", load_yaml_yaml)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@overload
|
|
146
|
+
def merge[K, T, KO, TO](
|
|
147
|
+
source: dict[K, T], overlay: dict[KO, TO]
|
|
148
|
+
) -> dict[K | KO, T | TO]:
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@overload
|
|
153
|
+
def merge[T](source: Any, overlay: T) -> T:
|
|
154
|
+
...
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def merge(source: dict | list | Any, overlay: dict | list | Any):
|
|
158
|
+
if isinstance(source, dict | list):
|
|
159
|
+
source = source.copy()
|
|
160
|
+
if isinstance(source, dict) and isinstance(overlay, dict):
|
|
161
|
+
for key, value in overlay.items():
|
|
162
|
+
source[key] = merge(source.get(key, None), value)
|
|
163
|
+
return source
|
|
164
|
+
return overlay
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def render_py(value, root=UNSET):
|
|
168
|
+
if root is UNSET:
|
|
169
|
+
root = value
|
|
170
|
+
if isinstance(value, dict):
|
|
171
|
+
new_value = {k: render_py(v, root=root) for k, v in value.items()}
|
|
172
|
+
value.update(new_value)
|
|
173
|
+
return EvalDict(value)
|
|
174
|
+
if isinstance(value, list):
|
|
175
|
+
new_value = [render_py(v, root=root) for v in value]
|
|
176
|
+
value.clear()
|
|
177
|
+
value.extend(new_value)
|
|
178
|
+
return value
|
|
179
|
+
if isinstance(value, PyCode):
|
|
180
|
+
return render_py(value.execute(), root=root)
|
|
181
|
+
return value
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def load_yaml(file_: str | Path):
|
|
185
|
+
cwd = cwdContext.get().removesuffix("/") + "/"
|
|
186
|
+
file_ = os.path.join(cwd, file_)
|
|
187
|
+
with (
|
|
188
|
+
open(file_) as f,
|
|
189
|
+
use_context_value(fileContext, file_),
|
|
190
|
+
use_context_value(cwdContext, os.path.dirname(file_)),
|
|
191
|
+
):
|
|
192
|
+
value = yaml.load(f, ConfigLoader)
|
|
193
|
+
return prepare_yaml(value)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def prepare_yaml(value):
|
|
197
|
+
with use_context_value(rootContext, None):
|
|
198
|
+
return EvalDict({"d": value}).d
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def set_by_key(key, value, data):
|
|
202
|
+
if key:
|
|
203
|
+
for k in key.split(".")[:-1]:
|
|
204
|
+
data = data.setdefault(k, {})
|
|
205
|
+
data[key.split(".")[-1]] = value
|
|
206
|
+
else:
|
|
207
|
+
data.update(value)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def get_by_key(key, data):
|
|
211
|
+
if key is None:
|
|
212
|
+
return data
|
|
213
|
+
if "." in key:
|
|
214
|
+
key, next_key = key.split(".", 1)
|
|
215
|
+
return get_by_key(next_key, data.get(key, {}))
|
|
216
|
+
return data.get(key, {})
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def merge_env(data: Dict): # /NOSONAR
|
|
220
|
+
remap_config = getenv("CONFIG_REWIRE_ENV_REMAP", {})
|
|
221
|
+
if isinstance(remap_config, str):
|
|
222
|
+
remap_config = json.loads(remap_config)
|
|
223
|
+
assert isinstance(remap_config, dict)
|
|
224
|
+
|
|
225
|
+
for key, value in os.environ.items():
|
|
226
|
+
remapped = key in remap_config
|
|
227
|
+
if remapped:
|
|
228
|
+
key = remap_config[key]
|
|
229
|
+
|
|
230
|
+
if not key.startswith("CONFIG_") and not remapped:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if not remapped:
|
|
234
|
+
key = key.removeprefix("CONFIG_")
|
|
235
|
+
key = key.replace("_", ".").replace("..", "_")
|
|
236
|
+
|
|
237
|
+
if key.endswith("_.JSON") or remapped and key.endswith(":json"):
|
|
238
|
+
key = key.removesuffix("_.JSON").removesuffix(":json")
|
|
239
|
+
value = json.loads(value)
|
|
240
|
+
elif key.endswith("_.YAML") or remapped and key.endswith(":yaml"):
|
|
241
|
+
key = key.removesuffix("_.YAML").removesuffix(":yaml")
|
|
242
|
+
value = yaml.load(value, ConfigLoader)
|
|
243
|
+
value = prepare_yaml(value)
|
|
244
|
+
|
|
245
|
+
set_by_key(key, value, data)
|
|
246
|
+
return data
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@lru_cache()
|
|
250
|
+
def parse_file(file: str | Path, silent: bool = False):
|
|
251
|
+
try:
|
|
252
|
+
raw_config = TypeAdapter(Dict[str, Any]).validate_python(load_yaml(file) or {})
|
|
253
|
+
except FileNotFoundError as e:
|
|
254
|
+
if not silent:
|
|
255
|
+
logger.error(e)
|
|
256
|
+
raw_config = {}
|
|
257
|
+
|
|
258
|
+
raw_config = merge_env(raw_config)
|
|
259
|
+
|
|
260
|
+
with use_context_value(rootContext, raw_config):
|
|
261
|
+
raw_config = EvalDict({"d": raw_config}).d
|
|
262
|
+
|
|
263
|
+
with use_context_value(rootContext, raw_config):
|
|
264
|
+
return render_py(raw_config)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def extract_config_by_path(path: str, model: BaseModel):
|
|
268
|
+
config = model
|
|
269
|
+
|
|
270
|
+
for part in path.split("."):
|
|
271
|
+
config = getattr(config, part, {})
|
|
272
|
+
|
|
273
|
+
return config
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def gen_config_model[TBM: BaseModel](path: str, model: Type[TBM], name: str = "Config"):
|
|
277
|
+
Model = model # /NOSONAR
|
|
278
|
+
|
|
279
|
+
field, _, next_ = path.partition(".")
|
|
280
|
+
if next_:
|
|
281
|
+
Model = gen_config_model(next_, model, f"{field.capitalize()}{name}")
|
|
282
|
+
|
|
283
|
+
store: SimpleStore[TBM] = SimpleStore()
|
|
284
|
+
no_required_args = True
|
|
285
|
+
|
|
286
|
+
for param in inspect.signature(Model).parameters.values(): # type: ignore
|
|
287
|
+
if param.default is inspect._empty:
|
|
288
|
+
no_required_args = False
|
|
289
|
+
|
|
290
|
+
exec(
|
|
291
|
+
f"field = Field(default_factory=Model) if noRequiredArgs else Field(...)\n"
|
|
292
|
+
f"class {name}(BaseModel):\n"
|
|
293
|
+
f" {field}: Model = field\n"
|
|
294
|
+
f"store.set({name})\n",
|
|
295
|
+
{
|
|
296
|
+
"store": store,
|
|
297
|
+
"Model": Model,
|
|
298
|
+
"BaseModel": BaseModel,
|
|
299
|
+
"Field": Field,
|
|
300
|
+
"noRequiredArgs": no_required_args,
|
|
301
|
+
},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
value = store.get()
|
|
305
|
+
if not value:
|
|
306
|
+
raise RuntimeError("evaluation failed")
|
|
307
|
+
|
|
308
|
+
return value
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class ConfigModule(Module):
|
|
312
|
+
config: dict = {}
|
|
313
|
+
config_file: str = CONFIG_FILE
|
|
314
|
+
|
|
315
|
+
def init(self):
|
|
316
|
+
self.config = cast(dict, parse_file(self.config_file)) # type: ignore
|
|
317
|
+
|
|
318
|
+
def patch(self, override: dict):
|
|
319
|
+
self.config = merge(self.config, override)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
class EnvironmentModule(Module):
|
|
323
|
+
env: Literal["production", "dev"] = "production"
|
|
324
|
+
|
|
325
|
+
def init(self):
|
|
326
|
+
config = ConfigModule.get(None)
|
|
327
|
+
if (
|
|
328
|
+
config is not None
|
|
329
|
+
and config.config.get("rewire", {}).get("env", getenv("ENV", None)) == "dev"
|
|
330
|
+
):
|
|
331
|
+
self.env = "dev"
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def current_env(cls):
|
|
335
|
+
self = EnvironmentModule.get(None)
|
|
336
|
+
return "dev" if self is None else self.env
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@overload
|
|
340
|
+
def config[TBM: BaseModel](
|
|
341
|
+
model: Type[TBM], *, path: Optional[str] = ".", fallback: Any = None
|
|
342
|
+
) -> TBM:
|
|
343
|
+
...
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@overload
|
|
347
|
+
def config[TBM: BaseModel](
|
|
348
|
+
*, path: Optional[str] = ".", fallback: Any = None
|
|
349
|
+
) -> Callable[[Type[TBM]], TBM]:
|
|
350
|
+
...
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def config(model: Any = UNSET, *, path: Optional[str] = ".", fallback: Any = None):
|
|
354
|
+
"""fills base model from file fields"""
|
|
355
|
+
|
|
356
|
+
def wrapper[TBM: BaseModel](config: Type[TBM]) -> TBM:
|
|
357
|
+
config_path = update_path(config.__module__, path or ".")
|
|
358
|
+
|
|
359
|
+
data = gen_config_model(config_path, config)
|
|
360
|
+
try:
|
|
361
|
+
raw_config = ConfigModule.get().config
|
|
362
|
+
|
|
363
|
+
return cast(
|
|
364
|
+
config,
|
|
365
|
+
extract_config_by_path(config_path, data.model_validate(raw_config)),
|
|
366
|
+
)
|
|
367
|
+
except LookupError:
|
|
368
|
+
if fallback is None:
|
|
369
|
+
raise
|
|
370
|
+
return config.model_validate(fallback)
|
|
371
|
+
|
|
372
|
+
if model is not UNSET:
|
|
373
|
+
return wrapper(model)
|
|
374
|
+
|
|
375
|
+
return wrapper
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def update_path(config_path: str, post_path: str):
|
|
379
|
+
level = 0
|
|
380
|
+
|
|
381
|
+
while post_path.startswith("."):
|
|
382
|
+
level -= 1
|
|
383
|
+
post_path = post_path[1:]
|
|
384
|
+
|
|
385
|
+
if level == 0:
|
|
386
|
+
return post_path
|
|
387
|
+
|
|
388
|
+
for _ in range(1, -level):
|
|
389
|
+
config_path = ".".join(config_path.split(".")[:-1])
|
|
390
|
+
|
|
391
|
+
if post_path:
|
|
392
|
+
config_path = f"{config_path}.{post_path}".strip(".")
|
|
393
|
+
return config_path
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class ConfigDependency(BaseModel):
|
|
397
|
+
_dependency: ClassVar[Dependency[Self] | None] = None
|
|
398
|
+
__location__: ClassVar[str] = "."
|
|
399
|
+
__fallback__: ClassVar[dict | None] = None
|
|
400
|
+
|
|
401
|
+
@classproperty
|
|
402
|
+
@classmethod
|
|
403
|
+
def dependency(cls) -> Dependency[Self]:
|
|
404
|
+
if cls._dependency is None:
|
|
405
|
+
|
|
406
|
+
async def resolve():
|
|
407
|
+
return config(cls, path=cls.__location__, fallback=cls.__fallback__)
|
|
408
|
+
|
|
409
|
+
cls._dependency = Dependency(
|
|
410
|
+
cb=resolve, type=cls, label=f"Config ({cls.__module__}.{cls.__name__})"
|
|
411
|
+
)
|
|
412
|
+
return cls._dependency
|
|
413
|
+
|
|
414
|
+
@classproperty
|
|
415
|
+
@classmethod
|
|
416
|
+
def Value(cls) -> Type[Self]:
|
|
417
|
+
return Annotated[cls, TypeRef(type=cls)]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from contextvars import ContextVar
|
|
3
|
+
from typing import Type, overload
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
from pydantic import BaseModel, PrivateAttr
|
|
6
|
+
|
|
7
|
+
UNSET = object()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Context[T](BaseModel):
|
|
11
|
+
_ctx: ContextVar[T] | None = PrivateAttr(None)
|
|
12
|
+
name: str | None = None
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def ctx(self):
|
|
16
|
+
if self._ctx is not None:
|
|
17
|
+
return self._ctx
|
|
18
|
+
self._ctx = ContextVar(self.name or str(uuid4()))
|
|
19
|
+
return self._ctx
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def use(self: "ContextVar[T] | Context[T]", value: T):
|
|
23
|
+
if isinstance(self, Context):
|
|
24
|
+
self = self.ctx
|
|
25
|
+
|
|
26
|
+
token = self.set(value)
|
|
27
|
+
try:
|
|
28
|
+
yield value
|
|
29
|
+
finally:
|
|
30
|
+
self.reset(token)
|
|
31
|
+
|
|
32
|
+
@overload
|
|
33
|
+
def get[D](self, default: D) -> T | D:
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@overload
|
|
37
|
+
def get(self) -> T:
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
def get(self, default=UNSET):
|
|
41
|
+
if default is UNSET:
|
|
42
|
+
return self.ctx.get()
|
|
43
|
+
return self.ctx.get(default)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def use_context_value[T](self: ContextVar[T] | Context[T], value: T):
|
|
47
|
+
return Context.use(self, value)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BoundCtx[T](Context[T]):
|
|
51
|
+
_value: T = PrivateAttr()
|
|
52
|
+
|
|
53
|
+
def use(self, value: T | None = None):
|
|
54
|
+
return super().use(value or self._value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CTX(property):
|
|
58
|
+
_context: Context | None = None
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def __get__[T](self, __instance: None, __owner: Type[T]) -> Context[T]:
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
@overload
|
|
65
|
+
def __get__[T](self, __instance: T, __owner: Type[T]) -> BoundCtx[T]:
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
def __get__[T](
|
|
69
|
+
self, __instance: T | None, __owner: Type[T]
|
|
70
|
+
) -> Context[T] | BoundCtx[T]:
|
|
71
|
+
if self._context is None:
|
|
72
|
+
self._context = Context()
|
|
73
|
+
if __instance is not None:
|
|
74
|
+
ctx = BoundCtx()
|
|
75
|
+
ctx._ctx = self._context.ctx
|
|
76
|
+
ctx._value = __instance
|
|
77
|
+
ctx.name = self._context.name
|
|
78
|
+
return ctx
|
|
79
|
+
return self._context
|