engin 0.1.0a2__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.1.0a2 → 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.1.0a2 → engin-0.1.0b2}/docs/concepts/invocations.md +3 -3
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/concepts/lifecycle.md +0 -2
- engin-0.1.0b2/docs/index.md +62 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/mkdocs.yaml +10 -6
- {engin-0.1.0a2 → engin-0.1.0b2}/pyproject.toml +1 -1
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/__init__.py +0 -2
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_engin.py +73 -88
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_supervisor.py +9 -27
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/acceptance/test_engin_signal_handling.py +8 -1
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/acceptance/test_error_in_shutdown.py +2 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/acceptance/test_error_in_start_up.py +1 -8
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/acceptance/test_error_in_supervised_task.py +10 -2
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/test_supervisor.py +21 -23
- {engin-0.1.0a2 → engin-0.1.0b2}/uv.lock +97 -97
- engin-0.1.0a2/PKG-INFO +0 -73
- engin-0.1.0a2/README.md +0 -55
- engin-0.1.0a2/docs/getting-started.md +0 -7
- engin-0.1.0a2/docs/index.md +0 -21
- engin-0.1.0a2/src/engin/_shutdown.py +0 -4
- {engin-0.1.0a2 → engin-0.1.0b2}/.github/workflows/benchmark.yaml +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/.github/workflows/check.yaml +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/.github/workflows/publish.yaml +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/.gitignore +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/.readthedocs.yaml +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/LICENSE +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/concepts/blocks.md +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/concepts/engin.md +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/concepts/providers.md +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/guides/fastapi-graph.png +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/guides/fastapi.md +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/js/readthedocs.js +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/overrides/main.html +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/docs/reference.md +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/app.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/db/block.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/features/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/asgi/main.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/app.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/main.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/cats/api.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/simple/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/examples/simple/main.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_assembler.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_block.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_cli/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_cli/_common.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_cli/_graph.html +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_cli/_graph.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_cli/_inspect.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_dependency.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_graph.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_introspect.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_lifecycle.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_option.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/_type_utils.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/exceptions.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/extensions/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/extensions/asgi.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/extensions/fastapi.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/src/engin/py.typed +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/acceptance/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/acceptance/test_fastapi.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/benchmarks/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/benchmarks/conftest.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/benchmarks/test_bench_assembler.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/cli/__init__.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/cli/test_graph.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/cli/test_inspect.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/conftest.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/deps.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/test_assembler.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/test_block.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/test_dependencies.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/test_engin.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/test_graph.py +0 -0
- {engin-0.1.0a2 → engin-0.1.0b2}/tests/test_lifecycle.py +0 -0
- {engin-0.1.0a2 → 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,7 +2,7 @@ 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
8
|
from enum import Enum
|
@@ -10,14 +10,13 @@ from itertools import chain
|
|
10
10
|
from types import FrameType
|
11
11
|
from typing import ClassVar
|
12
12
|
|
13
|
-
from anyio import
|
13
|
+
from anyio import create_task_group, get_cancelled_exc_class
|
14
14
|
|
15
15
|
from engin._assembler import AssembledDependency, Assembler
|
16
16
|
from engin._dependency import Invoke, Provide, Supply
|
17
17
|
from engin._graph import DependencyGrapher, Node
|
18
18
|
from engin._lifecycle import Lifecycle
|
19
19
|
from engin._option import Option
|
20
|
-
from engin._shutdown import ShutdownSwitch
|
21
20
|
from engin._supervisor import Supervisor
|
22
21
|
from engin._type_utils import TypeId
|
23
22
|
from engin.exceptions import EnginError
|
@@ -29,25 +28,15 @@ LOG = logging.getLogger("engin")
|
|
29
28
|
class _EnginState(Enum):
|
30
29
|
IDLE = 0
|
31
30
|
"""
|
32
|
-
|
31
|
+
Engin is not yet started.
|
33
32
|
"""
|
34
33
|
|
35
|
-
|
34
|
+
RUNNING = 1
|
36
35
|
"""
|
37
|
-
Engin
|
36
|
+
Engin is currently running.
|
38
37
|
"""
|
39
38
|
|
40
|
-
|
41
|
-
"""
|
42
|
-
Engin running via .run() call
|
43
|
-
"""
|
44
|
-
|
45
|
-
STOPPING = 3
|
46
|
-
"""
|
47
|
-
Engin stopped via .stop() call
|
48
|
-
"""
|
49
|
-
|
50
|
-
SHUTDOWN = 4
|
39
|
+
SHUTDOWN = 2
|
51
40
|
"""
|
52
41
|
Engin has performed shutdown
|
53
42
|
"""
|
@@ -113,16 +102,15 @@ class Engin:
|
|
113
102
|
*options: an instance of Provide, Supply, Invoke, Entrypoint or a Block.
|
114
103
|
"""
|
115
104
|
self._state = _EnginState.IDLE
|
116
|
-
self.
|
105
|
+
self._start_complete_event = Event()
|
106
|
+
self._stop_requested_event = Event()
|
117
107
|
self._stop_complete_event = Event()
|
118
|
-
self._exit_stack
|
119
|
-
self._shutdown_task: Task | None = None
|
120
|
-
self._run_task: Task | None = None
|
108
|
+
self._exit_stack = AsyncExitStack()
|
121
109
|
self._assembler = Assembler([])
|
110
|
+
self._async_context_run_task: asyncio.Task | None = None
|
122
111
|
|
123
112
|
self._providers: dict[TypeId, Provide] = {
|
124
113
|
TypeId.from_type(Assembler): Supply(self._assembler),
|
125
|
-
TypeId.from_type(ShutdownSwitch): Supply(self._stop_requested_event),
|
126
114
|
}
|
127
115
|
self._multiproviders: dict[TypeId, list[Provide]] = defaultdict(list)
|
128
116
|
self._invocations: list[Invoke] = []
|
@@ -147,31 +135,6 @@ class Engin:
|
|
147
135
|
The engin will run until it is stopped via an external signal (i.e. SIGTERM or
|
148
136
|
SIGINT), the `stop` method is called on the engin, or a lifecycle task errors.
|
149
137
|
"""
|
150
|
-
await self.start()
|
151
|
-
|
152
|
-
# engin failed to start, so exit early
|
153
|
-
if self._state != _EnginState.STARTED:
|
154
|
-
return
|
155
|
-
|
156
|
-
self._state = _EnginState.RUNNING
|
157
|
-
|
158
|
-
async with create_task_group() as tg:
|
159
|
-
tg.start_soon(_stop_engin_on_signal, self._stop_requested_event)
|
160
|
-
try:
|
161
|
-
await self._stop_requested_event.wait()
|
162
|
-
await self._shutdown()
|
163
|
-
except get_cancelled_exc_class():
|
164
|
-
with CancelScope(shield=True):
|
165
|
-
await self._shutdown()
|
166
|
-
|
167
|
-
async def start(self) -> None:
|
168
|
-
"""
|
169
|
-
Start the engin.
|
170
|
-
|
171
|
-
This is an alternative to calling `run`. This method waits for the startup
|
172
|
-
lifecycle to complete and then returns. The caller is then responsible for
|
173
|
-
calling `stop`.
|
174
|
-
"""
|
175
138
|
if self._state != _EnginState.IDLE:
|
176
139
|
raise EnginError("Engin is not idle, unable to start")
|
177
140
|
|
@@ -189,10 +152,6 @@ class Engin:
|
|
189
152
|
return
|
190
153
|
|
191
154
|
lifecycle = await self._assembler.build(Lifecycle)
|
192
|
-
supervisor = await self._assembler.build(Supervisor)
|
193
|
-
|
194
|
-
if not supervisor.empty:
|
195
|
-
lifecycle.append(supervisor)
|
196
155
|
|
197
156
|
try:
|
198
157
|
for hook in lifecycle.list():
|
@@ -206,8 +165,30 @@ class Engin:
|
|
206
165
|
await self._shutdown()
|
207
166
|
return
|
208
167
|
|
168
|
+
supervisor = await self._assembler.build(Supervisor)
|
169
|
+
|
209
170
|
LOG.info("startup complete")
|
210
|
-
self._state = _EnginState.
|
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()
|
184
|
+
|
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()
|
211
192
|
|
212
193
|
async def stop(self) -> None:
|
213
194
|
"""
|
@@ -217,18 +198,25 @@ class Engin:
|
|
217
198
|
Note this method can be safely called at any point, even before the engin is
|
218
199
|
started.
|
219
200
|
"""
|
220
|
-
|
221
|
-
|
222
|
-
await self._shutdown()
|
223
|
-
# If the Engin was ran via `run()` notify via event
|
224
|
-
elif self._state == _EnginState.RUNNING:
|
225
|
-
self._stop_requested_event.set()
|
226
|
-
await self._stop_complete_event.wait()
|
201
|
+
self._stop_requested_event.set()
|
202
|
+
await self._stop_complete_event.wait()
|
227
203
|
|
228
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
|
+
"""
|
229
211
|
grapher = DependencyGrapher({**self._providers, **self._multiproviders})
|
230
212
|
return grapher.resolve(self._invocations)
|
231
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
|
+
|
232
220
|
async def _shutdown(self) -> None:
|
233
221
|
LOG.info("stopping engin")
|
234
222
|
await self._exit_stack.aclose()
|
@@ -241,32 +229,29 @@ async def _stop_engin_on_signal(stop_requested_event: Event) -> None:
|
|
241
229
|
"""
|
242
230
|
A task that waits for a stop signal (SIGINT/SIGTERM) and notifies the given event.
|
243
231
|
"""
|
244
|
-
try
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
stop_requested_event.set()
|
271
|
-
except asyncio.CancelledError:
|
272
|
-
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)
|