engin 0.1.0b2__tar.gz → 0.1.0b4__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 (102) hide show
  1. {engin-0.1.0b2 → engin-0.1.0b4}/PKG-INFO +1 -1
  2. {engin-0.1.0b2 → engin-0.1.0b4}/docs/concepts/engin.md +1 -1
  3. {engin-0.1.0b2 → engin-0.1.0b4}/docs/concepts/invocations.md +3 -3
  4. engin-0.1.0b4/docs/index.md +86 -0
  5. engin-0.1.0b4/docs/tutorial.md +15 -0
  6. {engin-0.1.0b2 → engin-0.1.0b4}/mkdocs.yaml +2 -2
  7. {engin-0.1.0b2 → engin-0.1.0b4}/pyproject.toml +1 -1
  8. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_assembler.py +7 -5
  9. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_engin.py +19 -1
  10. {engin-0.1.0b2 → engin-0.1.0b4}/tests/acceptance/test_error_in_start_up.py +7 -0
  11. engin-0.1.0b4/tests/conftest.py +0 -0
  12. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_assembler.py +8 -6
  13. {engin-0.1.0b2 → engin-0.1.0b4}/uv.lock +1 -1
  14. engin-0.1.0b2/docs/index.md +0 -62
  15. {engin-0.1.0b2 → engin-0.1.0b4}/.github/workflows/benchmark.yaml +0 -0
  16. {engin-0.1.0b2 → engin-0.1.0b4}/.github/workflows/check.yaml +0 -0
  17. {engin-0.1.0b2 → engin-0.1.0b4}/.github/workflows/publish.yaml +0 -0
  18. {engin-0.1.0b2 → engin-0.1.0b4}/.gitignore +0 -0
  19. {engin-0.1.0b2 → engin-0.1.0b4}/.readthedocs.yaml +0 -0
  20. {engin-0.1.0b2 → engin-0.1.0b4}/CHANGELOG.md +0 -0
  21. {engin-0.1.0b2 → engin-0.1.0b4}/LICENSE +0 -0
  22. {engin-0.1.0b2 → engin-0.1.0b4}/README.md +0 -0
  23. {engin-0.1.0b2 → engin-0.1.0b4}/docs/concepts/blocks.md +0 -0
  24. {engin-0.1.0b2 → engin-0.1.0b4}/docs/concepts/lifecycle.md +0 -0
  25. {engin-0.1.0b2 → engin-0.1.0b4}/docs/concepts/providers.md +0 -0
  26. /engin-0.1.0b2/examples/__init__.py → /engin-0.1.0b4/docs/concepts/supervisor.md +0 -0
  27. {engin-0.1.0b2/docs/guides → engin-0.1.0b4/docs/integrations}/fastapi-graph.png +0 -0
  28. {engin-0.1.0b2/docs/guides → engin-0.1.0b4/docs/integrations}/fastapi.md +0 -0
  29. {engin-0.1.0b2 → engin-0.1.0b4}/docs/js/readthedocs.js +0 -0
  30. {engin-0.1.0b2 → engin-0.1.0b4}/docs/overrides/main.html +0 -0
  31. {engin-0.1.0b2 → engin-0.1.0b4}/docs/reference.md +0 -0
  32. {engin-0.1.0b2/examples/asgi → engin-0.1.0b4/examples}/__init__.py +0 -0
  33. {engin-0.1.0b2/examples/asgi/common → engin-0.1.0b4/examples/asgi}/__init__.py +0 -0
  34. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/app.py +0 -0
  35. {engin-0.1.0b2/examples/asgi/common/db → engin-0.1.0b4/examples/asgi/common}/__init__.py +0 -0
  36. {engin-0.1.0b2/examples/asgi/common/db/adapaters → engin-0.1.0b4/examples/asgi/common/db}/__init__.py +0 -0
  37. {engin-0.1.0b2/examples/asgi/common/starlette → engin-0.1.0b4/examples/asgi/common/db/adapaters}/__init__.py +0 -0
  38. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/common/db/adapaters/memory.py +0 -0
  39. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/common/db/block.py +0 -0
  40. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/common/db/ports.py +0 -0
  41. {engin-0.1.0b2/examples/asgi/features → engin-0.1.0b4/examples/asgi/common/starlette}/__init__.py +0 -0
  42. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/common/starlette/endpoint.py +0 -0
  43. {engin-0.1.0b2/examples/asgi/features/cats → engin-0.1.0b4/examples/asgi/features}/__init__.py +0 -0
  44. {engin-0.1.0b2/examples/asgi/features/cats/api → engin-0.1.0b4/examples/asgi/features/cats}/__init__.py +0 -0
  45. {engin-0.1.0b2/examples/fastapi → engin-0.1.0b4/examples/asgi/features/cats/api}/__init__.py +0 -0
  46. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/features/cats/api/get.py +0 -0
  47. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/features/cats/api/post.py +0 -0
  48. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/features/cats/block.py +0 -0
  49. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/features/cats/domain.py +0 -0
  50. {engin-0.1.0b2 → engin-0.1.0b4}/examples/asgi/main.py +0 -0
  51. {engin-0.1.0b2/examples/fastapi/routes → engin-0.1.0b4/examples/fastapi}/__init__.py +0 -0
  52. {engin-0.1.0b2 → engin-0.1.0b4}/examples/fastapi/app.py +0 -0
  53. {engin-0.1.0b2 → engin-0.1.0b4}/examples/fastapi/main.py +0 -0
  54. {engin-0.1.0b2/examples/fastapi/routes/cats → engin-0.1.0b4/examples/fastapi/routes}/__init__.py +0 -0
  55. {engin-0.1.0b2/examples/fastapi/routes/cats/adapters → engin-0.1.0b4/examples/fastapi/routes/cats}/__init__.py +0 -0
  56. {engin-0.1.0b2/examples/simple → engin-0.1.0b4/examples/fastapi/routes/cats/adapters}/__init__.py +0 -0
  57. {engin-0.1.0b2 → engin-0.1.0b4}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
  58. {engin-0.1.0b2 → engin-0.1.0b4}/examples/fastapi/routes/cats/api.py +0 -0
  59. {engin-0.1.0b2 → engin-0.1.0b4}/examples/fastapi/routes/cats/block.py +0 -0
  60. {engin-0.1.0b2 → engin-0.1.0b4}/examples/fastapi/routes/cats/domain.py +0 -0
  61. {engin-0.1.0b2 → engin-0.1.0b4}/examples/fastapi/routes/cats/ports.py +0 -0
  62. {engin-0.1.0b2/src/engin/extensions → engin-0.1.0b4/examples/simple}/__init__.py +0 -0
  63. {engin-0.1.0b2 → engin-0.1.0b4}/examples/simple/main.py +0 -0
  64. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/__init__.py +0 -0
  65. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_block.py +0 -0
  66. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_cli/__init__.py +0 -0
  67. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_cli/_common.py +0 -0
  68. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_cli/_graph.html +0 -0
  69. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_cli/_graph.py +0 -0
  70. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_cli/_inspect.py +0 -0
  71. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_dependency.py +0 -0
  72. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_graph.py +0 -0
  73. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_introspect.py +0 -0
  74. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_lifecycle.py +0 -0
  75. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_option.py +0 -0
  76. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_supervisor.py +0 -0
  77. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/_type_utils.py +0 -0
  78. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/exceptions.py +0 -0
  79. {engin-0.1.0b2/tests → engin-0.1.0b4/src/engin/extensions}/__init__.py +0 -0
  80. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/extensions/asgi.py +0 -0
  81. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/extensions/fastapi.py +0 -0
  82. {engin-0.1.0b2 → engin-0.1.0b4}/src/engin/py.typed +0 -0
  83. {engin-0.1.0b2/tests/acceptance → engin-0.1.0b4/tests}/__init__.py +0 -0
  84. {engin-0.1.0b2/tests/benchmarks → engin-0.1.0b4/tests/acceptance}/__init__.py +0 -0
  85. {engin-0.1.0b2 → engin-0.1.0b4}/tests/acceptance/test_engin_signal_handling.py +0 -0
  86. {engin-0.1.0b2 → engin-0.1.0b4}/tests/acceptance/test_error_in_shutdown.py +0 -0
  87. {engin-0.1.0b2 → engin-0.1.0b4}/tests/acceptance/test_error_in_supervised_task.py +0 -0
  88. {engin-0.1.0b2 → engin-0.1.0b4}/tests/acceptance/test_fastapi.py +0 -0
  89. {engin-0.1.0b2/tests/cli → engin-0.1.0b4/tests/benchmarks}/__init__.py +0 -0
  90. {engin-0.1.0b2 → engin-0.1.0b4}/tests/benchmarks/conftest.py +0 -0
  91. {engin-0.1.0b2 → engin-0.1.0b4}/tests/benchmarks/test_bench_assembler.py +0 -0
  92. /engin-0.1.0b2/tests/conftest.py → /engin-0.1.0b4/tests/cli/__init__.py +0 -0
  93. {engin-0.1.0b2 → engin-0.1.0b4}/tests/cli/test_graph.py +0 -0
  94. {engin-0.1.0b2 → engin-0.1.0b4}/tests/cli/test_inspect.py +0 -0
  95. {engin-0.1.0b2 → engin-0.1.0b4}/tests/deps.py +0 -0
  96. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_block.py +0 -0
  97. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_dependencies.py +0 -0
  98. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_engin.py +0 -0
  99. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_graph.py +0 -0
  100. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_lifecycle.py +0 -0
  101. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_supervisor.py +0 -0
  102. {engin-0.1.0b2 → engin-0.1.0b4}/tests/test_type_id.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.1.0b2
