engin 0.0.12__tar.gz → 0.0.14__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.12 → engin-0.0.14}/.readthedocs.yaml +1 -1
- {engin-0.0.12 → engin-0.0.14}/CHANGELOG.md +27 -0
- {engin-0.0.12 → engin-0.0.14}/PKG-INFO +3 -1
- {engin-0.0.12 → engin-0.0.14}/docs/concepts/engin.md +1 -1
- {engin-0.0.12 → engin-0.0.14}/docs/concepts/invocations.md +5 -8
- {engin-0.0.12 → engin-0.0.14}/docs/getting-started.md +1 -1
- {engin-0.0.12 → engin-0.0.14}/docs/guides/fastapi.md +1 -1
- engin-0.0.14/docs/js/readthedocs.js +7 -0
- {engin-0.0.12 → engin-0.0.14}/mkdocs.yaml +1 -1
- {engin-0.0.12 → engin-0.0.14}/pyproject.toml +16 -12
- {engin-0.0.12 → engin-0.0.14}/src/engin/__init__.py +4 -1
- {engin-0.0.12 → engin-0.0.14}/src/engin/_assembler.py +33 -29
- {engin-0.0.12 → engin-0.0.14}/src/engin/_block.py +26 -23
- engin-0.0.14/src/engin/_cli/__init__.py +13 -0
- engin-0.0.12/src/engin/scripts/graph.py → engin-0.0.14/src/engin/_cli/_graph.py +48 -31
- engin-0.0.14/src/engin/_cli/_utils.py +18 -0
- {engin-0.0.12 → engin-0.0.14}/src/engin/_dependency.py +28 -21
- {engin-0.0.12 → engin-0.0.14}/src/engin/_engin.py +13 -55
- engin-0.0.14/src/engin/_introspect.py +34 -0
- {engin-0.0.12 → engin-0.0.14}/src/engin/_lifecycle.py +68 -2
- engin-0.0.14/src/engin/_option.py +10 -0
- {engin-0.0.12 → engin-0.0.14}/src/engin/_type_utils.py +11 -12
- {engin-0.0.12 → engin-0.0.14}/src/engin/ext/asgi.py +2 -1
- {engin-0.0.12 → engin-0.0.14}/src/engin/ext/fastapi.py +9 -3
- engin-0.0.14/tests/cli/test_graph.py +39 -0
- {engin-0.0.12 → engin-0.0.14}/tests/test_assembler.py +44 -6
- engin-0.0.12/tests/test_modules.py → engin-0.0.14/tests/test_block.py +1 -1
- engin-0.0.14/tests/test_graph.py +33 -0
- engin-0.0.14/tests/test_lifecycle.py +82 -0
- engin-0.0.14/tests/test_utils.py +81 -0
- {engin-0.0.12 → engin-0.0.14}/uv.lock +193 -112
- engin-0.0.12/docs/js/readthedocs.js +0 -32
- engin-0.0.12/tests/test_utils.py +0 -62
- {engin-0.0.12 → engin-0.0.14}/.github/workflows/check.yaml +0 -0
- {engin-0.0.12 → engin-0.0.14}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.12 → engin-0.0.14}/.gitignore +0 -0
- {engin-0.0.12 → engin-0.0.14}/LICENSE +0 -0
- {engin-0.0.12 → engin-0.0.14}/README.md +0 -0
- {engin-0.0.12 → engin-0.0.14}/docs/concepts/lifecycle.md +0 -0
- {engin-0.0.12 → engin-0.0.14}/docs/concepts/providers.md +0 -0
- {engin-0.0.12 → engin-0.0.14}/docs/guides/dependency_injection.md +0 -0
- {engin-0.0.12 → engin-0.0.14}/docs/guides/fastapi-graph.png +0 -0
- {engin-0.0.12 → engin-0.0.14}/docs/index.md +0 -0
- {engin-0.0.12 → engin-0.0.14}/docs/overrides/main.html +0 -0
- {engin-0.0.12 → engin-0.0.14}/docs/reference.md +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/app.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/asgi/main.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/app.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/main.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/simple/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/examples/simple/main.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/src/engin/_exceptions.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/src/engin/_graph.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/src/engin/ext/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/src/engin/py.typed +0 -0
- {engin-0.0.12/src/engin/scripts → engin-0.0.14/tests}/__init__.py +0 -0
- {engin-0.0.12/tests → engin-0.0.14/tests/acceptance}/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/tests/acceptance/test_error_in_shutdown.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/tests/acceptance/test_error_in_start_up.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/tests/acceptance/test_fastapi.py +0 -0
- {engin-0.0.12/tests/acceptance → engin-0.0.14/tests/cli}/__init__.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/tests/conftest.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/tests/deps.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/tests/test_dependencies.py +0 -0
- {engin-0.0.12 → engin-0.0.14}/tests/test_engin.py +0 -0
@@ -6,6 +6,33 @@ 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.14] - 2025-03-23
|
10
|
+
|
11
|
+
### Added
|
12
|
+
|
13
|
+
- `LifecycleHook` class to help build simple lifecycles with a start and stop call.
|
14
|
+
- `TypeId` now attempts to introspect type aliases, this is experimental and currently on
|
15
|
+
used to enrich error messages.
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
|
19
|
+
- `engin-graph` has been replaced by `engin graph`.
|
20
|
+
- Engin now uses `typer` for an improved cli experience.
|
21
|
+
- The package now has an extra `cli` which must be installed to use the developer cli.
|
22
|
+
- `Assembler.add(...)` does not error when adding already registered providers.
|
23
|
+
- Use a more performant algorithm for inspecting frame stack.
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
|
27
|
+
- `ASGIEngin` now properly surfaces startup errors.
|
28
|
+
|
29
|
+
## [0.0.13] - 2025-03-22
|
30
|
+
|
31
|
+
### Changed
|
32
|
+
|
33
|
+
- `Provide` now supports union types.
|
34
|
+
|
35
|
+
|
9
36
|
## [0.0.12] - 2025-03-03
|
10
37
|
|
11
38
|
### Added
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: engin
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.14
|
4
4
|
Summary: An async-first modular application framework
|
5
5
|
Project-URL: Homepage, https://github.com/invokermain/engin
|
6
6
|
Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
|
@@ -10,6 +10,8 @@ License-Expression: MIT
|
|
10
10
|
License-File: LICENSE
|
11
11
|
Keywords: Application Framework,Dependency Injection
|
12
12
|
Requires-Python: >=3.10
|
13
|
+
Provides-Extra: cli
|
14
|
+
Requires-Dist: typer>=0.15; extra == 'cli'
|
13
15
|
Description-Content-Type: text/markdown
|
14
16
|
|
15
17
|
[](https://codecov.io/gh/invokermain/engin)
|
@@ -9,4 +9,4 @@ When ran the Engin takes care of the complete application lifecycle:
|
|
9
9
|
2. All Invocations are run sequentially in the order they were passed in to the Engin.
|
10
10
|
3. Any Lifecycle Startup defined by Provider that were assembled is ran.
|
11
11
|
4. The Engin waits for a stop signal, i.e. SIGINT or SIGTERM.
|
12
|
-
5. Any Lifecyce Shutdown
|
12
|
+
5. Any Lifecyce Shutdown tasks are run, in reverse order to the Startup order.
|
@@ -23,15 +23,12 @@ imports for brevity):
|
|
23
23
|
|
24
24
|
```python
|
25
25
|
def worker_factory(lifecycle: Lifecycle) -> Worker:
|
26
|
-
|
26
|
+
worker = Worker()
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
worker.shutdown()
|
33
|
-
|
34
|
-
lifecycle.append(worker_lifecycle)
|
28
|
+
lifecycle.hook(
|
29
|
+
on_start=worker.start,
|
30
|
+
on_stop=worker.shutdown
|
31
|
+
)
|
35
32
|
|
36
33
|
return worker
|
37
34
|
```
|
@@ -159,7 +159,7 @@ app = FastAPIEngin(AppBlock())
|
|
159
159
|
|
160
160
|
## Graphing Dependencies
|
161
161
|
|
162
|
-
Engin provides dependency visualisation functionality via the `engin
|
162
|
+
Engin provides dependency visualisation functionality via the `engin graph` script. When
|
163
163
|
working with a FastAPI application this can be used to visualise API Routes along with
|
164
164
|
their respective dependencies.
|
165
165
|
|
@@ -0,0 +1,7 @@
|
|
1
|
+
document.addEventListener("DOMContentLoaded", function(event) {
|
2
|
+
// Trigger Read the Docs' search addon instead of Material MkDocs default
|
3
|
+
document.querySelector(".md-search__input").addEventListener("focus", (e) => {
|
4
|
+
const event = new CustomEvent("readthedocs-search-show");
|
5
|
+
document.dispatchEvent(event);
|
6
|
+
});
|
7
|
+
});
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "engin"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.14"
|
4
4
|
description = "An async-first modular application framework"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
@@ -8,19 +8,21 @@ license = "MIT"
|
|
8
8
|
keywords = ["Dependency Injection", "Application Framework"]
|
9
9
|
dependencies = []
|
10
10
|
|
11
|
+
[project.optional-dependencies]
|
12
|
+
cli = ["typer>=0.15"]
|
13
|
+
|
14
|
+
[project.scripts]
|
15
|
+
engin = "engin._cli:app"
|
16
|
+
|
11
17
|
[project.urls]
|
12
18
|
Homepage = "https://github.com/invokermain/engin"
|
13
19
|
Documentation = "https://engin.readthedocs.io/en/latest/"
|
14
20
|
Repository = "https://github.com/invokermain/engin.git"
|
15
21
|
Changelog = "https://github.com/invokermain/engin/blob/main/CHANGELOG.md"
|
16
22
|
|
17
|
-
[build-system]
|
18
|
-
requires = ["hatchling"]
|
19
|
-
build-backend = "hatchling.build"
|
20
|
-
|
21
23
|
|
22
|
-
[
|
23
|
-
dev
|
24
|
+
[dependency-groups]
|
25
|
+
dev = [
|
24
26
|
"fastapi>=0.115.6",
|
25
27
|
"httpx>=0.27.2",
|
26
28
|
"mypy>=1",
|
@@ -33,18 +35,20 @@ dev-dependencies = [
|
|
33
35
|
"starlette>=0.39.2",
|
34
36
|
"uvicorn>=0.31.1",
|
35
37
|
"pytest-cov>=6.0.0",
|
38
|
+
"typer>=0.15.2",
|
39
|
+
"pytest-mock>=3.14.0",
|
36
40
|
]
|
37
|
-
|
38
|
-
|
39
|
-
[dependency-groups]
|
40
41
|
docs = [
|
41
42
|
"mkdocs-material>=9.5.50",
|
42
43
|
"mkdocstrings[python]>=0.27.0",
|
43
44
|
]
|
44
45
|
|
46
|
+
[tool.uv]
|
47
|
+
default-groups = ["dev", "docs"]
|
45
48
|
|
46
|
-
[
|
47
|
-
|
49
|
+
[build-system]
|
50
|
+
requires = ["hatchling"]
|
51
|
+
build-backend = "hatchling.build"
|
48
52
|
|
49
53
|
|
50
54
|
[tool.ruff]
|
@@ -2,9 +2,11 @@ from engin import ext
|
|
2
2
|
from engin._assembler import Assembler
|
3
3
|
from engin._block import Block, invoke, provide
|
4
4
|
from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
5
|
-
from engin._engin import Engin
|
5
|
+
from engin._engin import Engin
|
6
6
|
from engin._exceptions import ProviderError
|
7
7
|
from engin._lifecycle import Lifecycle
|
8
|
+
from engin._option import Option
|
9
|
+
from engin._type_utils import TypeId
|
8
10
|
|
9
11
|
__all__ = [
|
10
12
|
"Assembler",
|
@@ -17,6 +19,7 @@ __all__ = [
|
|
17
19
|
"Provide",
|
18
20
|
"ProviderError",
|
19
21
|
"Supply",
|
22
|
+
"TypeId",
|
20
23
|
"ext",
|
21
24
|
"invoke",
|
22
25
|
"provide",
|
@@ -8,7 +8,7 @@ from typing import Any, Generic, TypeVar, cast
|
|
8
8
|
|
9
9
|
from engin._dependency import Dependency, Provide, Supply
|
10
10
|
from engin._exceptions import ProviderError
|
11
|
-
from engin._type_utils import TypeId
|
11
|
+
from engin._type_utils import TypeId
|
12
12
|
|
13
13
|
LOG = logging.getLogger("engin")
|
14
14
|
|
@@ -54,8 +54,7 @@ class Assembler:
|
|
54
54
|
def __init__(self, providers: Iterable[Provide]) -> None:
|
55
55
|
self._providers: dict[TypeId, Provide[Any]] = {}
|
56
56
|
self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
|
57
|
-
self.
|
58
|
-
self._consumed_providers: set[Provide[Any]] = set()
|
57
|
+
self._assembled_outputs: dict[TypeId, Any] = {}
|
59
58
|
self._lock = asyncio.Lock()
|
60
59
|
|
61
60
|
for provider in providers:
|
@@ -106,13 +105,14 @@ class Assembler:
|
|
106
105
|
Returns:
|
107
106
|
The constructed value.
|
108
107
|
"""
|
109
|
-
type_id =
|
110
|
-
if type_id in self.
|
111
|
-
return cast(T, self.
|
108
|
+
type_id = TypeId.from_type(type_)
|
109
|
+
if type_id in self._assembled_outputs:
|
110
|
+
return cast("T", self._assembled_outputs[type_id])
|
112
111
|
if type_id.multi:
|
113
|
-
out = []
|
114
112
|
if type_id not in self._multiproviders:
|
115
113
|
raise LookupError(f"no provider found for target type id '{type_id}'")
|
114
|
+
|
115
|
+
out = []
|
116
116
|
for provider in self._multiproviders[type_id]:
|
117
117
|
assembled_dependency = await self.assemble(provider)
|
118
118
|
try:
|
@@ -123,11 +123,12 @@ class Assembler:
|
|
123
123
|
error_type=type(err),
|
124
124
|
error_message=str(err),
|
125
125
|
) from err
|
126
|
-
self.
|
126
|
+
self._assembled_outputs[type_id] = out
|
127
127
|
return out # type: ignore[return-value]
|
128
128
|
else:
|
129
129
|
if type_id not in self._providers:
|
130
130
|
raise LookupError(f"no provider found for target type id '{type_id}'")
|
131
|
+
|
131
132
|
assembled_dependency = await self.assemble(self._providers[type_id])
|
132
133
|
try:
|
133
134
|
value = await assembled_dependency()
|
@@ -137,7 +138,7 @@ class Assembler:
|
|
137
138
|
error_type=type(err),
|
138
139
|
error_message=str(err),
|
139
140
|
) from err
|
140
|
-
self.
|
141
|
+
self._assembled_outputs[type_id] = value
|
141
142
|
return value # type: ignore[return-value]
|
142
143
|
|
143
144
|
def has(self, type_: type[T]) -> bool:
|
@@ -150,7 +151,7 @@ class Assembler:
|
|
150
151
|
Returns:
|
151
152
|
True if the Assembler has a provider for type else False.
|
152
153
|
"""
|
153
|
-
type_id =
|
154
|
+
type_id = TypeId.from_type(type_)
|
154
155
|
if type_id.multi:
|
155
156
|
return type_id in self._multiproviders
|
156
157
|
else:
|
@@ -160,24 +161,23 @@ class Assembler:
|
|
160
161
|
"""
|
161
162
|
Add a provider to the Assembler post-initialisation.
|
162
163
|
|
164
|
+
If this replaces an existing provider, this will clear any previously assembled
|
165
|
+
output for the existing Provider.
|
166
|
+
|
163
167
|
Args:
|
164
168
|
provider: the Provide instance to add.
|
165
169
|
|
166
170
|
Returns:
|
167
171
|
None
|
168
|
-
|
169
|
-
Raises:
|
170
|
-
ValueError: if a provider for this type already exists.
|
171
172
|
"""
|
172
173
|
type_id = provider.return_type_id
|
173
174
|
if provider.is_multiprovider:
|
174
|
-
if type_id in self.
|
175
|
-
self.
|
176
|
-
|
177
|
-
self._multiproviders[type_id] = [provider]
|
175
|
+
if type_id in self._assembled_outputs:
|
176
|
+
del self._assembled_outputs[type_id]
|
177
|
+
self._multiproviders[type_id].append(provider)
|
178
178
|
else:
|
179
|
-
if type_id in self.
|
180
|
-
|
179
|
+
if type_id in self._assembled_outputs:
|
180
|
+
del self._assembled_outputs[type_id]
|
181
181
|
self._providers[type_id] = provider
|
182
182
|
|
183
183
|
def _resolve_providers(self, type_id: TypeId) -> Collection[Provide]:
|
@@ -192,7 +192,9 @@ class Assembler:
|
|
192
192
|
# store default to prevent the warning appearing multiple times
|
193
193
|
self._multiproviders[type_id] = providers
|
194
194
|
else:
|
195
|
-
|
195
|
+
available = sorted(str(k) for k in self._providers)
|
196
|
+
msg = f"Missing Provider for type '{type_id}', available: {available}"
|
197
|
+
raise LookupError(msg)
|
196
198
|
|
197
199
|
required_providers: list[Provide[Any]] = []
|
198
200
|
for provider in providers:
|
@@ -206,9 +208,11 @@ class Assembler:
|
|
206
208
|
|
207
209
|
async def _satisfy(self, target: TypeId) -> None:
|
208
210
|
for provider in self._resolve_providers(target):
|
209
|
-
if
|
211
|
+
if (
|
212
|
+
not provider.is_multiprovider
|
213
|
+
and provider.return_type_id in self._assembled_outputs
|
214
|
+
):
|
210
215
|
continue
|
211
|
-
self._consumed_providers.add(provider)
|
212
216
|
type_id = provider.return_type_id
|
213
217
|
bound_args = await self._bind_arguments(provider.signature)
|
214
218
|
try:
|
@@ -218,12 +222,12 @@ class Assembler:
|
|
218
222
|
provider=provider, error_type=type(err), error_message=str(err)
|
219
223
|
) from err
|
220
224
|
if provider.is_multiprovider:
|
221
|
-
if type_id in self.
|
222
|
-
self.
|
225
|
+
if type_id in self._assembled_outputs:
|
226
|
+
self._assembled_outputs[type_id].extend(value)
|
223
227
|
else:
|
224
|
-
self.
|
228
|
+
self._assembled_outputs[type_id] = value
|
225
229
|
else:
|
226
|
-
self.
|
230
|
+
self._assembled_outputs[type_id] = value
|
227
231
|
|
228
232
|
async def _bind_arguments(self, signature: Signature) -> BoundArguments:
|
229
233
|
args = []
|
@@ -232,11 +236,11 @@ class Assembler:
|
|
232
236
|
if param_name == "self":
|
233
237
|
args.append(object())
|
234
238
|
continue
|
235
|
-
param_key =
|
236
|
-
has_dependency = param_key in self.
|
239
|
+
param_key = TypeId.from_type(param.annotation)
|
240
|
+
has_dependency = param_key in self._assembled_outputs
|
237
241
|
if not has_dependency:
|
238
242
|
await self._satisfy(param_key)
|
239
|
-
val = self.
|
243
|
+
val = self._assembled_outputs[param_key]
|
240
244
|
if param.kind == param.POSITIONAL_ONLY:
|
241
245
|
args.append(val)
|
242
246
|
else:
|
@@ -1,8 +1,13 @@
|
|
1
1
|
import inspect
|
2
|
-
from collections.abc import Iterable,
|
3
|
-
from
|
2
|
+
from collections.abc import Iterable, Sequence
|
3
|
+
from itertools import chain
|
4
|
+
from typing import TYPE_CHECKING, ClassVar
|
4
5
|
|
5
|
-
from engin._dependency import Func, Invoke, Provide
|
6
|
+
from engin._dependency import Dependency, Func, Invoke, Provide
|
7
|
+
from engin._option import Option
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from engin._engin import Engin
|
6
11
|
|
7
12
|
|
8
13
|
def provide(func: Func) -> Func:
|
@@ -21,7 +26,7 @@ def invoke(func: Func) -> Func:
|
|
21
26
|
return func
|
22
27
|
|
23
28
|
|
24
|
-
class Block(
|
29
|
+
class Block(Option):
|
25
30
|
"""
|
26
31
|
A Block is a collection of providers and invocations.
|
27
32
|
|
@@ -48,23 +53,21 @@ class Block(Iterable[Provide | Invoke]):
|
|
48
53
|
```
|
49
54
|
"""
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
name: ClassVar[str | None] = None
|
57
|
+
options: ClassVar[Sequence[Option]] = []
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
def apply(cls, engin: "Engin") -> None:
|
61
|
+
block_name = cls.name or f"{cls.__name__}"
|
62
|
+
for option in chain(cls.options, cls._method_options()):
|
63
|
+
if isinstance(option, Dependency):
|
64
|
+
option._block_name = block_name
|
65
|
+
option.apply(engin)
|
66
|
+
|
67
|
+
@classmethod
|
68
|
+
def _method_options(cls) -> Iterable[Provide | Invoke]:
|
69
|
+
for _, method in inspect.getmembers(cls):
|
70
|
+
if option := getattr(method, "_opt", None):
|
71
|
+
if not isinstance(option, Provide | Invoke):
|
59
72
|
raise RuntimeError("Block option is not an instance of Provide or Invoke")
|
60
|
-
|
61
|
-
self._options.append(opt)
|
62
|
-
for opt in self.options:
|
63
|
-
opt.set_block_name(self._name)
|
64
|
-
|
65
|
-
@property
|
66
|
-
def name(self) -> str:
|
67
|
-
return self._name
|
68
|
-
|
69
|
-
def __iter__(self) -> Iterator[Provide | Invoke]:
|
70
|
-
return iter(self._options)
|
73
|
+
yield option
|
@@ -0,0 +1,13 @@
|
|
1
|
+
try:
|
2
|
+
import typer
|
3
|
+
except ImportError:
|
4
|
+
raise ImportError(
|
5
|
+
"Unable to import typer, to use the engin cli please install the"
|
6
|
+
" `cli` extra, e.g. pip install engin[cli]"
|
7
|
+
) from None
|
8
|
+
|
9
|
+
from engin._cli._graph import cli as graph_cli
|
10
|
+
|
11
|
+
app = typer.Typer()
|
12
|
+
|
13
|
+
app.add_typer(graph_cli)
|
@@ -1,63 +1,76 @@
|
|
1
|
+
import contextlib
|
1
2
|
import importlib
|
2
3
|
import logging
|
3
4
|
import socketserver
|
4
5
|
import sys
|
5
6
|
import threading
|
6
|
-
from argparse import ArgumentParser
|
7
7
|
from http.server import BaseHTTPRequestHandler
|
8
8
|
from time import sleep
|
9
|
-
from typing import Any
|
9
|
+
from typing import Annotated, Any
|
10
|
+
|
11
|
+
import typer
|
12
|
+
from rich import print
|
10
13
|
|
11
14
|
from engin import Engin, Entrypoint, Invoke
|
15
|
+
from engin._cli._utils import print_error
|
12
16
|
from engin._dependency import Dependency, Provide, Supply
|
13
17
|
from engin.ext.asgi import ASGIEngin
|
14
|
-
|
18
|
+
|
19
|
+
try:
|
20
|
+
from engin.ext.fastapi import APIRouteDependency
|
21
|
+
except ImportError:
|
22
|
+
APIRouteDependency = None # type: ignore[assignment,misc]
|
23
|
+
|
24
|
+
cli = typer.Typer()
|
25
|
+
|
15
26
|
|
16
27
|
# mute logging from importing of files + engin's debug logging.
|
17
28
|
logging.disable()
|
18
29
|
|
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
30
|
|
31
31
|
_APP_ORIGIN = ""
|
32
32
|
|
33
|
-
|
34
|
-
|
33
|
+
_CLI_HELP = {
|
34
|
+
"app": (
|
35
|
+
"The import path of your Engin instance, in the form 'package:application'"
|
36
|
+
", e.g. 'app.main:engin'"
|
37
|
+
)
|
38
|
+
}
|
39
|
+
|
40
|
+
|
41
|
+
@cli.command(name="graph")
|
42
|
+
def serve_graph(
|
43
|
+
app: Annotated[
|
44
|
+
str,
|
45
|
+
typer.Argument(help=_CLI_HELP["app"]),
|
46
|
+
],
|
47
|
+
) -> None:
|
48
|
+
"""
|
49
|
+
Creates a visualisation of your application's dependencies.
|
50
|
+
"""
|
35
51
|
# add cwd to path to enable local package imports
|
36
52
|
sys.path.insert(0, "")
|
37
53
|
|
38
|
-
parsed = args.parse_args()
|
39
|
-
|
40
|
-
app = parsed.app
|
41
|
-
|
42
54
|
try:
|
43
55
|
module_name, engin_name = app.split(":", maxsplit=1)
|
44
56
|
except ValueError:
|
45
|
-
|
46
|
-
"Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'"
|
47
|
-
) from None
|
57
|
+
print_error("Expected an argument of the form 'module:attribute', e.g. 'myapp:engin'")
|
48
58
|
|
49
59
|
global _APP_ORIGIN
|
50
60
|
_APP_ORIGIN = module_name.split(".", maxsplit=1)[0]
|
51
61
|
|
52
|
-
|
62
|
+
try:
|
63
|
+
module = importlib.import_module(module_name)
|
64
|
+
except ModuleNotFoundError:
|
65
|
+
print_error(f"unable to find module '{module_name}'")
|
53
66
|
|
54
67
|
try:
|
55
68
|
instance = getattr(module, engin_name)
|
56
|
-
except
|
57
|
-
|
69
|
+
except AttributeError:
|
70
|
+
print_error(f"module '{module_name}' has no attribute '{engin_name}'")
|
58
71
|
|
59
72
|
if not isinstance(instance, Engin):
|
60
|
-
|
73
|
+
print_error(f"'{app}' is not an Engin instance")
|
61
74
|
|
62
75
|
nodes = instance.graph()
|
63
76
|
|
@@ -96,10 +109,14 @@ def serve_graph() -> None:
|
|
96
109
|
server_thread.daemon = True # Daemonize the thread so it exits when the main script exits
|
97
110
|
server_thread.start()
|
98
111
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
112
|
+
with contextlib.suppress(KeyboardInterrupt):
|
113
|
+
wait_for_interrupt()
|
114
|
+
|
115
|
+
print("Exiting the server...")
|
116
|
+
|
117
|
+
|
118
|
+
def wait_for_interrupt() -> None:
|
119
|
+
sleep(10000)
|
103
120
|
|
104
121
|
|
105
122
|
_BLOCK_IDX: dict[str, int] = {}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Never
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from rich import print
|
5
|
+
from rich.panel import Panel
|
6
|
+
|
7
|
+
|
8
|
+
def print_error(msg: str) -> Never:
|
9
|
+
print(
|
10
|
+
Panel(
|
11
|
+
title="Error",
|
12
|
+
renderable=msg.capitalize(),
|
13
|
+
title_align="left",
|
14
|
+
border_style="red",
|
15
|
+
highlight=True,
|
16
|
+
)
|
17
|
+
)
|
18
|
+
raise typer.Exit(code=1)
|