engin 0.0.6__py3-none-any.whl → 0.0.8__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/_dependency.py CHANGED
@@ -18,7 +18,6 @@ from engin._type_utils import TypeId, type_id_of
18
18
  P = ParamSpec("P")
19
19
  T = TypeVar("T")
20
20
  Func: TypeAlias = Callable[P, T]
21
- _SELF = object()
22
21
 
23
22
 
24
23
  def _noop(*args: Any, **kwargs: Any) -> None: ...
@@ -31,6 +30,10 @@ class Dependency(ABC, Generic[P, T]):
31
30
  self._signature = inspect.signature(self._func)
32
31
  self._block_name = block_name
33
32
 
33
+ @property
34
+ def module(self) -> str:
35
+ return self._func.__module__
36
+
34
37
  @property
35
38
  def block_name(self) -> str | None:
36
39
  return self._block_name
@@ -136,7 +139,7 @@ class Provide(Dependency[Any, T]):
136
139
  return_type = self._func # __init__ returns self
137
140
  else:
138
141
  try:
139
- return_type = get_type_hints(self._func)["return"]
142
+ return_type = get_type_hints(self._func, include_extras=True)["return"]
140
143
  except KeyError as err:
141
144
  raise RuntimeError(
142
145
  f"Dependency '{self.name}' requires a return typehint"
engin/_engin.py CHANGED
@@ -13,6 +13,7 @@ from engin import Entrypoint
13
13
  from engin._assembler import AssembledDependency, Assembler
14
14
  from engin._block import Block
15
15
  from engin._dependency import Dependency, Invoke, Provide, Supply
16
+ from engin._graph import DependencyGrapher, Node
16
17
  from engin._lifecycle import Lifecycle
17
18
  from engin._type_utils import TypeId
18
19
 
@@ -35,8 +36,7 @@ class Engin:
35
36
  Supply) and at least one invocation (Invoke or Entrypoint).
36
37
 
37
38
  When instantiated the Engin can be run. This is typically done via the `run` method,
38
- but certain use cases, e.g. testing, it can be easier to use the `start` and `stop`
39
- methods.
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
  1. The Engin assembles all Invocations. Only Providers that are required to satisfy
@@ -81,7 +81,6 @@ class Engin:
81
81
  Args:
82
82
  *options: an instance of Provide, Supply, Invoke, Entrypoint or a Block.
83
83
  """
84
-
85
84
  self._stop_requested_event = Event()
86
85
  self._stop_complete_event = Event()
87
86
  self._exit_stack: AsyncExitStack = AsyncExitStack()
@@ -96,8 +95,6 @@ class Engin:
96
95
  self._destruct_options(chain(self._LIB_OPTIONS, options))
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
- self._providers.clear()
100
- self._multiproviders.clear()
101
98
 
102
99
  @property
103
100
  def assembler(self) -> Assembler:
@@ -163,6 +160,10 @@ class Engin:
163
160
  return
164
161
  await self._stop_complete_event.wait()
165
162
 
163
+ def graph(self) -> list[Node]:
164
+ grapher = DependencyGrapher({**self._providers, **self._multiproviders})
165
+ return grapher.resolve(self._invocations)
166
+
166
167
  async def _shutdown(self) -> None:
167
168
  LOG.info("stopping engin")
168
169
  await self._exit_stack.aclose()
engin/_graph.py ADDED
@@ -0,0 +1,39 @@
1
+ from collections.abc import Iterable
2
+ from typing import TypedDict
3
+
4
+ from engin._dependency import Dependency, Provide
5
+ from engin._type_utils import TypeId
6
+
7
+
8
+ class Node(TypedDict):
9
+ node: Dependency
10
+ parent: Dependency | None
11
+
12
+
13
+ class DependencyGrapher:
14
+ def __init__(self, providers: dict[TypeId, Provide | list[Provide]]) -> None:
15
+ self._providers: dict[TypeId, Provide | list[Provide]] = providers
16
+
17
+ def resolve(self, roots: Iterable[Dependency]) -> list[Node]:
18
+ seen: set[TypeId] = set()
19
+ nodes: list[Node] = []
20
+
21
+ for root in roots:
22
+ for parameter in root.parameter_types:
23
+ if parameter in seen:
24
+ continue
25
+
26
+ seen.add(parameter)
27
+ provider = self._providers[parameter]
28
+
29
+ # multiprovider
30
+ if isinstance(provider, list):
31
+ for p in provider:
32
+ nodes.append({"node": p, "parent": root})
33
+ nodes.extend(self.resolve([p]))
34
+ # single provider
35
+ else:
36
+ nodes.append({"node": provider, "parent": root})
37
+ nodes.extend(self.resolve([provider]))
38
+
39
+ return nodes
File without changes
engin/scripts/graph.py ADDED
@@ -0,0 +1,122 @@
1
+ import importlib
2
+ import logging
3
+ import socketserver
4
+ import sys
5
+ import threading
6
+ from argparse import ArgumentParser
7
+ from http.server import BaseHTTPRequestHandler
8
+ from time import sleep
9
+ from typing import Any
10
+
11
+ from engin import Engin
12
+ from engin._dependency import Dependency, Provide
13
+
14
+ # mute logging from importing of files + engin's debug logging.
15
+ logging.disable()
16
+
17
+ args = ArgumentParser(
18
+ prog="engin-graph",
19
+ description="Creates a visualisation of your application's dependencies",
20
+ )
21
+ args.add_argument(
22
+ "-e", "--exclude", help="a list of packages or module to exclude", default=["engin"]
23
+ )
24
+ args.add_argument(
25
+ "app",
26
+ help=(
27
+ "the import path of your Engin instance, in the form "
28
+ "'package:application', e.g. 'app.main:engin'"
29
+ ),
30
+ )
31
+
32
+
33
+ def serve_graph() -> None:
34
+ # add cwd to path to enable local package imports
35
+ sys.path.insert(0, "")
36
+
37
+ parsed = args.parse_args()
38
+
39
+ app = parsed.app
40
+ excluded_modules = parsed.exclude
41
+
42
+ try:
43
+ module_name, engin_name = app.split(":", maxsplit=1)
44
+ except ValueError:
45
+ raise ValueError(
46
+ "Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'"
47
+ ) from None
48
+
49
+ module = importlib.import_module(module_name)
50
+
51
+ try:
52
+ instance = getattr(module, engin_name)
53
+ except LookupError:
54
+ raise LookupError(f"Module '{module_name}' has no attribute '{engin_name}'") from None
55
+
56
+ if not isinstance(instance, Engin):
57
+ raise TypeError(f"'{app}' is not an Engin instance")
58
+
59
+ nodes = instance.graph()
60
+
61
+ # transform dependencies into mermaid syntax
62
+ dependencies = [
63
+ f"{_render_node(node['parent'])} --> {_render_node(node['node'])}"
64
+ for node in nodes
65
+ if node["parent"] is not None
66
+ and not _should_exclude(node["node"].module, excluded_modules)
67
+ ]
68
+
69
+ html = _GRAPH_HTML.replace("%%DATA%%", "\n".join(dependencies)).encode("utf8")
70
+
71
+ class Handler(BaseHTTPRequestHandler):
72
+ def do_GET(self) -> None:
73
+ self.send_response(200, "OK")
74
+ self.send_header("Content-type", "html")
75
+ self.end_headers()
76
+ self.wfile.write(html)
77
+
78
+ def log_message(self, format: str, *args: Any) -> None:
79
+ return
80
+
81
+ def _start_server() -> None:
82
+ with socketserver.TCPServer(("localhost", 8123), Handler) as httpd:
83
+ print("Serving dependency graph on http://localhost:8123")
84
+ httpd.serve_forever()
85
+
86
+ server_thread = threading.Thread(target=_start_server)
87
+ server_thread.daemon = True # Daemonize the thread so it exits when the main script exits
88
+ server_thread.start()
89
+
90
+ try:
91
+ sleep(10000)
92
+ except KeyboardInterrupt:
93
+ print("Exiting the server...")
94
+
95
+
96
+ def _render_node(node: Dependency) -> str:
97
+ if isinstance(node, Provide):
98
+ return str(node.return_type_id)
99
+ else:
100
+ return node.name
101
+
102
+
103
+ def _should_exclude(module: str, excluded: list[str]) -> bool:
104
+ return any(module.startswith(e) for e in excluded)
105
+
106
+
107
+ _GRAPH_HTML = """
108
+ <!doctype html>
109
+ <html lang="en">
110
+ <body>
111
+ <pre class="mermaid">
112
+ graph TD
113
+ %%DATA%%
114
+ </pre>
115
+ <script type="module">
116
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
117
+ let config = { flowchart: { useMaxWidth: false, htmlLabels: true } };
118
+ mermaid.initialize(config);
119
+ </script>
120
+ </body>
121
+ </html>
122
+ """
@@ -1,8 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.6
3
+ Version: 0.0.8
4
4
  Summary: An async-first modular application framework
5
+ Project-URL: Homepage, https://github.com/invokermain/engin
6
+ Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
7
+ Project-URL: Repository, https://github.com/invokermain/engin.git
8
+ Project-URL: Changelog, https://github.com/invokermain/engin/blob/main/CHANGELOG.md
9
+ License-Expression: MIT
5
10
  License-File: LICENSE
11
+ Keywords: Application Framework,Dependency Injection
6
12
  Requires-Python: >=3.10
7
13
  Description-Content-Type: text/markdown
8
14
 
@@ -1,16 +1,20 @@
1
1
  engin/__init__.py,sha256=yTc8k0HDGMIrxDdEEA90qGD_dExQjVIbXCyaOFRrnMg,508
2
2
  engin/_assembler.py,sha256=VCZA_Gq4hnH5LueB_vEVqsKbGXx-nI6KQ65YhzXw-VY,7575
3
3
  engin/_block.py,sha256=-5qTp1Hdm3H54nScDGitFpcXRHLIyVHlDYATg_3dnPw,2045
4
- engin/_dependency.py,sha256=oh1T7oR-c9MGcZ6ZFUgPnvHRf-n6AIvpbm59R97To80,5404
5
- engin/_engin.py,sha256=Dp1i0COcmaXWwrckBq3-vym-B7umsZG_0vXjasA5y70,9002
4
+ engin/_dependency.py,sha256=WjJKJY2KhseYpqM1Gg-VhYls4dcLg1CgdlpniJEUHjI,5489
5
+ engin/_engin.py,sha256=i2IxMIz-3yKjedyg5L6gQEpdFDUZnCbXi9Pwhw8Hsxk,9133
6
6
  engin/_exceptions.py,sha256=fsc4pTOIGHUh0x7oZhEXPJUTE268sIhswLoiqXaudiw,635
7
+ engin/_graph.py,sha256=piqcocrWt0mX_UAr0DVdkCtVYjkjVpkikLCdRVCoUnU,1230
7
8
  engin/_lifecycle.py,sha256=_jQnGFj4RYXsxMpcXPJQagFOwnoTVh7oSN8oUYoYuW0,3246
8
9
  engin/_type_utils.py,sha256=C71kX2Dr-gluGSL018K4uihX3zkTe7QNWaHhFU10ZmA,2127
9
10
  engin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
11
  engin/ext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
12
  engin/ext/asgi.py,sha256=6vuC4zIhsvAdmwRn2I6uuUWPYfqobox1dv7skg2OWwE,1940
12
13
  engin/ext/fastapi.py,sha256=CH2Zi7Oh_Va0TJGx05e7_LqAiCsoI1qcu0Z59_rgfRk,899
13
- engin-0.0.6.dist-info/METADATA,sha256=SSsLRciMAahwVMjw6Igdcq1jt1zv24FrPuXm_1uykjg,1806
14
- engin-0.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- engin-0.0.6.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
16
- engin-0.0.6.dist-info/RECORD,,
14
+ engin/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ engin/scripts/graph.py,sha256=f_Yk4TRrw9N-gltdriiOviNGcowQckeM2R-7CPVXkHY,3424
16
+ engin-0.0.8.dist-info/METADATA,sha256=xX0-36ECkPFBcDsYjiY04-dhn-066fQhKKzEceUPkhQ,2161
17
+ engin-0.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
18
+ engin-0.0.8.dist-info/entry_points.txt,sha256=Dehk4j5nK6zyuQtgOSRAoLE609V6eLzEp32bjqhO62Q,64
19
+ engin-0.0.8.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
20
+ engin-0.0.8.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ engin-graph = engin.scripts.graph:serve_graph
File without changes