3
+ Version: 0.1.0b4
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/
@@ -1,6 +1,6 @@
1
1
  # The Engin
2
2
 
3
- The Engin is a self-contained modular application.
3
+ The Engin is your self-contained modular application.
4
4
 
5
5
  When ran the Engin takes care of the complete application lifecycle:
6
6
 
@@ -1,13 +1,13 @@
1
1
  # Invocations
2
2
 
3
- Invocations define the behaviour of your application, therefore without any Invocations
3
+ 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
7
  parameters, but they should always return None as the return value will not used by Engin.
8
8
 
9
- As part of the Engin's startup, all declared invocations will be called sequentially in
10
- the order they were registered.
9
+ As part of the Engin's startup sequence, all declared invocations will be called
10
+ sequentially in the order they were registered.
11
11
 
12
12
  Invocations can be used to define behaviour in two ways.
13
13
 
@@ -0,0 +1,86 @@
1
+ # Engin 🏎️
2
+
3
+ Engin is a lightweight application framework powered by dependency injection, it helps
4
+ you build and maintain large monoliths and many 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
+ ## Example
40
+
41
+ A small example which shows some of the runtime features of Engin. This application
42
+ makes a http request and then performs a shutdown.
43
+
44
+ ```python
45
+ import asyncio
46
+ from httpx import AsyncClient
47
+ from engin import Engin, Invoke, Lifecycle, Provide, ShutdownSwitch, 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
+ shutdown: ShutdownSwitch,
61
+ supervisor: Supervisor,
62
+ ) -> None:
63
+ async def http_request():
64
+ await httpx_client.get("https://httpbin.org/get")
65
+ # one we've made the http request shutdown the application
66
+ shutdown.set()
67
+
68
+ # supervise the http request as part of the application's lifecycle
69
+ supervisor.supervise(http_request)
70
+
71
+ # define our modular application
72
+ engin = Engin(Provide(httpx_client_factory), Invoke(main))
73
+
74
+ # run it!
75
+ asyncio.run(engin.run())
76
+ ```
77
+
78
+ With logs enabled this will output:
79
+
80
+ ```shell
81
+ INFO:engin:starting engin
82
+ INFO:engin:startup complete
83
+ INFO:httpx:HTTP Request: GET https://httpbin.org/get "HTTP/1.1 200 OK"
84
+ INFO:engin:stopping engin
85
+ INFO:engin:shutdown complete
86
+ ```
@@ -0,0 +1,15 @@
1
+ # Tutorial
2
+
3
+ In this tutorial we will build a small toy application from scratch.
4
+
5
+ Our application will publish random numbers to a Valkey stream, and then consume them and
6
+ update a running total.
7
+
8
+ ## 1. Create a Valkey client
9
+
10
+ ```python
11
+ from valkey import Valkey
12
+
13
+ def valkey_client() -> Valkey:
14
+ ...
15
+ ```
@@ -33,8 +33,8 @@ nav:
33
33
  - Invocations: "concepts/invocations.md"
