engin 0.0.7__tar.gz → 0.0.9__tar.gz

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.
Files changed (80) hide show
  1. {engin-0.0.7 → engin-0.0.9}/CHANGELOG.md +24 -0
  2. {engin-0.0.7 → engin-0.0.9}/PKG-INFO +7 -1
  3. {engin-0.0.7 → engin-0.0.9}/examples/asgi/main.py +3 -1
  4. engin-0.0.9/examples/fastapi/app.py +29 -0
  5. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/main.py +3 -1
  6. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/block.py +4 -6
  7. {engin-0.0.7 → engin-0.0.9}/examples/simple/main.py +2 -1
  8. {engin-0.0.7 → engin-0.0.9}/pyproject.toml +15 -1
  9. {engin-0.0.7 → engin-0.0.9}/src/engin/_block.py +2 -0
  10. {engin-0.0.7 → engin-0.0.9}/src/engin/_dependency.py +22 -1
  11. {engin-0.0.7 → engin-0.0.9}/src/engin/_engin.py +5 -3
  12. engin-0.0.9/src/engin/_graph.py +50 -0
  13. {engin-0.0.7 → engin-0.0.9}/src/engin/ext/asgi.py +6 -1
  14. engin-0.0.9/src/engin/ext/fastapi.py +168 -0
  15. engin-0.0.9/src/engin/scripts/graph.py +174 -0
  16. engin-0.0.9/tests/conftest.py +0 -0
  17. {engin-0.0.7 → engin-0.0.9}/tests/test_engin.py +20 -0
  18. {engin-0.0.7 → engin-0.0.9}/uv.lock +30 -29
  19. engin-0.0.7/examples/fastapi/app.py +0 -25
  20. engin-0.0.7/src/engin/ext/fastapi.py +0 -38
  21. {engin-0.0.7 → engin-0.0.9}/.github/workflows/check.yaml +0 -0
  22. {engin-0.0.7 → engin-0.0.9}/.github/workflows/publish.yaml +0 -0
  23. {engin-0.0.7 → engin-0.0.9}/.gitignore +0 -0
  24. {engin-0.0.7 → engin-0.0.9}/.readthedocs.yaml +0 -0
  25. {engin-0.0.7 → engin-0.0.9}/LICENSE +0 -0
  26. {engin-0.0.7 → engin-0.0.9}/README.md +0 -0
  27. {engin-0.0.7 → engin-0.0.9}/docs/concepts/engin.md +0 -0
  28. {engin-0.0.7 → engin-0.0.9}/docs/concepts/invocations.md +0 -0
  29. {engin-0.0.7 → engin-0.0.9}/docs/concepts/lifecycle.md +0 -0
  30. {engin-0.0.7 → engin-0.0.9}/docs/concepts/providers.md +0 -0
  31. {engin-0.0.7 → engin-0.0.9}/docs/engin.md +0 -0
  32. {engin-0.0.7 → engin-0.0.9}/docs/guides/dependency_injection.md +0 -0
  33. {engin-0.0.7 → engin-0.0.9}/docs/index.md +0 -0
  34. {engin-0.0.7 → engin-0.0.9}/docs/js/readthedocs.js +0 -0
  35. {engin-0.0.7 → engin-0.0.9}/docs/overrides/main.html +0 -0
  36. {engin-0.0.7 → engin-0.0.9}/examples/__init__.py +0 -0
  37. {engin-0.0.7 → engin-0.0.9}/examples/asgi/__init__.py +0 -0
  38. {engin-0.0.7 → engin-0.0.9}/examples/asgi/app.py +0 -0
  39. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/__init__.py +0 -0
  40. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/__init__.py +0 -0
  41. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/adapaters/__init__.py +0 -0
  42. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/adapaters/memory.py +0 -0
  43. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/block.py +0 -0
  44. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/ports.py +0 -0
  45. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/starlette/__init__.py +0 -0
  46. {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/starlette/endpoint.py +0 -0
  47. {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/__init__.py +0 -0
  48. {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/__init__.py +0 -0
  49. {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/api/__init__.py +0 -0
  50. {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/api/get.py +0 -0
  51. {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/api/post.py +0 -0
  52. {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/block.py +0 -0
  53. {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/domain.py +0 -0
  54. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/__init__.py +0 -0
  55. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/__init__.py +0 -0
  56. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/__init__.py +0 -0
  57. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
  58. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
  59. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/api.py +0 -0
  60. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/domain.py +0 -0
  61. {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/ports.py +0 -0
  62. {engin-0.0.7 → engin-0.0.9}/examples/simple/__init__.py +0 -0
  63. {engin-0.0.7 → engin-0.0.9}/mkdocs.yaml +0 -0
  64. {engin-0.0.7 → engin-0.0.9}/src/engin/__init__.py +0 -0
  65. {engin-0.0.7 → engin-0.0.9}/src/engin/_assembler.py +0 -0
  66. {engin-0.0.7 → engin-0.0.9}/src/engin/_exceptions.py +0 -0
  67. {engin-0.0.7 → engin-0.0.9}/src/engin/_lifecycle.py +0 -0
  68. {engin-0.0.7 → engin-0.0.9}/src/engin/_type_utils.py +0 -0
  69. {engin-0.0.7 → engin-0.0.9}/src/engin/ext/__init__.py +0 -0
  70. {engin-0.0.7 → engin-0.0.9}/src/engin/py.typed +0 -0
  71. {engin-0.0.7/tests → engin-0.0.9/src/engin/scripts}/__init__.py +0 -0
  72. {engin-0.0.7/tests/acceptance → engin-0.0.9/tests}/__init__.py +0 -0
  73. /engin-0.0.7/tests/conftest.py → /engin-0.0.9/tests/acceptance/__init__.py +0 -0
  74. {engin-0.0.7 → engin-0.0.9}/tests/acceptance/test_error_in_shutdown.py +0 -0
  75. {engin-0.0.7 → engin-0.0.9}/tests/acceptance/test_error_in_start_up.py +0 -0
  76. {engin-0.0.7 → engin-0.0.9}/tests/deps.py +0 -0
  77. {engin-0.0.7 → engin-0.0.9}/tests/test_assembler.py +0 -0
  78. {engin-0.0.7 → engin-0.0.9}/tests/test_dependencies.py +0 -0
  79. {engin-0.0.7 → engin-0.0.9}/tests/test_modules.py +0 -0
  80. {engin-0.0.7 → engin-0.0.9}/tests/test_utils.py +0 -0
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+
9
+ ## [0.0.9] - 2025-02-22
10
+
11
+ ### Added
12
+
13
+ - Dependency class now has a new attribute: `func_name`.
14
+
15
+ ### Changed
16
+
17
+ - Improved `engin-graph` output.
18
+ - The `module` attribute of dependencies has been renamed to `origin`
19
+
20
+ ### Fixed
21
+
22
+ - Options provided under the `options` on a Block now have the `block_name` set.
23
+
24
+
25
+ ## [0.0.8] - 2025-02-22
26
+
27
+ ### Added
28
+
29
+ - A package script, `engin-graph` for visualising the dependency graph.
30
+
31
+
8
32
  ## [0.0.7] - 2025-02-20
9
33
 
10
34
  ### Changed
@@ -1,8 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.7
3
+ Version: 0.0.9
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
 
@@ -12,4 +12,6 @@ logging.basicConfig(level=logging.DEBUG)
12
12
 
13
13
  app = ASGIEngin(AppBlock(), DatabaseBlock(), CatBlock(), Supply(AppConfig(debug=True)))
14
14
 
15
- uvicorn.run(app)
15
+
16
+ if __name__ == "__main__":
17
+ uvicorn.run(app)
@@ -0,0 +1,29 @@
1
+ from fastapi import APIRouter, FastAPI
2
+ from pydantic_settings import BaseSettings
3
+
4
+ from engin import Block, provide
5
+
6
+
7
+ class AppConfig(BaseSettings):
8
+ debug: bool = False
9
+
10
+
11
+ class AppBlock(Block):
12
+ @provide
13
+ def default_config(self) -> AppConfig:
14
+ return AppConfig()
15
+
16
+ @provide
17
+ def app_factory(self, app_config: AppConfig, routers: list[APIRouter]) -> FastAPI:
18
+ app = FastAPI(debug=app_config.debug)
19
+
20
+ for router in routers:
21
+ app.include_router(router)
22
+
23
+ app.add_api_route(path="/health", endpoint=_health)
24
+
25
+ return app
26
+
27
+
28
+ async def _health() -> dict[str, bool]:
29
+ return {"ok": True}
@@ -11,4 +11,6 @@ logging.basicConfig(level=logging.DEBUG)
11
11
 
12
12
  app = FastAPIEngin(AppBlock(), CatBlock(), Supply(AppConfig(debug=True)))
13
13
 
14
- uvicorn.run(app)
14
+
15
+ if __name__ == "__main__":
16
+ uvicorn.run(app)
@@ -1,16 +1,14 @@
1
- from fastapi import FastAPI
1
+ from typing import ClassVar
2
2
 
3
- from engin import Block, invoke, provide
3
+ from engin import Block, Invoke, Provide, Supply, provide
4
4
  from examples.fastapi.routes.cats.adapters.repository import InMemoryCatRepository
5
5
  from examples.fastapi.routes.cats.api import router
6
6
  from examples.fastapi.routes.cats.ports import CatRepository
7
7
 
8
8
 
9
9
  class CatBlock(Block):
10
+ options: ClassVar[list[Provide | Invoke]] = [Supply([router])]
11
+
10
12
  @provide
11
13
  def cat_repository(self) -> CatRepository:
12
14
  return InMemoryCatRepository()
13
-
14
- @invoke
15
- def attach_router(self, app: FastAPI) -> None:
16
- app.include_router(router)
@@ -19,4 +19,5 @@ async def main(http_client: AsyncClient) -> None:
19
19
 
20
20
  engin = Engin(Provide(new_httpx_client), Invoke(main))
21
21
 
22
- asyncio.run(engin.run())
22
+ if __name__ == "__main__":
23
+ asyncio.run(engin.run())
@@ -1,11 +1,19 @@
1
1
  [project]
2
2
  name = "engin"
3
- version = "0.0.7"
3
+ version = "0.0.9"
4
4
  description = "An async-first modular application framework"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
+ license = "MIT"
8
+ keywords = ["Dependency Injection", "Application Framework"]
7
9
  dependencies = []
8
10
 
11
+ [project.urls]
12
+ Homepage = "https://github.com/invokermain/engin"
13
+ Documentation = "https://engin.readthedocs.io/en/latest/"
14
+ Repository = "https://github.com/invokermain/engin.git"
15
+ Changelog = "https://github.com/invokermain/engin/blob/main/CHANGELOG.md"
16
+
9
17
  [build-system]
10
18
  requires = ["hatchling"]
11
19
  build-backend = "hatchling.build"
@@ -34,6 +42,10 @@ docs = [
34
42
  ]
35
43
 
36
44
 
45
+ [project.scripts]
46
+ engin-graph = "engin.scripts.graph:serve_graph"
47
+
48
+
37
49
  [tool.ruff]
38
50
  line-length = 95
39
51
  target-version = "py310"
@@ -54,7 +66,9 @@ ignore = [
54
66
  [tool.ruff.lint.per-file-ignores]
55
67
  "**/src/*" = ["PT"]
56
68
  "**/tests/*" = ["S", "ANN"]
69
+ # allow print statements in examples/scripts
57
70
  "**/examples/*" = ["T201"]
71
+ "**/scripts/*" = ["T201"]
58
72
 
59
73
 
60
74
  [tool.pytest.ini_options]
@@ -59,6 +59,8 @@ class Block(Iterable[Provide | Invoke]):
59
59
  raise RuntimeError("Block option is not an instance of Provide or Invoke")
60
60
  opt.set_block_name(self._name)
61
61
  self._options.append(opt)
62
+ for opt in self.options:
63
+ opt.set_block_name(self._name)
62
64
 
63
65
  @property
64
66
  def name(self) -> str:
@@ -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,10 +30,24 @@ 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 origin(self) -> str:
35
+ """
36
+ The module that this Dependency originated from.
37
+
38
+ Returns:
39
+ A string, e.g. "examples.fastapi.app"
40
+ """
41
+ return self._func.__module__
42
+
34
43
  @property
35
44
  def block_name(self) -> str | None:
36
45
  return self._block_name
37
46
 
47
+ @property
48
+ def func_name(self) -> str:
49
+ return self._func.__name__
50
+
38
51
  @property
39
52
  def name(self) -> str:
40
53
  if self._block_name:
@@ -102,6 +115,10 @@ class Entrypoint(Invoke):
102
115
  self._type = type_
103
116
  super().__init__(invocation=_noop, block_name=block_name)
104
117
 
118
+ @property
119
+ def origin(self) -> str:
120
+ return self._type.__module__
121
+
105
122
  @property
106
123
  def parameter_types(self) -> list[TypeId]:
107
124
  return [type_id_of(self._type)]
@@ -169,6 +186,10 @@ class Supply(Provide, Generic[T]):
169
186
  self._get_val.__annotations__["return"] = type_hint
170
187
  super().__init__(builder=self._get_val, block_name=block_name)
171
188
 
189
+ @property
190
+ def origin(self) -> str:
191
+ return self._value.__module__
192
+
172
193
  @property
173
194
  def return_type(self) -> type[T]:
174
195
  if self._type_hint is not None:
@@ -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()
@@ -0,0 +1,50 @@
1
+ from collections.abc import Iterable
2
+ from dataclasses import dataclass
3
+
4
+ from engin import Provide
5
+ from engin._dependency import Dependency
6
+ from engin._type_utils import TypeId
7
+
8
+
9
+ @dataclass(slots=True, frozen=True, kw_only=True)
10
+ class Node:
11
+ """
12
+ A Node in the Dependency Graph.
13
+ """
14
+
15
+ node: Dependency
16
+ parent: Dependency | None
17
+
18
+ def __repr__(self) -> str:
19
+ return f"Node(node={self.node!s},parent={self.parent!s})"
20
+
21
+
22
+ class DependencyGrapher:
23
+ def __init__(self, providers: dict[TypeId, Provide | list[Provide]]) -> None:
24
+ self._providers: dict[TypeId, Provide | list[Provide]] = providers
25
+
26
+ def resolve(self, roots: Iterable[Dependency]) -> list[Node]:
27
+ return self._resolve_recursive(roots, seen=set())
28
+
29
+ def _resolve_recursive(
30
+ self, roots: Iterable[Dependency], *, seen: set[TypeId]
31
+ ) -> list[Node]:
32
+ nodes: list[Node] = []
33
+ for root in roots:
34
+ for parameter in root.parameter_types:
35
+ provider = self._providers[parameter]
36
+
37
+ # multiprovider
38
+ if isinstance(provider, list):
39
+ nodes.extend(Node(node=p, parent=root) for p in provider)
40
+ if parameter not in seen:
41
+ nodes.extend(self._resolve_recursive(provider, seen=seen))
42
+ # single provider
43
+ else:
44
+ nodes.append(Node(node=provider, parent=root))
45
+ if parameter not in seen:
46
+ nodes.extend(self._resolve_recursive([provider], seen=seen))
47
+
48
+ seen.add(parameter)
49
+
50
+ return nodes
@@ -2,10 +2,11 @@ import traceback
2
2
  from collections.abc import Awaitable, Callable, MutableMapping
3
3
  from typing import Any, ClassVar, Protocol, TypeAlias
4
4
 
5
- from engin import Engin, Option
5
+ from engin import Engin, Entrypoint, Option
6
6
 
7
7
  __all__ = ["ASGIEngin", "ASGIType"]
8
8
 
9
+ from engin._graph import DependencyGrapher, Node
9
10
 
10
11
  _Scope: TypeAlias = MutableMapping[str, Any]
11
12
  _Message: TypeAlias = MutableMapping[str, Any]
@@ -49,6 +50,10 @@ class ASGIEngin(Engin, ASGIType):
49
50
  await self.start()
50
51
  self._asgi_app = await self._assembler.get(self._asgi_type)
51
52
 
53
+ def graph(self) -> list[Node]:
54
+ grapher = DependencyGrapher({**self._providers, **self._multiproviders})
55
+ return grapher.resolve([Entrypoint(self._asgi_type), *self._invocations])
56
+
52
57
 
53
58
  class _Rereceive:
54
59
  def __init__(self, message: _Message) -> None:
@@ -0,0 +1,168 @@
1
+ import inspect
2
+ import typing
3
+ from collections.abc import Iterable
4
+ from inspect import Parameter
5
+ from typing import ClassVar, TypeVar
6
+
7
+ from fastapi.routing import APIRoute
8
+
9
+ from engin import Engin, Entrypoint, Invoke, Option
10
+ from engin._dependency import Dependency, Supply
11
+ from engin._graph import DependencyGrapher, Node
12
+ from engin._type_utils import TypeId, type_id_of
13
+ from engin.ext.asgi import ASGIEngin
14
+
15
+ try:
16
+ from fastapi import APIRouter, FastAPI
17
+ from fastapi.params import Depends
18
+ from starlette.requests import HTTPConnection
19
+ except ImportError as err:
20
+ raise ImportError(
21
+ "fastapi package must be installed to use the fastapi extension"
22
+ ) from err
23
+
24
+ __all__ = ["APIRouteDependency", "FastAPIEngin", "Inject"]
25
+
26
+
27
+ def _attach_engin(
28
+ app: FastAPI,
29
+ engin: Engin,
30
+ ) -> None:
31
+ app.state.engin = engin
32
+
33
+
34
+ class FastAPIEngin(ASGIEngin):
35
+ _LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)]
36
+ _asgi_type = FastAPI
37
+
38
+ def graph(self) -> list[Node]:
39
+ grapher = _FastAPIDependencyGrapher({**self._providers, **self._multiproviders})
40
+ return grapher.resolve(
41
+ [
42
+ Entrypoint(self._asgi_type),
43
+ *[i for i in self._invocations if i.func_name != "_attach_engin"],
44
+ ]
45
+ )
46
+
47
+
48
+ T = TypeVar("T")
49
+
50
+
51
+ def Inject(interface: type[T]) -> Depends:
52
+ async def inner(conn: HTTPConnection) -> T:
53
+ engin: Engin = conn.app.state.engin
54
+ return await engin.assembler.get(interface)
55
+
56
+ dep = Depends(inner)
57
+ dep.__engin__ = True # type: ignore[attr-defined]
58
+ return dep
59
+
60
+
61
+ class _FastAPIDependencyGrapher(DependencyGrapher):
62
+ """
63
+ This exists in order to bridge the gap between
64
+ """
65
+
66
+ def _resolve_recursive(
67
+ self, roots: Iterable[Dependency], *, seen: set[TypeId]
68
+ ) -> list[Node]:
69
+ nodes: list[Node] = []
70
+ for root in roots:
71
+ for parameter in root.parameter_types:
72
+ provider = self._providers[parameter]
73
+
74
+ # multiprovider
75
+ if isinstance(provider, list):
76
+ for p in provider:
77
+ nodes.append(Node(node=p, parent=root))
78
+
79
+ if isinstance(p, Supply):
80
+ route_dependencies = _extract_routes_from_supply(p)
81
+ nodes.extend(
82
+ Node(node=route_dependency, parent=p)
83
+ for route_dependency in route_dependencies
84
+ )
85
+ nodes.extend(
86
+ self._resolve_recursive(route_dependencies, seen=seen)
87
+ )
88
+
89
+ if parameter not in seen:
90
+ nodes.extend(self._resolve_recursive(provider, seen=seen))
91
+ # single provider
92
+ else:
93
+ nodes.append(Node(node=provider, parent=root))
94
+ # not sure why anyone would ever supply a single APIRouter in an
95
+ # application, but just in case
96
+ if isinstance(provider, Supply):
97
+ route_dependencies = _extract_routes_from_supply(provider)
98
+ nodes.extend(
99
+ Node(node=route_dependency, parent=provider)
100
+ for route_dependency in route_dependencies
101
+ )
102
+ nodes.extend(self._resolve_recursive(route_dependencies, seen=seen))
103
+ if parameter not in seen:
104
+ nodes.extend(self._resolve_recursive([provider], seen=seen))
105
+
106
+ seen.add(parameter)
107
+
108
+ return nodes
109
+
110
+
111
+ def _extract_routes_from_supply(supply: Supply) -> list[Dependency]:
112
+ if supply.is_multiprovider:
113
+ inner = supply._value[0]
114
+ if isinstance(inner, APIRouter):
115
+ return [
116
+ APIRouteDependency(route, block_name=supply.block_name)
117
+ for route in inner.routes
118
+ if isinstance(route, APIRoute)
119
+ ]
120
+ return []
121
+
122
+
123
+ class APIRouteDependency(Dependency):
124
+ """
125
+ This is a pseudo-dependency that is only used when calling FastAPIEngin.graph() in
126
+ order to provide richer metadata to the Node.
127
+
128
+ This class should never be constructed in application code.
129
+ """
130
+
131
+ def __init__(self, route: APIRoute, block_name: str | None = None) -> None:
132
+ """
133
+ Warning: this should never be constructed in application code.
134
+ """
135
+ self._route = route
136
+ self._signature = inspect.signature(route.endpoint)
137
+ self._block_name = block_name
138
+
139
+ @property
140
+ def route(self) -> APIRoute:
141
+ return self._route
142
+
143
+ @property
144
+ def parameter_types(self) -> list[TypeId]:
145
+ parameters = list(self._signature.parameters.values())
146
+ if not parameters:
147
+ return []
148
+ if parameters[0].name == "self":
149
+ parameters.pop(0)
150
+ return [
151
+ type_id_of(typing.get_args(param.annotation)[0])
152
+ for param in parameters
153
+ if self._is_injected_param(param)
154
+ ]
155
+
156
+ @staticmethod
157
+ def _is_injected_param(param: Parameter) -> bool:
158
+ if typing.get_origin(param.annotation) != typing.Annotated:
159
+ return False
160
+ args = typing.get_args(param.annotation)
161
+ if len(args) != 2:
162
+ return False
163
+ return isinstance(args[1], Depends) and hasattr(args[1], "__engin__")
164
+
165
+ @property
166
+ def name(self) -> str:
167
+ methods = ",".join(self._route.methods)
168
+ return f"{methods} {self._route.path}"
@@ -0,0 +1,174 @@
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, Entrypoint, Invoke
12
+ from engin._dependency import Dependency, Provide, Supply
13
+ from engin.ext.asgi import ASGIEngin
14
+ from engin.ext.fastapi import APIRouteDependency
15
+
16
+ # mute logging from importing of files + engin's debug logging.
17
+ logging.disable()
18
+
19
+ args = ArgumentParser(
20
+ prog="engin-graph",
21
+ description="Creates a visualisation of your application's dependencies",
22
+ )
23
+ args.add_argument(
24
+ "app",
25
+ help=(
26
+ "the import path of your Engin instance, in the form "
27
+ "'package:application', e.g. 'app.main:engin'"
28
+ ),
29
+ )
30
+
31
+
32
+ def serve_graph() -> None:
33
+ # add cwd to path to enable local package imports
34
+ sys.path.insert(0, "")
35
+
36
+ parsed = args.parse_args()
37
+
38
+ app = parsed.app
39
+
40
+ try:
41
+ module_name, engin_name = app.split(":", maxsplit=1)
42
+ except ValueError:
43
+ raise ValueError(
44
+ "Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'"
45
+ ) from None
46
+
47
+ module = importlib.import_module(module_name)
48
+
49
+ try:
50
+ instance = getattr(module, engin_name)
51
+ except LookupError:
52
+ raise LookupError(f"Module '{module_name}' has no attribute '{engin_name}'") from None
53
+
54
+ if not isinstance(instance, Engin):
55
+ raise TypeError(f"'{app}' is not an Engin instance")
56
+
57
+ nodes = instance.graph()
58
+
59
+ # transform dependencies into mermaid syntax
60
+ dependencies = [
61
+ f"{_render_node(node.parent)} --> {_render_node(node.node)}"
62
+ for node in nodes
63
+ if node.parent is not None
64
+ ]
65
+
66
+ html = (
67
+ _GRAPH_HTML.replace("%%DATA%%", "\n".join(dependencies))
68
+ .replace(
69
+ "%%LEGEND%%",
70
+ ASGI_ENGIN_LEGEND if isinstance(instance, ASGIEngin) else DEFAULT_LEGEND,
71
+ )
72
+ .encode("utf8")
73
+ )
74
+
75
+ class Handler(BaseHTTPRequestHandler):
76
+ def do_GET(self) -> None:
77
+ self.send_response(200, "OK")
78
+ self.send_header("Content-type", "html")
79
+ self.end_headers()
80
+ self.wfile.write(html)
81
+
82
+ def log_message(self, format: str, *args: Any) -> None:
83
+ return
84
+
85
+ def _start_server() -> None:
86
+ with socketserver.TCPServer(("localhost", 8123), Handler) as httpd:
87
+ print("Serving dependency graph on http://localhost:8123")
88
+ httpd.serve_forever()
89
+
90
+ server_thread = threading.Thread(target=_start_server)
91
+ server_thread.daemon = True # Daemonize the thread so it exits when the main script exits
92
+ server_thread.start()
93
+
94
+ try:
95
+ sleep(10000)
96
+ except KeyboardInterrupt:
97
+ print("Exiting the server...")
98
+
99
+
100
+ _BLOCK_IDX: dict[str, int] = {}
101
+ _SEEN_BLOCKS: list[str] = []
102
+
103
+
104
+ def _render_node(node: Dependency) -> str:
105
+ node_id = id(node)
106
+ md = ""
107
+ style = ""
108
+
109
+ # format block name
110
+ if n := node.block_name:
111
+ md += f"_{n}_\n"
112
+ if n not in _BLOCK_IDX:
113
+ _BLOCK_IDX[n] = len(_SEEN_BLOCKS) % 8
114
+ _SEEN_BLOCKS.append(n)
115
+ style = f":::b{_BLOCK_IDX[n]}"
116
+
117
+ if isinstance(node, Supply):
118
+ md += f"{node.return_type_id}"
119
+ return f'{node_id}("`{md}`"){style}'
120
+ if isinstance(node, Provide):
121
+ md += f"{node.return_type_id}"
122
+ return f'{node_id}["`{md}`"]{style}'
123
+ if isinstance(node, Entrypoint):
124
+ entrypoint_type = node.parameter_types[0]
125
+ md += f"{entrypoint_type}"
126
+ return f'{node_id}[/"`{md}`"\\]{style}'
127
+ if isinstance(node, Invoke):
128
+ md += f"{node.func_name}"
129
+ return f'{node_id}[/"`{md}`"/]{style}'
130
+ if isinstance(node, APIRouteDependency):
131
+ md += f"{node.name}"
132
+ return f'{node_id}[["`{md}`"]]{style}'
133
+ else:
134
+ return f'{node_id}["`{node.name}`"]{style}'
135
+
136
+
137
+ _GRAPH_HTML = """
138
+ <!doctype html>
139
+ <html lang="en">
140
+ <body>
141
+ <div style="border-style:outset">
142
+ <p>LEGEND</p>
143
+ <pre class="mermaid">
144
+ graph LR
145
+ %%LEGEND%%
146
+ classDef b0 fill:#7fc97f;
147
+ </pre>
148
+ </div>
149
+ <pre class="mermaid">
150
+ graph TD
151
+ %%DATA%%
152
+ classDef b0 fill:#7fc97f;
153
+ classDef b1 fill:#beaed4;
154
+ classDef b2 fill:#fdc086;
155
+ classDef b3 fill:#ffff99;
156
+ classDef b4 fill:#386cb0;
157
+ classDef b5 fill:#f0027f;
158
+ classDef b6 fill:#bf5b17;
159
+ classDef b7 fill:#666666;
160
+ </pre>
161
+ <script type="module">
162
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
163
+ let config = { flowchart: { useMaxWidth: false, htmlLabels: true } };
164
+ mermaid.initialize(config);
165
+ </script>
166
+ </body>
167
+ </html>
168
+ """
169
+
170
+ DEFAULT_LEGEND = (
171
+ "0[/Invoke/] ~~~ 1[/Entrypoint\\] ~~~ 2[Provide] ~~~ 3(Supply)"
172
+ ' ~~~ 4["`Block Grouping`"]:::b0'
173
+ )
174
+ ASGI_ENGIN_LEGEND = DEFAULT_LEGEND + " ~~~ 5[[API Route]]"
File without changes
@@ -136,3 +136,23 @@ async def test_engin_with_lifecycle_using_run():
136
136
  await asyncio.gather(engin.run(), _stop_task())
137
137
  # lifecycle should have stopped by now
138
138
  assert state == 2
139
+
140
+
141
+ def test_engin_graph():
142
+ def a() -> A:
143
+ return A()
144
+
145
+ def b(_: A) -> B:
146
+ return B()
147
+
148
+ def c(_: B) -> C:
149
+ return C()
150
+
151
+ def main(c: C) -> None:
152
+ assert isinstance(c, C)
153
+
154
+ engin = Engin(Provide(a), Provide(b), Provide(c), Invoke(main))
155
+
156
+ graph = engin.graph()
157
+
158
+ assert len(graph) == 3
@@ -1,4 +1,5 @@
1
1
  version = 1
2
+ revision = 1
2
3
  requires-python = ">=3.10"
3
4
 
4
5
  [[package]]
@@ -127,7 +128,7 @@ wheels = [
127
128
 
128
129
  [[package]]
129
130
  name = "engin"
130
- version = "0.0.6"
131
+ version = "0.0.8"
131
132
  source = { editable = "." }
132
133
 
133
134
  [package.dev-dependencies]
@@ -414,7 +415,7 @@ wheels = [
414
415
 
415
416
  [[package]]
416
417
  name = "mkdocs-material"
417
- version = "9.6.4"
418
+ version = "9.6.5"
418
419
  source = { registry = "https://pypi.org/simple" }
419
420
  dependencies = [
420
421
  { name = "babel" },
@@ -429,9 +430,9 @@ dependencies = [
429
430
  { name = "regex" },
430
431
  { name = "requests" },
431
432
  ]
432
- sdist = { url = "https://files.pythonhosted.org/packages/9b/80/4efbd3df76c6c1ec27130b43662612f9033adc5a4166f1df2acb8dd6fb1b/mkdocs_material-9.6.4.tar.gz", hash = "sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867", size = 3942628 }
433
+ sdist = { url = "https://files.pythonhosted.org/packages/38/4d/0a9f6f604f01eaa43df3b3b30b5218548efd7341913b302815585f48abb2/mkdocs_material-9.6.5.tar.gz", hash = "sha256:b714679a8c91b0ffe2188e11ed58c44d2523e9c2ae26a29cc652fa7478faa21f", size = 3946479 }
433
434
  wheels = [
434
- { url = "https://files.pythonhosted.org/packages/5b/a5/f3c0e86c1d28fe04f1b724700ff3dd8b3647c89df03a8e10c4bc6b4db1b8/mkdocs_material-9.6.4-py3-none-any.whl", hash = "sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f", size = 8688727 },
435
+ { url = "https://files.pythonhosted.org/packages/3d/05/7d440b23454c0fc8cdba21f73ce23369eb16e7f7ee475fac3a4ad15ad5e0/mkdocs_material-9.6.5-py3-none-any.whl", hash = "sha256:aad3e6fb860c20870f75fb2a69ef901f1be727891e41adb60b753efcae19453b", size = 8695060 },
435
436
  ]
436
437
 
437
438
  [[package]]
@@ -468,7 +469,7 @@ python = [
468
469
 
469
470
  [[package]]
470
471
  name = "mkdocstrings-python"
471
- version = "1.15.0"
472
+ version = "1.16.1"
472
473
  source = { registry = "https://pypi.org/simple" }
473
474
  dependencies = [
474
475
  { name = "griffe" },
@@ -476,9 +477,9 @@ dependencies = [
476
477
  { name = "mkdocstrings" },
477
478
  { name = "typing-extensions", marker = "python_full_version < '3.11'" },
478
479
  ]
479
- sdist = { url = "https://files.pythonhosted.org/packages/28/5e/ea531f1798d6b614f87b7a1191f8bfda864767adecef3c75ec87f30e0a3d/mkdocstrings_python-1.15.0.tar.gz", hash = "sha256:2bfecbbe1252c67281408a6567d59545f4979931110f01ab625aa8c227c06edc", size = 422613 }
480
+ sdist = { url = "https://files.pythonhosted.org/packages/82/a4/3475fd03f3d566ca05872cec76a86d94ead23d99bbf6a89035b924a3e9b6/mkdocstrings_python-1.16.1.tar.gz", hash = "sha256:d7152d17da74d3616a0f17df5d2da771ecf7340518c158650e5a64a0a95973f4", size = 423399 }
480
481
  wheels = [
481
- { url = "https://files.pythonhosted.org/packages/6f/d7/1d35cce198f76e8ae4010a71ff5acabe8b75aeb35f8c3d920e175a6476ca/mkdocstrings_python-1.15.0-py3-none-any.whl", hash = "sha256:77aced1bb28840d7d3510f77353319eeb450961880d87f9c53fdab331ba0120d", size = 449068 },
482
+ { url = "https://files.pythonhosted.org/packages/00/f7/433201c48d4b59208dcbae6e1481febdf732ae20ecb2aee84a4ea142f043/mkdocstrings_python-1.16.1-py3-none-any.whl", hash = "sha256:b88ff6fc6a293cee9cb42313f1cba37a2c5cdf37bcc60b241ec7ab66b5d41b58", size = 449139 },
482
483
  ]
483
484
 
484
485
  [[package]]
@@ -687,15 +688,15 @@ wheels = [
687
688
 
688
689
  [[package]]
689
690
  name = "pydantic-settings"
690
- version = "2.7.1"
691
+ version = "2.8.0"
691
692
  source = { registry = "https://pypi.org/simple" }
692
693
  dependencies = [
693
694
  { name = "pydantic" },
694
695
  { name = "python-dotenv" },
695
696
  ]
696
- sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 }
697
+ sdist = { url = "https://files.pythonhosted.org/packages/ca/a2/ad2511ede77bb424f3939e5148a56d968cdc6b1462620d24b2a1f4ab65b4/pydantic_settings-2.8.0.tar.gz", hash = "sha256:88e2ca28f6e68ea102c99c3c401d6c9078e68a5df600e97b43891c34e089500a", size = 83347 }
697
698
  wheels = [
698
- { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 },
699
+ { url = "https://files.pythonhosted.org/packages/c1/a9/3b9642025174bbe67e900785fb99c9bfe91ea584b0b7126ff99945c24a0e/pydantic_settings-2.8.0-py3-none-any.whl", hash = "sha256:c782c7dc3fb40e97b238e713c25d26f64314aece2e91abcff592fcac15f71820", size = 30746 },
699
700
  ]
700
701
 
701
702
  [[package]]
@@ -912,27 +913,27 @@ wheels = [
912
913
 
913
914
  [[package]]
914
915
  name = "ruff"
915
- version = "0.9.6"
916
+ version = "0.9.7"
916
917
  source = { registry = "https://pypi.org/simple" }
917
- sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
918
+ sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813 }
918
919
  wheels = [
919
- { url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
920
- { url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
921
- { url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
922
- { url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
923
- { url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
924
- { url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
925
- { url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
926
- { url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
927
- { url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
928
- { url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
929
- { url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
930
- { url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
931
- { url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
932
- { url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
933
- { url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
934
- { url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
935
- { url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
920
+ { url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588 },
921
+ { url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848 },
922
+ { url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525 },
923
+ { url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580 },
924
+ { url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674 },
925
+ { url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151 },
926
+ { url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128 },
927
+ { url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858 },
928
+ { url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046 },
929
+ { url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834 },
930
+ { url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307 },
931
+ { url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039 },
932
+ { url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177 },
933
+ { url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122 },
934
+ { url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751 },
935
+ { url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987 },
936
+ { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 },
936
937
  ]
937
938
 
938
939
  [[package]]
@@ -1,25 +0,0 @@
1
- from fastapi import FastAPI
2
- from pydantic_settings import BaseSettings
3
-
4
- from engin import Block, invoke, provide
5
-
6
-
7
- class AppConfig(BaseSettings):
8
- debug: bool = False
9
-
10
-
11
- class AppBlock(Block):
12
- @provide
13
- def app_factory(self, app_config: AppConfig) -> FastAPI:
14
- return FastAPI(debug=app_config.debug)
15
-
16
- @provide
17
- def default_config(self) -> AppConfig:
18
- return AppConfig()
19
-
20
- @invoke
21
- def add_health_endpoint(self, app: FastAPI) -> None:
22
- async def health() -> dict[str, bool]:
23
- return {"ok": True}
24
-
25
- app.add_api_route(path="/health", endpoint=health)
@@ -1,38 +0,0 @@
1
- from typing import ClassVar, TypeVar
2
-
3
- from engin import Engin, Invoke, Option
4
- from engin.ext.asgi import ASGIEngin
5
-
6
- try:
7
- from fastapi import FastAPI
8
- from fastapi.params import Depends
9
- from starlette.requests import HTTPConnection
10
- except ImportError as err:
11
- raise ImportError(
12
- "fastapi package must be installed to use the fastapi extension"
13
- ) from err
14
-
15
- __all__ = ["FastAPIEngin", "Inject"]
16
-
17
-
18
- def _attach_engin(
19
- app: FastAPI,
20
- engin: Engin,
21
- ) -> None:
22
- app.state.engin = engin
23
-
24
-
25
- class FastAPIEngin(ASGIEngin):
26
- _LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)]
27
- _asgi_type = FastAPI
28
-
29
-
30
- T = TypeVar("T")
31
-
32
-
33
- def Inject(interface: type[T]) -> Depends:
34
- async def inner(conn: HTTPConnection) -> T:
35
- engin: Engin = conn.app.state.engin
36
- return await engin.assembler.get(interface)
37
-
38
- return Depends(inner)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes