engin 0.0.20__tar.gz → 0.1.0__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 → engin-0.1.0}/.github/workflows/benchmark.yaml +3 -0
- {engin-0.0.20 → engin-0.1.0}/.github/workflows/check.yaml +4 -0
- {engin-0.0.20 → engin-0.1.0}/.gitignore +2 -0
- {engin-0.0.20 → engin-0.1.0}/CHANGELOG.md +17 -0
- engin-0.1.0/PKG-INFO +122 -0
- engin-0.1.0/README.md +103 -0
- engin-0.1.0/docs/cli.md +144 -0
- {engin-0.0.20 → engin-0.1.0}/docs/concepts/blocks.md +8 -7
- engin-0.1.0/docs/concepts/engin.md +62 -0
- {engin-0.0.20 → engin-0.1.0}/docs/concepts/invocations.md +8 -8
- {engin-0.0.20 → engin-0.1.0}/docs/concepts/lifecycle.md +13 -10
- {engin-0.0.20 → engin-0.1.0}/docs/concepts/providers.md +87 -3
- engin-0.1.0/docs/concepts/supervisor.md +64 -0
- engin-0.1.0/docs/engin-graph-output.png +0 -0
- engin-0.1.0/docs/index.md +98 -0
- {engin-0.0.20/docs/guides → engin-0.1.0/docs/integrations}/fastapi.md +9 -13
- engin-0.1.0/docs/tutorial/1_empty_application.md +37 -0
- engin-0.1.0/docs/tutorial/2_create_a_publisher.md +101 -0
- engin-0.1.0/docs/tutorial/3_run_the_application.md +61 -0
- engin-0.1.0/docs/tutorial/4_refactor_valkey_client.md +100 -0
- engin-0.1.0/docs/tutorial/index.md +8 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/main.py +3 -1
- engin-0.1.0/examples/tutorial/app.py +18 -0
- engin-0.1.0/examples/tutorial/publisher.py +28 -0
- engin-0.1.0/examples/tutorial/valkey_client.py +23 -0
- {engin-0.0.20 → engin-0.1.0}/mkdocs.yaml +24 -9
- {engin-0.0.20 → engin-0.1.0}/pyproject.toml +10 -5
- {engin-0.0.20 → engin-0.1.0}/src/engin/__init__.py +3 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_assembler.py +12 -12
- {engin-0.0.20 → engin-0.1.0}/src/engin/_cli/__init__.py +2 -0
- engin-0.1.0/src/engin/_cli/_check.py +56 -0
- engin-0.1.0/src/engin/_cli/_common.py +121 -0
- engin-0.1.0/src/engin/_cli/_graph.html +883 -0
- engin-0.1.0/src/engin/_cli/_graph.py +223 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_cli/_inspect.py +10 -8
- engin-0.1.0/src/engin/_engin.py +281 -0
- engin-0.1.0/src/engin/_supervisor.py +137 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/exceptions.py +21 -6
- {engin-0.0.20 → engin-0.1.0}/src/engin/extensions/asgi.py +2 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/extensions/fastapi.py +2 -2
- engin-0.1.0/tests/acceptance/test_engin_signal_handling.py +28 -0
- engin-0.1.0/tests/acceptance/test_error_in_invocation.py +28 -0
- engin-0.0.20/tests/acceptance/test_error_in_shutdown.py → engin-0.1.0/tests/acceptance/test_error_in_lifecycle_shutdown.py +13 -3
- engin-0.0.20/tests/acceptance/test_error_in_start_up.py → engin-0.1.0/tests/acceptance/test_error_in_lifecycle_startup.py +28 -4
- engin-0.1.0/tests/acceptance/test_error_in_provider.py +35 -0
- engin-0.1.0/tests/acceptance/test_error_in_supervisor_task.py +29 -0
- {engin-0.0.20 → engin-0.1.0}/tests/acceptance/test_fastapi.py +1 -0
- engin-0.1.0/tests/cli/test_check.py +94 -0
- engin-0.1.0/tests/cli/test_get_engin_instance.py +111 -0
- {engin-0.0.20 → engin-0.1.0}/tests/cli/test_graph.py +3 -5
- engin-0.1.0/tests/conftest.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/test_assembler.py +13 -11
- {engin-0.0.20 → engin-0.1.0}/tests/test_engin.py +0 -16
- engin-0.1.0/tests/test_supervisor.py +113 -0
- {engin-0.0.20 → engin-0.1.0}/uv.lock +289 -208
- engin-0.0.20/PKG-INFO +0 -71
- engin-0.0.20/README.md +0 -55
- engin-0.0.20/docs/concepts/engin.md +0 -12
- engin-0.0.20/docs/getting-started.md +0 -7
- engin-0.0.20/docs/index.md +0 -21
- engin-0.0.20/src/engin/_cli/_common.py +0 -51
- engin-0.0.20/src/engin/_cli/_graph.html +0 -78
- engin-0.0.20/src/engin/_cli/_graph.py +0 -160
- engin-0.0.20/src/engin/_engin.py +0 -210
- {engin-0.0.20 → engin-0.1.0}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.20 → engin-0.1.0}/.readthedocs.yaml +0 -0
- {engin-0.0.20 → engin-0.1.0}/LICENSE +0 -0
- {engin-0.0.20/docs/guides → engin-0.1.0/docs/integrations}/fastapi-graph.png +0 -0
- {engin-0.0.20 → engin-0.1.0}/docs/js/readthedocs.js +0 -0
- {engin-0.0.20 → engin-0.1.0}/docs/overrides/main.html +0 -0
- {engin-0.0.20 → engin-0.1.0}/docs/reference.md +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/app.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/app.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/main.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/simple/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/examples/simple/main.py +0 -0
- {engin-0.0.20/src/engin/extensions → engin-0.1.0/examples/tutorial}/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_block.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_dependency.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_graph.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_introspect.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_lifecycle.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_option.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/_type_utils.py +0 -0
- {engin-0.0.20/tests → engin-0.1.0/src/engin/extensions}/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/src/engin/py.typed +0 -0
- {engin-0.0.20/tests/acceptance → engin-0.1.0/tests}/__init__.py +0 -0
- {engin-0.0.20/tests/benchmarks → engin-0.1.0/tests/acceptance}/__init__.py +0 -0
- {engin-0.0.20/tests/cli → engin-0.1.0/tests/benchmarks}/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/benchmarks/conftest.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/benchmarks/test_bench_assembler.py +0 -0
- /engin-0.0.20/tests/conftest.py → /engin-0.1.0/tests/cli/__init__.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/cli/test_inspect.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/deps.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/test_block.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/test_dependencies.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/test_graph.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/test_lifecycle.py +0 -0
- {engin-0.0.20 → engin-0.1.0}/tests/test_type_id.py +0 -0
@@ -3,6 +3,9 @@ name: Check
|
|
3
3
|
on:
|
4
4
|
push:
|
5
5
|
|
6
|
+
env:
|
7
|
+
UV_FROZEN: "1"
|
8
|
+
|
6
9
|
jobs:
|
7
10
|
check:
|
8
11
|
name: python
|
@@ -40,6 +43,7 @@ jobs:
|
|
40
43
|
run: uv run poe ci-test
|
41
44
|
|
42
45
|
- name: Upload coverage reports to Codecov
|
46
|
+
if: matrix.os == 'ubuntu-latest'
|
43
47
|
uses: codecov/codecov-action@v5
|
44
48
|
with:
|
45
49
|
token: ${{ secrets.CODECOV_TOKEN }}
|
@@ -5,6 +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
|
+
## [0.1.0] - 2025-08-16
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- `Supervisor` class which can safely supervise long running tasks.
|
13
|
+
- A new cli option `engin check` that validates whether you have any missing providers.
|
14
|
+
- Support for specifying `default-instance` in your `pyproject.toml` under `[tool.engin]`
|
15
|
+
which is used as a default value for the `app` parameter when using the cli.
|
16
|
+
- A new exception class: `TypeNotProvidedError`.
|
17
|
+
|
18
|
+
### Changed
|
19
|
+
|
20
|
+
- If a Provider is missing during Assembly, the Assembler now raises `TypeNotProvidedError`
|
21
|
+
instead of a `LookupError`.
|
22
|
+
- `engin graph` has improved visualisations and options.
|
23
|
+
- `engin check` does not list all available providers anymore.
|
24
|
+
|
8
25
|
|
9
26
|
## [0.0.20] - 2025-06-18
|
10
27
|
|
engin-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: engin
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: An async-first modular application framework
|
5
|
+
Project-URL: Homepage, https://github.com/invokermain/engin
|
6
|
+
Project-URL: Documentation, https://engin.readthedocs.io/en/latest/
|
7
|
+
Project-URL: Repository, https://github.com/invokermain/engin.git
|
8
|
+
Project-URL: Changelog, https://github.com/invokermain/engin/blob/main/CHANGELOG.md
|
9
|
+
License-Expression: MIT
|
10
|
+
License-File: LICENSE
|
11
|
+
Keywords: Application Framework,Dependency Injection
|
12
|
+
Requires-Python: >=3.10
|
13
|
+
Requires-Dist: anyio>=4
|
14
|
+
Requires-Dist: exceptiongroup>=1
|
15
|
+
Provides-Extra: cli
|
16
|
+
Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'cli'
|
17
|
+
Requires-Dist: typer>=0.15; extra == 'cli'
|
18
|
+
Description-Content-Type: text/markdown
|
19
|
+
|
20
|
+
# Engin 🏎️
|
21
|
+
|
22
|
+
[](https://codecov.io/gh/invokermain/engin)
|
23
|
+
|
24
|
+
---
|
25
|
+
|
26
|
+
**Documentation**: [https://engin.readthedocs.io/](https://engin.readthedocs.io/)
|
27
|
+
|
28
|
+
**Source Code**: [https://github.com/invokermain/engin](https://github.com/invokermain/engin)
|
29
|
+
|
30
|
+
---
|
31
|
+
|
32
|
+
Engin is a lightweight application framework powered by dependency injection, it helps
|
33
|
+
you build and maintain large monoliths and many microservices.
|
34
|
+
|
35
|
+
|
36
|
+
## Feature
|
37
|
+
|
38
|
+
The Engin framework gives you:
|
39
|
+
|
40
|
+
- A fully-featured dependency injection system.
|
41
|
+
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
42
|
+
- Zero boilerplate code reuse across applications.
|
43
|
+
- Integrations for other frameworks such as FastAPI.
|
44
|
+
- Full async support.
|
45
|
+
- CLI commands to aid local development.
|
46
|
+
|
47
|
+
|
48
|
+
## Installation
|
49
|
+
|
50
|
+
Engin is available on PyPI, install it using your favourite dependency manager:
|
51
|
+
|
52
|
+
- `pip install engin`
|
53
|
+
- `poetry add engin`
|
54
|
+
- `uv add engin`
|
55
|
+
|
56
|
+
## Example
|
57
|
+
|
58
|
+
A small example which shows some of the features of Engin. This application
|
59
|
+
makes 3 http requests and shuts itself down.
|
60
|
+
|
61
|
+
```python
|
62
|
+
import asyncio
|
63
|
+
from httpx import AsyncClient
|
64
|
+
from engin import Engin, Invoke, Lifecycle, OnException, Provide, Supervisor
|
65
|
+
|
66
|
+
|
67
|
+
def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
|
68
|
+
# create our http client
|
69
|
+
client = AsyncClient()
|
70
|
+
# this will open and close the AsyncClient as part of the application's lifecycle
|
71
|
+
lifecycle.append(client)
|
72
|
+
return client
|
73
|
+
|
74
|
+
|
75
|
+
async def main(
|
76
|
+
httpx_client: AsyncClient,
|
77
|
+
supervisor: Supervisor,
|
78
|
+
) -> None:
|
79
|
+
async def http_requests_task():
|
80
|
+
# simulate a background task
|
81
|
+
for x in range(3):
|
82
|
+
await httpx_client.get("https://httpbin.org/get")
|
83
|
+
await asyncio.sleep(1.0)
|
84
|
+
# raise an error to shutdown the application, normally you wouldn't do this!
|
85
|
+
raise RuntimeError("Forcing shutdown")
|
86
|
+
|
87
|
+
# supervise the http requests as part of the application's lifecycle
|
88
|
+
supervisor.supervise(http_requests_task, on_exception=OnException.SHUTDOWN)
|
89
|
+
|
90
|
+
|
91
|
+
# define our modular application
|
92
|
+
engin = Engin(Provide(httpx_client_factory), Invoke(main))
|
93
|
+
|
94
|
+
# run it!
|
95
|
+
asyncio.run(engin.run())
|
96
|
+
```
|
97
|
+
|
98
|
+
With logs enabled this will output:
|
99
|
+
|
100
|
+
```shell
|
101
|
+
INFO:engin:starting engin
|
102
|
+
INFO:engin:startup complete
|
103
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
104
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
105
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
106
|
+
ERROR:engin:supervisor task 'http_requests_task' raised RuntimeError, starting shutdown
|
107
|
+
Traceback (most recent call last):
|
108
|
+
File "C:\dev\python\engin\src\engin\_supervisor.py", line 58, in __call__
|
109
|
+
await self.factory()
|
110
|
+
File "C:\dev\python\engin\readme_example.py", line 29, in http_requests_task
|
111
|
+
raise RuntimeError("Forcing shutdown")
|
112
|
+
RuntimeError: Forcing shutdown
|
113
|
+
INFO:engin:stopping engin
|
114
|
+
INFO:engin:shutdown complete
|
115
|
+
```
|
116
|
+
|
117
|
+
## Inspiration
|
118
|
+
|
119
|
+
Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
|
120
|
+
and the [Injector framework for Python](https://github.com/python-injector/injector).
|
121
|
+
|
122
|
+
They are both great projects, go check them out.
|
engin-0.1.0/README.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Engin 🏎️
|
2
|
+
|
3
|
+
[](https://codecov.io/gh/invokermain/engin)
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
**Documentation**: [https://engin.readthedocs.io/](https://engin.readthedocs.io/)
|
8
|
+
|
9
|
+
**Source Code**: [https://github.com/invokermain/engin](https://github.com/invokermain/engin)
|
10
|
+
|
11
|
+
---
|
12
|
+
|
13
|
+
Engin is a lightweight application framework powered by dependency injection, it helps
|
14
|
+
you build and maintain large monoliths and many microservices.
|
15
|
+
|
16
|
+
|
17
|
+
## Feature
|
18
|
+
|
19
|
+
The Engin framework gives you:
|
20
|
+
|
21
|
+
- A fully-featured dependency injection system.
|
22
|
+
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
23
|
+
- Zero boilerplate code reuse across applications.
|
24
|
+
- Integrations for other frameworks such as FastAPI.
|
25
|
+
- Full async support.
|
26
|
+
- CLI commands to aid local development.
|
27
|
+
|
28
|
+
|
29
|
+
## Installation
|
30
|
+
|
31
|
+
Engin is available on PyPI, install it using your favourite dependency manager:
|
32
|
+
|
33
|
+
- `pip install engin`
|
34
|
+
- `poetry add engin`
|
35
|
+
- `uv add engin`
|
36
|
+
|
37
|
+
## Example
|
38
|
+
|
39
|
+
A small example which shows some of the features of Engin. This application
|
40
|
+
makes 3 http requests and shuts itself down.
|
41
|
+
|
42
|
+
```python
|
43
|
+
import asyncio
|
44
|
+
from httpx import AsyncClient
|
45
|
+
from engin import Engin, Invoke, Lifecycle, OnException, Provide, Supervisor
|
46
|
+
|
47
|
+
|
48
|
+
def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
|
49
|
+
# create our http client
|
50
|
+
client = AsyncClient()
|
51
|
+
# this will open and close the AsyncClient as part of the application's lifecycle
|
52
|
+
lifecycle.append(client)
|
53
|
+
return client
|
54
|
+
|
55
|
+
|
56
|
+
async def main(
|
57
|
+
httpx_client: AsyncClient,
|
58
|
+
supervisor: Supervisor,
|
59
|
+
) -> None:
|
60
|
+
async def http_requests_task():
|
61
|
+
# simulate a background task
|
62
|
+
for x in range(3):
|
63
|
+
await httpx_client.get("https://httpbin.org/get")
|
64
|
+
await asyncio.sleep(1.0)
|
65
|
+
# raise an error to shutdown the application, normally you wouldn't do this!
|
66
|
+
raise RuntimeError("Forcing shutdown")
|
67
|
+
|
68
|
+
# supervise the http requests as part of the application's lifecycle
|
69
|
+
supervisor.supervise(http_requests_task, on_exception=OnException.SHUTDOWN)
|
70
|
+
|
71
|
+
|
72
|
+
# define our modular application
|
73
|
+
engin = Engin(Provide(httpx_client_factory), Invoke(main))
|
74
|
+
|
75
|
+
# run it!
|
76
|
+
asyncio.run(engin.run())
|
77
|
+
```
|
78
|
+
|
79
|
+
With logs enabled this will output:
|
80
|
+
|
81
|
+
```shell
|
82
|
+
INFO:engin:starting engin
|
83
|
+
INFO:engin:startup complete
|
84
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
85
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
86
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
87
|
+
ERROR:engin:supervisor task 'http_requests_task' raised RuntimeError, starting shutdown
|
88
|
+
Traceback (most recent call last):
|
89
|
+
File "C:\dev\python\engin\src\engin\_supervisor.py", line 58, in __call__
|
90
|
+
await self.factory()
|
91
|
+
File "C:\dev\python\engin\readme_example.py", line 29, in http_requests_task
|
92
|
+
raise RuntimeError("Forcing shutdown")
|
93
|
+
RuntimeError: Forcing shutdown
|
94
|
+
INFO:engin:stopping engin
|
95
|
+
INFO:engin:shutdown complete
|
96
|
+
```
|
97
|
+
|
98
|
+
## Inspiration
|
99
|
+
|
100
|
+
Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
|
101
|
+
and the [Injector framework for Python](https://github.com/python-injector/injector).
|
102
|
+
|
103
|
+
They are both great projects, go check them out.
|
engin-0.1.0/docs/cli.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
# CLI Commands
|
2
|
+
|
3
|
+
Engin provides a set of CLI commands to aid with application development. To use these commands,
|
4
|
+
you need to install Engin with the `cli` extra, which can safely be treated as a
|
5
|
+
development only dependency.
|
6
|
+
|
7
|
+
```shell
|
8
|
+
uv add engin[cli]
|
9
|
+
```
|
10
|
+
|
11
|
+
!!! tip
|
12
|
+
|
13
|
+
It is recommended to configure a default instance in your `pyproject.toml`.
|
14
|
+
|
15
|
+
```toml
|
16
|
+
[tool.engin]
|
17
|
+
default-instance = "myapp.main:engin"
|
18
|
+
```
|
19
|
+
|
20
|
+
When configured, you can run any CLI command without the app argument:
|
21
|
+
|
22
|
+
```shell
|
23
|
+
# Uses the default instance from pyproject.toml
|
24
|
+
engin check
|
25
|
+
|
26
|
+
# Passing an explicit instance always overrides the default instance
|
27
|
+
engin check myapp.main:engin
|
28
|
+
```
|
29
|
+
|
30
|
+
## Commands
|
31
|
+
|
32
|
+
- `engin check`: checks for missing providers.
|
33
|
+
- `engin inspect`: show metadata about providers.
|
34
|
+
- `engin graph`: visualise your dependency graph.
|
35
|
+
|
36
|
+
### engin check
|
37
|
+
|
38
|
+
Checks that all dependencies in your Engin instance are satisfied.
|
39
|
+
|
40
|
+
If there are any missing providers it will return with exit code 1.
|
41
|
+
|
42
|
+
Note, this command only validates there are providers for all dependencies required by
|
43
|
+
invocations, any dependencies built dynamically at runtime via the Assembler will not be
|
44
|
+
checked for as these cannot be statically analysed.
|
45
|
+
|
46
|
+
#### Usage
|
47
|
+
```shell
|
48
|
+
engin check [OPTIONS]
|
49
|
+
```
|
50
|
+
|
51
|
+
#### Options
|
52
|
+
|
53
|
+
- `--app`: the path to your application in the format `<module>:<attribute>`, e.g. `myapp.main:engin`. Not
|
54
|
+
required if you set a `default-instance` in your `pyproject.toml`.
|
55
|
+
|
56
|
+
#### Example
|
57
|
+
|
58
|
+
```shell
|
59
|
+
engin check myapp.main:engin
|
60
|
+
```
|
61
|
+
|
62
|
+
=== "Success"
|
63
|
+
|
64
|
+
```
|
65
|
+
✅ All dependencies are satisfied!
|
66
|
+
```
|
67
|
+
|
68
|
+
=== "Missing Dependencies"
|
69
|
+
|
70
|
+
```
|
71
|
+
❌ Missing providers found:
|
72
|
+
• httpx.AsyncClient
|
73
|
+
• DatabaseConfig
|
74
|
+
```
|
75
|
+
|
76
|
+
### engin inspect
|
77
|
+
|
78
|
+
Shows detailed metadata for providers in your Engin instance. You can filter providers by type
|
79
|
+
or module.
|
80
|
+
|
81
|
+
#### Usage
|
82
|
+
```shell
|
83
|
+
engin inspect [OPTIONS]
|
84
|
+
```
|
85
|
+
|
86
|
+
#### Options
|
87
|
+
|
88
|
+
- `--app`: the path to your application in the format `<module>:<attribute>`, e.g. `myapp.main:engin`. Not
|
89
|
+
required if you set a `default-instance` in your `pyproject.toml`.
|
90
|
+
- `--type`: filter providers by return type name. Note that multiproviders take the form of `type[]`.
|
91
|
+
- `--module`: filter providers by the return type's module.
|
92
|
+
- `--verbose`: enable verbose output.
|
93
|
+
|
94
|
+
#### Example
|
95
|
+
|
96
|
+
```shell
|
97
|
+
engin inspect myapp.main:engin --module httpx
|
98
|
+
```
|
99
|
+
|
100
|
+
=== "Output"
|
101
|
+
|
102
|
+
```
|
103
|
+
Found 1 matching provider
|
104
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
105
|
+
│ name │ Provide(factory=httpx_client, type=AsyncClient) │
|
106
|
+
│ scope │ N/A │
|
107
|
+
│ func │ httpx_client │
|
108
|
+
│ block │ N/A │
|
109
|
+
│ source module │ myapp.main │
|
110
|
+
│ source package │ myapp │
|
111
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
112
|
+
```
|
113
|
+
|
114
|
+
### engin graph
|
115
|
+
|
116
|
+
Creates a visual representation of your application's dependency graph.
|
117
|
+
|
118
|
+
This starts a local web server which displays an interactive graph of your dependencies.
|
119
|
+
|
120
|
+
#### Usage
|
121
|
+
```shell
|
122
|
+
engin graph [OPTIONS]
|
123
|
+
```
|
124
|
+
|
125
|
+
#### Options
|
126
|
+
|
127
|
+
- `--app`: the path to your application in the format `<module>:<attribute>`, e.g. `myapp.main:engin`. Not
|
128
|
+
required if you set a `default-instance` in your `pyproject.toml`.
|
129
|
+
|
130
|
+
#### Example
|
131
|
+
|
132
|
+
```shell
|
133
|
+
engin graph myapp.main:engin
|
134
|
+
```
|
135
|
+
|
136
|
+
=== "Visualisation"
|
137
|
+
|
138
|
+

|
139
|
+
|
140
|
+
=== "Console"
|
141
|
+
|
142
|
+
```
|
143
|
+
Serving dependency graph on http://localhost:8123
|
144
|
+
```
|
@@ -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
|
-
Blocks are themselves valid options, so Blocks can include other Blocks as options. This
|
53
|
-
|
53
|
+
Blocks are themselves valid options, so Blocks can include other Blocks as options. This
|
54
|
+
compositional approach can help you build and manage larger applications.
|
54
55
|
|
55
56
|
|
56
57
|
## Defining Providers & Invocations in the Block
|
@@ -77,6 +78,6 @@ class ExampleBlock(Block):
|
|
77
78
|
|
78
79
|
!!!note
|
79
80
|
|
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.
|
81
|
+
The `self` parameter in these methods is replaced with an empty object at runtime so
|
82
|
+
should not be used. Blocks do not need to be instantiated to be passed to Engin as an
|
83
|
+
option.
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# The Engin
|
2
|
+
|
3
|
+
An Engin instance is a self-contained application.
|
4
|
+
|
5
|
+
The Engin class manages your application's complete lifecycle, when ran it will:
|
6
|
+
|
7
|
+
1. Assemble the dependencies required by your invocations.
|
8
|
+
2. Runs all given invocations sequentially in the order they were passed in to the Engin.
|
9
|
+
3. Run all lifecycle startup tasks that were registered by assembled dependencies sequentially.
|
10
|
+
4. Start any supervised background tasks.
|
11
|
+
5. Wait for a shutdown signal, SIGINT or SIGTERM, or for a supervised task to cause a shutdown.
|
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.
|
14
|
+
|
15
|
+
|
16
|
+
## Creating an Engin
|
17
|
+
|
18
|
+
Instantiate an Engin with any combination of options, i.e. providers, invocations, and blocks:
|
19
|
+
|
20
|
+
```python
|
21
|
+
from engin import Engin, Entrypoint, Provide, Supervisor
|
22
|
+
|
23
|
+
def my_service_factory(supervisor: Supervisor) -> MyService:
|
24
|
+
my_service = MyService()
|
25
|
+
supervisor.supervise(my_service.run)
|
26
|
+
return my_service
|
27
|
+
|
28
|
+
engin = Engin(Provide(my_service_factory), Entrypoint(MyService))
|
29
|
+
```
|
30
|
+
|
31
|
+
## Running your application
|
32
|
+
|
33
|
+
### `engin.run()`
|
34
|
+
|
35
|
+
The recommended way to run your application.
|
36
|
+
|
37
|
+
This will not return until the application is stopped and shutdown has been performed. As it
|
38
|
+
listens for signals and handles cancellation this should be the top-most function called in
|
39
|
+
your application:
|
40
|
+
|
41
|
+
```python
|
42
|
+
import asyncio
|
43
|
+
|
44
|
+
asyncio.run(engin.run())
|
45
|
+
```
|
46
|
+
|
47
|
+
|
48
|
+
### `engin.start()` and `engin.stop()`
|
49
|
+
|
50
|
+
For advanced scenarios where you need more control over the application lifecycle:
|
51
|
+
|
52
|
+
```python
|
53
|
+
# Start the application in the background
|
54
|
+
await engin.start()
|
55
|
+
|
56
|
+
# Do other work...
|
57
|
+
|
58
|
+
# Gracefully stop the application
|
59
|
+
await engin.stop()
|
60
|
+
```
|
61
|
+
|
62
|
+
This approach can be useful when writing tests for an Engin application.
|
@@ -1,19 +1,19 @@
|
|
1
1
|
# Invocations
|
2
2
|
|
3
|
-
Invocations define the behaviour of your application,
|
3
|
+
Invocations define the behaviour of your application, without any Invocations
|
4
4
|
your application will not do anything.
|
5
5
|
|
6
|
-
Like providers invocations are functions that take one or more dependencies as
|
7
|
-
|
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 be used by Engin.
|
8
8
|
|
9
|
-
As part of the Engin's startup, all declared invocations will be called
|
10
|
-
the order they were registered.
|
9
|
+
As part of the Engin's startup sequence, all declared invocations will be called
|
10
|
+
sequentially in the order they were registered.
|
11
11
|
|
12
12
|
Invocations can be used to define behaviour in two ways.
|
13
13
|
|
14
14
|
**Implicit: Provider Lifecycle**
|
15
15
|
|
16
|
-
Invocations are always called therefore their dependencies are always assembled. This
|
16
|
+
Invocations are always called and therefore their dependencies are always assembled. This
|
17
17
|
means that any providers with lifecycles will register their lifecycles with the
|
18
18
|
application if directly or indirectly used by an invocation.
|
19
19
|
|
@@ -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!
|
@@ -1,5 +1,3 @@
|
|
1
|
-
from contextlib import asynccontextmanager
|
2
|
-
|
3
1
|
# Lifecycle
|
4
2
|
|
5
3
|
Certain types of object naturally have some form of startup and shutdown behaviour
|
@@ -10,13 +8,13 @@ connection pool on startup, and gracefully release the connections on shutdown.
|
|
10
8
|
Doing this yourself can be tricky and is application dependent: most will not have any
|
11
9
|
special support for this and will expect you to manage your lifecycle concerns in your
|
12
10
|
entrypoint function, leading to unwieldy code in larger applications, whilst other
|
13
|
-
types application might expected you to translate the
|
11
|
+
types application might expected you to translate the lifecycle tasks into something they
|
14
12
|
offer, e.g. an ASGI server would expect you to manage this via its lifespan. In both cases
|
15
13
|
you end up managing lifecycle in a completely different place to where you declare your
|
16
14
|
objects, which make the codebase more complicated to understand.
|
17
15
|
|
18
16
|
Luckily, engin makes declaring lifecycle tasks a breeze, and it can be done in the same
|
19
|
-
provider that
|
17
|
+
provider that builds your object keeping your code nicely collocated.
|
20
18
|
|
21
19
|
## The Lifecycle type
|
22
20
|
|
@@ -49,9 +47,9 @@ def httpx_client(lifecycle: Lifecycle) -> AsyncClient:
|
|
49
47
|
return client
|
50
48
|
```
|
51
49
|
|
52
|
-
### 2.
|
50
|
+
### 2. Explicit startup & shutdown methods
|
53
51
|
|
54
|
-
If your type exposes
|
52
|
+
If your type exposes methods that must be called as part of the lifecycle, e.g. `start()`
|
55
53
|
& `stop()`, then `lifecycle.hook(on_start=..., on_stop=...)` is the way.
|
56
54
|
|
57
55
|
Let's look at an example using `piccolo.engine.PostgresEngin`:
|
@@ -60,12 +58,12 @@ Let's look at an example using `piccolo.engine.PostgresEngin`:
|
|
60
58
|
from engin import Lifecycle
|
61
59
|
from piccolo.engine import PostgresEngine
|
62
60
|
|
63
|
-
def postgres_engine(
|
61
|
+
def postgres_engine(lifecycle: Lifecycle) -> PostgresEngine:
|
64
62
|
db_engine = PostgresEngine(...) # fill in actual connection details
|
65
63
|
|
66
|
-
|
67
|
-
on_start=db_engine.start_connection_pool
|
68
|
-
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,
|
69
67
|
)
|
70
68
|
|
71
69
|
return db_engine
|
@@ -81,6 +79,7 @@ therefore we want to manage it as a task.
|
|
81
79
|
|
82
80
|
```python
|
83
81
|
import asyncio
|
82
|
+
from contextlib import asynccontextmanager
|
84
83
|
from engin import Lifecycle
|
85
84
|
from some_package import BlockingAsyncWorker
|
86
85
|
|
@@ -98,3 +97,7 @@ def blocking_worker(lifecycle: Lifecycle) -> BlockingWorker:
|
|
98
97
|
|
99
98
|
return worker
|
100
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.
|