34
34
  - Lifecycle: "concepts/lifecycle.md"
35
35
  - Blocks: "concepts/blocks.md"
36
- - Guides:
37
- - FastAPI: "guides/fastapi.md"
36
+ - Integrations:
37
+ - FastAPI: "integrations/fastapi.md"
38
38
  - Reference: "reference.md"
39
39
 
40
40
  extra_javascript:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "engin"
3
- version = "0.1.0b2"
3
+ version = "0.1.0b4"
4
4
  description = "An async-first modular application framework"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -65,7 +65,7 @@ class Assembler:
65
65
  self._multiproviders: dict[TypeId, list[Provide[list[Any]]]] = defaultdict(list)
66
66
  self._assembled_outputs: dict[TypeId, Any] = {}
67
67
  self._lock = asyncio.Lock()
68
- self._graph_cache: dict[TypeId, set[Provide]] = defaultdict(set)
68
+ self._graph_cache: dict[TypeId, list[Provide]] = defaultdict(list)
69
69
 
70
70
  for provider in providers:
71
71
  type_id = provider.return_type_id
@@ -206,10 +206,10 @@ class Assembler:
206
206
  if provider.scope == scope:
207
207
  self._assembled_outputs.pop(type_id, None)
208
208
 
209
- def _resolve_providers(self, type_id: TypeId, resolved: set[TypeId]) -> set[Provide]:
209
+ def _resolve_providers(self, type_id: TypeId, resolved: set[TypeId]) -> Iterable[Provide]:
210
210
  """
211
211
  Resolves the chain of providers required to satisfy the provider of a given type.
212
- Ordering of the return value is very important!
212
+ Ordering of the return value is very important here!
213
213
  """
