engin 0.1.0rc1__tar.gz → 0.1.0rc2__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.
Files changed (113) hide show
  1. {engin-0.1.0rc1 → engin-0.1.0rc2}/CHANGELOG.md +6 -0
  2. {engin-0.1.0rc1 → engin-0.1.0rc2}/PKG-INFO +28 -15
  3. {engin-0.1.0rc1 → engin-0.1.0rc2}/README.md +27 -14
  4. engin-0.1.0rc2/docs/cli.md +149 -0
  5. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/concepts/blocks.md +5 -5
  6. engin-0.1.0rc2/docs/concepts/engin.md +62 -0
  7. engin-0.1.0rc2/docs/concepts/supervisor.md +64 -0
  8. engin-0.1.0rc2/docs/engin-graph-output.png +0 -0
  9. engin-0.1.0rc2/docs/index.md +98 -0
  10. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/integrations/fastapi.md +7 -11
  11. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/main.py +3 -1
  12. {engin-0.1.0rc1 → engin-0.1.0rc2}/mkdocs.yaml +3 -1
  13. {engin-0.1.0rc1 → engin-0.1.0rc2}/pyproject.toml +1 -1
  14. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_assembler.py +5 -7
  15. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_cli/_check.py +2 -10
  16. engin-0.1.0rc2/src/engin/_cli/_graph.html +883 -0
  17. engin-0.1.0rc2/src/engin/_cli/_graph.py +223 -0
  18. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_cli/_inspect.py +6 -3
  19. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_engin.py +2 -2
  20. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_supervisor.py +17 -3
  21. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/exceptions.py +21 -6
  22. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/cli/test_graph.py +3 -5
  23. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_assembler.py +5 -5
  24. {engin-0.1.0rc1 → engin-0.1.0rc2}/uv.lock +4 -4
  25. engin-0.1.0rc1/docs/concepts/engin.md +0 -12
  26. engin-0.1.0rc1/docs/concepts/supervisor.md +0 -0
  27. engin-0.1.0rc1/docs/index.md +0 -86
  28. engin-0.1.0rc1/src/engin/_cli/_graph.html +0 -78
  29. engin-0.1.0rc1/src/engin/_cli/_graph.py +0 -164
  30. {engin-0.1.0rc1 → engin-0.1.0rc2}/.github/workflows/benchmark.yaml +0 -0
  31. {engin-0.1.0rc1 → engin-0.1.0rc2}/.github/workflows/check.yaml +0 -0
  32. {engin-0.1.0rc1 → engin-0.1.0rc2}/.github/workflows/publish.yaml +0 -0
  33. {engin-0.1.0rc1 → engin-0.1.0rc2}/.gitignore +0 -0
  34. {engin-0.1.0rc1 → engin-0.1.0rc2}/.readthedocs.yaml +0 -0
  35. {engin-0.1.0rc1 → engin-0.1.0rc2}/LICENSE +0 -0
  36. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/concepts/invocations.md +0 -0
  37. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/concepts/lifecycle.md +0 -0
  38. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/concepts/providers.md +0 -0
  39. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/integrations/fastapi-graph.png +0 -0
  40. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/js/readthedocs.js +0 -0
  41. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/overrides/main.html +0 -0
  42. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/reference.md +0 -0
  43. {engin-0.1.0rc1 → engin-0.1.0rc2}/docs/tutorial.md +0 -0
  44. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/__init__.py +0 -0
  45. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/__init__.py +0 -0
  46. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/app.py +0 -0
  47. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/__init__.py +0 -0
  48. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/db/__init__.py +0 -0
  49. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/db/adapaters/__init__.py +0 -0
  50. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/db/adapaters/memory.py +0 -0
  51. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/db/block.py +0 -0
  52. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/db/ports.py +0 -0
  53. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/starlette/__init__.py +0 -0
  54. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/common/starlette/endpoint.py +0 -0
  55. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/features/__init__.py +0 -0
  56. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/features/cats/__init__.py +0 -0
  57. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/features/cats/api/__init__.py +0 -0
  58. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/features/cats/api/get.py +0 -0
  59. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/features/cats/api/post.py +0 -0
  60. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/features/cats/block.py +0 -0
  61. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/asgi/features/cats/domain.py +0 -0
  62. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/__init__.py +0 -0
  63. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/app.py +0 -0
  64. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/main.py +0 -0
  65. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/__init__.py +0 -0
  66. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/cats/__init__.py +0 -0
  67. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
  68. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
  69. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/cats/api.py +0 -0
  70. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/cats/block.py +0 -0
  71. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/cats/domain.py +0 -0
  72. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/fastapi/routes/cats/ports.py +0 -0
  73. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/simple/__init__.py +0 -0
  74. {engin-0.1.0rc1 → engin-0.1.0rc2}/examples/simple/main.py +0 -0
  75. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/__init__.py +0 -0
  76. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_block.py +0 -0
  77. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_cli/__init__.py +0 -0
  78. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_cli/_common.py +0 -0
  79. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_dependency.py +0 -0
  80. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_graph.py +0 -0
  81. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_introspect.py +0 -0
  82. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_lifecycle.py +0 -0
  83. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_option.py +0 -0
  84. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/_type_utils.py +0 -0
  85. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/extensions/__init__.py +0 -0
  86. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/extensions/asgi.py +0 -0
  87. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/extensions/fastapi.py +0 -0
  88. {engin-0.1.0rc1 → engin-0.1.0rc2}/src/engin/py.typed +0 -0
  89. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/__init__.py +0 -0
  90. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/__init__.py +0 -0
  91. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/test_engin_signal_handling.py +0 -0
  92. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/test_error_in_invocation.py +0 -0
  93. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/test_error_in_lifecycle_shutdown.py +0 -0
  94. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/test_error_in_lifecycle_startup.py +0 -0
  95. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/test_error_in_provider.py +0 -0
  96. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/test_error_in_supervisor_task.py +0 -0
  97. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/acceptance/test_fastapi.py +0 -0
  98. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/benchmarks/__init__.py +0 -0
  99. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/benchmarks/conftest.py +0 -0
  100. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/benchmarks/test_bench_assembler.py +0 -0
  101. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/cli/__init__.py +0 -0
  102. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/cli/test_check.py +0 -0
  103. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/cli/test_get_engin_instance.py +0 -0
  104. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/cli/test_inspect.py +0 -0
  105. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/conftest.py +0 -0
  106. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/deps.py +0 -0
  107. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_block.py +0 -0
  108. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_dependencies.py +0 -0
  109. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_engin.py +0 -0
  110. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_graph.py +0 -0
  111. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_lifecycle.py +0 -0
  112. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_supervisor.py +0 -0
  113. {engin-0.1.0rc1 → engin-0.1.0rc2}/tests/test_type_id.py +0 -0
@@ -13,6 +13,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
  - A new cli option `engin check` that validates whether you have any missing providers.
14
14
  - Support for specifying `default-instance` in your `pyproject.toml` under `[tool.engin]`
15
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`.
16
22
 
17
23
 
18
24
  ## [0.0.20] - 2025-06-18
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.1.0rc1
3
+ Version: 0.1.0rc2
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/
@@ -33,13 +33,13 @@ Engin is a lightweight application framework powered by dependency injection, it
33
33
  you build and maintain large monoliths and many microservices.
34
34
 
35
35
 
36
- ## Features
36
+ ## Feature
37
37
 
38
- The Engin framework includes:
38
+ 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 boiler-plate code reuse across multiple applications.
42
+ - Zero boiler-plate 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.
@@ -47,7 +47,7 @@ The Engin framework includes:
47
47
 
48
48
  ## Installation
49
49
 
50
- Engin is available on PyPI, install using your favourite dependency manager:
50
+ Engin is available on PyPI, install it using your favourite dependency manager:
51
51
 
52
52
  - `pip install engin`
53
53
  - `poetry add engin`
@@ -55,13 +55,13 @@ Engin is available on PyPI, install using your favourite dependency manager:
55
55
 
56
56
  ## Example
57
57
 
58
- A small example which shows some of the runtime features of Engin. This application
59
- makes a http request and then performs a shutdown.
58
+ A small example which shows some of the features of Engin. This application
59
+ makes 3 http requests and shuts itself down.
60
60
 
61
61
  ```python
62
62
  import asyncio
63
63
  from httpx import AsyncClient
64
- from engin import Engin, Invoke, Lifecycle, Provide, Supervisor
64
+ from engin import Engin, Invoke, Lifecycle, OnException, Provide, Supervisor
65
65
 
66
66
 
67
67
  def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
@@ -76,13 +76,17 @@ async def main(
76
76
  httpx_client: AsyncClient,
77
77
  supervisor: Supervisor,
78
78
  ) -> None:
79
- async def http_request():
80
- await httpx_client.get("https://httpbin.org/get")
81
- # one we've made the http request shutdown the application
82
- raise asyncio.CancelledError("Forcing shutdown")
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)
83
89
 
84
- # supervise the http request as part of the application's lifecycle
85
- supervisor.supervise(http_request)
86
90
 
87
91
  # define our modular application
