engin 0.0.13__py3-none-any.whl → 0.0.15__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/__init__.py +4 -1
- engin/_assembler.py +63 -55
- engin/_block.py +48 -29
- engin/_cli/__init__.py +13 -0
- engin/{scripts/graph.py → _cli/_graph.py} +48 -31
- engin/_cli/_utils.py +18 -0
- engin/_dependency.py +81 -36
- engin/_engin.py +21 -59
- engin/_introspect.py +34 -0
- engin/_lifecycle.py +68 -2
- engin/_option.py +10 -0
- engin/_type_utils.py +8 -10
- engin/ext/asgi.py +2 -1
- engin/ext/fastapi.py +12 -5
- {engin-0.0.13.dist-info → engin-0.0.15.dist-info}/METADATA +3 -1
- engin-0.0.15.dist-info/RECORD +23 -0
- engin-0.0.15.dist-info/entry_points.txt +2 -0
- engin/scripts/__init__.py +0 -0
- engin-0.0.13.dist-info/RECORD +0 -20
- engin-0.0.13.dist-info/entry_points.txt +0 -2
- {engin-0.0.13.dist-info → engin-0.0.15.dist-info}/WHEEL +0 -0
- {engin-0.0.13.dist-info → engin-0.0.15.dist-info}/licenses/LICENSE +0 -0
engin/__init__.py
CHANGED
@@ -2,9 +2,11 @@ from engin import ext
|
|
2
2
|
from engin._assembler import Assembler
|
3
3
|
from engin._block import Block, invoke, provide
|
4
4
|
from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
5
|
-
from engin._engin import Engin
|
5
|
+
from engin._engin import Engin
|
6
6
|
from engin._exceptions import ProviderError
|
7
7
|
from engin._lifecycle import Lifecycle
|
8
|
+
from engin._option import Option
|
9
|
+
from engin._type_utils import TypeId
|
8
10
|
|
9
11
|
__all__ = [
|
10
12
|
"Assembler",
|
@@ -17,6 +19,7 @@ __all__ = [
|
|
17
19
|
"Provide",
|
18
20
|
"ProviderError",
|
19
21
|
"Supply",
|
22
|
+
"TypeId",
|
20
23
|
"ext",
|
21
24
|
"invoke",
|
22
25
|
"provide",
|
engin/_assembler.py
CHANGED
@@ -1,14 +1,14 @@
|
|
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
5
|
from dataclasses import dataclass
|
6
6
|
from inspect import BoundArguments, Signature
|
7
7
|
from typing import Any, Generic, TypeVar, cast
|
8
8
|
|
9
9
|
from engin._dependency import Dependency, Provide, Supply
|
10
10
|
from engin._exceptions import ProviderError
|
11
|
-
from engin._type_utils import TypeId
|
11
|
+
from engin._type_utils import TypeId
|
12
12
|
|
13
13
|
LOG = logging.getLogger("engin")
|
14
14
|
|
@@ -39,7 +39,7 @@ class Assembler:
|
|
39
39
|
A container for Providers that is responsible for building provided types.
|
40
40
|
|
41
41
|
The Assembler acts as a cache for previously built types, meaning repeat calls
|
42
|
-
to `
|
42
|
+
to `build` will produce the same value.
|
43
43
|
|
44
44
|
Examples:
|
45
45
|
```python
|
@@ -47,15 +47,14 @@ class Assembler:
|
|
47
47
|
return "foo"
|
48
48
|
|
49
49
|
a = Assembler([Provide(build_str)])
|
50
|
-
await a.
|
50
|
+
await a.build(str)
|
51
51
|
```
|
52
52
|
"""
|
53
53
|
|
54
54
|
def __init__(self, providers: Iterable[Provide]) -> None:
|
55
55
|
self._providers: dict[TypeId, Provide[Any]] = {}
|
56
56
|
self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
|
57
|
-
self.
|
58
|
-
self._consumed_providers: set[Provide[Any]] = set()
|
57
|
+
self._assembled_outputs: dict[TypeId, Any] = {}
|
59
58
|
self._lock = asyncio.Lock()
|
60
59
|
|
61
60
|
for provider in providers:
|
@@ -86,17 +85,15 @@ class Assembler:
|
|
86
85
|
bound_args=await self._bind_arguments(dependency.signature),
|
87
86
|
)
|
88
87
|
|
89
|
-
async def
|
88
|
+
async def build(self, type_: type[T]) -> T:
|
90
89
|
"""
|
91
|
-
|
90
|
+
Build the type from Assembler's factories.
|
92
91
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
If the
|
92
|
+
If the type has been built previously the value will be cached and will return the
|
93
|
+
same instance.
|
97
94
|
|
98
95
|
Args:
|
99
|
-
type_: the type of the desired value.
|
96
|
+
type_: the type of the desired value to build.
|
100
97
|
|
101
98
|
Raises:
|
102
99
|
LookupError: When no provider is found for the given type.
|
@@ -106,13 +103,14 @@ class Assembler:
|
|
106
103
|
Returns:
|
107
104
|
The constructed value.
|
108
105
|
"""
|
109
|
-
type_id =
|
110
|
-
if type_id in self.
|
111
|
-
return cast(T, self.
|
106
|
+
type_id = TypeId.from_type(type_)
|
107
|
+
if type_id in self._assembled_outputs:
|
108
|
+
return cast("T", self._assembled_outputs[type_id])
|
112
109
|
if type_id.multi:
|
113
|
-
out = []
|
114
110
|
if type_id not in self._multiproviders:
|
115
111
|
raise LookupError(f"no provider found for target type id '{type_id}'")
|
112
|
+
|
113
|
+
out = []
|
116
114
|
for provider in self._multiproviders[type_id]:
|
117
115
|
assembled_dependency = await self.assemble(provider)
|
118
116
|
try:
|
@@ -123,11 +121,12 @@ class Assembler:
|
|
123
121
|
error_type=type(err),
|
124
122
|
error_message=str(err),
|
125
123
|
) from err
|
126
|
-
self.
|
124
|
+
self._assembled_outputs[type_id] = out
|
127
125
|
return out # type: ignore[return-value]
|
128
126
|
else:
|
129
127
|
if type_id not in self._providers:
|
130
128
|
raise LookupError(f"no provider found for target type id '{type_id}'")
|
129
|
+
|
131
130
|
assembled_dependency = await self.assemble(self._providers[type_id])
|
132
131
|
try:
|
133
132
|
value = await assembled_dependency()
|
@@ -137,7 +136,7 @@ class Assembler:
|
|
137
136
|
error_type=type(err),
|
138
137
|
error_message=str(err),
|
139
138
|
) from err
|
140
|
-
self.
|
139
|
+
self._assembled_outputs[type_id] = value
|
141
140
|
return value # type: ignore[return-value]
|
142
141
|
|
143
142
|
def has(self, type_: type[T]) -> bool:
|
@@ -150,7 +149,7 @@ class Assembler:
|
|
150
149
|
Returns:
|
151
150
|
True if the Assembler has a provider for type else False.
|
152
151
|
"""
|
153
|
-
type_id =
|
152
|
+
type_id = TypeId.from_type(type_)
|
154
153
|
if type_id.multi:
|
155
154
|
return type_id in self._multiproviders
|
156
155
|
else:
|
@@ -160,55 +159,64 @@ class Assembler:
|
|
160
159
|
"""
|
161
160
|
Add a provider to the Assembler post-initialisation.
|
162
161
|
|
162
|
+
If this replaces an existing provider, this will clear any previously assembled
|
163
|
+
output for the existing Provider.
|
164
|
+
|
163
165
|
Args:
|
164
166
|
provider: the Provide instance to add.
|
165
167
|
|
166
168
|
Returns:
|
167
169
|
None
|
168
|
-
|
169
|
-
Raises:
|
170
|
-
ValueError: if a provider for this type already exists.
|
171
170
|
"""
|
172
171
|
type_id = provider.return_type_id
|
173
172
|
if provider.is_multiprovider:
|
174
|
-
if type_id in self.
|
175
|
-
self.
|
176
|
-
|
177
|
-
self._multiproviders[type_id] = [provider]
|
173
|
+
if type_id in self._assembled_outputs:
|
174
|
+
del self._assembled_outputs[type_id]
|
175
|
+
self._multiproviders[type_id].append(provider)
|
178
176
|
else:
|
179
|
-
if type_id in self.
|
180
|
-
|
177
|
+
if type_id in self._assembled_outputs:
|
178
|
+
del self._assembled_outputs[type_id]
|
181
179
|
self._providers[type_id] = provider
|
182
180
|
|
183
|
-
def _resolve_providers(self, type_id: TypeId) ->
|
181
|
+
def _resolve_providers(self, type_id: TypeId) -> Iterable[Provide]:
|
182
|
+
"""
|
183
|
+
Resolves the chain of providers required to satisfy the provider of a given type.
|
184
|
+
Ordering of the return value is very important!
|
185
|
+
|
186
|
+
# TODO: performance optimisation, do not recurse for already satisfied providers?
|
187
|
+
"""
|
184
188
|
if type_id.multi:
|
185
|
-
|
189
|
+
root_providers = self._multiproviders.get(type_id)
|
186
190
|
else:
|
187
|
-
|
188
|
-
|
191
|
+
root_providers = [provider] if (provider := self._providers.get(type_id)) else None
|
192
|
+
|
193
|
+
if not root_providers:
|
189
194
|
if type_id.multi:
|
190
195
|
LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
|
191
|
-
|
196
|
+
root_providers = [(Supply([], as_type=list[type_id.type]))] # type: ignore[name-defined]
|
192
197
|
# store default to prevent the warning appearing multiple times
|
193
|
-
self._multiproviders[type_id] =
|
198
|
+
self._multiproviders[type_id] = root_providers
|
194
199
|
else:
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
200
|
+
available = sorted(str(k) for k in self._providers)
|
201
|
+
msg = f"Missing Provider for type '{type_id}', available: {available}"
|
202
|
+
raise LookupError(msg)
|
203
|
+
|
204
|
+
# providers that must be satisfied to satisfy the root level providers
|
205
|
+
yield from (
|
206
|
+
child_provider
|
207
|
+
for root_provider in root_providers
|
208
|
+
for root_provider_param in root_provider.parameter_types
|
209
|
+
for child_provider in self._resolve_providers(root_provider_param)
|
210
|
+
)
|
211
|
+
yield from root_providers
|
206
212
|
|
207
213
|
async def _satisfy(self, target: TypeId) -> None:
|
208
214
|
for provider in self._resolve_providers(target):
|
209
|
-
if
|
215
|
+
if (
|
216
|
+
not provider.is_multiprovider
|
217
|
+
and provider.return_type_id in self._assembled_outputs
|
218
|
+
):
|
210
219
|
continue
|
211
|
-
self._consumed_providers.add(provider)
|
212
220
|
type_id = provider.return_type_id
|
213
221
|
bound_args = await self._bind_arguments(provider.signature)
|
214
222
|
try:
|
@@ -218,12 +226,12 @@ class Assembler:
|
|
218
226
|
provider=provider, error_type=type(err), error_message=str(err)
|
219
227
|
) from err
|
220
228
|
if provider.is_multiprovider:
|
221
|
-
if type_id in self.
|
222
|
-
self.
|
229
|
+
if type_id in self._assembled_outputs:
|
230
|
+
self._assembled_outputs[type_id].extend(value)
|
223
231
|
else:
|
224
|
-
self.
|
232
|
+
self._assembled_outputs[type_id] = value
|
225
233
|
else:
|
226
|
-
self.
|
234
|
+
self._assembled_outputs[type_id] = value
|
227
235
|
|
228
236
|
async def _bind_arguments(self, signature: Signature) -> BoundArguments:
|
229
237
|
args = []
|
@@ -232,11 +240,11 @@ class Assembler:
|
|
232
240
|
if param_name == "self":
|
233
241
|
args.append(object())
|
234
242
|
continue
|
235
|
-
param_key =
|
236
|
-
has_dependency = param_key in self.
|
243
|
+
param_key = TypeId.from_type(param.annotation)
|
244
|
+
has_dependency = param_key in self._assembled_outputs
|
237
245
|
if not has_dependency:
|
238
246
|
await self._satisfy(param_key)
|
239
|
-
val = self.
|
247
|
+
val = self._assembled_outputs[param_key]
|
240
248
|
if param.kind == param.POSITIONAL_ONLY:
|
241
249
|
args.append(val)
|
242
250
|
else:
|
engin/_block.py
CHANGED
@@ -1,27 +1,48 @@
|
|
1
1
|
import inspect
|
2
|
-
from collections.abc import Iterable,
|
3
|
-
from
|
2
|
+
from collections.abc import Callable, Iterable, Sequence
|
3
|
+
from itertools import chain
|
4
|
+
from typing import TYPE_CHECKING, ClassVar
|
4
5
|
|
5
|
-
from engin._dependency import Func, Invoke, Provide
|
6
|
+
from engin._dependency import Dependency, Func, Invoke, Provide
|
7
|
+
from engin._option import Option
|
6
8
|
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from engin._engin import Engin
|
7
11
|
|
8
|
-
|
12
|
+
|
13
|
+
def provide(
|
14
|
+
func_: Func | None = None, *, override: bool = False
|
15
|
+
) -> Func | Callable[[Func], Func]:
|
9
16
|
"""
|
10
17
|
A decorator for defining a Provider in a Block.
|
11
18
|
"""
|
12
|
-
func._opt = Provide(func) # type: ignore[attr-defined]
|
13
|
-
return func
|
14
19
|
|
20
|
+
def _inner(func: Func) -> Func:
|
21
|
+
func._opt = Provide(func, override=override) # type: ignore[attr-defined]
|
22
|
+
return func
|
23
|
+
|
24
|
+
if func_ is None:
|
25
|
+
return _inner
|
26
|
+
else:
|
27
|
+
return _inner(func_)
|
15
28
|
|
16
|
-
|
29
|
+
|
30
|
+
def invoke(func_: Func | None = None) -> Func | Callable[[Func], Func]:
|
17
31
|
"""
|
18
32
|
A decorator for defining an Invocation in a Block.
|
19
33
|
"""
|
20
|
-
func._opt = Invoke(func) # type: ignore[attr-defined]
|
21
|
-
return func
|
22
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_)
|
23
43
|
|
24
|
-
|
44
|
+
|
45
|
+
class Block(Option):
|
25
46
|
"""
|
26
47
|
A Block is a collection of providers and invocations.
|
27
48
|
|
@@ -48,23 +69,21 @@ class Block(Iterable[Provide | Invoke]):
|
|
48
69
|
```
|
49
70
|
"""
|
50
71
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
72
|
+
name: ClassVar[str | None] = None
|
73
|
+
options: ClassVar[Sequence[Option]] = []
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
def apply(cls, engin: "Engin") -> None:
|
77
|
+
block_name = cls.name or f"{cls.__name__}"
|
78
|
+
for option in chain(cls.options, cls._method_options()):
|
79
|
+
if isinstance(option, Dependency):
|
80
|
+
option._block_name = block_name
|
81
|
+
option.apply(engin)
|
82
|
+
|
83
|
+
@classmethod
|
84
|
+
def _method_options(cls) -> Iterable[Provide | Invoke]:
|
85
|
+
for _, method in inspect.getmembers(cls):
|
86
|
+
if option := getattr(method, "_opt", None):
|
87
|
+
if not isinstance(option, Provide | Invoke):
|
59
88
|
raise RuntimeError("Block option is not an instance of Provide or Invoke")
|
60
|
-
|
61
|
-
self._options.append(opt)
|
62
|
-
for opt in self.options:
|
63
|
-
opt.set_block_name(self._name)
|
64
|
-
|
65
|
-
@property
|
66
|
-
def name(self) -> str:
|
67
|
-
return self._name
|
68
|
-
|
69
|
-
def __iter__(self) -> Iterator[Provide | Invoke]:
|
70
|
-
return iter(self._options)
|
89
|
+
yield option
|
engin/_cli/__init__.py
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
try:
|
2
|
+
import typer
|
3
|
+
except ImportError:
|
4
|
+
raise ImportError(
|
5
|
+
"Unable to import typer, to use the engin cli please install the"
|
6
|
+
" `cli` extra, e.g. pip install engin[cli]"
|
7
|
+
) from None
|
8
|
+
|
9
|
+
from engin._cli._graph import cli as graph_cli
|
10
|
+
|
11
|
+
app = typer.Typer()
|
12
|
+
|
13
|
+
app.add_typer(graph_cli)
|
@@ -1,63 +1,76 @@
|
|
1
|
+
import contextlib
|
1
2
|
import importlib
|
2
3
|
import logging
|
3
4
|
import socketserver
|
4
5
|
import sys
|
5
6
|
import threading
|
6
|
-
from argparse import ArgumentParser
|
7
7
|
from http.server import BaseHTTPRequestHandler
|
8
8
|
from time import sleep
|
9
|
-
from typing import Any
|
9
|
+
from typing import Annotated, Any
|
10
|
+
|
11
|
+
import typer
|
12
|
+
from rich import print
|
10
13
|
|
11
14
|
from engin import Engin, Entrypoint, Invoke
|
15
|
+
from engin._cli._utils import print_error
|
12
16
|
from engin._dependency import Dependency, Provide, Supply
|
13
17
|
from engin.ext.asgi import ASGIEngin
|
14
|
-
|
18
|
+
|
19
|
+
try:
|
20
|
+
from engin.ext.fastapi import APIRouteDependency
|
21
|
+
except ImportError:
|
22
|
+
APIRouteDependency = None # type: ignore[assignment,misc]
|
23
|
+
|
24
|
+
cli = typer.Typer()
|
25
|
+
|
15
26
|
|
16
27
|
# mute logging from importing of files + engin's debug logging.
|
17
28
|
logging.disable()
|
18
29
|
|
19
|
-
args = ArgumentParser(
|
20
|
-
prog="engin-graph",
|
21
|
-
description="Creates a visualisation of your application's dependencies",
|
22
|
-
)
|
23
|
-
args.add_argument(
|
24
|
-
"app",
|
25
|
-
help=(
|
26
|
-
"the import path of your Engin instance, in the form "
|
27
|
-
"'package:application', e.g. 'app.main:engin'"
|
28
|
-
),
|
29
|
-
)
|
30
30
|
|
31
31
|
_APP_ORIGIN = ""
|
32
32
|
|
33
|
-
|
34
|
-
|
33
|
+
_CLI_HELP = {
|
34
|
+
"app": (
|
35
|
+
"The import path of your Engin instance, in the form 'package:application'"
|
36
|
+
", e.g. 'app.main:engin'"
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
|
41
|
+
@cli.command(name="graph")
|
42
|
+
def serve_graph(
|
43
|
+
app: Annotated[
|
44
|
+
str,
|
45
|
+
typer.Argument(help=_CLI_HELP["app"]),
|
46
|
+
],
|
47
|
+
) -> None:
|
48
|
+
"""
|
49
|
+
Creates a visualisation of your application's dependencies.
|
50
|
+
"""
|
35
51
|
# add cwd to path to enable local package imports
|
36
52
|
sys.path.insert(0, "")
|
37
53
|
|
38
|
-
parsed = args.parse_args()
|
39
|
-
|
40
|
-
app = parsed.app
|
41
|
-
|
42
54
|
try:
|
43
55
|
module_name, engin_name = app.split(":", maxsplit=1)
|
44
56
|
except ValueError:
|
45
|
-
|
46
|
-
"Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'"
|
47
|
-
) from None
|
57
|
+
print_error("Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'")
|
48
58
|
|
49
59
|
global _APP_ORIGIN
|
50
60
|
_APP_ORIGIN = module_name.split(".", maxsplit=1)[0]
|
51
61
|
|
52
|
-
|
62
|
+
try:
|
63
|
+
module = importlib.import_module(module_name)
|
64
|
+
except ModuleNotFoundError:
|
65
|
+
print_error(f"unable to find module '{module_name}'")
|
53
66
|
|
54
67
|
try:
|
55
68
|
instance = getattr(module, engin_name)
|
56
|
-
except
|
57
|
-
|
69
|
+
except AttributeError:
|
70
|
+
print_error(f"module '{module_name}' has no attribute '{engin_name}'")
|
58
71
|
|
59
72
|
if not isinstance(instance, Engin):
|
60
|
-
|
73
|
+
print_error(f"'{app}' is not an Engin instance")
|
61
74
|
|
62
75
|
nodes = instance.graph()
|
63
76
|
|
@@ -96,10 +109,14 @@ def serve_graph() -> None:
|
|
96
109
|
server_thread.daemon = True # Daemonize the thread so it exits when the main script exits
|
97
110
|
server_thread.start()
|
98
111
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
112
|
+
with contextlib.suppress(KeyboardInterrupt):
|
113
|
+
wait_for_interrupt()
|
114
|
+
|
115
|
+
print("Exiting the server...")
|
116
|
+
|
117
|
+
|
118
|
+
def wait_for_interrupt() -> None:
|
119
|
+
sleep(10000)
|
103
120
|
|
104
121
|
|
105
122
|
_BLOCK_IDX: dict[str, int] = {}
|
engin/_cli/_utils.py
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Never
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from rich import print
|
5
|
+
from rich.panel import Panel
|
6
|
+
|
7
|
+
|
8
|
+
def print_error(msg: str) -> Never:
|
9
|
+
print(
|
10
|
+
Panel(
|
11
|
+
title="Error",
|
12
|
+
renderable=msg.capitalize(),
|
13
|
+
title_align="left",
|
14
|
+
border_style="red",
|
15
|
+
highlight=True,
|
16
|
+
)
|
17
|
+
)
|
18
|
+
raise typer.Exit(code=1)
|
engin/_dependency.py
CHANGED
@@ -3,8 +3,8 @@ import typing
|
|
3
3
|
from abc import ABC
|
4
4
|
from collections.abc import Awaitable, Callable
|
5
5
|
from inspect import Parameter, Signature, isclass, iscoroutinefunction
|
6
|
-
from types import FrameType
|
7
6
|
from typing import (
|
7
|
+
TYPE_CHECKING,
|
8
8
|
Any,
|
9
9
|
Generic,
|
10
10
|
ParamSpec,
|
@@ -14,7 +14,12 @@ from typing import (
|
|
14
14
|
get_type_hints,
|
15
15
|
)
|
16
16
|
|
17
|
-
from engin.
|
17
|
+
from engin._introspect import get_first_external_frame
|
18
|
+
from engin._option import Option
|
19
|
+
from engin._type_utils import TypeId
|
20
|
+
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from engin._engin import Engin
|
18
23
|
|
19
24
|
P = ParamSpec("P")
|
20
25
|
T = TypeVar("T")
|
@@ -24,23 +29,16 @@ Func: TypeAlias = Callable[P, T]
|
|
24
29
|
def _noop(*args: Any, **kwargs: Any) -> None: ...
|
25
30
|
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
frame = stack.frame
|
30
|
-
while True:
|
31
|
-
if frame.f_globals["__package__"] != "engin" or frame.f_back is None:
|
32
|
-
return frame
|
33
|
-
else:
|
34
|
-
frame = frame.f_back
|
35
|
-
|
36
|
-
|
37
|
-
class Dependency(ABC, Generic[P, T]):
|
38
|
-
def __init__(self, func: Func[P, T], block_name: str | None = None) -> None:
|
32
|
+
class Dependency(ABC, Option, Generic[P, T]):
|
33
|
+
def __init__(self, func: Func[P, T]) -> None:
|
39
34
|
self._func = func
|
40
35
|
self._is_async = iscoroutinefunction(func)
|
41
36
|
self._signature = inspect.signature(self._func)
|
42
|
-
self._block_name =
|
43
|
-
|
37
|
+
self._block_name: str | None = None
|
38
|
+
|
39
|
+
source_frame = get_first_external_frame()
|
40
|
+
self._source_package = cast("str", source_frame.frame.f_globals["__package__"])
|
41
|
+
self._source_frame = cast("str", source_frame.frame.f_globals["__name__"])
|
44
42
|
|
45
43
|
@property
|
46
44
|
def source_module(self) -> str:
|
@@ -50,7 +48,7 @@ class Dependency(ABC, Generic[P, T]):
|
|
50
48
|
Returns:
|
51
49
|
A string, e.g. "examples.fastapi.app"
|
52
50
|
"""
|
53
|
-
return self._source_frame
|
51
|
+
return self._source_frame
|
54
52
|
|
55
53
|
@property
|
56
54
|
def source_package(self) -> str:
|
@@ -60,7 +58,7 @@ class Dependency(ABC, Generic[P, T]):
|
|
60
58
|
Returns:
|
61
59
|
A string, e.g. "engin"
|
62
60
|
"""
|
63
|
-
return self.
|
61
|
+
return self._source_package
|
64
62
|
|
65
63
|
@property
|
66
64
|
def block_name(self) -> str | None:
|
@@ -84,18 +82,15 @@ class Dependency(ABC, Generic[P, T]):
|
|
84
82
|
return []
|
85
83
|
if parameters[0].name == "self":
|
86
84
|
parameters.pop(0)
|
87
|
-
return [
|
85
|
+
return [TypeId.from_type(param.annotation) for param in parameters]
|
88
86
|
|
89
87
|
@property
|
90
88
|
def signature(self) -> Signature:
|
91
89
|
return self._signature
|
92
90
|
|
93
|
-
def set_block_name(self, name: str) -> None:
|
94
|
-
self._block_name = name
|
95
|
-
|
96
91
|
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
97
92
|
if self._is_async:
|
98
|
-
return await cast(Awaitable[T], self._func(*args, **kwargs))
|
93
|
+
return await cast("Awaitable[T]", self._func(*args, **kwargs))
|
99
94
|
else:
|
100
95
|
return self._func(*args, **kwargs)
|
101
96
|
|
@@ -119,8 +114,11 @@ class Invoke(Dependency):
|
|
119
114
|
```
|
120
115
|
"""
|
121
116
|
|
122
|
-
def __init__(self, invocation: Func[P, T]
|
123
|
-
super().__init__(func=invocation
|
117
|
+
def __init__(self, invocation: Func[P, T]) -> None:
|
118
|
+
super().__init__(func=invocation)
|
119
|
+
|
120
|
+
def apply(self, engin: "Engin") -> None:
|
121
|
+
engin._invocations.append(self)
|
124
122
|
|
125
123
|
def __str__(self) -> str:
|
126
124
|
return f"Invoke({self.name})"
|
@@ -133,13 +131,13 @@ class Entrypoint(Invoke):
|
|
133
131
|
Entrypoints are a short hand for no-op Invocations that can be used to
|
134
132
|
"""
|
135
133
|
|
136
|
-
def __init__(self, type_: type[Any]
|
134
|
+
def __init__(self, type_: type[Any]) -> None:
|
137
135
|
self._type = type_
|
138
|
-
super().__init__(invocation=_noop
|
136
|
+
super().__init__(invocation=_noop)
|
139
137
|
|
140
138
|
@property
|
141
139
|
def parameter_types(self) -> list[TypeId]:
|
142
|
-
return [
|
140
|
+
return [TypeId.from_type(self._type)]
|
143
141
|
|
144
142
|
@property
|
145
143
|
def signature(self) -> Signature:
|
@@ -150,12 +148,21 @@ class Entrypoint(Invoke):
|
|
150
148
|
)
|
151
149
|
|
152
150
|
def __str__(self) -> str:
|
153
|
-
return f"Entrypoint({
|
151
|
+
return f"Entrypoint({TypeId.from_type(self._type)})"
|
154
152
|
|
155
153
|
|
156
154
|
class Provide(Dependency[Any, T]):
|
157
|
-
def __init__(self, builder: Func[P, T],
|
158
|
-
|
155
|
+
def __init__(self, builder: Func[P, T], *, override: bool = False) -> None:
|
156
|
+
"""
|
157
|
+
Provide a type via a builder or factory function.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
builder: the builder function that returns the type.
|
161
|
+
override: allow this provider to override existing providers from the same
|
162
|
+
package.
|
163
|
+
"""
|
164
|
+
super().__init__(func=builder)
|
165
|
+
self._override = override
|
159
166
|
self._is_multi = typing.get_origin(self.return_type) is list
|
160
167
|
|
161
168
|
# Validate that the provider does to depend on its own output value, as this will
|
@@ -190,12 +197,36 @@ class Provide(Dependency[Any, T]):
|
|
190
197
|
|
191
198
|
@property
|
192
199
|
def return_type_id(self) -> TypeId:
|
193
|
-
return
|
200
|
+
return TypeId.from_type(self.return_type)
|
194
201
|
|
195
202
|
@property
|
196
203
|
def is_multiprovider(self) -> bool:
|
197
204
|
return self._is_multi
|
198
205
|
|
206
|
+
def apply(self, engin: "Engin") -> None:
|
207
|
+
type_id = self.return_type_id
|
208
|
+
if self.is_multiprovider:
|
209
|
+
engin._multiproviders[type_id].append(self)
|
210
|
+
return
|
211
|
+
|
212
|
+
if type_id not in engin._providers:
|
213
|
+
engin._providers[type_id] = self
|
214
|
+
return
|
215
|
+
|
216
|
+
existing_provider = engin._providers[type_id]
|
217
|
+
is_same_package = existing_provider.source_package == self.source_package
|
218
|
+
|
219
|
+
# overwriting a dependency from the same package must be explicit
|
220
|
+
if is_same_package and not self._override:
|
221
|
+
msg = (
|
222
|
+
f"Provider '{self.name}' is implicitly overriding "
|
223
|
+
f"'{existing_provider.name}', if this is intended specify "
|
224
|
+
"`override=True` for the overriding Provider"
|
225
|
+
)
|
226
|
+
raise RuntimeError(msg)
|
227
|
+
|
228
|
+
engin._providers[type_id] = self
|
229
|
+
|
199
230
|
def __hash__(self) -> int:
|
200
231
|
return hash(self.return_type_id)
|
201
232
|
|
@@ -205,13 +236,27 @@ class Provide(Dependency[Any, T]):
|
|
205
236
|
|
206
237
|
class Supply(Provide, Generic[T]):
|
207
238
|
def __init__(
|
208
|
-
self, value: T, *,
|
239
|
+
self, value: T, *, as_type: type | None = None, override: bool = False
|
209
240
|
) -> None:
|
241
|
+
"""
|
242
|
+
Supply a value.
|
243
|
+
|
244
|
+
This is a shorthand which under the hood creates a Provider with a noop factory
|
245
|
+
function.
|
246
|
+
|
247
|
+
Args:
|
248
|
+
value: the value to Supply
|
249
|
+
as_type: allows you to specify the provided type, useful for type erasing,
|
250
|
+
e.g. Supply a concrete value but specify it as an interface or other
|
251
|
+
abstraction.
|
252
|
+
override: allow this provider to override existing providers from the same
|
253
|
+
package.
|
254
|
+
"""
|
210
255
|
self._value = value
|
211
|
-
self._type_hint =
|
256
|
+
self._type_hint = as_type
|
212
257
|
if self._type_hint is not None:
|
213
|
-
self._get_val.__annotations__["return"] =
|
214
|
-
super().__init__(builder=self._get_val,
|
258
|
+
self._get_val.__annotations__["return"] = as_type
|
259
|
+
super().__init__(builder=self._get_val, override=override)
|
215
260
|
|
216
261
|
@property
|
217
262
|
def return_type(self) -> type[T]:
|
engin/_engin.py
CHANGED
@@ -3,26 +3,21 @@ import logging
|
|
3
3
|
import os
|
4
4
|
import signal
|
5
5
|
from asyncio import Event, Task
|
6
|
-
from collections
|
6
|
+
from collections import defaultdict
|
7
7
|
from contextlib import AsyncExitStack
|
8
8
|
from itertools import chain
|
9
9
|
from types import FrameType
|
10
|
-
from typing import ClassVar
|
10
|
+
from typing import ClassVar
|
11
11
|
|
12
|
-
from engin import Entrypoint
|
13
12
|
from engin._assembler import AssembledDependency, Assembler
|
14
|
-
from engin.
|
15
|
-
from engin._dependency import Dependency, Invoke, Provide, Supply
|
13
|
+
from engin._dependency import Invoke, Provide, Supply
|
16
14
|
from engin._graph import DependencyGrapher, Node
|
17
15
|
from engin._lifecycle import Lifecycle
|
16
|
+
from engin._option import Option
|
18
17
|
from engin._type_utils import TypeId
|
19
18
|
|
20
|
-
LOG = logging.getLogger("engin")
|
21
|
-
|
22
|
-
Option: TypeAlias = Invoke | Provide | Supply | Block
|
23
|
-
_Opt: TypeAlias = Invoke | Provide | Supply
|
24
|
-
|
25
19
|
_OS_IS_WINDOWS = os.name == "nt"
|
20
|
+
LOG = logging.getLogger("engin")
|
26
21
|
|
27
22
|
|
28
23
|
class Engin:
|
@@ -88,12 +83,16 @@ class Engin:
|
|
88
83
|
self._shutdown_task: Task | None = None
|
89
84
|
self._run_task: Task | None = None
|
90
85
|
|
91
|
-
|
92
|
-
|
93
|
-
|
86
|
+
self._providers: dict[TypeId, Provide] = {
|
87
|
+
TypeId.from_type(Engin): Supply(self, as_type=Engin)
|
88
|
+
}
|
89
|
+
self._multiproviders: dict[TypeId, list[Provide]] = defaultdict(list)
|
94
90
|
self._invocations: list[Invoke] = []
|
91
|
+
|
95
92
|
# populates the above
|
96
|
-
|
93
|
+
for option in chain(self._LIB_OPTIONS, options):
|
94
|
+
option.apply(self)
|
95
|
+
|
97
96
|
multi_providers = [p for multi in self._multiproviders.values() for p in multi]
|
98
97
|
self._assembler = Assembler(chain(self._providers.values(), multi_providers))
|
99
98
|
|
@@ -111,7 +110,6 @@ class Engin:
|
|
111
110
|
await self.start()
|
112
111
|
self._run_task = asyncio.create_task(_wait_for_stop_signal(self._stop_requested_event))
|
113
112
|
await self._stop_requested_event.wait()
|
114
|
-
await self._shutdown()
|
115
113
|
|
116
114
|
async def start(self) -> None:
|
117
115
|
"""
|
@@ -134,14 +132,18 @@ class Engin:
|
|
134
132
|
LOG.error(f"invocation '{name}' errored, exiting", exc_info=err)
|
135
133
|
return
|
136
134
|
|
137
|
-
lifecycle = await self._assembler.
|
135
|
+
lifecycle = await self._assembler.build(Lifecycle)
|
138
136
|
|
139
137
|
try:
|
140
138
|
for hook in lifecycle.list():
|
141
|
-
await self._exit_stack.enter_async_context(hook)
|
139
|
+
await asyncio.wait_for(self._exit_stack.enter_async_context(hook), timeout=15)
|
142
140
|
except Exception as err:
|
143
|
-
|
144
|
-
|
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()
|
145
147
|
return
|
146
148
|
|
147
149
|
LOG.info("startup complete")
|
@@ -175,46 +177,6 @@ class Engin:
|
|
175
177
|
await self._stop_requested_event.wait()
|
176
178
|
await self._shutdown()
|
177
179
|
|
178
|
-
def _destruct_options(self, options: Iterable[Option]) -> None:
|
179
|
-
for opt in options:
|
180
|
-
if isinstance(opt, Block):
|
181
|
-
self._destruct_options(opt)
|
182
|
-
if isinstance(opt, Provide | Supply):
|
183
|
-
if not opt.is_multiprovider:
|
184
|
-
existing = self._providers.get(opt.return_type_id)
|
185
|
-
self._log_option(opt, overwrites=existing)
|
186
|
-
self._providers[opt.return_type_id] = opt
|
187
|
-
else:
|
188
|
-
self._log_option(opt)
|
189
|
-
if opt.return_type_id in self._multiproviders:
|
190
|
-
self._multiproviders[opt.return_type_id].append(opt)
|
191
|
-
else:
|
192
|
-
self._multiproviders[opt.return_type_id] = [opt]
|
193
|
-
elif isinstance(opt, Invoke):
|
194
|
-
self._log_option(opt)
|
195
|
-
self._invocations.append(opt)
|
196
|
-
|
197
|
-
@staticmethod
|
198
|
-
def _log_option(opt: Dependency, overwrites: Dependency | None = None) -> None:
|
199
|
-
if overwrites is not None:
|
200
|
-
extra = f"\tOVERWRITES {overwrites.name}"
|
201
|
-
if overwrites.block_name:
|
202
|
-
extra += f" [{overwrites.block_name}]"
|
203
|
-
else:
|
204
|
-
extra = ""
|
205
|
-
if isinstance(opt, Supply):
|
206
|
-
LOG.debug(f"SUPPLY {opt.return_type_id!s:<35}{extra}")
|
207
|
-
elif isinstance(opt, Provide):
|
208
|
-
LOG.debug(f"PROVIDE {opt.return_type_id!s:<35} <- {opt.name}() {extra}")
|
209
|
-
elif isinstance(opt, Entrypoint):
|
210
|
-
type_id = opt.parameter_types[0]
|
211
|
-
LOG.debug(f"ENTRYPOINT {type_id!s:<35}")
|
212
|
-
elif isinstance(opt, Invoke):
|
213
|
-
LOG.debug(f"INVOKE {opt.name:<35}")
|
214
|
-
|
215
|
-
def _self(self) -> "Engin":
|
216
|
-
return self
|
217
|
-
|
218
180
|
|
219
181
|
async def _wait_for_stop_signal(stop_requested_event: Event) -> None:
|
220
182
|
try:
|
engin/_introspect.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
import inspect
|
2
|
+
from collections.abc import Iterable
|
3
|
+
from inspect import FrameInfo
|
4
|
+
|
5
|
+
|
6
|
+
def walk_stack() -> Iterable[FrameInfo]:
|
7
|
+
"""
|
8
|
+
Fast alternative to `inspect.stack()`
|
9
|
+
|
10
|
+
Compared to `inspect.stack()`:
|
11
|
+
- Does not read source files to load neighboring context
|
12
|
+
- Less accurate filename determination, still correct for most cases
|
13
|
+
- Does not compute 3.11+ code positions (PEP 657)
|
14
|
+
"""
|
15
|
+
|
16
|
+
frame = inspect.currentframe()
|
17
|
+
|
18
|
+
while frame := frame and frame.f_back:
|
19
|
+
yield inspect.FrameInfo(
|
20
|
+
frame,
|
21
|
+
inspect.getfile(frame),
|
22
|
+
frame.f_lineno,
|
23
|
+
frame.f_code.co_name,
|
24
|
+
None,
|
25
|
+
None,
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
def get_first_external_frame() -> FrameInfo:
|
30
|
+
for frame_info in walk_stack():
|
31
|
+
frame = frame_info.frame
|
32
|
+
if frame.f_globals["__package__"] != "engin" or frame.f_back is None:
|
33
|
+
return frame_info
|
34
|
+
raise RuntimeError("Unable to find external frame")
|
engin/_lifecycle.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
import asyncio
|
2
2
|
import logging
|
3
|
+
from collections.abc import Awaitable, Callable
|
3
4
|
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
5
|
+
from inspect import iscoroutinefunction
|
4
6
|
from types import TracebackType
|
5
7
|
from typing import TypeAlias, TypeGuard, cast
|
6
8
|
|
7
9
|
LOG = logging.getLogger("engin")
|
8
10
|
|
9
11
|
_AnyContextManager: TypeAlias = AbstractAsyncContextManager | AbstractContextManager
|
12
|
+
_ParameterlessCallable: TypeAlias = Callable[[], None | Awaitable[None]]
|
10
13
|
|
11
14
|
|
12
15
|
class Lifecycle:
|
@@ -44,6 +47,18 @@ class Lifecycle:
|
|
44
47
|
|
45
48
|
lifecycle.append(task)
|
46
49
|
```
|
50
|
+
|
51
|
+
Defining a custom lifecycle using a LifecycleHook.
|
52
|
+
|
53
|
+
```python
|
54
|
+
def my_provider(lifecycle: Lifecycle) -> str:
|
55
|
+
connection_pool = ConnectionPool()
|
56
|
+
|
57
|
+
lifecycle.hook(
|
58
|
+
on_start=connection_pool.connect,
|
59
|
+
on_stop=connection_pool.close,
|
60
|
+
)
|
61
|
+
```
|
47
62
|
"""
|
48
63
|
|
49
64
|
def __init__(self) -> None:
|
@@ -59,6 +74,25 @@ class Lifecycle:
|
|
59
74
|
suppressed_cm = _AExitSuppressingAsyncContextManager(cm)
|
60
75
|
self._context_managers.append(suppressed_cm)
|
61
76
|
|
77
|
+
def hook(
|
78
|
+
self,
|
79
|
+
*,
|
80
|
+
on_start: _ParameterlessCallable | None = None,
|
81
|
+
on_stop: _ParameterlessCallable | None = None,
|
82
|
+
) -> None:
|
83
|
+
"""
|
84
|
+
Append a hook to the Lifecycle.
|
85
|
+
|
86
|
+
At least one of `on_start` or `on_stop` must be provided.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
on_start: a callable to be executed on Lifecycle startup.
|
90
|
+
on_stop: a callable to be executed on Lifecycle shutdown.
|
91
|
+
"""
|
92
|
+
if on_start is None and on_stop is None:
|
93
|
+
raise ValueError("At least one of on_start or on_stop must be provided")
|
94
|
+
self.append(LifecycleHook(on_start=on_start, on_stop=on_stop))
|
95
|
+
|
62
96
|
def list(self) -> list[AbstractAsyncContextManager]:
|
63
97
|
"""
|
64
98
|
List all the defined tasks.
|
@@ -69,6 +103,38 @@ class Lifecycle:
|
|
69
103
|
return self._context_managers[:]
|
70
104
|
|
71
105
|
|
106
|
+
class LifecycleHook(AbstractAsyncContextManager):
|
107
|
+
def __init__(
|
108
|
+
self,
|
109
|
+
on_start: _ParameterlessCallable | None = None,
|
110
|
+
on_stop: _ParameterlessCallable | None = None,
|
111
|
+
) -> None:
|
112
|
+
self._on_start = on_start
|
113
|
+
self._on_stop = on_stop
|
114
|
+
|
115
|
+
async def __aenter__(self) -> None:
|
116
|
+
if self._on_start is not None:
|
117
|
+
func = self._on_start
|
118
|
+
if iscoroutinefunction(func):
|
119
|
+
await func()
|
120
|
+
else:
|
121
|
+
await asyncio.to_thread(func)
|
122
|
+
|
123
|
+
async def __aexit__(
|
124
|
+
self,
|
125
|
+
exc_type: type[BaseException] | None,
|
126
|
+
exc_value: BaseException | None,
|
127
|
+
traceback: TracebackType | None,
|
128
|
+
/,
|
129
|
+
) -> None:
|
130
|
+
if self._on_stop is not None:
|
131
|
+
func = self._on_stop
|
132
|
+
if iscoroutinefunction(func):
|
133
|
+
await func()
|
134
|
+
else:
|
135
|
+
await asyncio.to_thread(func)
|
136
|
+
|
137
|
+
|
72
138
|
class _AExitSuppressingAsyncContextManager(AbstractAsyncContextManager):
|
73
139
|
def __init__(self, cm: _AnyContextManager) -> None:
|
74
140
|
self._cm = cm
|
@@ -77,7 +143,7 @@ class _AExitSuppressingAsyncContextManager(AbstractAsyncContextManager):
|
|
77
143
|
if self._is_async_cm(self._cm):
|
78
144
|
await self._cm.__aenter__()
|
79
145
|
else:
|
80
|
-
await asyncio.to_thread(cast(AbstractContextManager, self._cm).__enter__)
|
146
|
+
await asyncio.to_thread(cast("AbstractContextManager", self._cm).__enter__)
|
81
147
|
|
82
148
|
async def __aexit__(
|
83
149
|
self,
|
@@ -91,7 +157,7 @@ class _AExitSuppressingAsyncContextManager(AbstractAsyncContextManager):
|
|
91
157
|
await self._cm.__aexit__(exc_type, exc_value, traceback)
|
92
158
|
else:
|
93
159
|
await asyncio.to_thread(
|
94
|
-
cast(AbstractContextManager, self._cm).__exit__,
|
160
|
+
cast("AbstractContextManager", self._cm).__exit__,
|
95
161
|
exc_type,
|
96
162
|
exc_value,
|
97
163
|
traceback,
|
engin/_option.py
ADDED
engin/_type_utils.py
CHANGED
@@ -6,7 +6,7 @@ from typing import Any
|
|
6
6
|
_implict_modules = ["builtins", "typing", "collections.abc", "types"]
|
7
7
|
|
8
8
|
|
9
|
-
@dataclass(frozen=True,
|
9
|
+
@dataclass(frozen=True, slots=True)
|
10
10
|
class TypeId:
|
11
11
|
"""
|
12
12
|
Represents information about a Type in the Dependency Injection framework.
|
@@ -26,7 +26,7 @@ class TypeId:
|
|
26
26
|
Returns:
|
27
27
|
The corresponding TypeId for that type.
|
28
28
|
"""
|
29
|
-
if
|
29
|
+
if _is_multi_type(type_):
|
30
30
|
inner_obj = typing.get_args(type_)[0]
|
31
31
|
return TypeId(type=inner_obj, multi=True)
|
32
32
|
else:
|
@@ -40,6 +40,11 @@ class TypeId:
|
|
40
40
|
out += "[]"
|
41
41
|
return out
|
42
42
|
|
43
|
+
def __eq__(self, other: Any) -> bool:
|
44
|
+
if not isinstance(other, TypeId):
|
45
|
+
return False
|
46
|
+
return self.type == other.type and self.multi == other.multi
|
47
|
+
|
43
48
|
|
44
49
|
def _args_to_str(type_: Any) -> str:
|
45
50
|
args = typing.get_args(type_)
|
@@ -65,14 +70,7 @@ def _args_to_str(type_: Any) -> str:
|
|
65
70
|
return arg_str
|
66
71
|
|
67
72
|
|
68
|
-
def
|
69
|
-
"""
|
70
|
-
Generates a string TypeId for any type.
|
71
|
-
"""
|
72
|
-
return TypeId.from_type(type_)
|
73
|
-
|
74
|
-
|
75
|
-
def is_multi_type(type_: Any) -> bool:
|
73
|
+
def _is_multi_type(type_: Any) -> bool:
|
76
74
|
"""
|
77
75
|
Discriminates a type to determine whether it is the return type of a multiprovider.
|
78
76
|
"""
|
engin/ext/asgi.py
CHANGED
@@ -42,6 +42,7 @@ class ASGIEngin(Engin, ASGIType):
|
|
42
42
|
except Exception as err:
|
43
43
|
exc = "".join(traceback.format_exception(err))
|
44
44
|
await send({"type": "lifespan.startup.failed", "message": exc})
|
45
|
+
raise
|
45
46
|
|
46
47
|
elif message["type"] == "lifespan.shutdown":
|
47
48
|
await self.stop()
|
@@ -49,8 +50,8 @@ class ASGIEngin(Engin, ASGIType):
|
|
49
50
|
await self._asgi_app(scope, receive, send)
|
50
51
|
|
51
52
|
async def _startup(self) -> None:
|
53
|
+
self._asgi_app = await self._assembler.build(self._asgi_type)
|
52
54
|
await self.start()
|
53
|
-
self._asgi_app = await self._assembler.get(self._asgi_type)
|
54
55
|
|
55
56
|
def graph(self) -> list[Node]:
|
56
57
|
grapher = DependencyGrapher({**self._providers, **self._multiproviders})
|
engin/ext/fastapi.py
CHANGED
@@ -9,7 +9,7 @@ from fastapi.routing import APIRoute
|
|
9
9
|
from engin import Assembler, Engin, Entrypoint, Invoke, Option
|
10
10
|
from engin._dependency import Dependency, Supply, _noop
|
11
11
|
from engin._graph import DependencyGrapher, Node
|
12
|
-
from engin._type_utils import TypeId
|
12
|
+
from engin._type_utils import TypeId
|
13
13
|
from engin.ext.asgi import ASGIEngin
|
14
14
|
|
15
15
|
try:
|
@@ -33,7 +33,10 @@ def _attach_assembler(app: FastAPI, engin: Engin) -> None:
|
|
33
33
|
|
34
34
|
|
35
35
|
class FastAPIEngin(ASGIEngin):
|
36
|
-
_LIB_OPTIONS: ClassVar[list[Option]] = [
|
36
|
+
_LIB_OPTIONS: ClassVar[list[Option]] = [
|
37
|
+
*ASGIEngin._LIB_OPTIONS,
|
38
|
+
Invoke(_attach_assembler),
|
39
|
+
]
|
37
40
|
_asgi_type = FastAPI
|
38
41
|
|
39
42
|
def graph(self) -> list[Node]:
|
@@ -55,7 +58,7 @@ def Inject(interface: type[T]) -> Depends:
|
|
55
58
|
assembler: Assembler = conn.app.state.assembler
|
56
59
|
except AttributeError:
|
57
60
|
raise RuntimeError("Assembler is not attached to Application state") from None
|
58
|
-
return await assembler.
|
61
|
+
return await assembler.build(interface)
|
59
62
|
|
60
63
|
dep = Depends(inner)
|
61
64
|
dep.__engin__ = True # type: ignore[attr-defined]
|
@@ -140,7 +143,8 @@ class APIRouteDependency(Dependency):
|
|
140
143
|
"""
|
141
144
|
Warning: this should never be constructed in application code.
|
142
145
|
"""
|
143
|
-
super().__init__(_noop
|
146
|
+
super().__init__(_noop)
|
147
|
+
self._block_name = wraps.block_name
|
144
148
|
self._wrapped = wraps
|
145
149
|
self._route = route
|
146
150
|
self._signature = inspect.signature(route.endpoint)
|
@@ -165,7 +169,7 @@ class APIRouteDependency(Dependency):
|
|
165
169
|
if parameters[0].name == "self":
|
166
170
|
parameters.pop(0)
|
167
171
|
return [
|
168
|
-
|
172
|
+
TypeId.from_type(typing.get_args(param.annotation)[0])
|
169
173
|
for param in parameters
|
170
174
|
if self._is_injected_param(param)
|
171
175
|
]
|
@@ -183,3 +187,6 @@ class APIRouteDependency(Dependency):
|
|
183
187
|
def name(self) -> str:
|
184
188
|
methods = ",".join(self._route.methods)
|
185
189
|
return f"{methods} {self._route.path}"
|
190
|
+
|
191
|
+
def apply(self, engin: Engin) -> None:
|
192
|
+
raise NotImplementedError("APIRouteDependency is not a real dependency")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: engin
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.15
|
4
4
|
Summary: An async-first modular application framework
|
5
5
|
Project-URL: Homepage, https://github.com/invokermain/engin
|
6
6
|
Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
|
@@ -10,6 +10,8 @@ License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
11
11
|
Keywords: Application Framework,Dependency Injection
|
12
12
|
Requires-Python: >=3.10
|
13
|
+
Provides-Extra: cli
|
14
|
+
Requires-Dist: typer>=0.15; extra == 'cli'
|
13
15
|
Description-Content-Type: text/markdown
|
14
16
|
|
15
17
|
[](https://codecov.io/gh/invokermain/engin)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
engin/__init__.py,sha256=rBTteMLAVKg4TJSaMElJUwz72BA_X7nBTREg-I-bWhA,584
|
2
|
+
engin/_assembler.py,sha256=GpTLW9AmGChnwWWK3SUq5AsxJJ8ukH7yWpemBiH87pw,9294
|
3
|
+
engin/_block.py,sha256=Ypl6ffU52dgrHHgCcPokzfRD2-Lbu9b2wYMCgAZIx4g,2578
|
4
|
+
engin/_dependency.py,sha256=w-MxF6Ju1Rc2umc7pk3bXTlc65NVIs1VEBj8825WEcg,8328
|
5
|
+
engin/_engin.py,sha256=yIpZdeqvm8hv0RxOV0veFuvyu9xQ054JSaeuUWwHdOQ,7380
|
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=6V5Aad37MyGzkCtU5TlDrm0o5C04Un_LLvcomxnAmHY,3196
|
18
|
+
engin/ext/fastapi.py,sha256=e8UV521Mq9Iqr55CT7_jtd51iaIZjWlAacoqFBXsh-k,6356
|
19
|
+
engin-0.0.15.dist-info/METADATA,sha256=qYhQHzJ_YrJEaZ_p4ddZL4OZDOtzWHkQFLimPH_XNDE,2354
|
20
|
+
engin-0.0.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
+
engin-0.0.15.dist-info/entry_points.txt,sha256=sW247zZUMxm0b5UKYvPuqQQljYDtU-j2zK3cu7gHwM0,41
|
22
|
+
engin-0.0.15.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
23
|
+
engin-0.0.15.dist-info/RECORD,,
|
engin/scripts/__init__.py
DELETED
File without changes
|
engin-0.0.13.dist-info/RECORD
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
engin/__init__.py,sha256=yTc8k0HDGMIrxDdEEA90qGD_dExQjVIbXCyaOFRrnMg,508
|
2
|
-
engin/_assembler.py,sha256=1ODW3HenDlIQLetg0LNEPPbsI6HkFnPU_AHzkR9Zxmc,8844
|
3
|
-
engin/_block.py,sha256=0QJtqyP5uTFjXsdVGr4ZONLI2LhfzUKmQGnNQWouB3o,2121
|
4
|
-
engin/_dependency.py,sha256=RWOyGpMFp_5aLq5TP2wLKz0HT5f6CRoLvFyWvVif9jY,6825
|
5
|
-
engin/_engin.py,sha256=MTE4MkLrK45h0Nv7p5H92Kv5URa1nX246B9Pp1JkM3A,9134
|
6
|
-
engin/_exceptions.py,sha256=fsc4pTOIGHUh0x7oZhEXPJUTE268sIhswLoiqXaudiw,635
|
7
|
-
engin/_graph.py,sha256=1pMB0cr--uS0XJycDb1rS_X45RBpoyA6NkKqbeSuz1Q,1628
|
8
|
-
engin/_lifecycle.py,sha256=_jQnGFj4RYXsxMpcXPJQagFOwnoTVh7oSN8oUYoYuW0,3246
|
9
|
-
engin/_type_utils.py,sha256=EGyKZWuE2ZwuMlSgDhM1znF8giaEET1vcVoQcdGxFGQ,2210
|
10
|
-
engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
-
engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
-
engin/ext/asgi.py,sha256=RUxkG03VTlvI6EG19c1nEJY8FnQw6MQwolfJSFnhUFE,3168
|
13
|
-
engin/ext/fastapi.py,sha256=GO3AIZNQ69MtzbWuACffx_6Pp34wC5a5Fi_fIAaQvTg,6186
|
14
|
-
engin/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
15
|
-
engin/scripts/graph.py,sha256=por62FkzcWx72V2Ha9sIoki-o99fe2Ifm1w-mdoHZIQ,5922
|
16
|
-
engin-0.0.13.dist-info/METADATA,sha256=dLFQgnZcD2c_xfKK-Z-Mx_f6_Q0kCta-XqNgqrEqRmo,2291
|
17
|
-
engin-0.0.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
18
|
-
engin-0.0.13.dist-info/entry_points.txt,sha256=Dehk4j5nK6zyuQtgOSRAoLE609V6eLzEp32bjqhO62Q,64
|
19
|
-
engin-0.0.13.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
|
20
|
-
engin-0.0.13.dist-info/RECORD,,
|
File without changes
|
File without changes
|