engin 0.1.0rc2__tar.gz → 0.2.0a1__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.1.0rc2 → engin-0.2.0a1}/.gitignore +2 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/CHANGELOG.md +16 -2
- {engin-0.1.0rc2 → engin-0.2.0a1}/PKG-INFO +2 -2
- {engin-0.1.0rc2 → engin-0.2.0a1}/README.md +1 -1
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/cli.md +0 -5
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/concepts/blocks.md +4 -3
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/concepts/engin.md +5 -5
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/concepts/invocations.md +3 -3
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/concepts/lifecycle.md +13 -8
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/concepts/providers.md +87 -3
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/concepts/supervisor.md +12 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/index.md +1 -1
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/integrations/fastapi.md +3 -3
- engin-0.2.0a1/docs/tutorial/1_empty_application.md +37 -0
- engin-0.2.0a1/docs/tutorial/2_create_a_publisher.md +101 -0
- engin-0.2.0a1/docs/tutorial/3_run_the_application.md +61 -0
- engin-0.2.0a1/docs/tutorial/4_refactor_valkey_client.md +100 -0
- engin-0.2.0a1/docs/tutorial/index.md +8 -0
- engin-0.2.0a1/examples/tutorial/app.py +18 -0
- engin-0.2.0a1/examples/tutorial/publisher.py +28 -0
- engin-0.2.0a1/examples/tutorial/valkey_client.py +23 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/mkdocs.yaml +10 -1
- {engin-0.1.0rc2 → engin-0.2.0a1}/pyproject.toml +3 -2
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_cli/_check.py +0 -7
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_cli/_graph.html +1 -1
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_dependency.py +3 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_engin.py +4 -1
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_supervisor.py +19 -6
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/cli/test_check.py +0 -1
- engin-0.2.0a1/tests/conftest.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_dependencies.py +5 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_supervisor.py +22 -4
- {engin-0.1.0rc2 → engin-0.2.0a1}/uv.lock +258 -204
- engin-0.1.0rc2/docs/tutorial.md +0 -15
- {engin-0.1.0rc2 → engin-0.2.0a1}/.github/workflows/benchmark.yaml +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/.github/workflows/check.yaml +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/.github/workflows/publish.yaml +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/.readthedocs.yaml +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/LICENSE +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/engin-graph-output.png +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/integrations/fastapi-graph.png +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/js/readthedocs.js +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/overrides/main.html +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/docs/reference.md +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/app.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/db/block.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/features/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/asgi/main.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/app.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/main.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/simple/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/examples/simple/main.py +0 -0
- {engin-0.1.0rc2/src/engin/extensions → engin-0.2.0a1/examples/tutorial}/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_assembler.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_block.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_cli/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_cli/_common.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_cli/_graph.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_cli/_inspect.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_graph.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_introspect.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_lifecycle.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_option.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/_type_utils.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/exceptions.py +0 -0
- {engin-0.1.0rc2/tests → engin-0.2.0a1/src/engin/extensions}/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/extensions/asgi.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/extensions/fastapi.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/src/engin/py.typed +0 -0
- {engin-0.1.0rc2/tests/acceptance → engin-0.2.0a1/tests}/__init__.py +0 -0
- {engin-0.1.0rc2/tests/benchmarks → engin-0.2.0a1/tests/acceptance}/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/acceptance/test_engin_signal_handling.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/acceptance/test_error_in_invocation.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/acceptance/test_error_in_lifecycle_shutdown.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/acceptance/test_error_in_lifecycle_startup.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/acceptance/test_error_in_provider.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/acceptance/test_error_in_supervisor_task.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/acceptance/test_fastapi.py +0 -0
- {engin-0.1.0rc2/tests/cli → engin-0.2.0a1/tests/benchmarks}/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/benchmarks/conftest.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/benchmarks/test_bench_assembler.py +0 -0
- /engin-0.1.0rc2/tests/conftest.py → /engin-0.2.0a1/tests/cli/__init__.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/cli/test_get_engin_instance.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/cli/test_graph.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/cli/test_inspect.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/deps.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_assembler.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_block.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_engin.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_graph.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_lifecycle.py +0 -0
- {engin-0.1.0rc2 → engin-0.2.0a1}/tests/test_type_id.py +0 -0
@@ -5,11 +5,23 @@ 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
|
-
##
|
8
|
+
## UNRELEASED
|
9
9
|
|
10
10
|
### Added
|
11
11
|
|
12
|
-
-
|
12
|
+
- `Supevisor` now has a shutdown hook option for supervised tasks.
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
- `Provide` now raises a helpful error message when it is called with a static value
|
17
|
+
suggesting to use `Supply` instead.
|
18
|
+
|
19
|
+
|
20
|
+
## [0.1.0] - 2025-08-16
|
21
|
+
|
22
|
+
### Added
|
23
|
+
|
24
|
+
- `Supervisor` class which can safely supervise long running tasks.
|
13
25
|
- A new cli option `engin check` that validates whether you have any missing providers.
|
14
26
|
- Support for specifying `default-instance` in your `pyproject.toml` under `[tool.engin]`
|
15
27
|
which is used as a default value for the `app` parameter when using the cli.
|
@@ -19,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
19
31
|
|
20
32
|
- If a Provider is missing during Assembly, the Assembler now raises `TypeNotProvidedError`
|
21
33
|
instead of a `LookupError`.
|
34
|
+
- `engin graph` has improved visualisations and options.
|
35
|
+
- `engin check` does not list all available providers anymore.
|
22
36
|
|
23
37
|
|
24
38
|
## [0.0.20] - 2025-06-18
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: engin
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.0a1
|
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/
|
@@ -39,7 +39,7 @@ The Engin framework gives you:
|
|
39
39
|
|
40
40
|
- A fully-featured dependency injection system.
|
41
41
|
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
42
|
-
- Zero
|
42
|
+
- Zero boilerplate code reuse across applications.
|
43
43
|
- Integrations for other frameworks such as FastAPI.
|
44
44
|
- Full async support.
|
45
45
|
- CLI commands to aid local development.
|
@@ -20,7 +20,7 @@ The Engin framework gives you:
|
|
20
20
|
|
21
21
|
- A fully-featured dependency injection system.
|
22
22
|
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
23
|
-
- Zero
|
23
|
+
- Zero boilerplate code reuse across applications.
|
24
24
|
- Integrations for other frameworks such as FastAPI.
|
25
25
|
- Full async support.
|
26
26
|
- CLI commands to aid local development.
|
@@ -27,10 +27,11 @@ Blocks have a class attribute named `options` which can be used to include exist
|
|
27
27
|
options.
|
28
28
|
|
29
29
|
```python
|
30
|
+
import asyncio
|
30
31
|
from engin import Engin, Block, Invoke, Provide, Supply
|
31
32
|
|
32
33
|
|
33
|
-
def print_string(
|
34
|
+
def print_string(string: str) -> None:
|
34
35
|
print(string)
|
35
36
|
|
36
37
|
|
@@ -44,13 +45,13 @@ class ExampleBlock(Block):
|
|
44
45
|
# register it as a provider with the Engin
|
45
46
|
engin = Engin(ExampleBlock())
|
46
47
|
|
47
|
-
|
48
|
+
asyncio.run(engin.run()) # prints 'hello'
|
48
49
|
```
|
49
50
|
|
50
51
|
!!!tip
|
51
52
|
|
52
53
|
Blocks are themselves valid options, so Blocks can include other Blocks as options. This
|
53
|
-
|
54
|
+
compositional approach can help you build and manage larger applications.
|
54
55
|
|
55
56
|
|
56
57
|
## Defining Providers & Invocations in the Block
|
@@ -9,7 +9,8 @@ The Engin class manages your application's complete lifecycle, when ran it will:
|
|
9
9
|
3. Run all lifecycle startup tasks that were registered by assembled dependencies sequentially.
|
10
10
|
4. Start any supervised background tasks.
|
11
11
|
5. Wait for a shutdown signal, SIGINT or SIGTERM, or for a supervised task to cause a shutdown.
|
12
|
-
6.
|
12
|
+
6. Stop any supervised background tasks that are still running.
|
13
|
+
7. Run all corresponding lifecycle shutdown tasks in the reverse order to the startup order.
|
13
14
|
|
14
15
|
|
15
16
|
## Creating an Engin
|
@@ -17,15 +18,14 @@ The Engin class manages your application's complete lifecycle, when ran it will:
|
|
17
18
|
Instantiate an Engin with any combination of options, i.e. providers, invocations, and blocks:
|
18
19
|
|
19
20
|
```python
|
20
|
-
from engin import Engin,
|
21
|
-
|
21
|
+
from engin import Engin, Entrypoint, Provide, Supervisor
|
22
22
|
|
23
23
|
def my_service_factory(supervisor: Supervisor) -> MyService:
|
24
24
|
my_service = MyService()
|
25
25
|
supervisor.supervise(my_service.run)
|
26
26
|
return my_service
|
27
27
|
|
28
|
-
engin = Engin(Provide(
|
28
|
+
engin = Engin(Provide(my_service_factory), Entrypoint(MyService))
|
29
29
|
```
|
30
30
|
|
31
31
|
## Running your application
|
@@ -41,7 +41,7 @@ your application:
|
|
41
41
|
```python
|
42
42
|
import asyncio
|
43
43
|
|
44
|
-
|
44
|
+
asyncio.run(engin.run())
|
45
45
|
```
|
46
46
|
|
47
47
|
|
@@ -4,7 +4,7 @@ Invocations define the behaviour of your application, without any Invocations
|
|
4
4
|
your application will not do anything.
|
5
5
|
|
6
6
|
Like providers, invocations are functions that take one or more dependencies as
|
7
|
-
parameters, but they should always return None as the return value will not used by Engin.
|
7
|
+
parameters, but they should always return None as the return value will not be used by Engin.
|
8
8
|
|
9
9
|
As part of the Engin's startup sequence, all declared invocations will be called
|
10
10
|
sequentially in the order they were registered.
|
@@ -113,7 +113,7 @@ Invocations can use any types as long as they have the matching providers.
|
|
113
113
|
|
114
114
|
```python
|
115
115
|
import asyncio
|
116
|
-
from engin import Engin, Invoke
|
116
|
+
from engin import Engin, Invoke, Provide
|
117
117
|
|
118
118
|
# define a constructor
|
119
119
|
def name_factory() -> str:
|
@@ -123,7 +123,7 @@ def print_hello(name: str) -> None:
|
|
123
123
|
print(f"hello {name}!")
|
124
124
|
|
125
125
|
# register it as a invocation with the Engin
|
126
|
-
engin = Engin(Provide(name_factory
|
126
|
+
engin = Engin(Provide(name_factory), Invoke(print_hello))
|
127
127
|
|
128
128
|
# run your application
|
129
129
|
asyncio.run(engin.run()) # hello Dmitrii!
|
@@ -8,13 +8,13 @@ connection pool on startup, and gracefully release the connections on shutdown.
|
|
8
8
|
Doing this yourself can be tricky and is application dependent: most will not have any
|
9
9
|
special support for this and will expect you to manage your lifecycle concerns in your
|
10
10
|
entrypoint function, leading to unwieldy code in larger applications, whilst other
|
11
|
-
types application might expected you to translate the
|
11
|
+
types application might expected you to translate the lifecycle tasks into something they
|
12
12
|
offer, e.g. an ASGI server would expect you to manage this via its lifespan. In both cases
|
13
13
|
you end up managing lifecycle in a completely different place to where you declare your
|
14
14
|
objects, which make the codebase more complicated to understand.
|
15
15
|
|
16
16
|
Luckily, engin makes declaring lifecycle tasks a breeze, and it can be done in the same
|
17
|
-
provider that
|
17
|
+
provider that builds your object keeping your code nicely collocated.
|
18
18
|
|
19
19
|
## The Lifecycle type
|
20
20
|
|
@@ -47,9 +47,9 @@ def httpx_client(lifecycle: Lifecycle) -> AsyncClient:
|
|
47
47
|
return client
|
48
48
|
```
|
49
49
|
|
50
|
-
### 2.
|
50
|
+
### 2. Explicit startup & shutdown methods
|
51
51
|
|
52
|
-
If your type exposes
|
52
|
+
If your type exposes methods that must be called as part of the lifecycle, e.g. `start()`
|
53
53
|
& `stop()`, then `lifecycle.hook(on_start=..., on_stop=...)` is the way.
|
54
54
|
|
55
55
|
Let's look at an example using `piccolo.engine.PostgresEngin`:
|
@@ -58,12 +58,12 @@ Let's look at an example using `piccolo.engine.PostgresEngin`:
|
|
58
58
|
from engin import Lifecycle
|
59
59
|
from piccolo.engine import PostgresEngine
|
60
60
|
|
61
|
-
def postgres_engine(
|
61
|
+
def postgres_engine(lifecycle: Lifecycle) -> PostgresEngine:
|
62
62
|
db_engine = PostgresEngine(...) # fill in actual connection details
|
63
63
|
|
64
|
-
|
65
|
-
on_start=db_engine.start_connection_pool
|
66
|
-
on_stop=db_engine.close_connection_pool
|
64
|
+
lifecycle.hook(
|
65
|
+
on_start=db_engine.start_connection_pool,
|
66
|
+
on_stop=db_engine.close_connection_pool,
|
67
67
|
)
|
68
68
|
|
69
69
|
return db_engine
|
@@ -79,6 +79,7 @@ therefore we want to manage it as a task.
|
|
79
79
|
|
80
80
|
```python
|
81
81
|
import asyncio
|
82
|
+
from contextlib import asynccontextmanager
|
82
83
|
from engin import Lifecycle
|
83
84
|
from some_package import BlockingAsyncWorker
|
84
85
|
|
@@ -96,3 +97,7 @@ def blocking_worker(lifecycle: Lifecycle) -> BlockingWorker:
|
|
96
97
|
|
97
98
|
return worker
|
98
99
|
```
|
100
|
+
|
101
|
+
!!! note
|
102
|
+
|
103
|
+
The above case is only given as a reference, running background tasks should be done via the `Supervisor` depedency.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# Providers
|
2
2
|
|
3
|
-
Providers are the factories of your application, they are
|
3
|
+
Providers are the factories of your application, they are responsible for the construction
|
4
4
|
of the objects that your application needs.
|
5
5
|
|
6
6
|
Remember, the Engin only calls the providers that are necessary to run your application.
|
@@ -74,7 +74,6 @@ greeter = await engin.assembler.build(Greeter)
|
|
74
74
|
greeter.greet("Bob") # hello, Bob!
|
75
75
|
```
|
76
76
|
|
77
|
-
|
78
77
|
## Providers are only called when required
|
79
78
|
|
80
79
|
The Assembler will only call a provider when the type is requested, directly or indirectly
|
@@ -186,4 +185,89 @@ engin = Engin(Supply("hello"))
|
|
186
185
|
a_string = await engin.assembler.build(str)
|
187
186
|
|
188
187
|
print(a_string) # hello
|
189
|
-
```
|
188
|
+
```
|
189
|
+
|
190
|
+
## Overriding providers from the same package
|
191
|
+
|
192
|
+
Sometimes you need to replace an existing provider for the same type. If both providers
|
193
|
+
originate from the same Python package, overrides must be explicit by setting
|
194
|
+
`override=True` on the replacement provider. This prevents accidental overrides.
|
195
|
+
|
196
|
+
```python
|
197
|
+
from engin import Engin, Provide, Supply
|
198
|
+
|
199
|
+
|
200
|
+
def make_number() -> int:
|
201
|
+
return 1
|
202
|
+
|
203
|
+
|
204
|
+
def make_number_override() -> int:
|
205
|
+
return 2
|
206
|
+
|
207
|
+
|
208
|
+
engin = Engin(
|
209
|
+
Provide(make_number),
|
210
|
+
# Explicitly override the previous provider from the same package
|
211
|
+
Provide(make_number_override, override=True),
|
212
|
+
)
|
213
|
+
|
214
|
+
# this will return 2
|
215
|
+
await engin.assembler.build(int)
|
216
|
+
```
|
217
|
+
|
218
|
+
You can also use `override=True` with `Supply`, and with the `@provide` decorator inside `Block`
|
219
|
+
classes: `@provide(override=True)`.
|
220
|
+
|
221
|
+
!!!tip
|
222
|
+
|
223
|
+
Overriding providers from a different package is allowed implicitly. Explicit overrides are
|
224
|
+
only required when replacing a provider defined in the same package. Adding or overriding a
|
225
|
+
provider clears previously assembled values so subsequent builds use the new provider.
|
226
|
+
|
227
|
+
Multiproviders (providers that return lists) are not replaced; new ones are always appended.
|
228
|
+
|
229
|
+
|
230
|
+
## Provider scopes
|
231
|
+
|
232
|
+
Providers can be associated with a named scope. A scoped provider can only be used while that
|
233
|
+
scope is active, and its cached value is cleared when the scope exits.
|
234
|
+
|
235
|
+
To set a scope, pass `scope="..."` to `Provide`, or use the `@provide(scope=...)` decorator
|
236
|
+
when defining providers inside a `Block`.
|
237
|
+
|
238
|
+
```python
|
239
|
+
from engin import Engin, Provide
|
240
|
+
import time
|
241
|
+
|
242
|
+
|
243
|
+
def make_timestamp() -> int:
|
244
|
+
return time.time_ns()
|
245
|
+
|
246
|
+
|
247
|
+
# Register a provider that is only valid in the "request" scope
|
248
|
+
engin = Engin(Provide(make_timestamp, scope="request"))
|
249
|
+
|
250
|
+
# Outside the scope this will raise an error
|
251
|
+
# await engin.assembler.build(int) # NotInScopeError
|
252
|
+
|
253
|
+
# Within the scope the value can be built and is cached for the duration
|
254
|
+
with engin.assembler.scope("request"):
|
255
|
+
t1 = await engin.assembler.build(int)
|
256
|
+
t2 = await engin.assembler.build(int)
|
257
|
+
assert t1 == t2 # cached within the active scope
|
258
|
+
|
259
|
+
# After leaving the scope, the scoped cache is cleared
|
260
|
+
# await engin.assembler.build(int) # NotInScopeError
|
261
|
+
```
|
262
|
+
|
263
|
+
Scopes compose via a stack. Nested scopes can be entered with additional
|
264
|
+
`with engin.assembler.scope("..."):` contexts. When a scope exits, any values produced by
|
265
|
+
providers in that scope are removed from the cache, while values produced by
|
266
|
+
unscoped providers remain cached.
|
267
|
+
|
268
|
+
!!!note
|
269
|
+
|
270
|
+
In web applications built with `ASGIEngin`, each request is automatically wrapped in
|
271
|
+
`with engin.assembler.scope("request"):`. Marking providers with `scope="request"` yields
|
272
|
+
per-request values that are reused within the same request and discarded at the end of the
|
273
|
+
request.
|
@@ -62,3 +62,15 @@ supervisor.supervise(
|
|
62
62
|
on_exception=OnException.IGNORE
|
63
63
|
)
|
64
64
|
```
|
65
|
+
|
66
|
+
## Task Shutdown Hook
|
67
|
+
|
68
|
+
When the Engin is shutdown (e.g. when receiving a SIGTERM) the Supervisor will cancel all
|
69
|
+
supervised tasks using the underlying async backend cancellation mechanism, for
|
70
|
+
example by raising a `CancellationError` for `asyncio` projects. This is OK for the
|
71
|
+
majority of cases, however some supervised tasks might manage their own shutdown procedure
|
72
|
+
and do not handle cancellation well. For these cases the `Supervisor` exposes a
|
73
|
+
`shutdown_hook` which will be called just before the task is cancelled.
|
74
|
+
|
75
|
+
To set a `shutdown_hook` simply pass in the relevant parameter when calling
|
76
|
+
`Supervisor.supervise`.
|
@@ -10,7 +10,7 @@ The Engin framework gives you:
|
|
10
10
|
|
11
11
|
- A fully-featured dependency injection system.
|
12
12
|
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
13
|
-
- Zero
|
13
|
+
- Zero boilerplate code reuse across applications.
|
14
14
|
- Integrations for other frameworks such as FastAPI.
|
15
15
|
- Full async support.
|
16
16
|
- CLI commands to aid local development.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# FastAPI
|
2
2
|
|
3
|
-
Engin ships with a FastAPI integration that is available under the `engin.
|
3
|
+
Engin ships with a FastAPI integration that is available under the `engin.extensions.fastapi`
|
4
4
|
module. The integration allows one to write idiomatic FastAPI code whilst leveraging Engin
|
5
5
|
for Dependency Injection and modularising the application.
|
6
6
|
|
@@ -37,7 +37,7 @@ instance that you provided.
|
|
37
37
|
!!!tip
|
38
38
|
|
39
39
|
It is also easy to integrate Engin with an existing FastAPI application by using the
|
40
|
-
`engin_to_lifespan` function in the `engin.
|
40
|
+
`engin_to_lifespan` function in the `engin.extensions.asgi` module.
|
41
41
|
|
42
42
|
|
43
43
|
## Dependency Injection
|
@@ -85,7 +85,7 @@ async def database_session(
|
|
85
85
|
session.commit()
|
86
86
|
|
87
87
|
@app.post("/{id}")
|
88
|
-
async def add_item(session: Annotated[Session,
|
88
|
+
async def add_item(session: Annotated[Session, Depends(database_session)]):
|
89
89
|
session.add(MyORMModel(...))
|
90
90
|
```
|
91
91
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
Let's start by creating a minimal application using Engin. We do this by instantiating the
|
2
|
+
Engin class which will serve as our application runtime.
|
3
|
+
|
4
|
+
```python title="app.py"
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
|
8
|
+
from engin import Engin
|
9
|
+
|
10
|
+
engin = Engin()
|
11
|
+
|
12
|
+
if __name__ == "__main__":
|
13
|
+
logging.basicConfig(level=logging.INFO)
|
14
|
+
asyncio.run(engin.run())
|
15
|
+
```
|
16
|
+
|
17
|
+
If we run the application using `python app.py` we will see the following output:
|
18
|
+
|
19
|
+
```
|
20
|
+
INFO:engin:starting engin
|
21
|
+
INFO:engin:startup complete
|
22
|
+
```
|
23
|
+
|
24
|
+
And if we stop the application using `ctrl + c` or equivalent we will see:
|
25
|
+
|
26
|
+
```
|
27
|
+
INFO:engin:stopping engin
|
28
|
+
INFO:engin:shutdown complete
|
29
|
+
```
|
30
|
+
|
31
|
+
Now that we have an empty application, let's give it something to do.
|
32
|
+
|
33
|
+
!!! note
|
34
|
+
|
35
|
+
Engin is typically used for long running application such as Web Servers or an Event
|
36
|
+
Consumers. In these scenarios the engin would run until it receives a SIGINT signal (for
|
37
|
+
example when a new deployment happens) at which point it would shutdown.
|
@@ -0,0 +1,101 @@
|
|
1
|
+
Let's write our Publisher class next. To simulate some sort of real work we can sleep
|
2
|
+
and publish a number in a loop, mimicking a sensor reader for example.
|
3
|
+
|
4
|
+
```python title="publisher.py"
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
import random
|
8
|
+
|
9
|
+
from valkey.asyncio import Valkey
|
10
|
+
|
11
|
+
|
12
|
+
class Publisher:
|
13
|
+
def __init__(self, valkey: Valkey) -> None:
|
14
|
+
self._valkey = valkey
|
15
|
+
|
16
|
+
async def run(self) -> None:
|
17
|
+
while True:
|
18
|
+
number = random.randint(-100, 100)
|
19
|
+
logging.info(f"Publishing: {number}")
|
20
|
+
await self._valkey.xadd("numbers", {"number": str(number)})
|
21
|
+
await asyncio.sleep(1)
|
22
|
+
|
23
|
+
```
|
24
|
+
|
25
|
+
!!! note
|
26
|
+
The Publisher asking for the Valkey instance when being initialised is a form of
|
27
|
+
[Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection), specifically
|
28
|
+
Constructor injection. Doing this separates out the concerns of configuring the client and
|
29
|
+
using it.
|
30
|
+
|
31
|
+
|
32
|
+
Let's register the `Publisher` with our application so we can use it later. We do this by:
|
33
|
+
|
34
|
+
1. Creating a factory function which is responsible for creating the `Publisher` instance.
|
35
|
+
2. Registering this factory function with our `Engin` instance as a "provider".
|
36
|
+
|
37
|
+
We can write a simple factory function below the `Publisher` class. Notice that the factory
|
38
|
+
function also asks for the `Valkey` client to be injected. We will provide the `Valkey`
|
39
|
+
dependency later and Engin will automatically take care of giving it to the
|
40
|
+
`publisher_factory`.
|
41
|
+
|
42
|
+
```python
|
43
|
+
def publisher_factory(valkey: Valkey) -> Publisher:
|
44
|
+
return Publisher(valkey=valkey)
|
45
|
+
```
|
46
|
+
|
47
|
+
We need to tell the application how to run the `Publisher` as well. We want Engin to call
|
48
|
+
`Publisher.run` when the application is run which we can do by using the `Supervisor`
|
49
|
+
dependency. The `Supervisor` is a dependency that is provided by Engin and it can be used to
|
50
|
+
supervise long running tasks.
|
51
|
+
|
52
|
+
```python
|
53
|
+
def publisher_factory(valkey: Valkey, supervisor: Supervisor) -> Publisher:
|
54
|
+
publisher = Publisher(valkey=valkey)
|
55
|
+
|
56
|
+
# run the publisher as a supervised application task
|
57
|
+
supervisor.supervise(publisher.run)
|
58
|
+
|
59
|
+
return publisher
|
60
|
+
```
|
61
|
+
|
62
|
+
!!! tip
|
63
|
+
|
64
|
+
Supervised tasks can handle exceptions in different ways, controlled by the `OnException`
|
65
|
+
enum. By default if the supervised task errors then it will cause the engin to shutdown,
|
66
|
+
but you can also choose for the error to be ignored or the task to be restarted.
|
67
|
+
|
68
|
+
Now we just need to register our `publisher_factory` with the engin. We can do this using the
|
69
|
+
`Provide` marker class which allows us to "provide" a dependency to our application.
|
70
|
+
|
71
|
+
```python title="app.py"
|
72
|
+
# ... existing code ...
|
73
|
+
from engin import Provide
|
74
|
+
from examples.tutorial.publisher import publisher_factory
|
75
|
+
|
76
|
+
|
77
|
+
engin = Engin(Provide(publisher_factory))
|
78
|
+
```
|
79
|
+
|
80
|
+
Our `Publisher` requires a `Valkey` client, so let's create a factory for that too, we can
|
81
|
+
hardcode the url to make this simple for now.
|
82
|
+
|
83
|
+
|
84
|
+
```python title="valkey_client.py"
|
85
|
+
from valkey.asyncio import Valkey
|
86
|
+
|
87
|
+
def valkey_client_factory() -> Valkey:
|
88
|
+
return Valkey.from_url("valkey://localhost:6379")
|
89
|
+
```
|
90
|
+
|
91
|
+
And let's provide this dependency to the application as well.
|
92
|
+
|
93
|
+
```python title="app.py"
|
94
|
+
# ... existing code ...
|
95
|
+
from engin import Provide
|
96
|
+
from examples.tutorial.publisher import publisher_factory
|
97
|
+
from examples.tutorial.valkey_client import valkey_client_factory
|
98
|
+
|
99
|
+
|
100
|
+
engin = Engin(Provide(publisher_factory), Provide(valkey_client_factory))
|
101
|
+
```
|
@@ -0,0 +1,61 @@
|
|
1
|
+
Let's try to run our application.
|
2
|
+
|
3
|
+
```
|
4
|
+
INFO:engin:starting engin
|
5
|
+
INFO:engin:startup complete
|
6
|
+
```
|
7
|
+
|
8
|
+
And then stop it.
|
9
|
+
|
10
|
+
```
|
11
|
+
INFO:engin:stopping engin
|
12
|
+
INFO:engin:shutdown complete
|
13
|
+
```
|
14
|
+
|
15
|
+
Where are the publisher logs? Well we haven't actually told our application to actually *do*
|
16
|
+
anything yet. We have registered providers, but nothing is using them. Engin will only assemble
|
17
|
+
dependencies that are required by an `Invocation` or `Entrypoint`.
|
18
|
+
|
19
|
+
To fix this, we can mark the `Publisher` as an `Entrypoint`. This tells our application that
|
20
|
+
the `Publisher` should always be assembled, which will cause the `Publisher.run` method to be
|
21
|
+
registered as a supervised task.
|
22
|
+
|
23
|
+
```python title="app.py"
|
24
|
+
# ... existing code ...
|
25
|
+
from examples.tutorial.publisher import Publisher, publisher_factory
|
26
|
+
from engin import Entrypoint
|
27
|
+
|
28
|
+
engin = Engin(
|
29
|
+
Provide(publisher_factory),
|
30
|
+
Provide(valkey_client_factory),
|
31
|
+
Entrypoint(Publisher),
|
32
|
+
)
|
33
|
+
```
|
34
|
+
|
35
|
+
Now if you run the application, you will see the publisher running and logging messages.
|
36
|
+
|
37
|
+
```
|
38
|
+
INFO:engin:starting engin
|
39
|
+
INFO:engin:startup complete
|
40
|
+
INFO:engin:supervising task: Publisher.run
|
41
|
+
INFO:root:Publishing: -55
|
42
|
+
INFO:root:Publishing: 15
|
43
|
+
```
|
44
|
+
|
45
|
+
However, when we stop the application we see a weird exception to do with the `Valkey` client.
|
46
|
+
|
47
|
+
```
|
48
|
+
INFO:engin:stopping engin
|
49
|
+
INFO:engin:shutdown complete
|
50
|
+
Exception ignored in: <function AbstractConnection.__del__ at 0x0000021E6273B880>
|
51
|
+
Traceback (most recent call last):
|
52
|
+
File "C:\dev\python\engin\.venv\Lib\site-packages\valkey\asyncio\connection.py", line 243, in __del__
|
53
|
+
File "C:\dev\python\engin\.venv\Lib\site-packages\valkey\asyncio\connection.py", line 250, in _close
|
54
|
+
File "C:\Users\tutorial\AppData\Roaming\uv\python\cpython-3.13.0-windows-x86_64-none\Lib\asyncio\streams.py", line 352, in close
|
55
|
+
File "C:\Users\tutorial\AppData\Roaming\uv\python\cpython-3.13.0-windows-x86_64-none\Lib\asyncio\proactor_events.py", line 109, in close
|
56
|
+
File "C:\Users\tutorial\AppData\Roaming\uv\python\cpython-3.13.0-windows-x86_64-none\Lib\asyncio\base_events.py", line 829, in call_soon
|
57
|
+
File "C:\Users\tutorial\AppData\Roaming\uv\python\cpython-3.13.0-windows-x86_64-none\Lib\asyncio\base_events.py", line 552, in _check_closed
|
58
|
+
RuntimeError: Event loop is closed
|
59
|
+
```
|
60
|
+
|
61
|
+
Let's take another look at the `Valkey` client factory.
|