engin 0.0.9__py3-none-any.whl → 0.0.11__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
@@ -152,6 +152,8 @@ class Assembler:
152
152
  if type_id.multi:
153
153
  LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
154
154
  providers = [(Supply([], type_hint=list[type_id.type]))] # type: ignore[name-defined]
155
+ # store default to prevent the warning appearing multiple times
156
+ self._multiproviders[type_id] = providers
155
157
  else:
156
158
  raise LookupError(f"No Provider registered for dependency '{type_id}'")
157
159
 
engin/_dependency.py CHANGED
@@ -3,6 +3,7 @@ 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
6
7
  from typing import (
7
8
  Any,
8
9
  Generic,
@@ -23,22 +24,43 @@ Func: TypeAlias = Callable[P, T]
23
24
  def _noop(*args: Any, **kwargs: Any) -> None: ...
24
25
 
25
26
 
27
+ def _walk_stack() -> FrameType:
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
+
26
37
  class Dependency(ABC, Generic[P, T]):
27
38
  def __init__(self, func: Func[P, T], block_name: str | None = None) -> None:
28
39
  self._func = func
29
40
  self._is_async = iscoroutinefunction(func)
30
41
  self._signature = inspect.signature(self._func)
31
42
  self._block_name = block_name
43
+ self._source_frame = _walk_stack()
32
44
 
33
45
  @property
34
- def origin(self) -> str:
46
+ def source_module(self) -> str:
35
47
  """
36
48
  The module that this Dependency originated from.
37
49
 
38
50
  Returns:
39
51
  A string, e.g. "examples.fastapi.app"
40
52
  """
41
- return self._func.__module__
53
+ return self._source_frame.f_globals["__name__"] # type: ignore[no-any-return]
54
+
55
+ @property
56
+ def source_package(self) -> str:
57
+ """
58
+ The package that this Dependency originated from.
59
+
60
+ Returns:
61
+ A string, e.g. "engin"
62
+ """
63
+ return self._source_frame.f_globals["__package__"] # type: ignore[no-any-return]
42
64
 
43
65
  @property
44
66
  def block_name(self) -> str | None:
@@ -115,10 +137,6 @@ class Entrypoint(Invoke):
115
137
  self._type = type_
116
138
  super().__init__(invocation=_noop, block_name=block_name)
117
139
 
118
- @property
119
- def origin(self) -> str:
120
- return self._type.__module__
121
-
122
140
  @property
123
141
  def parameter_types(self) -> list[TypeId]:
124
142
  return [type_id_of(self._type)]
@@ -186,10 +204,6 @@ class Supply(Provide, Generic[T]):
186
204
  self._get_val.__annotations__["return"] = type_hint
187
205
  super().__init__(builder=self._get_val, block_name=block_name)
188
206
 
189
- @property
190
- def origin(self) -> str:
191
- return self._value.__module__
192
-
193
207
  @property
194
208
  def return_type(self) -> type[T]:
195
209
  if self._type_hint is not None:
engin/_engin.py CHANGED
@@ -39,6 +39,7 @@ class Engin:
39
39
  but for advanced usecases it can be easier to use the `start` and `stop` methods.
40
40
 
41
41
  When ran the Engin takes care of the complete application lifecycle:
42
+
42
43
  1. The Engin assembles all Invocations. Only Providers that are required to satisfy
43
44
  the Invoke options parameters are assembled.
44
45
  2. All Invocations are run sequentially in the order they were passed in to the Engin.
engin/ext/asgi.py CHANGED
@@ -1,10 +1,11 @@
1
1
  import traceback
2
- from collections.abc import Awaitable, Callable, MutableMapping
2
+ from collections.abc import AsyncIterator, Awaitable, Callable, MutableMapping
3
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
3
4
  from typing import Any, ClassVar, Protocol, TypeAlias
4
5
 
5
6
  from engin import Engin, Entrypoint, Option
6
7
 
7
- __all__ = ["ASGIEngin", "ASGIType"]
8
+ __all__ = ["ASGIEngin", "ASGIType", "engin_to_lifespan"]
8
9
 
9
10
  from engin._graph import DependencyGrapher, Node
10
11
 
@@ -61,3 +62,26 @@ class _Rereceive:
61
62
 
62
63
  async def __call__(self, *args: Any, **kwargs: Any) -> _Message:
63
64
  return self._message
65
+
66
+
67
+ def engin_to_lifespan(engin: Engin) -> Callable[[ASGIType], AbstractAsyncContextManager[None]]:
68
+ """
69
+ Transforms the Engin instance into an ASGI lifespan task.
70
+
71
+ This is to enable users to use the Engin framework with existing ASGI applications,
72
+ where it is not desired to replace the ASGI application with an ASGIEngin.
73
+
74
+ Args:
75
+ engin: the engin instance to transform.
76
+
77
+ Returns:
78
+ An ASGI lifespan task.
79
+ """
80
+
81
+ @asynccontextmanager
82
+ async def engin_lifespan(_: ASGIType) -> AsyncIterator[None]:
83
+ await engin.start()
84
+ yield
85
+ await engin.stop()
86
+
87
+ return engin_lifespan
engin/ext/fastapi.py CHANGED
@@ -7,7 +7,7 @@ from typing import ClassVar, TypeVar
7
7
  from fastapi.routing import APIRoute
8
8
 
9
9
  from engin import Engin, Entrypoint, Invoke, Option
10
- from engin._dependency import Dependency, Supply
10
+ from engin._dependency import Dependency, Supply, _noop
11
11
  from engin._graph import DependencyGrapher, Node
12
12
  from engin._type_utils import TypeId, type_id_of
13
13
  from engin.ext.asgi import ASGIEngin
@@ -113,7 +113,7 @@ def _extract_routes_from_supply(supply: Supply) -> list[Dependency]:
113
113
  inner = supply._value[0]
114
114
  if isinstance(inner, APIRouter):
115
115
  return [
116
- APIRouteDependency(route, block_name=supply.block_name)
116
+ APIRouteDependency(supply, route)
117
117
  for route in inner.routes
118
118
  if isinstance(route, APIRoute)
119
119
  ]
@@ -128,13 +128,26 @@ class APIRouteDependency(Dependency):
128
128
  This class should never be constructed in application code.
129
129
  """
130
130
 
131
- def __init__(self, route: APIRoute, block_name: str | None = None) -> None:
131
+ def __init__(
132
+ self,
133
+ wraps: Dependency,
134
+ route: APIRoute,
135
+ ) -> None:
132
136
  """
133
137
  Warning: this should never be constructed in application code.
134
138
  """
139
+ super().__init__(_noop, wraps.block_name)
140
+ self._wrapped = wraps
135
141
  self._route = route
136
142
  self._signature = inspect.signature(route.endpoint)
137
- self._block_name = block_name
143
+
144
+ @property
145
+ def source_module(self) -> str:
146
+ return self._wrapped.source_module
147
+
148
+ @property
149
+ def source_package(self) -> str:
150
+ return self._wrapped.source_package
138
151
 
139
152
  @property
140
153
  def route(self) -> APIRoute:
engin/scripts/graph.py CHANGED
@@ -28,6 +28,8 @@ args.add_argument(
28
28
  ),
29
29
  )
30
30
 
31
+ _APP_ORIGIN = ""
32
+
31
33
 
32
34
  def serve_graph() -> None:
33
35
  # add cwd to path to enable local package imports
@@ -44,6 +46,9 @@ def serve_graph() -> None:
44
46
  "Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'"
45
47
  ) from None
46
48
 
49
+ global _APP_ORIGIN
50
+ _APP_ORIGIN = module_name.split(".", maxsplit=1)[0]
51
+
47
52
  module = importlib.import_module(module_name)
48
53
 
49
54
  try:
@@ -112,7 +117,17 @@ def _render_node(node: Dependency) -> str:
112
117
  if n not in _BLOCK_IDX:
113
118
  _BLOCK_IDX[n] = len(_SEEN_BLOCKS) % 8
114
119
  _SEEN_BLOCKS.append(n)
115
- style = f":::b{_BLOCK_IDX[n]}"
120
+ style = f"b{_BLOCK_IDX[n]}"
121
+
122
+ node_root_package = node.source_package.split(".", maxsplit=1)[0]
123
+ if node_root_package != _APP_ORIGIN:
124
+ if style:
125
+ style += "E"
126
+ else:
127
+ style = "external"
128
+
129
+ if style:
130
+ style = f":::{style}"
116
131
 
117
132
  if isinstance(node, Supply):
118
133
  md += f"{node.return_type_id}"
@@ -144,6 +159,7 @@ _GRAPH_HTML = """
144
159
  graph LR
145
160
  %%LEGEND%%
146
161
  classDef b0 fill:#7fc97f;
162
+ classDef external stroke-dasharray: 5 5;
147
163
  </pre>
148
164
  </div>
149
165
  <pre class="mermaid">
@@ -157,6 +173,15 @@ _GRAPH_HTML = """
157
173
  classDef b5 fill:#f0027f;
158
174
  classDef b6 fill:#bf5b17;
159
175
  classDef b7 fill:#666666;
176
+ classDef b0E fill:#7fc97f,stroke-dasharray: 5 5;
177
+ classDef b1E fill:#beaed4,stroke-dasharray: 5 5;
178
+ classDef b2E fill:#fdc086,stroke-dasharray: 5 5;
179
+ classDef b3E fill:#ffff99,stroke-dasharray: 5 5;
180
+ classDef b4E fill:#386cb0,stroke-dasharray: 5 5;
181
+ classDef b5E fill:#f0027f,stroke-dasharray: 5 5;
182
+ classDef b6E fill:#bf5b17,stroke-dasharray: 5 5;
183
+ classDef b7E fill:#666666,stroke-dasharray: 5 5;
184
+ classDef external stroke-dasharray: 5 5;
160
185
  </pre>
161
186
  <script type="module">
162
187
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
@@ -169,6 +194,6 @@ _GRAPH_HTML = """
169
194
 
170
195
  DEFAULT_LEGEND = (
171
196
  "0[/Invoke/] ~~~ 1[/Entrypoint\\] ~~~ 2[Provide] ~~~ 3(Supply)"
172
- ' ~~~ 4["`Block Grouping`"]:::b0'
197
+ ' ~~~ 4["`Block Grouping`"]:::b0 ~~~ 5[External Dependency]:::external'
173
198
  )
174
- ASGI_ENGIN_LEGEND = DEFAULT_LEGEND + " ~~~ 5[[API Route]]"
199
+ ASGI_ENGIN_LEGEND = DEFAULT_LEGEND + " ~~~ 6[[API Route]]"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.9
3
+ Version: 0.0.11
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/
@@ -12,6 +12,8 @@ Keywords: Application Framework,Dependency Injection
12
12
  Requires-Python: >=3.10
13
13
  Description-Content-Type: text/markdown
14
14
 
15
+ [![codecov](https://codecov.io/gh/invokermain/engin/graph/badge.svg?token=4PJOIMV6IB)](https://codecov.io/gh/invokermain/engin)
16
+
15
17
  # Engin 🏎️
16
18
 
17
19
  Engin is a zero-dependency application framework for modern Python.
@@ -0,0 +1,20 @@
1
+ engin/__init__.py,sha256=yTc8k0HDGMIrxDdEEA90qGD_dExQjVIbXCyaOFRrnMg,508
2
+ engin/_assembler.py,sha256=6n8RBOwano3U3WM1P8Y6HLHo8IuPD_7jUMoTHBJoEA8,7713
3
+ engin/_block.py,sha256=0QJtqyP5uTFjXsdVGr4ZONLI2LhfzUKmQGnNQWouB3o,2121
4
+ engin/_dependency.py,sha256=URAuzA8pfLxAnwhrEZQGqO1wNCVN-WQLlFg1j1Kn-vs,6380
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=C71kX2Dr-gluGSL018K4uihX3zkTe7QNWaHhFU10ZmA,2127
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=PCnemGVVB0jUZVcphz7y283sZ7D5Gog1o6rJ6g2TuCQ,2944
13
+ engin/ext/fastapi.py,sha256=whlzhkyG0l-fF2JQ0IeBBtg1NjwjaIUxKrM3FuAgKyE,5885
14
+ engin/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ engin/scripts/graph.py,sha256=por62FkzcWx72V2Ha9sIoki-o99fe2Ifm1w-mdoHZIQ,5922
16
+ engin-0.0.11.dist-info/METADATA,sha256=p63ZZiEGzl4jLTj6mQc0n7t-rLoStbp-c5vKI49cF2Y,2291
17
+ engin-0.0.11.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ engin-0.0.11.dist-info/entry_points.txt,sha256=Dehk4j5nK6zyuQtgOSRAoLE609V6eLzEp32bjqhO62Q,64
19
+ engin-0.0.11.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
20
+ engin-0.0.11.dist-info/RECORD,,
@@ -1,20 +0,0 @@
1
- engin/__init__.py,sha256=yTc8k0HDGMIrxDdEEA90qGD_dExQjVIbXCyaOFRrnMg,508
2
- engin/_assembler.py,sha256=VCZA_Gq4hnH5LueB_vEVqsKbGXx-nI6KQ65YhzXw-VY,7575
3
- engin/_block.py,sha256=0QJtqyP5uTFjXsdVGr4ZONLI2LhfzUKmQGnNQWouB3o,2121
4
- engin/_dependency.py,sha256=Nmyk4cGcK6ZZA8YWNZWgckLpDdKsRwOeRQhqRlxZmI0,5883
5
- engin/_engin.py,sha256=i2IxMIz-3yKjedyg5L6gQEpdFDUZnCbXi9Pwhw8Hsxk,9133
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=C71kX2Dr-gluGSL018K4uihX3zkTe7QNWaHhFU10ZmA,2127
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=ViAWw-PNaP6y5fHK2rSGRfaxj7xxo217eqU2z2rhnq8,2200
13
- engin/ext/fastapi.py,sha256=p9NekfRHbdNzK_1ttVNr500AO-IwLpBlG9Fhk6R0mmg,5649
14
- engin/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- engin/scripts/graph.py,sha256=kmC_sSypGvIH89GddHz3KoRBEvooGNsGtrwLgw_uxNQ,4968
16
- engin-0.0.9.dist-info/METADATA,sha256=fzCqyXhGBzC1hMcTtbRM9-oXVaHEP000abzPi9QZbFQ,2161
17
- engin-0.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
- engin-0.0.9.dist-info/entry_points.txt,sha256=Dehk4j5nK6zyuQtgOSRAoLE609V6eLzEp32bjqhO62Q,64
19
- engin-0.0.9.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
20
- engin-0.0.9.dist-info/RECORD,,
File without changes