engin 0.1a1__tar.gz → 0.1.0b2__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.1a1 → engin-0.1.0b2}/CHANGELOG.md +4 -0
- engin-0.1.0b2/PKG-INFO +108 -0
- engin-0.1.0b2/README.md +90 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/concepts/invocations.md +3 -3
- {engin-0.1a1 → engin-0.1.0b2}/docs/concepts/lifecycle.md +0 -2
- engin-0.1.0b2/docs/index.md +62 -0
- {engin-0.1a1 → engin-0.1.0b2}/mkdocs.yaml +10 -6
- {engin-0.1a1 → engin-0.1.0b2}/pyproject.toml +1 -1
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/__init__.py +0 -2
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_engin.py +89 -67
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_supervisor.py +9 -27
- engin-0.1.0b2/tests/acceptance/test_engin_signal_handling.py +24 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/acceptance/test_error_in_shutdown.py +2 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/acceptance/test_error_in_start_up.py +4 -3
- engin-0.1.0b2/tests/acceptance/test_error_in_supervised_task.py +29 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_supervisor.py +21 -23
- {engin-0.1a1 → engin-0.1.0b2}/uv.lock +111 -110
- engin-0.1a1/PKG-INFO +0 -73
- engin-0.1a1/README.md +0 -55
- engin-0.1a1/docs/getting-started.md +0 -7
- engin-0.1a1/docs/index.md +0 -21
- engin-0.1a1/src/engin/_shutdown.py +0 -4
- engin-0.1a1/tests/acceptance/test_error_in_supervised_task.py +0 -17
- {engin-0.1a1 → engin-0.1.0b2}/.github/workflows/benchmark.yaml +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/.github/workflows/check.yaml +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/.github/workflows/publish.yaml +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/.gitignore +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/.readthedocs.yaml +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/LICENSE +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/concepts/blocks.md +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/concepts/engin.md +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/concepts/providers.md +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/guides/fastapi-graph.png +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/guides/fastapi.md +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/js/readthedocs.js +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/overrides/main.html +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/docs/reference.md +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/app.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/db/block.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/features/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/asgi/main.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/app.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/main.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/simple/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/examples/simple/main.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_assembler.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_block.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_cli/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_cli/_common.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_cli/_graph.html +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_cli/_graph.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_cli/_inspect.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_dependency.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_graph.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_introspect.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_lifecycle.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_option.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/_type_utils.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/exceptions.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/extensions/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/extensions/asgi.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/extensions/fastapi.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/src/engin/py.typed +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/acceptance/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/acceptance/test_fastapi.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/benchmarks/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/benchmarks/conftest.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/benchmarks/test_bench_assembler.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/cli/__init__.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/cli/test_graph.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/cli/test_inspect.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/conftest.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/deps.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_assembler.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_block.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_dependencies.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_engin.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_graph.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_lifecycle.py +0 -0
- {engin-0.1a1 → engin-0.1.0b2}/tests/test_type_id.py +0 -0
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
11
11
|
|
12
12
|
- Supervisor class which can safely supervise long running tasks.
|
13
13
|
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
- `ASGIEngin.run()` now raises an error to prevent incorrect usage.
|
17
|
+
|
14
18
|
|
15
19
|
## [0.0.20] - 2025-06-18
|
16
20
|
|
engin-0.1.0b2/PKG-INFO
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: engin
|
3
|
+
Version: 0.1.0b2
|
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: typer>=0.15; extra == 'cli'
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
|
19
|
+
# Engin 🏎️
|
20
|
+
|
21
|
+
[](https://codecov.io/gh/invokermain/engin)
|
22
|
+
|
23
|
+
---
|
24
|
+
|
25
|
+
**Documentation**: [https://engin.readthedocs.io/](https://engin.readthedocs.io/)
|
26
|
+
|
27
|
+
**Source Code**: [https://github.com/invokermain/engin](https://github.com/invokermain/engin)
|
28
|
+
|
29
|
+
---
|
30
|
+
|
31
|
+
Engin is a lightweight application framework powered by dependency injection, it helps
|
32
|
+
you build and maintain large monoliths and many microservices.
|
33
|
+
|
34
|
+
|
35
|
+
## Features
|
36
|
+
|
37
|
+
The Engin framework includes:
|
38
|
+
|
39
|
+
- A fully-featured dependency injection system.
|
40
|
+
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
41
|
+
- Zero boiler-plate code reuse across multiple applications.
|
42
|
+
- Integrations for other frameworks such as FastAPI.
|
43
|
+
- Full async support.
|
44
|
+
- CLI commands to aid local development.
|
45
|
+
|
46
|
+
|
47
|
+
## Installation
|
48
|
+
|
49
|
+
Engin is available on PyPI, install using your favourite dependency manager:
|
50
|
+
|
51
|
+
- `pip install engin`
|
52
|
+
- `poetry add engin`
|
53
|
+
- `uv add engin`
|
54
|
+
|
55
|
+
## Example
|
56
|
+
|
57
|
+
A small example which shows some of the runtime features of Engin. This application
|
58
|
+
makes a http request and then performs a shutdown.
|
59
|
+
|
60
|
+
```python
|
61
|
+
import asyncio
|
62
|
+
from httpx import AsyncClient
|
63
|
+
from engin import Engin, Invoke, Lifecycle, Provide, Supervisor
|
64
|
+
|
65
|
+
|
66
|
+
def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
|
67
|
+
# create our http client
|
68
|
+
client = AsyncClient()
|
69
|
+
# this will open and close the AsyncClient as part of the application's lifecycle
|
70
|
+
lifecycle.append(client)
|
71
|
+
return client
|
72
|
+
|
73
|
+
|
74
|
+
async def main(
|
75
|
+
httpx_client: AsyncClient,
|
76
|
+
supervisor: Supervisor,
|
77
|
+
) -> None:
|
78
|
+
async def http_request():
|
79
|
+
await httpx_client.get("https://httpbin.org/get")
|
80
|
+
# one we've made the http request shutdown the application
|
81
|
+
raise asyncio.CancelledError("Forcing shutdown")
|
82
|
+
|
83
|
+
# supervise the http request as part of the application's lifecycle
|
84
|
+
supervisor.supervise(http_request)
|
85
|
+
|
86
|
+
# define our modular application
|
87
|
+
engin = Engin(Provide(httpx_client_factory), Invoke(main))
|
88
|
+
|
89
|
+
# run it!
|
90
|
+
asyncio.run(engin.run())
|
91
|
+
```
|
92
|
+
|
93
|
+
With logs enabled this will output:
|
94
|
+
|
95
|
+
```shell
|
96
|
+
INFO:engin:starting engin
|
97
|
+
INFO:engin:startup complete
|
98
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
99
|
+
INFO:engin:stopping engin
|
100
|
+
INFO:engin:shutdown complete
|
101
|
+
```
|
102
|
+
|
103
|
+
## Inspiration
|
104
|
+
|
105
|
+
Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
|
106
|
+
and the [Injector framework for Python](https://github.com/python-injector/injector).
|
107
|
+
|
108
|
+
They are both great projects, check them out.
|
engin-0.1.0b2/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
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
|
+
## Features
|
18
|
+
|
19
|
+
The Engin framework includes:
|
20
|
+
|
21
|
+
- A fully-featured dependency injection system.
|
22
|
+
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
23
|
+
- Zero boiler-plate code reuse across multiple 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 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 runtime features of Engin. This application
|
40
|
+
makes a http request and then performs a shutdown.
|
41
|
+
|
42
|
+
```python
|
43
|
+
import asyncio
|
44
|
+
from httpx import AsyncClient
|
45
|
+
from engin import Engin, Invoke, Lifecycle, 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_request():
|
61
|
+
await httpx_client.get("https://httpbin.org/get")
|
62
|
+
# one we've made the http request shutdown the application
|
63
|
+
raise asyncio.CancelledError("Forcing shutdown")
|
64
|
+
|
65
|
+
# supervise the http request as part of the application's lifecycle
|
66
|
+
supervisor.supervise(http_request)
|
67
|
+
|
68
|
+
# define our modular application
|
69
|
+
engin = Engin(Provide(httpx_client_factory), Invoke(main))
|
70
|
+
|
71
|
+
# run it!
|
72
|
+
asyncio.run(engin.run())
|
73
|
+
```
|
74
|
+
|
75
|
+
With logs enabled this will output:
|
76
|
+
|
77
|
+
```shell
|
78
|
+
INFO:engin:starting engin
|
79
|
+
INFO:engin:startup complete
|
80
|
+
INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
|
81
|
+
INFO:engin:stopping engin
|
82
|
+
INFO:engin:shutdown complete
|
83
|
+
```
|
84
|
+
|
85
|
+
## Inspiration
|
86
|
+
|
87
|
+
Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
|
88
|
+
and the [Injector framework for Python](https://github.com/python-injector/injector).
|
89
|
+
|
90
|
+
They are both great projects, check them out.
|
@@ -3,8 +3,8 @@
|
|
3
3
|
Invocations define the behaviour of your application, therefore 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 used by Engin.
|
8
8
|
|
9
9
|
As part of the Engin's startup, all declared invocations will be called sequentially in
|
10
10
|
the order they were registered.
|
@@ -13,7 +13,7 @@ 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
|
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Engin 🏎️
|
2
|
+
|
3
|
+
Engin is a lightweight application framework powered by dependency injection, it helps
|
4
|
+
you build both large monoliths and multiple microservices.
|
5
|
+
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
The Engin framework includes:
|
10
|
+
|
11
|
+
- A fully-featured dependency injection system.
|
12
|
+
- A robust application runtime with lifecycle hooks and supervised background tasks.
|
13
|
+
- Zero boiler-plate code reuse across multiple applications.
|
14
|
+
- Integrations for other frameworks such as FastAPI.
|
15
|
+
- Full async support.
|
16
|
+
- CLI commands to aid local development.
|
17
|
+
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
=== "uv"
|
22
|
+
|
23
|
+
```shell
|
24
|
+
uv add engin
|
25
|
+
```
|
26
|
+
|
27
|
+
=== "poetry"
|
28
|
+
|
29
|
+
```shell
|
30
|
+
poetry add engin
|
31
|
+
```
|
32
|
+
|
33
|
+
=== "pip"
|
34
|
+
|
35
|
+
```shell
|
36
|
+
pip install engin
|
37
|
+
```
|
38
|
+
|
39
|
+
## Getting Started
|
40
|
+
|
41
|
+
A minimal example:
|
42
|
+
|
43
|
+
```python
|
44
|
+
import asyncio
|
45
|
+
|
46
|
+
from httpx import AsyncClient
|
47
|
+
|
48
|
+
from engin import Engin, Invoke, Provide
|
49
|
+
|
50
|
+
|
51
|
+
def httpx_client() -> AsyncClient:
|
52
|
+
return AsyncClient()
|
53
|
+
|
54
|
+
|
55
|
+
async def main(http_client: AsyncClient) -> None:
|
56
|
+
print(await http_client.get("https://httpbin.org/get"))
|
57
|
+
|
58
|
+
engin = Engin(Provide(httpx_client), Invoke(main))
|
59
|
+
|
60
|
+
asyncio.run(engin.run())
|
61
|
+
```
|
62
|
+
|
@@ -7,7 +7,7 @@ theme:
|
|
7
7
|
custom_dir: 'docs/overrides'
|
8
8
|
features:
|
9
9
|
- navigation.instant
|
10
|
-
-
|
10
|
+
- content.code.copy
|
11
11
|
palette:
|
12
12
|
- scheme: 'default'
|
13
13
|
media: '(prefers-color-scheme: light)'
|
@@ -27,7 +27,6 @@ edit_uri: ""
|
|
27
27
|
|
28
28
|
nav:
|
29
29
|
- Home: "index.md"
|
30
|
-
- Getting Started: "getting-started.md"
|
31
30
|
- Concepts:
|
32
31
|
- Engin: "concepts/engin.md"
|
33
32
|
- Providers: "concepts/providers.md"
|
@@ -60,6 +59,7 @@ plugins:
|
|
60
59
|
inventories:
|
61
60
|
- url: https://docs.python.org/3/objects.inv
|
62
61
|
domains: [py, std]
|
62
|
+
- search
|
63
63
|
|
64
64
|
watch:
|
65
65
|
- src
|
@@ -71,9 +71,13 @@ markdown_extensions:
|
|
71
71
|
- pymdownx.blocks.caption
|
72
72
|
- pymdownx.details
|
73
73
|
- pymdownx.highlight:
|
74
|
-
|
75
|
-
|
76
|
-
|
74
|
+
anchor_linenums: true
|
75
|
+
line_spans: __span
|
76
|
+
pygments_lang_class: true
|
77
77
|
- pymdownx.inlinehilite
|
78
|
-
- pymdownx.snippets
|
78
|
+
- pymdownx.snippets:
|
79
|
+
base_path: [!relative $config_dir]
|
80
|
+
check_paths: true
|
79
81
|
- pymdownx.superfences
|
82
|
+
- pymdownx.tabbed:
|
83
|
+
alternate_style: true
|
@@ -4,7 +4,6 @@ from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
|
4
4
|
from engin._engin import Engin
|
5
5
|
from engin._lifecycle import Lifecycle
|
6
6
|
from engin._option import Option
|
7
|
-
from engin._shutdown import ShutdownSwitch
|
8
7
|
from engin._supervisor import OnException, Supervisor
|
9
8
|
from engin._type_utils import TypeId
|
10
9
|
|
@@ -18,7 +17,6 @@ __all__ = [
|
|
18
17
|
"OnException",
|
19
18
|
"Option",
|
20
19
|
"Provide",
|
21
|
-
"ShutdownSwitch",
|
22
20
|
"Supervisor",
|
23
21
|
"Supply",
|
24
22
|
"TypeId",
|
@@ -2,28 +2,46 @@ import asyncio
|
|
2
2
|
import logging
|
3
3
|
import os
|
4
4
|
import signal
|
5
|
-
from asyncio import Event
|
5
|
+
from asyncio import Event
|
6
6
|
from collections import defaultdict
|
7
7
|
from contextlib import AsyncExitStack
|
8
|
+
from enum import Enum
|
8
9
|
from itertools import chain
|
9
10
|
from types import FrameType
|
10
11
|
from typing import ClassVar
|
11
12
|
|
12
|
-
from anyio import
|
13
|
+
from anyio import create_task_group, get_cancelled_exc_class
|
13
14
|
|
14
15
|
from engin._assembler import AssembledDependency, Assembler
|
15
16
|
from engin._dependency import Invoke, Provide, Supply
|
16
17
|
from engin._graph import DependencyGrapher, Node
|
17
18
|
from engin._lifecycle import Lifecycle
|
18
19
|
from engin._option import Option
|
19
|
-
from engin._shutdown import ShutdownSwitch
|
20
20
|
from engin._supervisor import Supervisor
|
21
21
|
from engin._type_utils import TypeId
|
22
|
+
from engin.exceptions import EnginError
|
22
23
|
|
23
24
|
_OS_IS_WINDOWS = os.name == "nt"
|
24
25
|
LOG = logging.getLogger("engin")
|
25
26
|
|
26
27
|
|
28
|
+
class _EnginState(Enum):
|
29
|
+
IDLE = 0
|
30
|
+
"""
|
31
|
+
Engin is not yet started.
|
32
|
+
"""
|
33
|
+
|
34
|
+
RUNNING = 1
|
35
|
+
"""
|
36
|
+
Engin is currently running.
|
37
|
+
"""
|
38
|
+
|
39
|
+
SHUTDOWN = 2
|
40
|
+
"""
|
41
|
+
Engin has performed shutdown
|
42
|
+
"""
|
43
|
+
|
44
|
+
|
27
45
|
class Engin:
|
28
46
|
"""
|
29
47
|
The Engin is a modular application defined by a collection of options.
|
@@ -83,16 +101,16 @@ class Engin:
|
|
83
101
|
Args:
|
84
102
|
*options: an instance of Provide, Supply, Invoke, Entrypoint or a Block.
|
85
103
|
"""
|
86
|
-
self.
|
104
|
+
self._state = _EnginState.IDLE
|
105
|
+
self._start_complete_event = Event()
|
106
|
+
self._stop_requested_event = Event()
|
87
107
|
self._stop_complete_event = Event()
|
88
|
-
self._exit_stack
|
89
|
-
self._shutdown_task: Task | None = None
|
90
|
-
self._run_task: Task | None = None
|
108
|
+
self._exit_stack = AsyncExitStack()
|
91
109
|
self._assembler = Assembler([])
|
110
|
+
self._async_context_run_task: asyncio.Task | None = None
|
92
111
|
|
93
112
|
self._providers: dict[TypeId, Provide] = {
|
94
113
|
TypeId.from_type(Assembler): Supply(self._assembler),
|
95
|
-
TypeId.from_type(ShutdownSwitch): Supply(self._stop_requested_event),
|
96
114
|
}
|
97
115
|
self._multiproviders: dict[TypeId, list[Provide]] = defaultdict(list)
|
98
116
|
self._invocations: list[Invoke] = []
|
@@ -117,26 +135,9 @@ class Engin:
|
|
117
135
|
The engin will run until it is stopped via an external signal (i.e. SIGTERM or
|
118
136
|
SIGINT), the `stop` method is called on the engin, or a lifecycle task errors.
|
119
137
|
"""
|
120
|
-
|
121
|
-
|
122
|
-
self._shutdown_task.cancel("redundant")
|
123
|
-
async with create_task_group() as tg:
|
124
|
-
tg.start_soon(_stop_engin_on_signal, self._stop_requested_event)
|
125
|
-
try:
|
126
|
-
await self._stop_requested_event.wait()
|
127
|
-
await self._shutdown()
|
128
|
-
except get_cancelled_exc_class():
|
129
|
-
with CancelScope(shield=True):
|
130
|
-
await self._shutdown()
|
131
|
-
|
132
|
-
async def start(self) -> None:
|
133
|
-
"""
|
134
|
-
Start the engin.
|
138
|
+
if self._state != _EnginState.IDLE:
|
139
|
+
raise EnginError("Engin is not idle, unable to start")
|
135
140
|
|
136
|
-
This is an alternative to calling `run`. This method waits for the startup
|
137
|
-
lifecycle to complete and then returns. The caller is then responsible for
|
138
|
-
calling `stop`.
|
139
|
-
"""
|
140
141
|
LOG.info("starting engin")
|
141
142
|
assembled_invocations: list[AssembledDependency] = [
|
142
143
|
await self._assembler.assemble(invocation) for invocation in self._invocations
|
@@ -151,10 +152,6 @@ class Engin:
|
|
151
152
|
return
|
152
153
|
|
153
154
|
lifecycle = await self._assembler.build(Lifecycle)
|
154
|
-
supervisor = await self._assembler.build(Supervisor)
|
155
|
-
|
156
|
-
if not supervisor.empty:
|
157
|
-
lifecycle.append(supervisor)
|
158
155
|
|
159
156
|
try:
|
160
157
|
for hook in lifecycle.list():
|
@@ -168,9 +165,30 @@ class Engin:
|
|
168
165
|
await self._shutdown()
|
169
166
|
return
|
170
167
|
|
168
|
+
supervisor = await self._assembler.build(Supervisor)
|
169
|
+
|
171
170
|
LOG.info("startup complete")
|
171
|
+
self._state = _EnginState.RUNNING
|
172
|
+
self._start_complete_event.set()
|
173
|
+
|
174
|
+
async with create_task_group() as tg:
|
175
|
+
tg.start_soon(_stop_engin_on_signal, self._stop_requested_event)
|
176
|
+
|
177
|
+
try:
|
178
|
+
async with supervisor:
|
179
|
+
await self._stop_requested_event.wait()
|
180
|
+
except get_cancelled_exc_class():
|
181
|
+
pass
|
182
|
+
tg.cancel_scope.cancel()
|
183
|
+
await self._shutdown()
|
172
184
|
|
173
|
-
|
185
|
+
async def start(self) -> None:
|
186
|
+
"""
|
187
|
+
Starts the engin in the background. This method will wait until the engin is fully
|
188
|
+
started to return so it is safe to use immediately after.
|
189
|
+
"""
|
190
|
+
self._async_context_run_task = asyncio.create_task(self.run())
|
191
|
+
await self._start_complete_event.wait()
|
174
192
|
|
175
193
|
async def stop(self) -> None:
|
176
194
|
"""
|
@@ -181,55 +199,59 @@ class Engin:
|
|
181
199
|
started.
|
182
200
|
"""
|
183
201
|
self._stop_requested_event.set()
|
184
|
-
if self._shutdown_task is None:
|
185
|
-
return
|
186
202
|
await self._stop_complete_event.wait()
|
187
203
|
|
188
204
|
def graph(self) -> list[Node]:
|
205
|
+
"""
|
206
|
+
Creates a graph representation of the engin's dependencies which can be used for
|
207
|
+
introspection or visualisations.
|
208
|
+
|
209
|
+
Returns: a list of Node objects.
|
210
|
+
"""
|
189
211
|
grapher = DependencyGrapher({**self._providers, **self._multiproviders})
|
190
212
|
return grapher.resolve(self._invocations)
|
191
213
|
|
214
|
+
def is_running(self) -> bool:
|
215
|
+
return self._state == _EnginState.RUNNING
|
216
|
+
|
217
|
+
def is_stopped(self) -> bool:
|
218
|
+
return self._state == _EnginState.SHUTDOWN
|
219
|
+
|
192
220
|
async def _shutdown(self) -> None:
|
193
221
|
LOG.info("stopping engin")
|
194
222
|
await self._exit_stack.aclose()
|
195
223
|
self._stop_complete_event.set()
|
196
224
|
LOG.info("shutdown complete")
|
197
|
-
|
198
|
-
async def _shutdown_when_stopped(self) -> None:
|
199
|
-
await self._stop_requested_event.wait()
|
200
|
-
await self._shutdown()
|
225
|
+
self._state = _EnginState.SHUTDOWN
|
201
226
|
|
202
227
|
|
203
228
|
async def _stop_engin_on_signal(stop_requested_event: Event) -> None:
|
204
229
|
"""
|
205
230
|
A task that waits for a stop signal (SIGINT/SIGTERM) and notifies the given event.
|
206
231
|
"""
|
207
|
-
try
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
stop_requested_event.set()
|
234
|
-
except asyncio.CancelledError:
|
235
|
-
pass
|
232
|
+
# try to gracefully handle sigint/sigterm
|
233
|
+
if not _OS_IS_WINDOWS:
|
234
|
+
loop = asyncio.get_running_loop()
|
235
|
+
for signame in (signal.SIGINT, signal.SIGTERM):
|
236
|
+
loop.add_signal_handler(signame, stop_requested_event.set)
|
237
|
+
|
238
|
+
await stop_requested_event.wait()
|
239
|
+
else:
|
240
|
+
should_stop = False
|
241
|
+
|
242
|
+
# windows does not support signal_handlers, so this is the workaround
|
243
|
+
def ctrlc_handler(sig: int, frame: FrameType | None) -> None:
|
244
|
+
nonlocal should_stop
|
245
|
+
if should_stop:
|
246
|
+
raise KeyboardInterrupt("Forced keyboard interrupt")
|
247
|
+
should_stop = True
|
248
|
+
|
249
|
+
signal.signal(signal.SIGINT, ctrlc_handler)
|
250
|
+
|
251
|
+
while not should_stop:
|
252
|
+
# In case engin is stopped via external `stop` call.
|
253
|
+
if stop_requested_event.is_set():
|
254
|
+
return
|
255
|
+
await asyncio.sleep(0.1)
|
256
|
+
|
257
|
+
stop_requested_event.set()
|
@@ -2,17 +2,13 @@ import inspect
|
|
2
2
|
import logging
|
3
3
|
import typing
|
4
4
|
from collections.abc import Awaitable, Callable
|
5
|
-
from contextlib import AsyncExitStack
|
6
5
|
from dataclasses import dataclass
|
7
6
|
from enum import Enum
|
8
7
|
from types import TracebackType
|
9
|
-
from typing import TypeAlias
|
8
|
+
from typing import TypeAlias, assert_never
|
10
9
|
|
11
10
|
import anyio
|
12
11
|
from anyio import get_cancelled_exc_class
|
13
|
-
from exceptiongroup import BaseExceptionGroup, catch
|
14
|
-
|
15
|
-
from engin._shutdown import ShutdownSwitch
|
16
12
|
|
17
13
|
if typing.TYPE_CHECKING:
|
18
14
|
from anyio.abc import TaskGroup
|
@@ -72,13 +68,12 @@ class _SupervisorTask:
|
|
72
68
|
if self.on_exception == OnException.IGNORE:
|
73
69
|
self.complete = True
|
74
70
|
return
|
75
|
-
|
76
71
|
if self.on_exception == OnException.RETRY:
|
77
72
|
continue
|
78
|
-
|
79
73
|
if self.on_exception == OnException.SHUTDOWN:
|
80
74
|
self.complete = True
|
81
|
-
raise
|
75
|
+
raise get_cancelled_exc_class() from None
|
76
|
+
assert_never(self.on_exception)
|
82
77
|
|
83
78
|
@property
|
84
79
|
def name(self) -> str:
|
@@ -93,12 +88,8 @@ class _SupervisorTask:
|
|
93
88
|
|
94
89
|
|
95
90
|
class Supervisor:
|
96
|
-
def __init__(self
|
91
|
+
def __init__(self) -> None:
|
97
92
|
self._tasks: list[_SupervisorTask] = []
|
98
|
-
self._shutdown = shutdown
|
99
|
-
self._is_complete: bool = False
|
100
|
-
|
101
|
-
self._exit_stack: AsyncExitStack | None = None
|
102
93
|
self._task_group: TaskGroup | None = None
|
103
94
|
|
104
95
|
def supervise(
|
@@ -114,15 +105,7 @@ class Supervisor:
|
|
114
105
|
if not self._tasks:
|
115
106
|
return
|
116
107
|
|
117
|
-
|
118
|
-
self._shutdown.set()
|
119
|
-
|
120
|
-
self._exit_stack = AsyncExitStack()
|
121
|
-
await self._exit_stack.__aenter__()
|
122
|
-
self._exit_stack.enter_context(catch({Exception: _handler}))
|
123
|
-
self._task_group = await self._exit_stack.enter_async_context(
|
124
|
-
anyio.create_task_group()
|
125
|
-
)
|
108
|
+
self._task_group = await anyio.create_task_group().__aenter__()
|
126
109
|
|
127
110
|
for task in self._tasks:
|
128
111
|
self._task_group.start_soon(task, name=task.name)
|
@@ -134,8 +117,7 @@ class Supervisor:
|
|
134
117
|
traceback: TracebackType | None,
|
135
118
|
/,
|
136
119
|
) -> None:
|
137
|
-
if
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
await self._exit_stack.__aexit__(exc_type, exc_value, traceback)
|
120
|
+
if self._task_group:
|
121
|
+
if not self._task_group.cancel_scope.cancel_called:
|
122
|
+
self._task_group.cancel_scope.cancel()
|
123
|
+
await self._task_group.__aexit__(exc_type, exc_value, traceback)
|