88
92
  engin = Engin(Provide(httpx_client_factory), Invoke(main))
@@ -97,6 +101,15 @@ With logs enabled this will output:
97
101
  INFO:engin:starting engin
98
102
  INFO:engin:startup complete
99
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
100
113
  INFO:engin:stopping engin
101
114
  INFO:engin:shutdown complete
102
115
  ```
@@ -106,4 +119,4 @@ INFO:engin:shutdown complete
106
119
  Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
107
120
  and the [Injector framework for Python](https://github.com/python-injector/injector).
108
121
 
109
- They are both great projects, check them out.
122
+ They are both great projects, go check them out.
@@ -14,13 +14,13 @@ Engin is a lightweight application framework powered by dependency injection, it
14
14
  you build and maintain large monoliths and many microservices.
15
15
 
16
16
 
17
- ## Features
17
+ ## Feature
18
18
 
19
- The Engin framework includes:
19
+ 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 boiler-plate code reuse across multiple applications.
23
+ - Zero boiler-plate 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.
@@ -28,7 +28,7 @@ The Engin framework includes:
28
28
 
29
29
  ## Installation
30
30
 
31
- Engin is available on PyPI, install using your favourite dependency manager:
31
+ Engin is available on PyPI, install it using your favourite dependency manager:
32
32
 
33
33
  - `pip install engin`
34
34
  - `poetry add engin`
@@ -36,13 +36,13 @@ Engin is available on PyPI, install using your favourite dependency manager:
36
36
 
37
37
  ## Example
38
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.
39
+ A small example which shows some of the features of Engin. This application
40
+ makes 3 http requests and shuts itself down.
41
41
 
42
42
  ```python
43
43
  import asyncio
44
44
  from httpx import AsyncClient
45
- from engin import Engin, Invoke, Lifecycle, Provide, Supervisor
45
+ from engin import Engin, Invoke, Lifecycle, OnException, Provide, Supervisor
46
46
 
47
47
 
48
48
  def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
@@ -57,13 +57,17 @@ async def main(
57
57
  httpx_client: AsyncClient,
58
58
  supervisor: Supervisor,
59
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")
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)
64
70
 
65
- # supervise the http request as part of the application's lifecycle
66
- supervisor.supervise(http_request)
67
71
 
68
72
  # define our modular application
69
73
  engin = Engin(Provide(httpx_client_factory), Invoke(main))
@@ -78,6 +82,15 @@ With logs enabled this will output:
78
82
  INFO:engin:starting engin
79
83
  INFO:engin:startup complete
80
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
81
94
  INFO:engin:stopping engin
82
95
  INFO:engin:shutdown complete
83
96
  ```
@@ -87,4 +100,4 @@ INFO:engin:shutdown complete
87
100
  Engin is heavily inspired by [Uber's Fx framework for Go](https://github.com/uber-go/fx)
88
101
  and the [Injector framework for Python](https://github.com/python-injector/injector).
89
102
 
90
- They are both great projects, check them out.
103
+ They are both great projects, go check them out.
@@ -0,0 +1,149 @@
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
+ Available providers:
76
+ • str
77
+ • engin._assembler.Assembler
78
+ • engin._lifecycle.Lifecycle
79
+ ```
80
+
81
+ ### engin inspect
82
+
83
+ Shows detailed metadata for providers in your Engin instance. You can filter providers by type
84
+ or module.
85
+
86
+ #### Usage
87
+ ```shell
88
+ engin inspect [OPTIONS]
89
+ ```
90
+
91
+ #### Options
92
+
93
+ - `--app`: the path to your application in the format `<module>:<attribute>`, e.g. `myapp.main:engin`. Not
94
+ required if you set a `default-instance` in your `pyproject.toml`.
95
+ - `--type`: filter providers by return type name. Note that multiproviders take the form of `type[]`.
96
+ - `--module`: filter providers by the return type's module.
97
+ - `--verbose`: enable verbose output.
98
+
99
+ #### Example
100
+
101
+ ```shell
102
+ engin inspect myapp.main:engin --module httpx
103
+ ```
104
+
105
+ === "Output"
106
+
107
+ ```
108
+ Found 1 matching provider
109
+ ┌─────────────────────────────────────────────────────────────────────────┐
110
+ │ name │ Provide(factory=httpx_client, type=AsyncClient) │
111
+ │ scope │ N/A │
112
+ │ func │ httpx_client │
113
+ │ block │ N/A │
114
+ │ source module │ myapp.main │
115
+ │ source package │ myapp │
116
+ └─────────────────────────────────────────────────────────────────────────┘
117
+ ```
118
+
119
+ ### engin graph
120
+
121
+ Creates a visual representation of your application's dependency graph.
122
+
123
+ This starts a local web server which displays an interactive graph of your dependencies.
124
+
125
+ #### Usage
126
+ ```shell
127
+ engin graph [OPTIONS]
128
+ ```
129
+
130
+ #### Options
131
+
132
+ - `--app`: the path to your application in the format `<module>:<attribute>`, e.g. `myapp.main:engin`. Not
133
+ required if you set a `default-instance` in your `pyproject.toml`.
134
+
135
+ #### Example
136
+
137
+ ```shell
138
+ engin graph myapp.main:engin
139
+ ```
140
+
141
+ === "Visualisation"
142
+
143
+ ![engin-graph-output.png](engin-graph-output.png)
144
+
145
+ === "Console"
146
+
147
+ ```
148
+ Serving dependency graph on http://localhost:8123
149
+ ```
@@ -49,8 +49,8 @@ await engin.run() # prints 'hello'
49
49
 
50
50
  !!!tip
51
51
 
52
- Blocks are themselves valid options, so Blocks can include other Blocks as options. This
53
- compisitional approach can help you build and manage larger applications.
52
+ Blocks are themselves valid options, so Blocks can include other Blocks as options. This
53
+ compisitional approach can help you build and manage larger applications.
54
54
 
55
55
 
56
56
  ## Defining Providers & Invocations in the Block
@@ -77,6 +77,6 @@ class ExampleBlock(Block):
77
77
 
78
78
  !!!note
79
79
 
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.
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.
@@ -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. Run all corresponding lifecycle shutdown tasks in the reverse order to the startup order.
13
+
14
+
15
+ ## Creating an Engin
16
+
17
+ Instantiate an Engin with any combination of options, i.e. providers, invocations, and blocks:
18
+
19
+ ```python
20
+ from engin import Engin, Provide, Invoke, Supervisor
21
+
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(build_service), 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
+ await 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.
@@ -0,0 +1,64 @@
1
+ # Supervisor
2
+
3
+ The Supervisor manages background tasks for your application.
4
+
5
+ Background tasks are long-running coroutines that need to run concurrently while your
6
+ application is running. The Supervisor handles starting these tasks, monitoring them for errors,
7
+ and cancelling them when the application is shutting down.
8
+
9
+ ## Using the Supervisor
10
+
11
+ The Supervisor is automatically provided by the Engin and can be injected into any provider or invocation:
12
+
13
+ ```python
14
+ from engin import Engin, Provide, Supervisor, OnException
15
+
16
+
17
+ def background_worker(supervisor: Supervisor) -> WorkerService:
18
+ worker = WorkerService()
19
+
20
+ # Register the worker's run method as a supervised task
21
+ supervisor.supervise(worker.run)
22
+
23
+ return worker
24
+
25
+
26
+ engin = Engin(Provide(background_worker))
27
+ ```
28
+
29
+
30
+ ## Error Handling
31
+
32
+ The Supervisor provides three error handling strategies via the `OnException` enum:
33
+
34
+ ### `OnException.SHUTDOWN`
35
+
36
+ Stops the entire application when the task fails:
37
+
38
+ ```python
39
+ supervisor.supervise(critical_task) # Will shutdown app on error
40
+ ```
41
+
42
+ This is the default behaviour.
43
+
44
+ ### `OnException.RETRY`
45
+
46
+ Automatically restarts the task when it fails:
47
+
48
+ ```python
49
+ supervisor.supervise(
50
+ flaky_network_task,
51
+ on_exception=OnException.RETRY
52
+ )
53
+ ```
54
+
55
+ ### `OnException.IGNORE`
56
+
57
+ Logs the error but continues running other tasks:
58
+
59
+ ```python
60
+ supervisor.supervise(
61
+ optional_monitoring_task,
62
+ on_exception=OnException.IGNORE
63
+ )
64
+ ```
@@ -0,0 +1,98 @@
1
+ # Engin 🏎️
2
+
3
+ Engin is a lightweight application framework powered by dependency injection, it helps
4
+ you build and maintain large monoliths and microservices.
5
+
6
+
7
+ ## Features
8
+
9
+ The Engin framework gives you:
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 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
+ ## Example
40
+
41
+ A small example which shows some of the features of Engin. This application
42
+ makes 3 http requests and shuts itself down.
43
+
44
+ ```python
45
+ import asyncio
46
+ from httpx import AsyncClient
47
+ from engin import Engin, Invoke, Lifecycle, OnException, Provide, Supervisor
48
+
49
+
50
+ def httpx_client_factory(lifecycle: Lifecycle) -> AsyncClient:
51
+ # create our http client
52
+ client = AsyncClient()
53
+ # this will open and close the AsyncClient as part of the application's lifecycle
54
+ lifecycle.append(client)
55
+ return client
56
+
57
+
58
+ async def main(
59
+ httpx_client: AsyncClient,
60
+ supervisor: Supervisor,
61
+ ) -> None:
62
+ async def http_requests_task():
63
+ # simulate a background task
64
+ for x in range(3):
65
+ await httpx_client.get("https://httpbin.org/get")
66
+ await asyncio.sleep(1.0)
67
+ # raise an error to shutdown the application, normally you wouldn't do this!
68
+ raise RuntimeError("Forcing shutdown")
69
+
70
+ # supervise the http requests as part of the application's lifecycle
71
+ supervisor.supervise(http_requests_task, on_exception=OnException.SHUTDOWN)
72
+
73
+
74
+ # define our modular application
75
+ engin = Engin(Provide(httpx_client_factory), Invoke(main))
76
+
77
+ # run it!
78
+ asyncio.run(engin.run())
79
+ ```
80
+
81
+ With logs enabled this will output:
82
+
83
+ ```shell
84
+ INFO:engin:starting engin
85
+ INFO:engin:startup complete
86
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
87
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
88
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
89
+ ERROR:engin:supervisor task 'http_requests_task' raised RuntimeError, starting shutdown
90
+ Traceback (most recent call last):
91
+ File "C:\dev\python\engin\src\engin\_supervisor.py", line 58, in __call__
92
+ await self.factory()
93
+ File "C:\dev\python\engin\readme_example.py", line 29, in http_requests_task
94
+ raise RuntimeError("Forcing shutdown")
95
+ RuntimeError: Forcing shutdown
96
+ INFO:engin:stopping engin
97
+ INFO:engin:shutdown complete
98
+ ```
@@ -75,28 +75,24 @@ reusable SQL session per request, we could use a nested dependency:
75
75
  from typing import Annotated, AsyncIterable
76
76
 
77
77
  from engin.extensions.fastapi import Inject
78
- from fastapi import Depends
79
78
 
80
79
 
81
80
  async def database_session(
82
- database: Annotated[Database, Inject(Database)])
83
-
84
- ) -> AsyncIterable[Session]:
81
+ database: Annotated[Database, Inject(Database)]
82
+ ) -> AsyncIterable[Session]:
85
83
  with database.new_session() as session:
86
84
  yield session
87
- session.commit()
88
-
89
- @ app.post("/{id}")
90
- async
85
+ session.commit()
91
86
 
92
- def add_item(session: Annotated[Session, Depends(database_session)]):
93
- session.add(MyORMModel(...))
87
+ @app.post("/{id}")
88
+ async def add_item(session: Annotated[Session, Inject(database_session)]):
89
+ session.add(MyORMModel(...))
94
90
  ```
95
91
 
96
92
 
97
93
  ## Attaching Routers to Engin
98
94
 
99
- The usual way to declare an `APIRouter` is as a module level variable, for example:
95
+ The idiomatic way to declare an `APIRouter` is as a module level variable, for example:
100
96
 
101
97
  ```python title="api.py"
102
98
  from fastapi import APIRouter
@@ -10,7 +10,9 @@ from examples.asgi.features.cats.block import CatBlock
10
10
 
11
11
  logging.basicConfig(level=logging.DEBUG)
12
12
 
13
- app = ASGIEngin(AppBlock(), DatabaseBlock(), CatBlock(), Supply(AppConfig(debug=True)))
13
+ app = ASGIEngin(
14
+ AppBlock(), DatabaseBlock(), CatBlock(), Supply(AppConfig(debug=True), override=True)
15
+ )
14
16
 
15
17
 
16
18
  if __name__ == "__main__":
@@ -31,8 +31,10 @@ nav:
31
31
  - Engin: "concepts/engin.md"
32
32
  - Providers: "concepts/providers.md"
33
33
  - Invocations: "concepts/invocations.md"
34
- - Lifecycle: "concepts/lifecycle.md"
35
34
  - Blocks: "concepts/blocks.md"
35
+ - Lifecycle: "concepts/lifecycle.md"
36
+ - Supervisor: "concepts/supervisor.md"
37
+ - CLI Commands: "cli.md"
36
38
  - Integrations:
37
39
  - FastAPI: "integrations/fastapi.md"
38
40
  - Reference: "reference.md"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "engin"
3
- version = "0.1.0rc1"
3
+ version = "0.1.0rc2"
4
4
  description = "An async-first modular application framework"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"