engin 0.0.14__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/_assembler.py +29 -25
- engin/_block.py +23 -7
- engin/_dependency.py +56 -18
- engin/_engin.py +9 -5
- engin/ext/asgi.py +1 -1
- engin/ext/fastapi.py +3 -2
- {engin-0.0.14.dist-info → engin-0.0.15.dist-info}/METADATA +1 -1
- {engin-0.0.14.dist-info → engin-0.0.15.dist-info}/RECORD +11 -11
- {engin-0.0.14.dist-info → engin-0.0.15.dist-info}/WHEEL +0 -0
- {engin-0.0.14.dist-info → engin-0.0.15.dist-info}/entry_points.txt +0 -0
- {engin-0.0.14.dist-info → engin-0.0.15.dist-info}/licenses/LICENSE +0 -0
engin/_assembler.py
CHANGED
@@ -1,7 +1,7 @@
|
|
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
|
@@ -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,7 +47,7 @@ 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
|
|
@@ -85,17 +85,15 @@ class Assembler:
|
|
85
85
|
bound_args=await self._bind_arguments(dependency.signature),
|
86
86
|
)
|
87
87
|
|
88
|
-
async def
|
88
|
+
async def build(self, type_: type[T]) -> T:
|
89
89
|
"""
|
90
|
-
|
90
|
+
Build the type from Assembler's factories.
|
91
91
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
If the
|
92
|
+
If the type has been built previously the value will be cached and will return the
|
93
|
+
same instance.
|
96
94
|
|
97
95
|
Args:
|
98
|
-
type_: the type of the desired value.
|
96
|
+
type_: the type of the desired value to build.
|
99
97
|
|
100
98
|
Raises:
|
101
99
|
LookupError: When no provider is found for the given type.
|
@@ -180,31 +178,37 @@ class Assembler:
|
|
180
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
200
|
available = sorted(str(k) for k in self._providers)
|
196
201
|
msg = f"Missing Provider for type '{type_id}', available: {available}"
|
197
202
|
raise LookupError(msg)
|
198
203
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
return {*required_providers, *providers}
|
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
|
208
212
|
|
209
213
|
async def _satisfy(self, target: TypeId) -> None:
|
210
214
|
for provider in self._resolve_providers(target):
|
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, *, 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) # 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/_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,17 @@ class Entrypoint(Invoke):
|
|
155
152
|
|
156
153
|
|
157
154
|
class Provide(Dependency[Any, T]):
|
158
|
-
def __init__(self, builder: Func[P, T],
|
159
|
-
|
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
|
160
166
|
self._is_multi = typing.get_origin(self.return_type) is list
|
161
167
|
|
162
168
|
# Validate that the provider does to depend on its own output value, as this will
|
@@ -198,10 +204,28 @@ class Provide(Dependency[Any, T]):
|
|
198
204
|
return self._is_multi
|
199
205
|
|
200
206
|
def apply(self, engin: "Engin") -> None:
|
207
|
+
type_id = self.return_type_id
|
201
208
|
if self.is_multiprovider:
|
202
|
-
engin._multiproviders[
|
203
|
-
|
204
|
-
|
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
|
205
229
|
|
206
230
|
def __hash__(self) -> int:
|
207
231
|
return hash(self.return_type_id)
|
@@ -212,13 +236,27 @@ class Provide(Dependency[Any, T]):
|
|
212
236
|
|
213
237
|
class Supply(Provide, Generic[T]):
|
214
238
|
def __init__(
|
215
|
-
self, value: T, *,
|
239
|
+
self, value: T, *, as_type: type | None = None, override: bool = False
|
216
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
|
+
"""
|
217
255
|
self._value = value
|
218
|
-
self._type_hint =
|
256
|
+
self._type_hint = as_type
|
219
257
|
if self._type_hint is not None:
|
220
|
-
self._get_val.__annotations__["return"] =
|
221
|
-
super().__init__(builder=self._get_val,
|
258
|
+
self._get_val.__annotations__["return"] = as_type
|
259
|
+
super().__init__(builder=self._get_val, override=override)
|
222
260
|
|
223
261
|
@property
|
224
262
|
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/ext/asgi.py
CHANGED
@@ -50,7 +50,7 @@ class ASGIEngin(Engin, ASGIType):
|
|
50
50
|
await self._asgi_app(scope, receive, send)
|
51
51
|
|
52
52
|
async def _startup(self) -> None:
|
53
|
-
self._asgi_app = await self._assembler.
|
53
|
+
self._asgi_app = await self._assembler.build(self._asgi_type)
|
54
54
|
await self.start()
|
55
55
|
|
56
56
|
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)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
engin/__init__.py,sha256=rBTteMLAVKg4TJSaMElJUwz72BA_X7nBTREg-I-bWhA,584
|
2
|
-
engin/_assembler.py,sha256=
|
3
|
-
engin/_block.py,sha256=
|
4
|
-
engin/_dependency.py,sha256=
|
5
|
-
engin/_engin.py,sha256=
|
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
6
|
engin/_exceptions.py,sha256=fsc4pTOIGHUh0x7oZhEXPJUTE268sIhswLoiqXaudiw,635
|
7
7
|
engin/_graph.py,sha256=1pMB0cr--uS0XJycDb1rS_X45RBpoyA6NkKqbeSuz1Q,1628
|
8
8
|
engin/_introspect.py,sha256=VdREX6Lhhga5SnEP9G7mjHkgJR4mpqk_SMnmL2zTcqY,966
|
@@ -14,10 +14,10 @@ engin/_cli/__init__.py,sha256=lp1KiBpcgk_dZU5V9DjgLPwmp0ja444fwLH2CYCscNc,302
|
|
14
14
|
engin/_cli/_graph.py,sha256=1Kj09BnKh5BTmuM4tqaGICS4KVDGNWT4oGFIrUa9xdU,6230
|
15
15
|
engin/_cli/_utils.py,sha256=AQFtLO8qjYRCTQc9A8Z1HVf7eZr8iGWogxbYzsgIkS4,360
|
16
16
|
engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
|
-
engin/ext/asgi.py,sha256=
|
18
|
-
engin/ext/fastapi.py,sha256=
|
19
|
-
engin-0.0.
|
20
|
-
engin-0.0.
|
21
|
-
engin-0.0.
|
22
|
-
engin-0.0.
|
23
|
-
engin-0.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,,
|
File without changes
|
File without changes
|
File without changes
|