engin 0.0.13__py3-none-any.whl → 0.0.14__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 +33 -29
- engin/_block.py +26 -23
- engin/_cli/__init__.py +13 -0
- engin/{scripts/graph.py → _cli/_graph.py} +48 -31
- engin/_cli/_utils.py +18 -0
- engin/_dependency.py +28 -21
- engin/_engin.py +13 -55
- 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 +9 -3
- {engin-0.0.13.dist-info → engin-0.0.14.dist-info}/METADATA +3 -1
- engin-0.0.14.dist-info/RECORD +23 -0
- engin-0.0.14.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.14.dist-info}/WHEEL +0 -0
- {engin-0.0.13.dist-info → engin-0.0.14.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
@@ -8,7 +8,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
|
|
@@ -54,8 +54,7 @@ class Assembler:
|
|
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:
|
@@ -106,13 +105,14 @@ class Assembler:
|
|
106
105
|
Returns:
|
107
106
|
The constructed value.
|
108
107
|
"""
|
109
|
-
type_id =
|
110
|
-
if type_id in self.
|
111
|
-
return cast(T, self.
|
108
|
+
type_id = TypeId.from_type(type_)
|
109
|
+
if type_id in self._assembled_outputs:
|
110
|
+
return cast("T", self._assembled_outputs[type_id])
|
112
111
|
if type_id.multi:
|
113
|
-
out = []
|
114
112
|
if type_id not in self._multiproviders:
|
115
113
|
raise LookupError(f"no provider found for target type id '{type_id}'")
|
114
|
+
|
115
|
+
out = []
|
116
116
|
for provider in self._multiproviders[type_id]:
|
117
117
|
assembled_dependency = await self.assemble(provider)
|
118
118
|
try:
|
@@ -123,11 +123,12 @@ class Assembler:
|
|
123
123
|
error_type=type(err),
|
124
124
|
error_message=str(err),
|
125
125
|
) from err
|
126
|
-
self.
|
126
|
+
self._assembled_outputs[type_id] = out
|
127
127
|
return out # type: ignore[return-value]
|
128
128
|
else:
|
129
129
|
if type_id not in self._providers:
|
130
130
|
raise LookupError(f"no provider found for target type id '{type_id}'")
|
131
|
+
|
131
132
|
assembled_dependency = await self.assemble(self._providers[type_id])
|
132
133
|
try:
|
133
134
|
value = await assembled_dependency()
|
@@ -137,7 +138,7 @@ class Assembler:
|
|
137
138
|
error_type=type(err),
|
138
139
|
error_message=str(err),
|
139
140
|
) from err
|
140
|
-
self.
|
141
|
+
self._assembled_outputs[type_id] = value
|
141
142
|
return value # type: ignore[return-value]
|
142
143
|
|
143
144
|
def has(self, type_: type[T]) -> bool:
|
@@ -150,7 +151,7 @@ class Assembler:
|
|
150
151
|
Returns:
|
151
152
|
True if the Assembler has a provider for type else False.
|
152
153
|
"""
|
153
|
-
type_id =
|
154
|
+
type_id = TypeId.from_type(type_)
|
154
155
|
if type_id.multi:
|
155
156
|
return type_id in self._multiproviders
|
156
157
|
else:
|
@@ -160,24 +161,23 @@ class Assembler:
|
|
160
161
|
"""
|
161
162
|
Add a provider to the Assembler post-initialisation.
|
162
163
|
|
164
|
+
If this replaces an existing provider, this will clear any previously assembled
|
165
|
+
output for the existing Provider.
|
166
|
+
|
163
167
|
Args:
|
164
168
|
provider: the Provide instance to add.
|
165
169
|
|
166
170
|
Returns:
|
167
171
|
None
|
168
|
-
|
169
|
-
Raises:
|
170
|
-
ValueError: if a provider for this type already exists.
|
171
172
|
"""
|
172
173
|
type_id = provider.return_type_id
|
173
174
|
if provider.is_multiprovider:
|
174
|
-
if type_id in self.
|
175
|
-
self.
|
176
|
-
|
177
|
-
self._multiproviders[type_id] = [provider]
|
175
|
+
if type_id in self._assembled_outputs:
|
176
|
+
del self._assembled_outputs[type_id]
|
177
|
+
self._multiproviders[type_id].append(provider)
|
178
178
|
else:
|
179
|
-
if type_id in self.
|
180
|
-
|
179
|
+
if type_id in self._assembled_outputs:
|
180
|
+
del self._assembled_outputs[type_id]
|
181
181
|
self._providers[type_id] = provider
|
182
182
|
|
183
183
|
def _resolve_providers(self, type_id: TypeId) -> Collection[Provide]:
|
@@ -192,7 +192,9 @@ class Assembler:
|
|
192
192
|
# store default to prevent the warning appearing multiple times
|
193
193
|
self._multiproviders[type_id] = providers
|
194
194
|
else:
|
195
|
-
|
195
|
+
available = sorted(str(k) for k in self._providers)
|
196
|
+
msg = f"Missing Provider for type '{type_id}', available: {available}"
|
197
|
+
raise LookupError(msg)
|
196
198
|
|
197
199
|
required_providers: list[Provide[Any]] = []
|
198
200
|
for provider in providers:
|
@@ -206,9 +208,11 @@ class Assembler:
|
|
206
208
|
|
207
209
|
async def _satisfy(self, target: TypeId) -> None:
|
208
210
|
for provider in self._resolve_providers(target):
|
209
|
-
if
|
211
|
+
if (
|
212
|
+
not provider.is_multiprovider
|
213
|
+
and provider.return_type_id in self._assembled_outputs
|
214
|
+
):
|
210
215
|
continue
|
211
|
-
self._consumed_providers.add(provider)
|
212
216
|
type_id = provider.return_type_id
|
213
217
|
bound_args = await self._bind_arguments(provider.signature)
|
214
218
|
try:
|
@@ -218,12 +222,12 @@ class Assembler:
|
|
218
222
|
provider=provider, error_type=type(err), error_message=str(err)
|
219
223
|
) from err
|
220
224
|
if provider.is_multiprovider:
|
221
|
-
if type_id in self.
|
222
|
-
self.
|
225
|
+
if type_id in self._assembled_outputs:
|
226
|
+
self._assembled_outputs[type_id].extend(value)
|
223
227
|
else:
|
224
|
-
self.
|
228
|
+
self._assembled_outputs[type_id] = value
|
225
229
|
else:
|
226
|
-
self.
|
230
|
+
self._assembled_outputs[type_id] = value
|
227
231
|
|
228
232
|
async def _bind_arguments(self, signature: Signature) -> BoundArguments:
|
229
233
|
args = []
|
@@ -232,11 +236,11 @@ class Assembler:
|
|
232
236
|
if param_name == "self":
|
233
237
|
args.append(object())
|
234
238
|
continue
|
235
|
-
param_key =
|
236
|
-
has_dependency = param_key in self.
|
239
|
+
param_key = TypeId.from_type(param.annotation)
|
240
|
+
has_dependency = param_key in self._assembled_outputs
|
237
241
|
if not has_dependency:
|
238
242
|
await self._satisfy(param_key)
|
239
|
-
val = self.
|
243
|
+
val = self._assembled_outputs[param_key]
|
240
244
|
if param.kind == param.POSITIONAL_ONLY:
|
241
245
|
args.append(val)
|
242
246
|
else:
|
engin/_block.py
CHANGED
@@ -1,8 +1,13 @@
|
|
1
1
|
import inspect
|
2
|
-
from collections.abc import Iterable,
|
3
|
-
from
|
2
|
+
from collections.abc import 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
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from engin._engin import Engin
|
6
11
|
|
7
12
|
|
8
13
|
def provide(func: Func) -> Func:
|
@@ -21,7 +26,7 @@ def invoke(func: Func) -> Func:
|
|
21
26
|
return func
|
22
27
|
|
23
28
|
|
24
|
-
class Block(
|
29
|
+
class Block(Option):
|
25
30
|
"""
|
26
31
|
A Block is a collection of providers and invocations.
|
27
32
|
|
@@ -48,23 +53,21 @@ class Block(Iterable[Provide | Invoke]):
|
|
48
53
|
```
|
49
54
|
"""
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
name: ClassVar[str | None] = None
|
57
|
+
options: ClassVar[Sequence[Option]] = []
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
def apply(cls, engin: "Engin") -> None:
|
61
|
+
block_name = cls.name or f"{cls.__name__}"
|
62
|
+
for option in chain(cls.options, cls._method_options()):
|
63
|
+
if isinstance(option, Dependency):
|
64
|
+
option._block_name = block_name
|
65
|
+
option.apply(engin)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def _method_options(cls) -> Iterable[Provide | Invoke]:
|
69
|
+
for _, method in inspect.getmembers(cls):
|
70
|
+
if option := getattr(method, "_opt", None):
|
71
|
+
if not isinstance(option, Provide | Invoke):
|
59
72
|
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)
|
73
|
+
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
|
-
stack = inspect.stack()[1]
|
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]):
|
32
|
+
class Dependency(ABC, Option, Generic[P, T]):
|
38
33
|
def __init__(self, func: Func[P, T], block_name: str | None = None) -> None:
|
39
34
|
self._func = func
|
40
35
|
self._is_async = iscoroutinefunction(func)
|
41
36
|
self._signature = inspect.signature(self._func)
|
42
37
|
self._block_name = block_name
|
43
|
-
|
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,7 +82,7 @@ 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:
|
@@ -95,7 +93,7 @@ class Dependency(ABC, Generic[P, T]):
|
|
95
93
|
|
96
94
|
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
97
95
|
if self._is_async:
|
98
|
-
return await cast(Awaitable[T], self._func(*args, **kwargs))
|
96
|
+
return await cast("Awaitable[T]", self._func(*args, **kwargs))
|
99
97
|
else:
|
100
98
|
return self._func(*args, **kwargs)
|
101
99
|
|
@@ -122,6 +120,9 @@ class Invoke(Dependency):
|
|
122
120
|
def __init__(self, invocation: Func[P, T], block_name: str | None = None) -> None:
|
123
121
|
super().__init__(func=invocation, block_name=block_name)
|
124
122
|
|
123
|
+
def apply(self, engin: "Engin") -> None:
|
124
|
+
engin._invocations.append(self)
|
125
|
+
|
125
126
|
def __str__(self) -> str:
|
126
127
|
return f"Invoke({self.name})"
|
127
128
|
|
@@ -139,7 +140,7 @@ class Entrypoint(Invoke):
|
|
139
140
|
|
140
141
|
@property
|
141
142
|
def parameter_types(self) -> list[TypeId]:
|
142
|
-
return [
|
143
|
+
return [TypeId.from_type(self._type)]
|
143
144
|
|
144
145
|
@property
|
145
146
|
def signature(self) -> Signature:
|
@@ -150,7 +151,7 @@ class Entrypoint(Invoke):
|
|
150
151
|
)
|
151
152
|
|
152
153
|
def __str__(self) -> str:
|
153
|
-
return f"Entrypoint({
|
154
|
+
return f"Entrypoint({TypeId.from_type(self._type)})"
|
154
155
|
|
155
156
|
|
156
157
|
class Provide(Dependency[Any, T]):
|
@@ -190,12 +191,18 @@ class Provide(Dependency[Any, T]):
|
|
190
191
|
|
191
192
|
@property
|
192
193
|
def return_type_id(self) -> TypeId:
|
193
|
-
return
|
194
|
+
return TypeId.from_type(self.return_type)
|
194
195
|
|
195
196
|
@property
|
196
197
|
def is_multiprovider(self) -> bool:
|
197
198
|
return self._is_multi
|
198
199
|
|
200
|
+
def apply(self, engin: "Engin") -> None:
|
201
|
+
if self.is_multiprovider:
|
202
|
+
engin._multiproviders[self.return_type_id].append(self)
|
203
|
+
else:
|
204
|
+
engin._providers[self.return_type_id] = self
|
205
|
+
|
199
206
|
def __hash__(self) -> int:
|
200
207
|
return hash(self.return_type_id)
|
201
208
|
|
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, type_hint=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
|
"""
|
@@ -175,46 +173,6 @@ class Engin:
|
|
175
173
|
await self._stop_requested_event.wait()
|
176
174
|
await self._shutdown()
|
177
175
|
|
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
176
|
|
219
177
|
async def _wait_for_stop_signal(stop_requested_event: Event) -> None:
|
220
178
|
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:
|
52
|
-
await self.start()
|
53
53
|
self._asgi_app = await self._assembler.get(self._asgi_type)
|
54
|
+
await self.start()
|
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]:
|
@@ -165,7 +168,7 @@ class APIRouteDependency(Dependency):
|
|
165
168
|
if parameters[0].name == "self":
|
166
169
|
parameters.pop(0)
|
167
170
|
return [
|
168
|
-
|
171
|
+
TypeId.from_type(typing.get_args(param.annotation)[0])
|
169
172
|
for param in parameters
|
170
173
|
if self._is_injected_param(param)
|
171
174
|
]
|
@@ -183,3 +186,6 @@ class APIRouteDependency(Dependency):
|
|
183
186
|
def name(self) -> str:
|
184
187
|
methods = ",".join(self._route.methods)
|
185
188
|
return f"{methods} {self._route.path}"
|
189
|
+
|
190
|
+
def apply(self, engin: Engin) -> None:
|
191
|
+
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.14
|
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=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,,
|
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
|