engin 0.0.7__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
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
 
@@ -80,7 +81,6 @@ class Engin:
80
81
  Args:
81
82
  *options: an instance of Provide, Supply, Invoke, Entrypoint or a Block.
82
83
  """
83
-
84
84
  self._stop_requested_event = Event()
85
85
  self._stop_complete_event = Event()
86
86
  self._exit_stack: AsyncExitStack = AsyncExitStack()
@@ -95,8 +95,6 @@ class Engin:
95
95
  self._destruct_options(chain(self._LIB_OPTIONS, options))
96
96
  multi_providers = [p for multi in self._multiproviders.values() for p in multi]
97
97
  self._assembler = Assembler(chain(self._providers.values(), multi_providers))
98
- self._providers.clear()
99
- self._multiproviders.clear()
100
98
 
101
99
  @property
102
100
  def assembler(self) -> Assembler:
@@ -162,6 +160,10 @@ class Engin:
162
160
  return
163
161
  await self._stop_complete_event.wait()
164
162
 
163
+ def graph(self) -> list[Node]:
164
+ grapher = DependencyGrapher({**self._providers, **self._multiproviders})
165
+ return grapher.resolve(self._invocations)
166
+
165
167
  async def _shutdown(self) -> None:
166
168
  LOG.info("stopping engin")
167
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.7
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=vqBwKNHH_V3MZZQ29A1zWknFSkkzghJgpWN0mjvjtyY,5425
5
- engin/_engin.py,sha256=lPce5p4fCz2AdBdmXdEFvehryIi4FPmD0zjvDFcL8Jc,8987
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.7.dist-info/METADATA,sha256=mnVPfy7Bdtd_ilWcVn2zHyw1utyepBx3inRlAi3BCHc,1806
14
- engin-0.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- engin-0.0.7.dist-info/licenses/LICENSE,sha256=XHh5LPUPKZWTBqBv2xxN2RU7D59nHoiJGb5RIt8f45w,1070
16
- engin-0.0.7.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