engin 0.1.0rc3__tar.gz → 0.2.0a1__tar.gz

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