engin 0.0.14__py3-none-any.whl → 0.0.16__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.
- engin/_assembler.py +79 -28
- engin/_block.py +23 -7
- engin/_cli/_graph.html +78 -0
- engin/_cli/_graph.py +28 -53
- engin/_dependency.py +64 -18
- engin/_engin.py +9 -5
- engin/_exceptions.py +29 -1
- engin/ext/asgi.py +3 -2
- engin/ext/fastapi.py +3 -2
- {engin-0.0.14.dist-info → engin-0.0.16.dist-info}/METADATA +1 -1
- engin-0.0.16.dist-info/RECORD +24 -0
- engin-0.0.14.dist-info/RECORD +0 -23
- {engin-0.0.14.dist-info → engin-0.0.16.dist-info}/WHEEL +0 -0
- {engin-0.0.14.dist-info → engin-0.0.16.dist-info}/entry_points.txt +0 -0
- {engin-0.0.14.dist-info → engin-0.0.16.dist-info}/licenses/LICENSE +0 -0
engin/_assembler.py
CHANGED
@@ -1,18 +1,27 @@
|
|
1
1
|
import asyncio
|
2
2
|
import logging
|
3
3
|
from collections import defaultdict
|
4
|
-
from collections.abc import
|
4
|
+
from collections.abc import Iterable
|
5
|
+
from contextvars import ContextVar
|
5
6
|
from dataclasses import dataclass
|
6
7
|
from inspect import BoundArguments, Signature
|
8
|
+
from types import TracebackType
|
7
9
|
from typing import Any, Generic, TypeVar, cast
|
8
10
|
|
9
11
|
from engin._dependency import Dependency, Provide, Supply
|
10
|
-
from engin._exceptions import ProviderError
|
12
|
+
from engin._exceptions import NotInScopeError, ProviderError
|
11
13
|
from engin._type_utils import TypeId
|
12
14
|
|
13
15
|
LOG = logging.getLogger("engin")
|
14
16
|
|
15
17
|
T = TypeVar("T")
|
18
|
+
_SCOPE: ContextVar[list[str] | None] = ContextVar("_SCOPE", default=None)
|
19
|
+
|
20
|
+
|
21
|
+
def _get_scope() -> list[str]:
|
22
|
+
if _SCOPE.get() is None:
|
23
|
+
_SCOPE.set([])
|
24
|
+
return cast("list[str]", _SCOPE.get())
|
16
25
|
|
17
26
|
|
18
27
|
@dataclass(slots=True, kw_only=True, frozen=True)
|
@@ -39,7 +48,7 @@ class Assembler:
|
|
39
48
|
A container for Providers that is responsible for building provided types.
|
40
49
|
|
41
50
|
The Assembler acts as a cache for previously built types, meaning repeat calls
|
42
|
-
to `
|
51
|
+
to `build` will produce the same value.
|
43
52
|
|
44
53
|
Examples:
|
45
54
|
```python
|
@@ -47,7 +56,7 @@ class Assembler:
|
|
47
56
|
return "foo"
|
48
57
|
|
49
58
|
a = Assembler([Provide(build_str)])
|
50
|
-
await a.
|
59
|
+
await a.build(str)
|
51
60
|
```
|
52
61
|
"""
|
53
62
|
|
@@ -85,17 +94,15 @@ class Assembler:
|
|
85
94
|
bound_args=await self._bind_arguments(dependency.signature),
|
86
95
|
)
|
87
96
|
|
88
|
-
async def
|
97
|
+
async def build(self, type_: type[T]) -> T:
|
89
98
|
"""
|
90
|
-
|
99
|
+
Build the type from Assembler's factories.
|
91
100
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
If the
|
101
|
+
If the type has been built previously the value will be cached and will return the
|
102
|
+
same instance.
|
96
103
|
|
97
104
|
Args:
|
98
|
-
type_: the type of the desired value.
|
105
|
+
type_: the type of the desired value to build.
|
99
106
|
|
100
107
|
Raises:
|
101
108
|
LookupError: When no provider is found for the given type.
|
@@ -114,6 +121,8 @@ class Assembler:
|
|
114
121
|
|
115
122
|
out = []
|
116
123
|
for provider in self._multiproviders[type_id]:
|
124
|
+
if provider.scope and provider.scope not in _get_scope():
|
125
|
+
raise NotInScopeError(provider=provider, scope_stack=_get_scope())
|
117
126
|
assembled_dependency = await self.assemble(provider)
|
118
127
|
try:
|
119
128
|
out.extend(await assembled_dependency())
|
@@ -129,12 +138,16 @@ class Assembler:
|
|
129
138
|
if type_id not in self._providers:
|
130
139
|
raise LookupError(f"no provider found for target type id '{type_id}'")
|
131
140
|
|
132
|
-
|
141
|
+
provider = self._providers[type_id]
|
142
|
+
if provider.scope and provider.scope not in _get_scope():
|
143
|
+
raise NotInScopeError(provider=provider, scope_stack=_get_scope())
|
144
|
+
|
145
|
+
assembled_dependency = await self.assemble(provider)
|
133
146
|
try:
|
134
147
|
value = await assembled_dependency()
|
135
148
|
except Exception as err:
|
136
149
|
raise ProviderError(
|
137
|
-
provider=
|
150
|
+
provider=provider,
|
138
151
|
error_type=type(err),
|
139
152
|
error_message=str(err),
|
140
153
|
) from err
|
@@ -180,31 +193,45 @@ class Assembler:
|
|
180
193
|
del self._assembled_outputs[type_id]
|
181
194
|
self._providers[type_id] = provider
|
182
195
|
|
183
|
-
def
|
196
|
+
def scope(self, scope: str) -> "_ScopeContextManager":
|
197
|
+
return _ScopeContextManager(scope=scope, assembler=self)
|
198
|
+
|
199
|
+
def _exit_scope(self, scope: str) -> None:
|
200
|
+
for type_id, provider in self._providers.items():
|
201
|
+
if provider.scope == scope:
|
202
|
+
self._assembled_outputs.pop(type_id, None)
|
203
|
+
|
204
|
+
def _resolve_providers(self, type_id: TypeId) -> Iterable[Provide]:
|
205
|
+
"""
|
206
|
+
Resolves the chain of providers required to satisfy the provider of a given type.
|
207
|
+
Ordering of the return value is very important!
|
208
|
+
|
209
|
+
# TODO: performance optimisation, do not recurse for already satisfied providers?
|
210
|
+
"""
|
184
211
|
if type_id.multi:
|
185
|
-
|
212
|
+
root_providers = self._multiproviders.get(type_id)
|
186
213
|
else:
|
187
|
-
|
188
|
-
|
214
|
+
root_providers = [provider] if (provider := self._providers.get(type_id)) else None
|
215
|
+
|
216
|
+
if not root_providers:
|
189
217
|
if type_id.multi:
|
190
218
|
LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
|
191
|
-
|
219
|
+
root_providers = [(Supply([], as_type=list[type_id.type]))] # type: ignore[name-defined]
|
192
220
|
# store default to prevent the warning appearing multiple times
|
193
|
-
self._multiproviders[type_id] =
|
221
|
+
self._multiproviders[type_id] = root_providers
|
194
222
|
else:
|
195
223
|
available = sorted(str(k) for k in self._providers)
|
196
224
|
msg = f"Missing Provider for type '{type_id}', available: {available}"
|
197
225
|
raise LookupError(msg)
|
198
226
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
return {*required_providers, *providers}
|
227
|
+
# providers that must be satisfied to satisfy the root level providers
|
228
|
+
yield from (
|
229
|
+
child_provider
|
230
|
+
for root_provider in root_providers
|
231
|
+
for root_provider_param in root_provider.parameter_types
|
232
|
+
for child_provider in self._resolve_providers(root_provider_param)
|
233
|
+
)
|
234
|
+
yield from root_providers
|
208
235
|
|
209
236
|
async def _satisfy(self, target: TypeId) -> None:
|
210
237
|
for provider in self._resolve_providers(target):
|
@@ -247,3 +274,27 @@ class Assembler:
|
|
247
274
|
kwargs[param.name] = val
|
248
275
|
|
249
276
|
return signature.bind(*args, **kwargs)
|
277
|
+
|
278
|
+
|
279
|
+
class _ScopeContextManager:
|
280
|
+
def __init__(self, scope: str, assembler: Assembler) -> None:
|
281
|
+
self._scope = scope
|
282
|
+
self._assembler = assembler
|
283
|
+
|
284
|
+
def __enter__(self) -> Assembler:
|
285
|
+
_get_scope().append(self._scope)
|
286
|
+
return self._assembler
|
287
|
+
|
288
|
+
def __exit__(
|
289
|
+
self,
|
290
|
+
exc_type: type[BaseException] | None,
|
291
|
+
exc_value: BaseException | None,
|
292
|
+
traceback: TracebackType | None,
|
293
|
+
/,
|
294
|
+
) -> None:
|
295
|
+
popped = _get_scope().pop()
|
296
|
+
if popped != self._scope:
|
297
|
+
raise RuntimeError(
|
298
|
+
f"Exited scope '{popped}' is not the expected scope '{self._scope}'"
|
299
|
+
)
|
300
|
+
self._assembler._exit_scope(self._scope)
|
engin/_block.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import inspect
|
2
|
-
from collections.abc import Iterable, Sequence
|
2
|
+
from collections.abc import Callable, Iterable, Sequence
|
3
3
|
from itertools import chain
|
4
4
|
from typing import TYPE_CHECKING, ClassVar
|
5
5
|
|
@@ -10,20 +10,36 @@ if TYPE_CHECKING:
|
|
10
10
|
from engin._engin import Engin
|
11
11
|
|
12
12
|
|
13
|
-
def provide(
|
13
|
+
def provide(
|
14
|
+
func_: Func | None = None, *, scope: str | None = None, override: bool = False
|
15
|
+
) -> Func | Callable[[Func], Func]:
|
14
16
|
"""
|
15
17
|
A decorator for defining a Provider in a Block.
|
16
18
|
"""
|
17
|
-
func._opt = Provide(func) # type: ignore[attr-defined]
|
18
|
-
return func
|
19
19
|
|
20
|
+
def _inner(func: Func) -> Func:
|
21
|
+
func._opt = Provide(func, override=override, scope=scope) # type: ignore[attr-defined]
|
22
|
+
return func
|
20
23
|
|
21
|
-
|
24
|
+
if func_ is None:
|
25
|
+
return _inner
|
26
|
+
else:
|
27
|
+
return _inner(func_)
|
28
|
+
|
29
|
+
|
30
|
+
def invoke(func_: Func | None = None) -> Func | Callable[[Func], Func]:
|
22
31
|
"""
|
23
32
|
A decorator for defining an Invocation in a Block.
|
24
33
|
"""
|
25
|
-
|
26
|
-
|
34
|
+
|
35
|
+
def _inner(func: Func) -> Func:
|
36
|
+
func._opt = Invoke(func) # type: ignore[attr-defined]
|
37
|
+
return func
|
38
|
+
|
39
|
+
if func_ is None:
|
40
|
+
return _inner
|
41
|
+
else:
|
42
|
+
return _inner(func_)
|
27
43
|
|
28
44
|
|
29
45
|
class Block(Option):
|
engin/_cli/_graph.html
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
<!doctype html>
|
2
|
+
<html lang="en">
|
3
|
+
<style>
|
4
|
+
#mermaid-container {
|
5
|
+
width: 100%;
|
6
|
+
height: 100%;
|
7
|
+
overflow: auto; /* Enables scrolling */
|
8
|
+
border: 1px solid #ddd;
|
9
|
+
cursor: grab;
|
10
|
+
position: relative;
|
11
|
+
white-space: nowrap; /* Prevents wrapping */
|
12
|
+
}
|
13
|
+
|
14
|
+
#mermaid-content {
|
15
|
+
width: max-content; /* Ensures content can expand */
|
16
|
+
height: max-content;
|
17
|
+
}
|
18
|
+
</style>
|
19
|
+
<script type="module">
|
20
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
21
|
+
let config = { flowchart: { useMaxWidth: false, htmlLabels: true, defaultRenderer: "elk" } };
|
22
|
+
mermaid.initialize(config);
|
23
|
+
|
24
|
+
// Drag-to-Move Functionality
|
25
|
+
const container = document.getElementById("mermaid-container");
|
26
|
+
|
27
|
+
let isDragging = false;
|
28
|
+
let startX, startY, scrollLeft, scrollTop;
|
29
|
+
|
30
|
+
container.addEventListener("pointerdown", (e) => {
|
31
|
+
isDragging = true;
|
32
|
+
startX = e.clientX;
|
33
|
+
startY = e.clientY;
|
34
|
+
scrollLeft = container.scrollLeft;
|
35
|
+
scrollTop = container.scrollTop;
|
36
|
+
container.style.cursor = "grabbing";
|
37
|
+
});
|
38
|
+
|
39
|
+
container.addEventListener("pointermove", (e) => {
|
40
|
+
if (!isDragging) return;
|
41
|
+
const x = e.clientX - startX;
|
42
|
+
const y = e.clientY - startY;
|
43
|
+
container.scrollLeft = scrollLeft - x;
|
44
|
+
container.scrollTop = scrollTop - y;
|
45
|
+
});
|
46
|
+
|
47
|
+
container.addEventListener("pointerup", () => {
|
48
|
+
isDragging = false;
|
49
|
+
container.style.cursor = "grab";
|
50
|
+
});
|
51
|
+
|
52
|
+
container.addEventListener("pointerleave", () => {
|
53
|
+
isDragging = false;
|
54
|
+
container.style.cursor = "grab";
|
55
|
+
});
|
56
|
+
</script>
|
57
|
+
<body>
|
58
|
+
<div style="border-style:outset">
|
59
|
+
<p>LEGEND</p>
|
60
|
+
<pre class="mermaid" id="legend">
|
61
|
+
graph LR
|
62
|
+
%%LEGEND%%
|
63
|
+
classDef b0 fill:#7fc97f;
|
64
|
+
classDef external stroke-dasharray: 5 5;
|
65
|
+
</pre>
|
66
|
+
</div>
|
67
|
+
<div id="mermaid-container" style="width: 100%; overflow-x: auto; border: 1px solid #ddd; cursor: grab; position: relative;">
|
68
|
+
<div id="mermaid-content" style="width: max-content; height: max-content;">
|
69
|
+
<pre class="mermaid" id="graph">
|
70
|
+
%%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%
|
71
|
+
graph LR
|
72
|
+
%%DATA%%
|
73
|
+
classDef external stroke-dasharray: 5 5;
|
74
|
+
</pre>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
</body>
|
78
|
+
</html>
|
engin/_cli/_graph.py
CHANGED
@@ -5,13 +5,14 @@ import socketserver
|
|
5
5
|
import sys
|
6
6
|
import threading
|
7
7
|
from http.server import BaseHTTPRequestHandler
|
8
|
+
from pathlib import Path
|
8
9
|
from time import sleep
|
9
10
|
from typing import Annotated, Any
|
10
11
|
|
11
12
|
import typer
|
12
13
|
from rich import print
|
13
14
|
|
14
|
-
from engin import Engin, Entrypoint, Invoke
|
15
|
+
from engin import Engin, Entrypoint, Invoke, TypeId
|
15
16
|
from engin._cli._utils import print_error
|
16
17
|
from engin._dependency import Dependency, Provide, Supply
|
17
18
|
from engin.ext.asgi import ASGIEngin
|
@@ -23,11 +24,9 @@ except ImportError:
|
|
23
24
|
|
24
25
|
cli = typer.Typer()
|
25
26
|
|
26
|
-
|
27
27
|
# mute logging from importing of files + engin's debug logging.
|
28
28
|
logging.disable()
|
29
29
|
|
30
|
-
|
31
30
|
_APP_ORIGIN = ""
|
32
31
|
|
33
32
|
_CLI_HELP = {
|
@@ -79,8 +78,25 @@ def serve_graph(
|
|
79
78
|
f"{_render_node(node.parent)} --> {_render_node(node.node)}"
|
80
79
|
for node in nodes
|
81
80
|
if node.parent is not None
|
81
|
+
and not (node.node.block_name and node.node.block_name == node.parent.block_name)
|
82
82
|
]
|
83
83
|
|
84
|
+
blocks = {node.node.block_name for node in nodes if node.node.block_name is not None}
|
85
|
+
|
86
|
+
# group blocks into subgraphs
|
87
|
+
for block in blocks:
|
88
|
+
dependencies.append(f"subgraph {block}")
|
89
|
+
dependencies.extend(
|
90
|
+
[
|
91
|
+
f"{_render_node(node.parent, False)} --> {_render_node(node.node, False)}"
|
92
|
+
for node in nodes
|
93
|
+
if node.parent is not None
|
94
|
+
and node.node.block_name == block
|
95
|
+
and node.parent.block_name == block
|
96
|
+
]
|
97
|
+
)
|
98
|
+
dependencies.append("end")
|
99
|
+
|
84
100
|
html = (
|
85
101
|
_GRAPH_HTML.replace("%%DATA%%", "\n".join(dependencies))
|
86
102
|
.replace(
|
@@ -123,18 +139,14 @@ _BLOCK_IDX: dict[str, int] = {}
|
|
123
139
|
_SEEN_BLOCKS: list[str] = []
|
124
140
|
|
125
141
|
|
126
|
-
def _render_node(node: Dependency) -> str:
|
142
|
+
def _render_node(node: Dependency, render_block: bool = True) -> str:
|
127
143
|
node_id = id(node)
|
128
144
|
md = ""
|
129
145
|
style = ""
|
130
146
|
|
131
147
|
# format block name
|
132
|
-
if n := node.block_name:
|
148
|
+
if render_block and (n := node.block_name):
|
133
149
|
md += f"_{n}_\n"
|
134
|
-
if n not in _BLOCK_IDX:
|
135
|
-
_BLOCK_IDX[n] = len(_SEEN_BLOCKS) % 8
|
136
|
-
_SEEN_BLOCKS.append(n)
|
137
|
-
style = f"b{_BLOCK_IDX[n]}"
|
138
150
|
|
139
151
|
node_root_package = node.source_package.split(".", maxsplit=1)[0]
|
140
152
|
if node_root_package != _APP_ORIGIN:
|
@@ -147,10 +159,10 @@ def _render_node(node: Dependency) -> str:
|
|
147
159
|
style = f":::{style}"
|
148
160
|
|
149
161
|
if isinstance(node, Supply):
|
150
|
-
md += f"{node.return_type_id}"
|
162
|
+
md += f"{_short_name(node.return_type_id)}"
|
151
163
|
return f'{node_id}("`{md}`"){style}'
|
152
164
|
if isinstance(node, Provide):
|
153
|
-
md += f"{node.return_type_id}"
|
165
|
+
md += f"{_short_name(node.return_type_id)}"
|
154
166
|
return f'{node_id}["`{md}`"]{style}'
|
155
167
|
if isinstance(node, Entrypoint):
|
156
168
|
entrypoint_type = node.parameter_types[0]
|
@@ -166,48 +178,11 @@ def _render_node(node: Dependency) -> str:
|
|
166
178
|
return f'{node_id}["`{node.name}`"]{style}'
|
167
179
|
|
168
180
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
<p>LEGEND</p>
|
175
|
-
<pre class="mermaid">
|
176
|
-
graph LR
|
177
|
-
%%LEGEND%%
|
178
|
-
classDef b0 fill:#7fc97f;
|
179
|
-
classDef external stroke-dasharray: 5 5;
|
180
|
-
</pre>
|
181
|
-
</div>
|
182
|
-
<pre class="mermaid">
|
183
|
-
graph TD
|
184
|
-
%%DATA%%
|
185
|
-
classDef b0 fill:#7fc97f;
|
186
|
-
classDef b1 fill:#beaed4;
|
187
|
-
classDef b2 fill:#fdc086;
|
188
|
-
classDef b3 fill:#ffff99;
|
189
|
-
classDef b4 fill:#386cb0;
|
190
|
-
classDef b5 fill:#f0027f;
|
191
|
-
classDef b6 fill:#bf5b17;
|
192
|
-
classDef b7 fill:#666666;
|
193
|
-
classDef b0E fill:#7fc97f,stroke-dasharray: 5 5;
|
194
|
-
classDef b1E fill:#beaed4,stroke-dasharray: 5 5;
|
195
|
-
classDef b2E fill:#fdc086,stroke-dasharray: 5 5;
|
196
|
-
classDef b3E fill:#ffff99,stroke-dasharray: 5 5;
|
197
|
-
classDef b4E fill:#386cb0,stroke-dasharray: 5 5;
|
198
|
-
classDef b5E fill:#f0027f,stroke-dasharray: 5 5;
|
199
|
-
classDef b6E fill:#bf5b17,stroke-dasharray: 5 5;
|
200
|
-
classDef b7E fill:#666666,stroke-dasharray: 5 5;
|
201
|
-
classDef external stroke-dasharray: 5 5;
|
202
|
-
</pre>
|
203
|
-
<script type="module">
|
204
|
-
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
205
|
-
let config = { flowchart: { useMaxWidth: false, htmlLabels: true } };
|
206
|
-
mermaid.initialize(config);
|
207
|
-
</script>
|
208
|
-
</body>
|
209
|
-
</html>
|
210
|
-
"""
|
181
|
+
def _short_name(name: TypeId) -> str:
|
182
|
+
return str(name).rsplit(".", maxsplit=1)[-1]
|
183
|
+
|
184
|
+
|
185
|
+
_GRAPH_HTML = (Path(__file__).parent / "_graph.html").read_text()
|
211
186
|
|
212
187
|
DEFAULT_LEGEND = (
|
213
188
|
"0[/Invoke/] ~~~ 1[/Entrypoint\\] ~~~ 2[Provide] ~~~ 3(Supply)"
|
engin/_dependency.py
CHANGED
@@ -30,11 +30,11 @@ def _noop(*args: Any, **kwargs: Any) -> None: ...
|
|
30
30
|
|
31
31
|
|
32
32
|
class Dependency(ABC, Option, Generic[P, T]):
|
33
|
-
def __init__(self, func: Func[P, T]
|
33
|
+
def __init__(self, func: Func[P, T]) -> None:
|
34
34
|
self._func = func
|
35
35
|
self._is_async = iscoroutinefunction(func)
|
36
36
|
self._signature = inspect.signature(self._func)
|
37
|
-
self._block_name =
|
37
|
+
self._block_name: str | None = None
|
38
38
|
|
39
39
|
source_frame = get_first_external_frame()
|
40
40
|
self._source_package = cast("str", source_frame.frame.f_globals["__package__"])
|
@@ -88,9 +88,6 @@ class Dependency(ABC, Option, Generic[P, T]):
|
|
88
88
|
def signature(self) -> Signature:
|
89
89
|
return self._signature
|
90
90
|
|
91
|
-
def set_block_name(self, name: str) -> None:
|
92
|
-
self._block_name = name
|
93
|
-
|
94
91
|
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
95
92
|
if self._is_async:
|
96
93
|
return await cast("Awaitable[T]", self._func(*args, **kwargs))
|
@@ -117,8 +114,8 @@ class Invoke(Dependency):
|
|
117
114
|
```
|
118
115
|
"""
|
119
116
|
|
120
|
-
def __init__(self, invocation: Func[P, T]
|
121
|
-
super().__init__(func=invocation
|
117
|
+
def __init__(self, invocation: Func[P, T]) -> None:
|
118
|
+
super().__init__(func=invocation)
|
122
119
|
|
123
120
|
def apply(self, engin: "Engin") -> None:
|
124
121
|
engin._invocations.append(self)
|
@@ -134,9 +131,9 @@ class Entrypoint(Invoke):
|
|
134
131
|
Entrypoints are a short hand for no-op Invocations that can be used to
|
135
132
|
"""
|
136
133
|
|
137
|
-
def __init__(self, type_: type[Any]
|
134
|
+
def __init__(self, type_: type[Any]) -> None:
|
138
135
|
self._type = type_
|
139
|
-
super().__init__(invocation=_noop
|
136
|
+
super().__init__(invocation=_noop)
|
140
137
|
|
141
138
|
@property
|
142
139
|
def parameter_types(self) -> list[TypeId]:
|
@@ -155,8 +152,21 @@ class Entrypoint(Invoke):
|
|
155
152
|
|
156
153
|
|
157
154
|
class Provide(Dependency[Any, T]):
|
158
|
-
def __init__(
|
159
|
-
|
155
|
+
def __init__(
|
156
|
+
self, builder: Func[P, T], *, scope: str | None = None, override: bool = False
|
157
|
+
) -> None:
|
158
|
+
"""
|
159
|
+
Provide a type via a builder or factory function.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
builder: the builder function that returns the type.
|
163
|
+
scope: (optional) associate this provider with a specific scope.
|
164
|
+
override: (optional) allow this provider to override existing providers from
|
165
|
+
the same package.
|
166
|
+
"""
|
167
|
+
super().__init__(func=builder)
|
168
|
+
self._scope = scope
|
169
|
+
self._override = override
|
160
170
|
self._is_multi = typing.get_origin(self.return_type) is list
|
161
171
|
|
162
172
|
# Validate that the provider does to depend on its own output value, as this will
|
@@ -197,11 +207,33 @@ class Provide(Dependency[Any, T]):
|
|
197
207
|
def is_multiprovider(self) -> bool:
|
198
208
|
return self._is_multi
|
199
209
|
|
210
|
+
@property
|
211
|
+
def scope(self) -> str | None:
|
212
|
+
return self._scope
|
213
|
+
|
200
214
|
def apply(self, engin: "Engin") -> None:
|
215
|
+
type_id = self.return_type_id
|
201
216
|
if self.is_multiprovider:
|
202
|
-
engin._multiproviders[
|
203
|
-
|
204
|
-
|
217
|
+
engin._multiproviders[type_id].append(self)
|
218
|
+
return
|
219
|
+
|
220
|
+
if type_id not in engin._providers:
|
221
|
+
engin._providers[type_id] = self
|
222
|
+
return
|
223
|
+
|
224
|
+
existing_provider = engin._providers[type_id]
|
225
|
+
is_same_package = existing_provider.source_package == self.source_package
|
226
|
+
|
227
|
+
# overwriting a dependency from the same package must be explicit
|
228
|
+
if is_same_package and not self._override:
|
229
|
+
msg = (
|
230
|
+
f"Provider '{self.name}' is implicitly overriding "
|
231
|
+
f"'{existing_provider.name}', if this is intended specify "
|
232
|
+
"`override=True` for the overriding Provider"
|
233
|
+
)
|
234
|
+
raise RuntimeError(msg)
|
235
|
+
|
236
|
+
engin._providers[type_id] = self
|
205
237
|
|
206
238
|
def __hash__(self) -> int:
|
207
239
|
return hash(self.return_type_id)
|
@@ -212,13 +244,27 @@ class Provide(Dependency[Any, T]):
|
|
212
244
|
|
213
245
|
class Supply(Provide, Generic[T]):
|
214
246
|
def __init__(
|
215
|
-
self, value: T, *,
|
247
|
+
self, value: T, *, as_type: type | None = None, override: bool = False
|
216
248
|
) -> None:
|
249
|
+
"""
|
250
|
+
Supply a value.
|
251
|
+
|
252
|
+
This is a shorthand which under the hood creates a Provider with a noop factory
|
253
|
+
function.
|
254
|
+
|
255
|
+
Args:
|
256
|
+
value: the value to Supply
|
257
|
+
as_type: allows you to specify the provided type, useful for type erasing,
|
258
|
+
e.g. Supply a concrete value but specify it as an interface or other
|
259
|
+
abstraction.
|
260
|
+
override: allow this provider to override existing providers from the same
|
261
|
+
package.
|
262
|
+
"""
|
217
263
|
self._value = value
|
218
|
-
self._type_hint =
|
264
|
+
self._type_hint = as_type
|
219
265
|
if self._type_hint is not None:
|
220
|
-
self._get_val.__annotations__["return"] =
|
221
|
-
super().__init__(builder=self._get_val,
|
266
|
+
self._get_val.__annotations__["return"] = as_type
|
267
|
+
super().__init__(builder=self._get_val, override=override)
|
222
268
|
|
223
269
|
@property
|
224
270
|
def return_type(self) -> type[T]:
|
engin/_engin.py
CHANGED
@@ -84,7 +84,7 @@ class Engin:
|
|
84
84
|
self._run_task: Task | None = None
|
85
85
|
|
86
86
|
self._providers: dict[TypeId, Provide] = {
|
87
|
-
TypeId.from_type(Engin): Supply(self,
|
87
|
+
TypeId.from_type(Engin): Supply(self, as_type=Engin)
|
88
88
|
}
|
89
89
|
self._multiproviders: dict[TypeId, list[Provide]] = defaultdict(list)
|
90
90
|
self._invocations: list[Invoke] = []
|
@@ -132,14 +132,18 @@ class Engin:
|
|
132
132
|
LOG.error(f"invocation '{name}' errored, exiting", exc_info=err)
|
133
133
|
return
|
134
134
|
|
135
|
-
lifecycle = await self._assembler.
|
135
|
+
lifecycle = await self._assembler.build(Lifecycle)
|
136
136
|
|
137
137
|
try:
|
138
138
|
for hook in lifecycle.list():
|
139
|
-
await self._exit_stack.enter_async_context(hook)
|
139
|
+
await asyncio.wait_for(self._exit_stack.enter_async_context(hook), timeout=15)
|
140
140
|
except Exception as err:
|
141
|
-
|
142
|
-
|
141
|
+
if isinstance(err, TimeoutError):
|
142
|
+
msg = "lifecycle startup task timed out after 15s, exiting"
|
143
|
+
else:
|
144
|
+
msg = "lifecycle startup task errored, exiting"
|
145
|
+
LOG.error(msg, exc_info=err)
|
146
|
+
await self._shutdown()
|
143
147
|
return
|
144
148
|
|
145
149
|
LOG.info("startup complete")
|
engin/_exceptions.py
CHANGED
@@ -3,7 +3,19 @@ from typing import Any
|
|
3
3
|
from engin._dependency import Provide
|
4
4
|
|
5
5
|
|
6
|
-
class
|
6
|
+
class EnginError(Exception):
|
7
|
+
"""
|
8
|
+
Base class for all custom exceptions in the Engin library.
|
9
|
+
"""
|
10
|
+
|
11
|
+
|
12
|
+
class AssemblerError(EnginError):
|
13
|
+
"""
|
14
|
+
Base class for all custom exceptions raised by the Assembler.
|
15
|
+
"""
|
16
|
+
|
17
|
+
|
18
|
+
class ProviderError(AssemblerError):
|
7
19
|
"""
|
8
20
|
Raised when a Provider errors during Assembly.
|
9
21
|
"""
|
@@ -24,3 +36,19 @@ class ProviderError(Exception):
|
|
24
36
|
|
25
37
|
def __str__(self) -> str:
|
26
38
|
return self.message
|
39
|
+
|
40
|
+
|
41
|
+
class NotInScopeError(AssemblerError):
|
42
|
+
"""
|
43
|
+
Raised when a Provider is requested outside of its scope.
|
44
|
+
"""
|
45
|
+
|
46
|
+
def __init__(self, provider: Provide[Any], scope_stack: list[str]) -> None:
|
47
|
+
self.provider = provider
|
48
|
+
self.message = (
|
49
|
+
f"provider '{provider.name}' was requested outside of its specified scope "
|
50
|
+
f"'{provider.scope}', current scope stack is {scope_stack}"
|
51
|
+
)
|
52
|
+
|
53
|
+
def __str__(self) -> str:
|
54
|
+
return self.message
|
engin/ext/asgi.py
CHANGED
@@ -47,10 +47,11 @@ class ASGIEngin(Engin, ASGIType):
|
|
47
47
|
elif message["type"] == "lifespan.shutdown":
|
48
48
|
await self.stop()
|
49
49
|
|
50
|
-
|
50
|
+
with self._assembler.scope("request"):
|
51
|
+
await self._asgi_app(scope, receive, send)
|
51
52
|
|
52
53
|
async def _startup(self) -> None:
|
53
|
-
self._asgi_app = await self._assembler.
|
54
|
+
self._asgi_app = await self._assembler.build(self._asgi_type)
|
54
55
|
await self.start()
|
55
56
|
|
56
57
|
def graph(self) -> list[Node]:
|
engin/ext/fastapi.py
CHANGED
@@ -58,7 +58,7 @@ def Inject(interface: type[T]) -> Depends:
|
|
58
58
|
assembler: Assembler = conn.app.state.assembler
|
59
59
|
except AttributeError:
|
60
60
|
raise RuntimeError("Assembler is not attached to Application state") from None
|
61
|
-
return await assembler.
|
61
|
+
return await assembler.build(interface)
|
62
62
|
|
63
63
|
dep = Depends(inner)
|
64
64
|
dep.__engin__ = True # type: ignore[attr-defined]
|
@@ -143,7 +143,8 @@ class APIRouteDependency(Dependency):
|
|
143
143
|
"""
|
144
144
|
Warning: this should never be constructed in application code.
|
145
145
|
"""
|
146
|
-
super().__init__(_noop
|
146
|
+
super().__init__(_noop)
|
147
|
+
self._block_name = wraps.block_name
|
147
148
|
self._wrapped = wraps
|
148
149
|
self._route = route
|
149
150
|
self._signature = inspect.signature(route.endpoint)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
engin/__init__.py,sha256=rBTteMLAVKg4TJSaMElJUwz72BA_X7nBTREg-I-bWhA,584
|
2
|
+
engin/_assembler.py,sha256=8rt16LGvPpXHtjSdEDQJ6XC6DVwSbr_4_Mcfcfnpf70,10949
|
3
|
+
engin/_block.py,sha256=8ysWrmHkWpTm6bmSc6jZVoO0Ax5Svu1HwxpZwAtIF_o,2617
|
4
|
+
engin/_dependency.py,sha256=KM_d4TEu7NaoOSuIC7lRO7UvPzBFb0sxR74ZbInLMng,8561
|
5
|
+
engin/_engin.py,sha256=yIpZdeqvm8hv0RxOV0veFuvyu9xQ054JSaeuUWwHdOQ,7380
|
6
|
+
engin/_exceptions.py,sha256=UzMppJWDk_Hx3qWAypcPVLw9OYCibqiZjLYeTl22zaE,1355
|
7
|
+
engin/_graph.py,sha256=1pMB0cr--uS0XJycDb1rS_X45RBpoyA6NkKqbeSuz1Q,1628
|
8
|
+
engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
|
9
|
+
engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
|
10
|
+
engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
|
11
|
+
engin/_type_utils.py,sha256=Pmm4m1_WdevT5KTe8tzY_BseNxPyhu_nKsLGgyNcPpo,2247
|
12
|
+
engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
+
engin/_cli/__init__.py,sha256=lp1KiBpcgk_dZU5V9DjgLPwmp0ja444fwLH2CYCscNc,302
|
14
|
+
engin/_cli/_graph.html,sha256=rR5dnDKoz7KtSff0ERCi2UKuoH_Z03MRYiXI_W03G5k,2430
|
15
|
+
engin/_cli/_graph.py,sha256=2v-l5rEC4zm36SWgmzQ2UK-nIHofYpexTo3et55AtE0,5539
|
16
|
+
engin/_cli/_utils.py,sha256=AQFtLO8qjYRCTQc9A8Z1HVf7eZr8iGWogxbYzsgIkS4,360
|
17
|
+
engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
18
|
+
engin/ext/asgi.py,sha256=d5Z6gtMVWDZdAlvrTaMt987sKyiq__A0X4gJQ7IETmA,3247
|
19
|
+
engin/ext/fastapi.py,sha256=e8UV521Mq9Iqr55CT7_jtd51iaIZjWlAacoqFBXsh-k,6356
|
20
|
+
engin-0.0.16.dist-info/METADATA,sha256=1-9KPa3HdnKUM38_OD3yAdNHHTb4-cOBds8dlqedv9s,2354
|
21
|
+
engin-0.0.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
22
|
+
engin-0.0.16.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
|
23
|
+
engin-0.0.16.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
24
|
+
engin-0.0.16.dist-info/RECORD,,
|
engin-0.0.14.dist-info/RECORD
DELETED
@@ -1,23 +0,0 @@
|
|
1
|
-
engin/__init__.py,sha256=rBTteMLAVKg4TJSaMElJUwz72BA_X7nBTREg-I-bWhA,584
|
2
|
-
engin/_assembler.py,sha256=AJHrFKSlZiSNCDOpS-0-C16ns9XFYJUXrUX-phdtjKs,9003
|
3
|
-
engin/_block.py,sha256=qOM3tSULwPEjNDkIERF0PSMe-1_Ea8Ihtv4Z8f94U0Y,2178
|
4
|
-
engin/_dependency.py,sha256=aG-pW0hvW9ERuVQjPJLr0SJ4Ju7kXXS9qUozkHk4q48,7102
|
5
|
-
engin/_engin.py,sha256=GwsR9iQGUIuIt0OeTpi2jr6XtWZfyh4PZUM4fz36axk,7186
|
6
|
-
engin/_exceptions.py,sha256=fsc4pTOIGHUh0x7oZhEXPJUTE268sIhswLoiqXaudiw,635
|
7
|
-
engin/_graph.py,sha256=1pMB0cr--uS0XJycDb1rS_X45RBpoyA6NkKqbeSuz1Q,1628
|
8
|
-
engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
|
9
|
-
engin/_lifecycle.py,sha256=cSWe3euZkmpxmUPFvph2lsTtvuZbxttEfBL-RnOI7lo,5325
|
10
|
-
engin/_option.py,sha256=nZcdrehp1QwgxMUoIpsM0PJuu1q1pbXzhcVsetbsHpc,223
|
11
|
-
engin/_type_utils.py,sha256=Pmm4m1_WdevT5KTe8tzY_BseNxPyhu_nKsLGgyNcPpo,2247
|
12
|
-
engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
|
-
engin/_cli/__init__.py,sha256=lp1KiBpcgk_dZU5V9DjgLPwmp0ja444fwLH2CYCscNc,302
|
14
|
-
engin/_cli/_graph.py,sha256=1Kj09BnKh5BTmuM4tqaGICS4KVDGNWT4oGFIrUa9xdU,6230
|
15
|
-
engin/_cli/_utils.py,sha256=AQFtLO8qjYRCTQc9A8Z1HVf7eZr8iGWogxbYzsgIkS4,360
|
16
|
-
engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
engin/ext/asgi.py,sha256=cpplCnJSKB3yZb-AL6w70CW0SWsRmrR0S6mxfyJI-w8,3194
|
18
|
-
engin/ext/fastapi.py,sha256=Z8pA8hrfcXbVKfDIuSfL94wHzW0E5WLJoYOjEVzuNMk,6328
|
19
|
-
engin-0.0.14.dist-info/METADATA,sha256=VDeCZ_auc2cIoRHPuGf7_Cbarqf3r7_eEOjm72HVHpY,2354
|
20
|
-
engin-0.0.14.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
-
engin-0.0.14.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
|
22
|
-
engin-0.0.14.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
23
|
-
engin-0.0.14.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|