engin 0.0.8__tar.gz → 0.0.10__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.8 → engin-0.0.10}/CHANGELOG.md +29 -0
- {engin-0.0.8 → engin-0.0.10}/PKG-INFO +1 -1
- {engin-0.0.8 → engin-0.0.10}/docs/concepts/engin.md +1 -0
- {engin-0.0.8 → engin-0.0.10}/docs/concepts/invocations.md +2 -1
- engin-0.0.10/docs/getting-started.md +7 -0
- engin-0.0.10/docs/guides/fastapi-graph.png +0 -0
- engin-0.0.10/docs/guides/fastapi.md +174 -0
- {engin-0.0.8 → engin-0.0.10}/docs/index.md +4 -12
- engin-0.0.10/docs/reference.md +12 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/main.py +3 -1
- engin-0.0.10/examples/fastapi/app.py +29 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/cats/block.py +4 -6
- {engin-0.0.8 → engin-0.0.10}/mkdocs.yaml +22 -0
- {engin-0.0.8 → engin-0.0.10}/pyproject.toml +3 -2
- {engin-0.0.8 → engin-0.0.10}/src/engin/_assembler.py +2 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/_block.py +2 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/_dependency.py +19 -1
- {engin-0.0.8 → engin-0.0.10}/src/engin/_engin.py +1 -0
- engin-0.0.10/src/engin/_graph.py +50 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/ext/asgi.py +32 -3
- engin-0.0.10/src/engin/ext/fastapi.py +168 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/scripts/graph.py +68 -16
- {engin-0.0.8 → engin-0.0.10}/uv.lock +1 -1
- engin-0.0.8/docs/engin.md +0 -1
- engin-0.0.8/examples/fastapi/app.py +0 -25
- engin-0.0.8/src/engin/_graph.py +0 -39
- engin-0.0.8/src/engin/ext/fastapi.py +0 -38
- {engin-0.0.8 → engin-0.0.10}/.github/workflows/check.yaml +0 -0
- {engin-0.0.8 → engin-0.0.10}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.8 → engin-0.0.10}/.gitignore +0 -0
- {engin-0.0.8 → engin-0.0.10}/.readthedocs.yaml +0 -0
- {engin-0.0.8 → engin-0.0.10}/LICENSE +0 -0
- {engin-0.0.8 → engin-0.0.10}/README.md +0 -0
- {engin-0.0.8 → engin-0.0.10}/docs/concepts/lifecycle.md +0 -0
- {engin-0.0.8 → engin-0.0.10}/docs/concepts/providers.md +0 -0
- {engin-0.0.8 → engin-0.0.10}/docs/guides/dependency_injection.md +0 -0
- {engin-0.0.8 → engin-0.0.10}/docs/js/readthedocs.js +0 -0
- {engin-0.0.8 → engin-0.0.10}/docs/overrides/main.html +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/app.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/main.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/simple/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/examples/simple/main.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/_exceptions.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/_lifecycle.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/_type_utils.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/ext/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/py.typed +0 -0
- {engin-0.0.8 → engin-0.0.10}/src/engin/scripts/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/acceptance/__init__.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/acceptance/test_error_in_shutdown.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/acceptance/test_error_in_start_up.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/conftest.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/deps.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/test_assembler.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/test_dependencies.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/test_engin.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/test_modules.py +0 -0
- {engin-0.0.8 → engin-0.0.10}/tests/test_utils.py +0 -0
@@ -6,6 +6,35 @@ 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
8
|
|
9
|
+
## [0.0.10] - 2025-02-27
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- A utility function for ASGI extension `engin_to_lifespan` enabling users to easily
|
14
|
+
integrate Engin into an existing ASGI application.
|
15
|
+
- Further documentation work, including a FastAPI guide.
|
16
|
+
|
17
|
+
### Fixed
|
18
|
+
|
19
|
+
- The warning for missing multiproviders is only logged once for each given type now.
|
20
|
+
|
21
|
+
|
22
|
+
## [0.0.9] - 2025-02-22
|
23
|
+
|
24
|
+
### Added
|
25
|
+
|
26
|
+
- Dependency class now has a new attribute: `func_name`.
|
27
|
+
|
28
|
+
### Changed
|
29
|
+
|
30
|
+
- Improved `engin-graph` output.
|
31
|
+
- The `module` attribute of dependencies has been renamed to `origin`
|
32
|
+
|
33
|
+
### Fixed
|
34
|
+
|
35
|
+
- Options provided under the `options` on a Block now have the `block_name` set.
|
36
|
+
|
37
|
+
|
9
38
|
## [0.0.8] - 2025-02-22
|
10
39
|
|
11
40
|
### Added
|
@@ -3,6 +3,7 @@
|
|
3
3
|
The Engin is a self-contained modular application.
|
4
4
|
|
5
5
|
When ran the Engin takes care of the complete application lifecycle:
|
6
|
+
|
6
7
|
1. The Engin assembles all Invocations. Only Providers that are required to satisfy
|
7
8
|
the Invocations parameters are assembled.
|
8
9
|
2. All Invocations are run sequentially in the order they were passed in to the Engin.
|
@@ -71,7 +71,8 @@ considered idiomatic as they have more explicit semantics.
|
|
71
71
|
|
72
72
|
Sometimes you want to have logic that runs before the lifecycle startup occurs and after
|
73
73
|
the dependency graph is built, some examples might be:
|
74
|
-
|
74
|
+
|
75
|
+
- Pinging a server to check its healthy.
|
75
76
|
- Running database migrations.
|
76
77
|
- Configuring a logger.
|
77
78
|
|
Binary file
|
@@ -0,0 +1,174 @@
|
|
1
|
+
# FastAPI
|
2
|
+
|
3
|
+
Engin ships with a FastAPI integration that is available under the `engin.ext.fastapi`
|
4
|
+
module. The integration allows one to write idiomatic FastAPI code whilst leveraging Engin
|
5
|
+
for Dependency Injection and modularising the application.
|
6
|
+
|
7
|
+
!!! note
|
8
|
+
|
9
|
+
There is also a
|
10
|
+
[fastapi example](https://github.com/invokermain/engin/tree/main/examples/fastapi) in
|
11
|
+
the Github repo if you want to see it in action.
|
12
|
+
|
13
|
+
|
14
|
+
## Setup
|
15
|
+
|
16
|
+
To run an empty FastAPI server with Engin, simply use the `FastAPIEngin` class and
|
17
|
+
provide an instance of a `FastAPI` application:
|
18
|
+
|
19
|
+
```python
|
20
|
+
from engin import Supply
|
21
|
+
from engin.ext.fastapi import FastAPIEngin
|
22
|
+
from fastapi import FastAPI
|
23
|
+
import uvicorn
|
24
|
+
|
25
|
+
app = FastAPIEngin(Supply(FastAPI()))
|
26
|
+
|
27
|
+
if __name__ == "__main__":
|
28
|
+
uvicorn.run(app)
|
29
|
+
```
|
30
|
+
|
31
|
+
The `FastAPIEngin` instance is just a thin wrapper on top of the FastAPI application and
|
32
|
+
exposes the normal ASGI application interface and therefore can be run by uvicorn or
|
33
|
+
other server implementations. Under the hood it will just pass calls to the `FastAPI`
|
34
|
+
instance that you provided.
|
35
|
+
|
36
|
+
|
37
|
+
!!!tip
|
38
|
+
|
39
|
+
It is also easy to integrate Engin with an existing FastAPI application by using the
|
40
|
+
`engin_to_lifespan` function in the `engin.ext.asgi` module.
|
41
|
+
|
42
|
+
|
43
|
+
## Dependency Injection
|
44
|
+
|
45
|
+
FastAPI comes with its own simple dependency injection system which allows you inject
|
46
|
+
depenendencies into a route by declaring a parameter with a special type hint of the form
|
47
|
+
`Annotated[T, Depends(func)]`. For example if we wanted to inject an instance of
|
48
|
+
`SomeClass`:
|
49
|
+
|
50
|
+
```python
|
51
|
+
async def make_some_class():
|
52
|
+
return SomeClass(a=1, b=2)
|
53
|
+
|
54
|
+
@app.get("/")
|
55
|
+
async def read_items(some_class: Annotated[SomeClass, Depends(make_some_class)]):
|
56
|
+
# do something with some_class
|
57
|
+
return "hello"
|
58
|
+
```
|
59
|
+
|
60
|
+
Engin ships with a similar marker, called `Inject`, which can be used to inject
|
61
|
+
dependencies it has providers for, for example if Engin provided `SomeClass` instead:
|
62
|
+
|
63
|
+
```python
|
64
|
+
@app.get("/")
|
65
|
+
async def read_items(some_class: Annotated[SomeClass, Inject(SomeClass)]):
|
66
|
+
# do something with some_class
|
67
|
+
return "hello"
|
68
|
+
```
|
69
|
+
|
70
|
+
The `Inject` marker can be used anywhere that `Depends` can be used. This can be useful as
|
71
|
+
FastAPI dependencies can have per request lifecycle, for example if we wanted to have a
|
72
|
+
reusable SQL session per request, we could use a nested dependency:
|
73
|
+
|
74
|
+
```python
|
75
|
+
from typing import Annotated, AsyncIterable
|
76
|
+
|
77
|
+
from engin.ext.fastapi import Inject
|
78
|
+
from fastapi import Depends
|
79
|
+
|
80
|
+
async def database_session(
|
81
|
+
database: Annotated[Database, Inject(Database)])
|
82
|
+
) -> AsyncIterable[Session]:
|
83
|
+
with database.new_session() as session:
|
84
|
+
yield session
|
85
|
+
session.commit()
|
86
|
+
|
87
|
+
@app.post("/{id}")
|
88
|
+
async def add_item(session: Annotated[Session, Depends(database_session)]):
|
89
|
+
session.add(MyORMModel(...))
|
90
|
+
```
|
91
|
+
|
92
|
+
|
93
|
+
## Attaching Routers to Engin
|
94
|
+
|
95
|
+
The usual way to declare an `APIRouter` is as a module level variable, for example:
|
96
|
+
|
97
|
+
```python title="api.py"
|
98
|
+
from fastapi import APIRouter
|
99
|
+
|
100
|
+
users_router = APIRouter(prefix="/users")
|
101
|
+
|
102
|
+
@users_router.get("/{user_id}")
|
103
|
+
def get_user(user_id: int) -> dict[str, Any]:
|
104
|
+
return {"id": user_id, "name": "Rakim"}
|
105
|
+
```
|
106
|
+
|
107
|
+
To attach this to our `FastAPIEngin`, we need to provide it. The recommended way to do
|
108
|
+
this is to use `Supply` as the router is already instantiated. We also need to add the
|
109
|
+
`APIRouter` to our `FastAPI` application, we can do this in the provider for `FastAPI`.
|
110
|
+
|
111
|
+
```python title="app.py"
|
112
|
+
from engin.ext.fastapi import FastAPIEngin
|
113
|
+
from fastapi import FastAPI
|
114
|
+
|
115
|
+
from api import users_router
|
116
|
+
|
117
|
+
def create_fastapi_app(api_routers: list[APIRouter]) -> FastAPI:
|
118
|
+
app = FastAPI()
|
119
|
+
|
120
|
+
for api_router in api_routers:
|
121
|
+
app.include_router(api_router)
|
122
|
+
|
123
|
+
return app
|
124
|
+
|
125
|
+
|
126
|
+
app = FastAPIEngin(Provide(create_fastapi_app), Supply([users_router]))
|
127
|
+
```
|
128
|
+
|
129
|
+
!!!info
|
130
|
+
|
131
|
+
Notice that the `users_router` is supplied in a list, as we want to be able to
|
132
|
+
support multiple APIRouters as our application grows.
|
133
|
+
|
134
|
+
Or similarly, we could use a block instead:
|
135
|
+
|
136
|
+
```python title="app.py"
|
137
|
+
from engin import Block, provide
|
138
|
+
from engin.ext.fastapi import FastAPIEngin
|
139
|
+
from fastapi import FastAPI
|
140
|
+
|
141
|
+
from api import users_router
|
142
|
+
|
143
|
+
class AppBlock(Block):
|
144
|
+
options = [Supply([users_router])]
|
145
|
+
|
146
|
+
@provide
|
147
|
+
def create_fastapi_app(self, api_routers: list[APIRouter]) -> FastAPI:
|
148
|
+
app = FastAPI()
|
149
|
+
|
150
|
+
for api_router in api_routers:
|
151
|
+
app.include_router(api_router)
|
152
|
+
|
153
|
+
return app
|
154
|
+
|
155
|
+
|
156
|
+
app = FastAPIEngin(AppBlock())
|
157
|
+
```
|
158
|
+
|
159
|
+
|
160
|
+
## Graphing Dependencies
|
161
|
+
|
162
|
+
Engin provides dependency visualisation functionality via the `engin-graph` script. When
|
163
|
+
working with a FastAPI application this can be used to visualise API Routes along with
|
164
|
+
their respective dependencies.
|
165
|
+
|
166
|
+
{ width="500", loading=lazy }
|
167
|
+
/// caption
|
168
|
+
Visualisation of the FastAPI example's dependency graph.
|
169
|
+
///
|
170
|
+
|
171
|
+
Note that due to the split between Engin's dependency injection framework and FastAPI's,
|
172
|
+
resolving API Routers and their dependencies is slightly harder for Engin. Due to this
|
173
|
+
there is currently a limitation where Engin will only be aware of APIRouters that have
|
174
|
+
been provided using `Supply` and not via `Provide` or `@provide` in a Block.
|
@@ -9,21 +9,13 @@ Engin is inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
|
|
9
9
|
|
10
10
|
- **Dependency Injection** - Engin includes a fully-featured Dependency Injection system,
|
11
11
|
powered by type hints.
|
12
|
-
- **
|
13
|
-
|
12
|
+
- **Applicaton Management** - Engin can run your whole application from start to end with
|
13
|
+
a simple call to `run()` including managing lifecycle startup and shutdown tasks.
|
14
14
|
- **Code Reuse** - Engin's modular components, called Blocks, work great as distributed
|
15
15
|
packages allowing zero boiler-plate code reuse across multiple applications. Perfect for
|
16
16
|
maintaining many services across your organisation.
|
17
17
|
- **Ecosystem Compatability** - Engin ships with integrations for popular frameworks that
|
18
|
-
provide their own Dependency Injection,
|
18
|
+
provide their own Dependency Injection, such as FastAPI, allowing you to integrate
|
19
19
|
Engin into existing code bases incrementally.
|
20
20
|
- **Async Native**: Engin is an async framework, meaning first class support for async
|
21
|
-
dependencies
|
22
|
-
|
23
|
-
## Installation
|
24
|
-
|
25
|
-
Engin is available on PyPI, install using your favourite dependency manager:
|
26
|
-
|
27
|
-
- **pip**:`pip install engin`
|
28
|
-
- **poetry**: `poetry add engin`
|
29
|
-
- **uv**: `uv add engin`
|
21
|
+
dependencies and applications, but can easily run synchronous code as well.
|
@@ -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)
|
@@ -5,6 +5,9 @@ site_url: !ENV READTHEDOCS_CANONICAL_URL
|
|
5
5
|
theme:
|
6
6
|
name: 'material'
|
7
7
|
custom_dir: 'docs/overrides'
|
8
|
+
features:
|
9
|
+
- navigation.instant
|
10
|
+
- navigation.tabs
|
8
11
|
palette:
|
9
12
|
- scheme: 'default'
|
10
13
|
media: '(prefers-color-scheme: light)'
|
@@ -22,6 +25,19 @@ repo_name: invokermain/engin
|
|
22
25
|
repo_url: https://github.com/invokermain/engin/
|
23
26
|
edit_uri: ""
|
24
27
|
|
28
|
+
nav:
|
29
|
+
- Home: "index.md"
|
30
|
+
- Getting Started: "getting-started.md"
|
31
|
+
- Concepts:
|
32
|
+
- Engin: "concepts/engin.md"
|
33
|
+
- Providers: "concepts/providers.md"
|
34
|
+
- Invocations: "concepts/invocations.md"
|
35
|
+
- Lifecycle: "concepts/lifecycle.md"
|
36
|
+
- Guides:
|
37
|
+
- Dependency Injection: "guides/dependency_injection.md"
|
38
|
+
- FastAPI: "guides/fastapi.md"
|
39
|
+
- Reference: "reference.md"
|
40
|
+
|
25
41
|
extra_javascript:
|
26
42
|
- js/readthedocs.js
|
27
43
|
|
@@ -39,6 +55,7 @@ plugins:
|
|
39
55
|
ignore_init_summary: true
|
40
56
|
merge_init_into_class: true
|
41
57
|
show_signature_annotations: true
|
58
|
+
show_source: false
|
42
59
|
signature_crossrefs: true
|
43
60
|
import:
|
44
61
|
- url: https://docs.python.org/3/objects.inv
|
@@ -48,6 +65,11 @@ watch:
|
|
48
65
|
- src
|
49
66
|
|
50
67
|
markdown_extensions:
|
68
|
+
- admonition
|
69
|
+
- attr_list
|
70
|
+
- md_in_html
|
71
|
+
- pymdownx.blocks.caption
|
72
|
+
- pymdownx.details
|
51
73
|
- pymdownx.highlight:
|
52
74
|
anchor_linenums: true
|
53
75
|
line_spans: __span
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "engin"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.10"
|
4
4
|
description = "An async-first modular application framework"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
@@ -100,6 +100,7 @@ check.sequence = [
|
|
100
100
|
]
|
101
101
|
|
102
102
|
fix.default_item_type = "cmd"
|
103
|
-
fix.sequence = ["ruff check src tests --fix"]
|
103
|
+
fix.sequence = ["ruff check src tests examples --fix"]
|
104
104
|
|
105
105
|
test = "pytest -s tests"
|
106
|
+
docs = "mkdocs serve"
|
@@ -152,6 +152,8 @@ class Assembler:
|
|
152
152
|
if type_id.multi:
|
153
153
|
LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
|
154
154
|
providers = [(Supply([], type_hint=list[type_id.type]))] # type: ignore[name-defined]
|
155
|
+
# store default to prevent the warning appearing multiple times
|
156
|
+
self._multiproviders[type_id] = providers
|
155
157
|
else:
|
156
158
|
raise LookupError(f"No Provider registered for dependency '{type_id}'")
|
157
159
|
|
@@ -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:
|
@@ -31,13 +31,23 @@ class Dependency(ABC, Generic[P, T]):
|
|
31
31
|
self._block_name = block_name
|
32
32
|
|
33
33
|
@property
|
34
|
-
def
|
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
|
+
"""
|
35
41
|
return self._func.__module__
|
36
42
|
|
37
43
|
@property
|
38
44
|
def block_name(self) -> str | None:
|
39
45
|
return self._block_name
|
40
46
|
|
47
|
+
@property
|
48
|
+
def func_name(self) -> str:
|
49
|
+
return self._func.__name__
|
50
|
+
|
41
51
|
@property
|
42
52
|
def name(self) -> str:
|
43
53
|
if self._block_name:
|
@@ -105,6 +115,10 @@ class Entrypoint(Invoke):
|
|
105
115
|
self._type = type_
|
106
116
|
super().__init__(invocation=_noop, block_name=block_name)
|
107
117
|
|
118
|
+
@property
|
119
|
+
def origin(self) -> str:
|
120
|
+
return self._type.__module__
|
121
|
+
|
108
122
|
@property
|
109
123
|
def parameter_types(self) -> list[TypeId]:
|
110
124
|
return [type_id_of(self._type)]
|
@@ -172,6 +186,10 @@ class Supply(Provide, Generic[T]):
|
|
172
186
|
self._get_val.__annotations__["return"] = type_hint
|
173
187
|
super().__init__(builder=self._get_val, block_name=block_name)
|
174
188
|
|
189
|
+
@property
|
190
|
+
def origin(self) -> str:
|
191
|
+
return self._value.__module__
|
192
|
+
|
175
193
|
@property
|
176
194
|
def return_type(self) -> type[T]:
|
177
195
|
if self._type_hint is not None:
|
@@ -39,6 +39,7 @@ class Engin:
|
|
39
39
|
but for advanced usecases it can be easier to use the `start` and `stop` methods.
|
40
40
|
|
41
41
|
When ran the Engin takes care of the complete application lifecycle:
|
42
|
+
|
42
43
|
1. The Engin assembles all Invocations. Only Providers that are required to satisfy
|
43
44
|
the Invoke options parameters are assembled.
|
44
45
|
2. All Invocations are run sequentially in the order they were passed in to the Engin.
|
@@ -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
|
@@ -1,11 +1,13 @@
|
|
1
1
|
import traceback
|
2
|
-
from collections.abc import Awaitable, Callable, MutableMapping
|
2
|
+
from collections.abc import AsyncIterator, Awaitable, Callable, MutableMapping
|
3
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
3
4
|
from typing import Any, ClassVar, Protocol, TypeAlias
|
4
5
|
|
5
|
-
from engin import Engin, Option
|
6
|
+
from engin import Engin, Entrypoint, Option
|
6
7
|
|
7
|
-
__all__ = ["ASGIEngin", "ASGIType"]
|
8
|
+
__all__ = ["ASGIEngin", "ASGIType", "engin_to_lifespan"]
|
8
9
|
|
10
|
+
from engin._graph import DependencyGrapher, Node
|
9
11
|
|
10
12
|
_Scope: TypeAlias = MutableMapping[str, Any]
|
11
13
|
_Message: TypeAlias = MutableMapping[str, Any]
|
@@ -49,6 +51,10 @@ class ASGIEngin(Engin, ASGIType):
|
|
49
51
|
await self.start()
|
50
52
|
self._asgi_app = await self._assembler.get(self._asgi_type)
|
51
53
|
|
54
|
+
def graph(self) -> list[Node]:
|
55
|
+
grapher = DependencyGrapher({**self._providers, **self._multiproviders})
|
56
|
+
return grapher.resolve([Entrypoint(self._asgi_type), *self._invocations])
|
57
|
+
|
52
58
|
|
53
59
|
class _Rereceive:
|
54
60
|
def __init__(self, message: _Message) -> None:
|
@@ -56,3 +62,26 @@ class _Rereceive:
|
|
56
62
|
|
57
63
|
async def __call__(self, *args: Any, **kwargs: Any) -> _Message:
|
58
64
|
return self._message
|
65
|
+
|
66
|
+
|
67
|
+
def engin_to_lifespan(engin: Engin) -> Callable[[ASGIType], AbstractAsyncContextManager[None]]:
|
68
|
+
"""
|
69
|
+
Transforms the Engin instance into an ASGI lifespan task.
|
70
|
+
|
71
|
+
This is to enable users to use the Engin framework with existing ASGI applications,
|
72
|
+
where it is not desired to replace the ASGI application with an ASGIEngin.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
engin: the engin instance to transform.
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
An ASGI lifespan task.
|
79
|
+
"""
|
80
|
+
|
81
|
+
@asynccontextmanager
|
82
|
+
async def engin_lifespan(_: ASGIType) -> AsyncIterator[None]:
|
83
|
+
await engin.start()
|
84
|
+
yield
|
85
|
+
await engin.stop()
|
86
|
+
|
87
|
+
return engin_lifespan
|
@@ -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}"
|
@@ -8,8 +8,10 @@ from http.server import BaseHTTPRequestHandler
|
|
8
8
|
from time import sleep
|
9
9
|
from typing import Any
|
10
10
|
|
11
|
-
from engin import Engin
|
12
|
-
from engin._dependency import Dependency, Provide
|
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
|
13
15
|
|
14
16
|
# mute logging from importing of files + engin's debug logging.
|
15
17
|
logging.disable()
|
@@ -18,9 +20,6 @@ args = ArgumentParser(
|
|
18
20
|
prog="engin-graph",
|
19
21
|
description="Creates a visualisation of your application's dependencies",
|
20
22
|
)
|
21
|
-
args.add_argument(
|
22
|
-
"-e", "--exclude", help="a list of packages or module to exclude", default=["engin"]
|
23
|
-
)
|
24
23
|
args.add_argument(
|
25
24
|
"app",
|
26
25
|
help=(
|
@@ -37,7 +36,6 @@ def serve_graph() -> None:
|
|
37
36
|
parsed = args.parse_args()
|
38
37
|
|
39
38
|
app = parsed.app
|
40
|
-
excluded_modules = parsed.exclude
|
41
39
|
|
42
40
|
try:
|
43
41
|
module_name, engin_name = app.split(":", maxsplit=1)
|
@@ -60,13 +58,19 @@ def serve_graph() -> None:
|
|
60
58
|
|
61
59
|
# transform dependencies into mermaid syntax
|
62
60
|
dependencies = [
|
63
|
-
f"{_render_node(node
|
61
|
+
f"{_render_node(node.parent)} --> {_render_node(node.node)}"
|
64
62
|
for node in nodes
|
65
|
-
if node
|
66
|
-
and not _should_exclude(node["node"].module, excluded_modules)
|
63
|
+
if node.parent is not None
|
67
64
|
]
|
68
65
|
|
69
|
-
html =
|
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
|
+
)
|
70
74
|
|
71
75
|
class Handler(BaseHTTPRequestHandler):
|
72
76
|
def do_GET(self) -> None:
|
@@ -93,24 +97,66 @@ def serve_graph() -> None:
|
|
93
97
|
print("Exiting the server...")
|
94
98
|
|
95
99
|
|
100
|
+
_BLOCK_IDX: dict[str, int] = {}
|
101
|
+
_SEEN_BLOCKS: list[str] = []
|
102
|
+
|
103
|
+
|
96
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}'
|
97
120
|
if isinstance(node, Provide):
|
98
|
-
|
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}'
|
99
133
|
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)
|
134
|
+
return f'{node_id}["`{node.name}`"]{style}'
|
105
135
|
|
106
136
|
|
107
137
|
_GRAPH_HTML = """
|
108
138
|
<!doctype html>
|
109
139
|
<html lang="en">
|
110
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>
|
111
149
|
<pre class="mermaid">
|
112
150
|
graph TD
|
113
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;
|
114
160
|
</pre>
|
115
161
|
<script type="module">
|
116
162
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
@@ -120,3 +166,9 @@ _GRAPH_HTML = """
|
|
120
166
|
</body>
|
121
167
|
</html>
|
122
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]]"
|
engin-0.0.8/docs/engin.md
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
::: engin.Engin
|
@@ -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)
|
engin-0.0.8/src/engin/_graph.py
DELETED
@@ -1,39 +0,0 @@
|
|
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
|
@@ -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
|