engin 0.0.18__tar.gz → 0.0.20__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.20/.github/workflows/benchmark.yaml +38 -0
- {engin-0.0.18 → engin-0.0.20}/.github/workflows/check.yaml +6 -1
- {engin-0.0.18 → engin-0.0.20}/CHANGELOG.md +31 -0
- {engin-0.0.18 → engin-0.0.20}/PKG-INFO +1 -1
- engin-0.0.20/docs/concepts/blocks.md +82 -0
- {engin-0.0.18 → engin-0.0.20}/docs/guides/fastapi.md +21 -15
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/app.py +1 -1
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/main.py +1 -1
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/main.py +1 -1
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/cats/api.py +1 -1
- {engin-0.0.18 → engin-0.0.20}/mkdocs.yaml +1 -0
- {engin-0.0.18 → engin-0.0.20}/pyproject.toml +29 -3
- {engin-0.0.18 → engin-0.0.20}/src/engin/__init__.py +0 -4
- {engin-0.0.18 → engin-0.0.20}/src/engin/_assembler.py +24 -17
- {engin-0.0.18 → engin-0.0.20}/src/engin/_block.py +16 -4
- {engin-0.0.18 → engin-0.0.20}/src/engin/_cli/_graph.py +2 -2
- {engin-0.0.18 → engin-0.0.20}/src/engin/_dependency.py +41 -30
- {engin-0.0.18 → engin-0.0.20}/src/engin/_type_utils.py +2 -2
- engin-0.0.18/src/engin/_exceptions.py → engin-0.0.20/src/engin/exceptions.py +27 -1
- {engin-0.0.18/src/engin/ext → engin-0.0.20/src/engin/extensions}/fastapi.py +1 -1
- {engin-0.0.18 → engin-0.0.20}/tests/acceptance/test_error_in_shutdown.py +1 -1
- {engin-0.0.18 → engin-0.0.20}/tests/acceptance/test_error_in_start_up.py +1 -1
- {engin-0.0.18 → engin-0.0.20}/tests/acceptance/test_fastapi.py +27 -5
- engin-0.0.20/tests/benchmarks/conftest.py +21 -0
- engin-0.0.20/tests/benchmarks/test_bench_assembler.py +114 -0
- {engin-0.0.18 → engin-0.0.20}/tests/cli/test_graph.py +23 -2
- {engin-0.0.18 → engin-0.0.20}/tests/cli/test_inspect.py +4 -1
- engin-0.0.20/tests/conftest.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/tests/deps.py +8 -1
- {engin-0.0.18 → engin-0.0.20}/tests/test_assembler.py +28 -13
- engin-0.0.20/tests/test_block.py +54 -0
- {engin-0.0.18 → engin-0.0.20}/tests/test_dependencies.py +25 -16
- {engin-0.0.18 → engin-0.0.20}/tests/test_engin.py +2 -1
- engin-0.0.18/tests/test_utils.py → engin-0.0.20/tests/test_type_id.py +1 -1
- engin-0.0.20/uv.lock +1286 -0
- engin-0.0.18/tests/test_block.py +0 -25
- engin-0.0.18/uv.lock +0 -1201
- {engin-0.0.18 → engin-0.0.20}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.18 → engin-0.0.20}/.gitignore +0 -0
- {engin-0.0.18 → engin-0.0.20}/.readthedocs.yaml +0 -0
- {engin-0.0.18 → engin-0.0.20}/LICENSE +0 -0
- {engin-0.0.18 → engin-0.0.20}/README.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/concepts/engin.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/concepts/invocations.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/concepts/lifecycle.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/concepts/providers.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/getting-started.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/guides/fastapi-graph.png +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/index.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/js/readthedocs.js +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/overrides/main.html +0 -0
- {engin-0.0.18 → engin-0.0.20}/docs/reference.md +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/app.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/simple/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/examples/simple/main.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_cli/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_cli/_common.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_cli/_graph.html +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_cli/_inspect.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_engin.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_graph.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_introspect.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_lifecycle.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/_option.py +0 -0
- {engin-0.0.18/src/engin/ext → engin-0.0.20/src/engin/extensions}/__init__.py +0 -0
- {engin-0.0.18/src/engin/ext → engin-0.0.20/src/engin/extensions}/asgi.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/src/engin/py.typed +0 -0
- {engin-0.0.18 → engin-0.0.20}/tests/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/tests/acceptance/__init__.py +0 -0
- {engin-0.0.18/tests/cli → engin-0.0.20/tests/benchmarks}/__init__.py +0 -0
- /engin-0.0.18/tests/conftest.py → /engin-0.0.20/tests/cli/__init__.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/tests/test_graph.py +0 -0
- {engin-0.0.18 → engin-0.0.20}/tests/test_lifecycle.py +0 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
name: Benchmark Main
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [main]
|
6
|
+
|
7
|
+
jobs:
|
8
|
+
benchmark-main:
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
|
11
|
+
steps:
|
12
|
+
- uses: actions/checkout@v4
|
13
|
+
|
14
|
+
- name: Install uv
|
15
|
+
uses: astral-sh/setup-uv@v5
|
16
|
+
with:
|
17
|
+
enable-cache: true
|
18
|
+
cache-dependency-glob: "uv.lock"
|
19
|
+
|
20
|
+
- name: Set up Python
|
21
|
+
uses: actions/setup-python@v5
|
22
|
+
with:
|
23
|
+
python-version-file: "pyproject.toml"
|
24
|
+
|
25
|
+
- name: Install the project
|
26
|
+
run: uv sync --dev
|
27
|
+
|
28
|
+
- name: Run benchmark
|
29
|
+
run: |
|
30
|
+
uv run pytest tests --benchmark-only --benchmark-json bench.json
|
31
|
+
|
32
|
+
- name: Store benchmark result
|
33
|
+
uses: benchmark-action/github-action-benchmark@v1
|
34
|
+
with:
|
35
|
+
tool: 'pytest'
|
36
|
+
output-file-path: bench.json
|
37
|
+
github-token: ${{ secrets.PAT }}
|
38
|
+
auto-push: true
|
@@ -6,6 +6,37 @@ 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.20] - 2025-06-18
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
|
13
|
+
- Improved string representation of Provide & Supply to make error messages more helpful.
|
14
|
+
|
15
|
+
### Fixed
|
16
|
+
|
17
|
+
- Engin now correctly supports postponed evaluation of annotations, e.g. `x: "MyType"` in
|
18
|
+
a factory function.
|
19
|
+
|
20
|
+
|
21
|
+
## [0.0.19] - 2025-04-27
|
22
|
+
|
23
|
+
### Added
|
24
|
+
|
25
|
+
- A new exception: `InvalidBlockError`.
|
26
|
+
|
27
|
+
### Changed
|
28
|
+
|
29
|
+
- Improved performance of Provide & Assembler by a factor of >2x (in certain scenarios).
|
30
|
+
- Renamed the `ext` subpackage to `extensions`.
|
31
|
+
- Errors are now imported from `engin.exceptions.*` instead of `engin.*`
|
32
|
+
- Blocks will now raise an `InvalidBlockError` if the block has methods which are not
|
33
|
+
decorated with `@provide` & `@invoke`.
|
34
|
+
|
35
|
+
### Fixed
|
36
|
+
|
37
|
+
- `Assembler.add` incorrect cache invalidation logic.
|
38
|
+
|
39
|
+
|
9
40
|
## [0.0.18] - 2025-04-25
|
10
41
|
|
11
42
|
### Added
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Blocks
|
2
|
+
|
3
|
+
A Block is a collection of options defined as a class.
|
4
|
+
|
5
|
+
Blocks are useful for grouping related options, as they can then be passed to the Engin in
|
6
|
+
one go helping manage complex applications.
|
7
|
+
|
8
|
+
Blocks are preferred over other data structures such as a list of options as they
|
9
|
+
integrate with other functionality in the rest of the framework.
|
10
|
+
|
11
|
+
|
12
|
+
## Defining an empty Block
|
13
|
+
|
14
|
+
A Block is just a class that inherits from `Block`:
|
15
|
+
|
16
|
+
```python
|
17
|
+
from engin import Engin, Block, provide, invoke
|
18
|
+
|
19
|
+
|
20
|
+
class ExampleBlock(Block):
|
21
|
+
...
|
22
|
+
```
|
23
|
+
|
24
|
+
## Adding options to the Block
|
25
|
+
|
26
|
+
Blocks have a class attribute named `options` which can be used to include existing
|
27
|
+
options.
|
28
|
+
|
29
|
+
```python
|
30
|
+
from engin import Engin, Block, Invoke, Provide, Supply
|
31
|
+
|
32
|
+
|
33
|
+
def print_string(self, string: str) -> None:
|
34
|
+
print(string)
|
35
|
+
|
36
|
+
|
37
|
+
class ExampleBlock(Block):
|
38
|
+
options = [
|
39
|
+
Supply("hello"),
|
40
|
+
Invoke(print_string)
|
41
|
+
]
|
42
|
+
|
43
|
+
|
44
|
+
# register it as a provider with the Engin
|
45
|
+
engin = Engin(ExampleBlock())
|
46
|
+
|
47
|
+
await engin.run() # prints 'hello'
|
48
|
+
```
|
49
|
+
|
50
|
+
!!!tip
|
51
|
+
|
52
|
+
Blocks are themselves valid options, so Blocks can include other Blocks as options. This
|
53
|
+
compisitional approach can help you build and manage larger applications.
|
54
|
+
|
55
|
+
|
56
|
+
## Defining Providers & Invocations in the Block
|
57
|
+
|
58
|
+
Engin ships two decorators: `@provide` & `@invoke` that can be used to define providers
|
59
|
+
and invocations within a Block as methods. These decorators mirror the signature of their
|
60
|
+
respective classes `Provide` & `Invoke`.
|
61
|
+
|
62
|
+
|
63
|
+
```python
|
64
|
+
from engin import Engin, Block, provide, invoke
|
65
|
+
|
66
|
+
|
67
|
+
# this block is equivalent to the one in the example above
|
68
|
+
class ExampleBlock(Block):
|
69
|
+
@provide
|
70
|
+
def string_factory() -> str:
|
71
|
+
return "hello"
|
72
|
+
|
73
|
+
@invoke
|
74
|
+
def print_string(self, string: str) -> None:
|
75
|
+
print(string)
|
76
|
+
```
|
77
|
+
|
78
|
+
!!!note
|
79
|
+
|
80
|
+
The `self` parameter in these methods is replaced with an empty object at runtime so
|
81
|
+
should not be used. Blocks do not need to be instantiated to be passed to Engin as an
|
82
|
+
option.
|
@@ -18,7 +18,7 @@ provide an instance of a `FastAPI` application:
|
|
18
18
|
|
19
19
|
```python
|
20
20
|
from engin import Supply
|
21
|
-
from engin.
|
21
|
+
from engin.extensions.fastapi import FastAPIEngin
|
22
22
|
from fastapi import FastAPI
|
23
23
|
import uvicorn
|
24
24
|
|
@@ -74,19 +74,23 @@ reusable SQL session per request, we could use a nested dependency:
|
|
74
74
|
```python
|
75
75
|
from typing import Annotated, AsyncIterable
|
76
76
|
|
77
|
-
from engin.
|
77
|
+
from engin.extensions.fastapi import Inject
|
78
78
|
from fastapi import Depends
|
79
79
|
|
80
|
+
|
80
81
|
async def database_session(
|
81
|
-
|
82
|
-
|
82
|
+
database: Annotated[Database, Inject(Database)])
|
83
|
+
|
84
|
+
) -> AsyncIterable[Session]:
|
83
85
|
with database.new_session() as session:
|
84
86
|
yield session
|
85
|
-
|
87
|
+
session.commit()
|
88
|
+
|
89
|
+
@ app.post("/{id}")
|
90
|
+
async
|
86
91
|
|
87
|
-
|
88
|
-
|
89
|
-
session.add(MyORMModel(...))
|
92
|
+
def add_item(session: Annotated[Session, Depends(database_session)]):
|
93
|
+
session.add(MyORMModel(...))
|
90
94
|
```
|
91
95
|
|
92
96
|
|
@@ -109,17 +113,18 @@ this is to use `Supply` as the router is already instantiated. We also need to a
|
|
109
113
|
`APIRouter` to our `FastAPI` application, we can do this in the provider for `FastAPI`.
|
110
114
|
|
111
115
|
```python title="app.py"
|
112
|
-
from engin.
|
116
|
+
from engin.extensions.fastapi import FastAPIEngin
|
113
117
|
from fastapi import FastAPI
|
114
118
|
|
115
119
|
from api import users_router
|
116
120
|
|
121
|
+
|
117
122
|
def create_fastapi_app(api_routers: list[APIRouter]) -> FastAPI:
|
118
123
|
app = FastAPI()
|
119
|
-
|
124
|
+
|
120
125
|
for api_router in api_routers:
|
121
126
|
app.include_router(api_router)
|
122
|
-
|
127
|
+
|
123
128
|
return app
|
124
129
|
|
125
130
|
|
@@ -135,21 +140,22 @@ Or similarly, we could use a block instead:
|
|
135
140
|
|
136
141
|
```python title="app.py"
|
137
142
|
from engin import Block, provide
|
138
|
-
from engin.
|
143
|
+
from engin.extensions.fastapi import FastAPIEngin
|
139
144
|
from fastapi import FastAPI
|
140
145
|
|
141
146
|
from api import users_router
|
142
147
|
|
148
|
+
|
143
149
|
class AppBlock(Block):
|
144
150
|
options = [Supply([users_router])]
|
145
|
-
|
151
|
+
|
146
152
|
@provide
|
147
153
|
def create_fastapi_app(self, api_routers: list[APIRouter]) -> FastAPI:
|
148
154
|
app = FastAPI()
|
149
|
-
|
155
|
+
|
150
156
|
for api_router in api_routers:
|
151
157
|
app.include_router(api_router)
|
152
|
-
|
158
|
+
|
153
159
|
return app
|
154
160
|
|
155
161
|
|
@@ -3,7 +3,7 @@ import logging
|
|
3
3
|
import uvicorn
|
4
4
|
|
5
5
|
from engin import Supply
|
6
|
-
from engin.
|
6
|
+
from engin.extensions.asgi import ASGIEngin
|
7
7
|
from examples.asgi.app import AppBlock, AppConfig
|
8
8
|
from examples.asgi.common.db.block import DatabaseBlock
|
9
9
|
from examples.asgi.features.cats.block import CatBlock
|
@@ -3,7 +3,7 @@ import logging
|
|
3
3
|
import uvicorn
|
4
4
|
|
5
5
|
from engin import Supply
|
6
|
-
from engin.
|
6
|
+
from engin.extensions.fastapi import FastAPIEngin
|
7
7
|
from examples.fastapi.app import AppBlock, AppConfig
|
8
8
|
from examples.fastapi.routes.cats.block import CatBlock
|
9
9
|
|
@@ -3,7 +3,7 @@ from typing import Annotated
|
|
3
3
|
from fastapi import APIRouter
|
4
4
|
from pydantic import BaseModel
|
5
5
|
|
6
|
-
from engin.
|
6
|
+
from engin.extensions.fastapi import Inject
|
7
7
|
from examples.fastapi.routes.cats.domain import Cat, CatPersonality
|
8
8
|
from examples.fastapi.routes.cats.ports import CatRepository
|
9
9
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "engin"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.20"
|
4
4
|
description = "An async-first modular application framework"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
@@ -37,6 +37,8 @@ dev = [
|
|
37
37
|
"pytest-cov>=6.0.0",
|
38
38
|
"typer>=0.15.2",
|
39
39
|
"pytest-mock>=3.14.0",
|
40
|
+
"pytest-benchmark>=5.1.0",
|
41
|
+
"websockets>=15.0.1",
|
40
42
|
]
|
41
43
|
docs = [
|
42
44
|
"mkdocs-material>=9.5.50",
|
@@ -87,6 +89,29 @@ asyncio_default_fixture_loop_scope = "session"
|
|
87
89
|
source = ["src"]
|
88
90
|
omit = ["src/engin/scripts/**"]
|
89
91
|
|
92
|
+
[tool.coverage.report]
|
93
|
+
precision = 1
|
94
|
+
skip_covered = true
|
95
|
+
exclude_lines = [
|
96
|
+
"pragma: no cover",
|
97
|
+
"abc.abstractmethod",
|
98
|
+
"if TYPE_CHECKING.*:",
|
99
|
+
"if _t.TYPE_CHECKING:",
|
100
|
+
"if t.TYPE_CHECKING:",
|
101
|
+
"@overload",
|
102
|
+
'class .*\bProtocol\b.*\):',
|
103
|
+
"raise NotImplementedError",
|
104
|
+
]
|
105
|
+
partial_branches = [
|
106
|
+
"pragma: no branch",
|
107
|
+
"if not TYPE_CHECKING:",
|
108
|
+
"if not _t.TYPE_CHECKING:",
|
109
|
+
"if not t.TYPE_CHECKING:",
|
110
|
+
"if .* or not TYPE_CHECKING:",
|
111
|
+
"if .* or not _t.TYPE_CHECKING:",
|
112
|
+
"if .* or not t.TYPE_CHECKING:",
|
113
|
+
]
|
114
|
+
|
90
115
|
|
91
116
|
[tool.mypy]
|
92
117
|
strict = true
|
@@ -112,6 +137,7 @@ check.sequence = [
|
|
112
137
|
fix.default_item_type = "cmd"
|
113
138
|
fix.sequence = ["ruff check src tests examples --fix"]
|
114
139
|
|
115
|
-
test = "pytest tests"
|
116
|
-
ci-test = "pytest --cov=engin --cov-branch --cov-report=xml tests"
|
140
|
+
test = "pytest tests --benchmark-skip"
|
141
|
+
ci-test = "pytest --cov=engin --cov-branch --cov-report=xml tests --benchmark-skip"
|
142
|
+
bench = "pytest tests --benchmark-only"
|
117
143
|
docs = "mkdocs serve"
|
@@ -1,9 +1,7 @@
|
|
1
|
-
from engin import ext
|
2
1
|
from engin._assembler import Assembler
|
3
2
|
from engin._block import Block, invoke, provide
|
4
3
|
from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
5
4
|
from engin._engin import Engin
|
6
|
-
from engin._exceptions import ProviderError
|
7
5
|
from engin._lifecycle import Lifecycle
|
8
6
|
from engin._option import Option
|
9
7
|
from engin._type_utils import TypeId
|
@@ -17,10 +15,8 @@ __all__ = [
|
|
17
15
|
"Lifecycle",
|
18
16
|
"Option",
|
19
17
|
"Provide",
|
20
|
-
"ProviderError",
|
21
18
|
"Supply",
|
22
19
|
"TypeId",
|
23
|
-
"ext",
|
24
20
|
"invoke",
|
25
21
|
"provide",
|
26
22
|
]
|
@@ -9,8 +9,8 @@ from types import TracebackType
|
|
9
9
|
from typing import Any, Generic, TypeVar, cast
|
10
10
|
|
11
11
|
from engin._dependency import Dependency, Provide, Supply
|
12
|
-
from engin._exceptions import NotInScopeError, ProviderError
|
13
12
|
from engin._type_utils import TypeId
|
13
|
+
from engin.exceptions import NotInScopeError, ProviderError
|
14
14
|
|
15
15
|
LOG = logging.getLogger("engin")
|
16
16
|
|
@@ -65,6 +65,7 @@ class Assembler:
|
|
65
65
|
self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
|
66
66
|
self._assembled_outputs: dict[TypeId, Any] = {}
|
67
67
|
self._lock = asyncio.Lock()
|
68
|
+
self._graph_cache: dict[TypeId, set[Provide]] = defaultdict(set)
|
68
69
|
|
69
70
|
for provider in providers:
|
70
71
|
type_id = provider.return_type_id
|
@@ -179,8 +180,8 @@ class Assembler:
|
|
179
180
|
"""
|
180
181
|
Add a provider to the Assembler post-initialisation.
|
181
182
|
|
182
|
-
If this replaces an existing provider, this will clear
|
183
|
-
output
|
183
|
+
If this replaces an existing provider, this will clear all previously assembled
|
184
|
+
output. Note: multiproviders cannot be replaced, they are always appended.
|
184
185
|
|
185
186
|
Args:
|
186
187
|
provider: the Provide instance to add.
|
@@ -190,14 +191,13 @@ class Assembler:
|
|
190
191
|
"""
|
191
192
|
type_id = provider.return_type_id
|
192
193
|
if provider.is_multiprovider:
|
193
|
-
if type_id in self._assembled_outputs:
|
194
|
-
del self._assembled_outputs[type_id]
|
195
194
|
self._multiproviders[type_id].append(provider)
|
196
195
|
else:
|
197
|
-
if type_id in self._assembled_outputs:
|
198
|
-
del self._assembled_outputs[type_id]
|
199
196
|
self._providers[type_id] = provider
|
200
197
|
|
198
|
+
self._assembled_outputs.clear()
|
199
|
+
self._graph_cache.clear()
|
200
|
+
|
201
201
|
def scope(self, scope: str) -> "_ScopeContextManager":
|
202
202
|
return _ScopeContextManager(scope=scope, assembler=self)
|
203
203
|
|
@@ -206,13 +206,14 @@ class Assembler:
|
|
206
206
|
if provider.scope == scope:
|
207
207
|
self._assembled_outputs.pop(type_id, None)
|
208
208
|
|
209
|
-
def _resolve_providers(self, type_id: TypeId) ->
|
209
|
+
def _resolve_providers(self, type_id: TypeId, resolved: set[TypeId]) -> set[Provide]:
|
210
210
|
"""
|
211
211
|
Resolves the chain of providers required to satisfy the provider of a given type.
|
212
212
|
Ordering of the return value is very important!
|
213
|
-
|
214
|
-
# TODO: performance optimisation, do not recurse for already satisfied providers?
|
215
213
|
"""
|
214
|
+
if type_id in self._graph_cache:
|
215
|
+
return self._graph_cache[type_id]
|
216
|
+
|
216
217
|
if type_id.multi:
|
217
218
|
root_providers = self._multiproviders.get(type_id)
|
218
219
|
else:
|
@@ -230,22 +231,28 @@ class Assembler:
|
|
230
231
|
raise LookupError(msg)
|
231
232
|
|
232
233
|
# providers that must be satisfied to satisfy the root level providers
|
233
|
-
|
234
|
+
resolved_providers = {
|
234
235
|
child_provider
|
235
236
|
for root_provider in root_providers
|
236
237
|
for root_provider_param in root_provider.parameter_type_ids
|
237
|
-
for child_provider in self._resolve_providers(root_provider_param)
|
238
|
-
|
239
|
-
|
238
|
+
for child_provider in self._resolve_providers(root_provider_param, resolved)
|
239
|
+
if root_provider_param not in resolved
|
240
|
+
} | set(root_providers)
|
241
|
+
|
242
|
+
resolved.add(type_id)
|
243
|
+
self._graph_cache[type_id] = resolved_providers
|
244
|
+
|
245
|
+
return resolved_providers
|
240
246
|
|
241
247
|
async def _satisfy(self, target: TypeId) -> None:
|
242
|
-
for provider in self._resolve_providers(target):
|
248
|
+
for provider in self._resolve_providers(target, set()):
|
243
249
|
if (
|
244
250
|
not provider.is_multiprovider
|
245
251
|
and provider.return_type_id in self._assembled_outputs
|
246
252
|
):
|
247
253
|
continue
|
248
254
|
type_id = provider.return_type_id
|
255
|
+
|
249
256
|
bound_args = await self._bind_arguments(provider.signature)
|
250
257
|
try:
|
251
258
|
value = await provider(*bound_args.args, **bound_args.kwargs)
|
@@ -253,6 +260,7 @@ class Assembler:
|
|
253
260
|
raise ProviderError(
|
254
261
|
provider=provider, error_type=type(err), error_message=str(err)
|
255
262
|
) from err
|
263
|
+
|
256
264
|
if provider.is_multiprovider:
|
257
265
|
if type_id in self._assembled_outputs:
|
258
266
|
self._assembled_outputs[type_id].extend(value)
|
@@ -269,8 +277,7 @@ class Assembler:
|
|
269
277
|
args.append(object())
|
270
278
|
continue
|
271
279
|
param_key = TypeId.from_type(param.annotation)
|
272
|
-
|
273
|
-
if not has_dependency:
|
280
|
+
if param_key not in self._assembled_outputs:
|
274
281
|
await self._satisfy(param_key)
|
275
282
|
val = self._assembled_outputs[param_key]
|
276
283
|
if param.kind == param.POSITIONAL_ONLY:
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, ClassVar
|
|
5
5
|
|
6
6
|
from engin._dependency import Dependency, Func, Invoke, Provide
|
7
7
|
from engin._option import Option
|
8
|
+
from engin.exceptions import InvalidBlockError
|
8
9
|
|
9
10
|
if TYPE_CHECKING:
|
10
11
|
from engin._engin import Engin
|
@@ -42,7 +43,7 @@ def invoke(func_: Func | None = None) -> Func | Callable[[Func], Func]:
|
|
42
43
|
return _inner(func_)
|
43
44
|
|
44
45
|
|
45
|
-
class Block
|
46
|
+
class Block:
|
46
47
|
"""
|
47
48
|
A Block is a collection of providers and invocations.
|
48
49
|
|
@@ -74,7 +75,7 @@ class Block(Option):
|
|
74
75
|
|
75
76
|
@classmethod
|
76
77
|
def apply(cls, engin: "Engin") -> None:
|
77
|
-
block_name = cls.name or
|
78
|
+
block_name = cls.name or cls.__name__
|
78
79
|
for option in chain(cls.options, cls._method_options()):
|
79
80
|
if isinstance(option, Dependency):
|
80
81
|
option._block_name = block_name
|
@@ -82,8 +83,19 @@ class Block(Option):
|
|
82
83
|
|
83
84
|
@classmethod
|
84
85
|
def _method_options(cls) -> Iterable[Provide | Invoke]:
|
85
|
-
for
|
86
|
+
for name, method in inspect.getmembers(cls, inspect.isfunction):
|
86
87
|
if option := getattr(method, "_opt", None):
|
87
88
|
if not isinstance(option, Provide | Invoke):
|
88
|
-
raise
|
89
|
+
raise InvalidBlockError(
|
90
|
+
block=cls,
|
91
|
+
reason="Block option is not an instance of Provide or Invoke",
|
92
|
+
)
|
89
93
|
yield option
|
94
|
+
else:
|
95
|
+
raise InvalidBlockError(
|
96
|
+
block=cls,
|
97
|
+
reason=(
|
98
|
+
f"Method '{name}' is not a Provider or Invocation, did you "
|
99
|
+
"forget to decorate it?"
|
100
|
+
),
|
101
|
+
)
|
@@ -12,10 +12,10 @@ from rich import print
|
|
12
12
|
from engin import Entrypoint, Invoke, TypeId
|
13
13
|
from engin._cli._common import COMMON_HELP, get_engin_instance
|
14
14
|
from engin._dependency import Dependency, Provide, Supply
|
15
|
-
from engin.
|
15
|
+
from engin.extensions.asgi import ASGIEngin
|
16
16
|
|
17
17
|
try:
|
18
|
-
from engin.
|
18
|
+
from engin.extensions.fastapi import APIRouteDependency
|
19
19
|
except ImportError:
|
20
20
|
APIRouteDependency = None # type: ignore[assignment,misc]
|
21
21
|
|