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 CHANGED
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import logging
3
3
  from collections import defaultdict
4
- from collections.abc import Collection, Iterable
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 `get` will produce the same value.
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.get(str)
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 get(self, type_: type[T]) -> T:
88
+ async def build(self, type_: type[T]) -> T:
89
89
  """
90
- Return the constructed value for the given type.
90
+ Build the type from Assembler's factories.
91
91
 
92
- This method assembles the required Providers and constructs their corresponding
93
- values.
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) -> Collection[Provide]:
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
- providers = self._multiproviders.get(type_id)
189
+ root_providers = self._multiproviders.get(type_id)
186
190
  else:
187
- providers = [provider] if (provider := self._providers.get(type_id)) else None
188
- if not providers:
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
- providers = [(Supply([], type_hint=list[type_id.type]))] # type: ignore[name-defined]
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] = providers
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
- required_providers: list[Provide[Any]] = []
200
- for provider in providers:
201
- required_providers.extend(
202
- provider
203
- for provider_param in provider.parameter_types
204
- for provider in self._resolve_providers(provider_param)
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(func: Func) -> Func:
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
- def invoke(func: Func) -> Func:
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
- func._opt = Invoke(func) # type: ignore[attr-defined]
26
- return func
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], block_name: str | None = None) -> None:
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 = 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], block_name: str | None = None) -> None:
121
- super().__init__(func=invocation, block_name=block_name)
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], *, block_name: str | None = None) -> None:
134
+ def __init__(self, type_: type[Any]) -> None:
138
135
  self._type = type_
139
- super().__init__(invocation=_noop, block_name=block_name)
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], block_name: str | None = None) -> None:
159
- super().__init__(func=builder, block_name=block_name)
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[self.return_type_id].append(self)
203
- else:
204
- engin._providers[self.return_type_id] = self
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, *, type_hint: type | None = None, block_name: str | None = None
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 = type_hint
256
+ self._type_hint = as_type
219
257
  if self._type_hint is not None:
220
- self._get_val.__annotations__["return"] = type_hint
221
- super().__init__(builder=self._get_val, block_name=block_name)
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, type_hint=Engin)
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.get(Lifecycle)
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
- LOG.error("lifecycle startup error, exiting", exc_info=err)
142
- await self._exit_stack.aclose()
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.get(self._asgi_type)
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.get(interface)
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, wraps.block_name)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.14
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/
@@ -1,8 +1,8 @@
1
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
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=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,,
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