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.
- {engin-0.0.7 → engin-0.0.9}/CHANGELOG.md +24 -0
- {engin-0.0.7 → engin-0.0.9}/PKG-INFO +7 -1
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/main.py +3 -1
- engin-0.0.9/examples/fastapi/app.py +29 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/main.py +3 -1
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/block.py +4 -6
- {engin-0.0.7 → engin-0.0.9}/examples/simple/main.py +2 -1
- {engin-0.0.7 → engin-0.0.9}/pyproject.toml +15 -1
- {engin-0.0.7 → engin-0.0.9}/src/engin/_block.py +2 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/_dependency.py +22 -1
- {engin-0.0.7 → engin-0.0.9}/src/engin/_engin.py +5 -3
- engin-0.0.9/src/engin/_graph.py +50 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/ext/asgi.py +6 -1
- engin-0.0.9/src/engin/ext/fastapi.py +168 -0
- engin-0.0.9/src/engin/scripts/graph.py +174 -0
- engin-0.0.9/tests/conftest.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/tests/test_engin.py +20 -0
- {engin-0.0.7 → engin-0.0.9}/uv.lock +30 -29
- engin-0.0.7/examples/fastapi/app.py +0 -25
- engin-0.0.7/src/engin/ext/fastapi.py +0 -38
- {engin-0.0.7 → engin-0.0.9}/.github/workflows/check.yaml +0 -0
- {engin-0.0.7 → engin-0.0.9}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.7 → engin-0.0.9}/.gitignore +0 -0
- {engin-0.0.7 → engin-0.0.9}/.readthedocs.yaml +0 -0
- {engin-0.0.7 → engin-0.0.9}/LICENSE +0 -0
- {engin-0.0.7 → engin-0.0.9}/README.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/concepts/engin.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/concepts/invocations.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/concepts/lifecycle.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/concepts/providers.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/engin.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/guides/dependency_injection.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/index.md +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/js/readthedocs.js +0 -0
- {engin-0.0.7 → engin-0.0.9}/docs/overrides/main.html +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/app.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/examples/simple/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/mkdocs.yaml +0 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/_assembler.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/_exceptions.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/_lifecycle.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/_type_utils.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/ext/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/src/engin/py.typed +0 -0
- {engin-0.0.7/tests → engin-0.0.9/src/engin/scripts}/__init__.py +0 -0
- {engin-0.0.7/tests/acceptance → engin-0.0.9/tests}/__init__.py +0 -0
- /engin-0.0.7/tests/conftest.py → /engin-0.0.9/tests/acceptance/__init__.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/tests/acceptance/test_error_in_shutdown.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/tests/acceptance/test_error_in_start_up.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/tests/deps.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/tests/test_assembler.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/tests/test_dependencies.py +0 -0
- {engin-0.0.7 → engin-0.0.9}/tests/test_modules.py +0 -0
- {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.
|
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
|
|
@@ -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}
|
@@ -1,16 +1,14 @@
|
|
1
|
-
from
|
1
|
+
from typing import ClassVar
|
2
2
|
|
3
|
-
from engin import Block,
|
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)
|
@@ -1,11 +1,19 @@
|
|
1
1
|
[project]
|
2
2
|
name = "engin"
|
3
|
-
version = "0.0.
|
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.
|
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.
|
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/
|
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/
|
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.
|
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/
|
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/
|
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.
|
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/
|
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/
|
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.
|
916
|
+
version = "0.9.7"
|
916
917
|
source = { registry = "https://pypi.org/simple" }
|
917
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
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/
|
920
|
-
{ url = "https://files.pythonhosted.org/packages/
|
921
|
-
{ url = "https://files.pythonhosted.org/packages/
|
922
|
-
{ url = "https://files.pythonhosted.org/packages/
|
923
|
-
{ url = "https://files.pythonhosted.org/packages/
|
924
|
-
{ url = "https://files.pythonhosted.org/packages/
|
925
|
-
{ url = "https://files.pythonhosted.org/packages/
|
926
|
-
{ url = "https://files.pythonhosted.org/packages/
|
927
|
-
{ url = "https://files.pythonhosted.org/packages/
|
928
|
-
{ url = "https://files.pythonhosted.org/packages/
|
929
|
-
{ url = "https://files.pythonhosted.org/packages/
|
930
|
-
{ url = "https://files.pythonhosted.org/packages/
|
931
|
-
{ url = "https://files.pythonhosted.org/packages/
|
932
|
-
{ url = "https://files.pythonhosted.org/packages/
|
933
|
-
{ url = "https://files.pythonhosted.org/packages/
|
934
|
-
{ url = "https://files.pythonhosted.org/packages/
|
935
|
-
{ url = "https://files.pythonhosted.org/packages/
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|