214
214
  if type_id in self._graph_cache:
215
215
  return self._graph_cache[type_id]
@@ -231,13 +231,15 @@ class Assembler:
231
231
  raise LookupError(msg)
232
232
 
233
233
  # providers that must be satisfied to satisfy the root level providers
234
- resolved_providers = {
234
+ resolved_providers = [
235
235
  child_provider
236
236
  for root_provider in root_providers
237
237
  for root_provider_param in root_provider.parameter_type_ids
238
238
  for child_provider in self._resolve_providers(root_provider_param, resolved)
239
239
  if root_provider_param not in resolved
240
- } | set(root_providers)
240
+ ]
241
+
242
+ resolved_providers.extend(root_providers)
241
243
 
242
244
  resolved.add(type_id)
243
245
  self._graph_cache[type_id] = resolved_providers
@@ -188,7 +188,25 @@ class Engin:
188
188
  started to return so it is safe to use immediately after.
189
189
  """
190
190
  self._async_context_run_task = asyncio.create_task(self.run())
191
- await self._start_complete_event.wait()
191
+ wait_tasks = [
192
+ asyncio.create_task(self._start_complete_event.wait()),
193
+ asyncio.create_task(self._stop_complete_event.wait()),
194
+ ]
195
+ await asyncio.wait(
196
+ [
197
+ self._async_context_run_task, # if a provider errors this will return first
198
+ *wait_tasks,
199
+ ],
200
+ return_when=asyncio.FIRST_COMPLETED,
201
+ )
202
+ for task in wait_tasks:
203
+ task.cancel()
204
+
205
+ # raise the exception from the startup during run
206
+ if self._async_context_run_task.done():
207
+ startup_exception = self._async_context_run_task.exception()
208
+ if startup_exception is not None:
209
+ raise startup_exception
192
210
 
193
211
  async def stop(self) -> None:
194
212
  """
@@ -36,6 +36,13 @@ async def test_error_in_startup_handled_when_run():
36
36
  assert not B_LIFECYCLE_STATE
37
37
 
38
38
 
39
+ async def test_error_in_startup_handled_when_start():
40
+ engin = Engin(Invoke(a), Invoke(b))
41
+
42
+ await asyncio.wait_for(engin.start(), timeout=0.5)
43
+ assert not B_LIFECYCLE_STATE
44
+
45
+
39
46
  async def test_error_in_startup_asgi():
40
47
  def asgi_type() -> ASGIType:
41
48
  return Starlette()
File without changes
@@ -22,15 +22,17 @@ async def test_assembler():
22
22
 
23
23
 
24
24
  async def test_assembler_with_multiproviders():
25
- assembler = Assembler([Provide(make_many_int), Provide(make_many_int_alt)])
25
+ # catch any non-deterministic ordering bugs
26
+ for _ in range(10):
27
+ assembler = Assembler([Provide(make_many_int), Provide(make_many_int_alt)])
26
28
 
27
- def assert_all(many_ints: list[int]):
28
- expected_ints = [*make_many_int(), *make_many_int_alt()]
29
- assert sorted(many_ints) == sorted(expected_ints)
29
+ def assert_all(many_ints: list[int]):
30
+ expected_ints = [*make_many_int(), *make_many_int_alt()]
31
+ assert many_ints == expected_ints
30
32
 
31
- assembled_dependency = await assembler.assemble(Invoke(assert_all))
33
+ assembled_dependency = await assembler.assemble(Invoke(assert_all))
32
34
 
33
- await assembled_dependency()
35
+ await assembled_dependency()
34
36
 
35
37
 
36
38
  async def test_assembler_providers_only_called_once():
@@ -211,7 +211,7 @@ toml = [
211
211
 
212
212
  [[package]]
213
213
  name = "engin"
214
- version = "0.1.0b2"
214
+ version = "0.1.0b3"
215
215
  source = { editable = "." }
216
216
  dependencies = [
217
217
  { name = "anyio" },
@@ -1,62 +0,0 @@
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
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes