engin 0.0.2__tar.gz → 0.0.3__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 (62) hide show
  1. engin-0.0.3/CHANGELOG.md +34 -0
  2. {engin-0.0.2 → engin-0.0.3}/PKG-INFO +1 -1
  3. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/main.py +1 -1
  4. {engin-0.0.2 → engin-0.0.3}/pyproject.toml +7 -1
  5. {engin-0.0.2 → engin-0.0.3}/src/engin/_assembler.py +2 -2
  6. {engin-0.0.2 → engin-0.0.3}/src/engin/_block.py +3 -3
  7. {engin-0.0.2 → engin-0.0.3}/src/engin/_dependency.py +1 -1
  8. {engin-0.0.2 → engin-0.0.3}/src/engin/_engin.py +6 -2
  9. {engin-0.0.2 → engin-0.0.3}/src/engin/_lifecycle.py +3 -3
  10. {engin-0.0.2 → engin-0.0.3}/src/engin/ext/asgi.py +7 -7
  11. {engin-0.0.2 → engin-0.0.3}/tests/test_engin.py +52 -1
  12. engin-0.0.2/CHANGELOG.md +0 -19
  13. {engin-0.0.2 → engin-0.0.3}/.github/workflows/check.yaml +0 -0
  14. {engin-0.0.2 → engin-0.0.3}/.github/workflows/publish.yaml +0 -0
  15. {engin-0.0.2 → engin-0.0.3}/.gitignore +0 -0
  16. {engin-0.0.2 → engin-0.0.3}/LICENSE +0 -0
  17. {engin-0.0.2 → engin-0.0.3}/README.md +0 -0
  18. {engin-0.0.2 → engin-0.0.3}/examples/__init__.py +0 -0
  19. {engin-0.0.2 → engin-0.0.3}/examples/asgi/__init__.py +0 -0
  20. {engin-0.0.2 → engin-0.0.3}/examples/asgi/app.py +0 -0
  21. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/__init__.py +0 -0
  22. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/db/__init__.py +0 -0
  23. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/db/adapaters/__init__.py +0 -0
  24. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/db/adapaters/memory.py +0 -0
  25. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/db/block.py +0 -0
  26. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/db/ports.py +0 -0
  27. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/starlette/__init__.py +0 -0
  28. {engin-0.0.2 → engin-0.0.3}/examples/asgi/common/starlette/endpoint.py +0 -0
  29. {engin-0.0.2 → engin-0.0.3}/examples/asgi/features/__init__.py +0 -0
  30. {engin-0.0.2 → engin-0.0.3}/examples/asgi/features/cats/__init__.py +0 -0
  31. {engin-0.0.2 → engin-0.0.3}/examples/asgi/features/cats/api/__init__.py +0 -0
  32. {engin-0.0.2 → engin-0.0.3}/examples/asgi/features/cats/api/get.py +0 -0
  33. {engin-0.0.2 → engin-0.0.3}/examples/asgi/features/cats/api/post.py +0 -0
  34. {engin-0.0.2 → engin-0.0.3}/examples/asgi/features/cats/block.py +0 -0
  35. {engin-0.0.2 → engin-0.0.3}/examples/asgi/features/cats/domain.py +0 -0
  36. {engin-0.0.2 → engin-0.0.3}/examples/asgi/main.py +0 -0
  37. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/__init__.py +0 -0
  38. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/app.py +0 -0
  39. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/__init__.py +0 -0
  40. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/cats/__init__.py +0 -0
  41. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
  42. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
  43. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/cats/api.py +0 -0
  44. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/cats/block.py +0 -0
  45. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/cats/domain.py +0 -0
  46. {engin-0.0.2 → engin-0.0.3}/examples/fastapi/routes/cats/ports.py +0 -0
  47. {engin-0.0.2 → engin-0.0.3}/examples/simple/__init__.py +0 -0
  48. {engin-0.0.2 → engin-0.0.3}/examples/simple/main.py +0 -0
  49. {engin-0.0.2 → engin-0.0.3}/src/engin/__init__.py +0 -0
  50. {engin-0.0.2 → engin-0.0.3}/src/engin/_exceptions.py +0 -0
  51. {engin-0.0.2 → engin-0.0.3}/src/engin/_type_utils.py +0 -0
  52. {engin-0.0.2 → engin-0.0.3}/src/engin/ext/__init__.py +0 -0
  53. {engin-0.0.2 → engin-0.0.3}/src/engin/ext/fastapi.py +0 -0
  54. {engin-0.0.2 → engin-0.0.3}/src/engin/py.typed +0 -0
  55. {engin-0.0.2 → engin-0.0.3}/tests/__init__.py +0 -0
  56. {engin-0.0.2 → engin-0.0.3}/tests/conftest.py +0 -0
  57. {engin-0.0.2 → engin-0.0.3}/tests/deps.py +0 -0
  58. {engin-0.0.2 → engin-0.0.3}/tests/test_assembler.py +0 -0
  59. {engin-0.0.2 → engin-0.0.3}/tests/test_dependencies.py +0 -0
  60. {engin-0.0.2 → engin-0.0.3}/tests/test_modules.py +0 -0
  61. {engin-0.0.2 → engin-0.0.3}/tests/test_utils.py +0 -0
  62. {engin-0.0.2 → engin-0.0.3}/uv.lock +0 -0
@@ -0,0 +1,34 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+
9
+ ## [0.0.3] - 2025-01-15
10
+
11
+ ### Added
12
+
13
+ - Blocks can now provide options via the `options` class variable. This allows packaged
14
+ Blocks to easily expose Providers and Invocations as normal functions whilst allowing
15
+ them to be part of a Block as well. This makes usage of the Block optional which makes
16
+ it more flexible for end users.
17
+ - Added missing type hints and enabled mypy strict mode.
18
+
19
+ ### Fixed
20
+
21
+ - Engin now performs Lifecycle shutdown.
22
+
23
+ ## [0.0.2] - 2025-01-10
24
+
25
+ ### Added
26
+
27
+ - The `ext` sub-package is now explicitly exported in the package `__init__.py`
28
+
29
+
30
+ ## [0.0.1] - 2024-12-12
31
+
32
+ ### Added
33
+
34
+ - Initial release
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: engin
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: An async-first modular application framework
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -3,7 +3,7 @@ import logging
3
3
  import uvicorn
4
4
 
5
5
  from engin import Supply
6
- from engin.extensions.fastapi import FastAPIEngin
6
+ from engin.ext.fastapi import FastAPIEngin
7
7
  from examples.fastapi.app import AppBlock, AppConfig
8
8
  from examples.fastapi.routes.cats.block import CatBlock
9
9
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "engin"
3
- version = "0.0.2"
3
+ version = "0.0.3"
4
4
  description = "An async-first modular application framework"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -37,3 +37,9 @@ log_cli = true
37
37
  log_cli_level = "DEBUG"
38
38
  asyncio_mode = "auto"
39
39
  asyncio_default_fixture_loop_scope = "session"
40
+
41
+ [tool.mypy]
42
+ strict = true
43
+ disable_error_code = [
44
+ "type-arg", # allow generic types without type arguments
45
+ ]
@@ -4,7 +4,7 @@ from collections import defaultdict
4
4
  from collections.abc import Collection, Iterable
5
5
  from dataclasses import dataclass
6
6
  from inspect import BoundArguments, Signature
7
- from typing import Any, Generic, TypeVar
7
+ from typing import Any, Generic, TypeVar, cast
8
8
 
9
9
  from engin._dependency import Dependency, Provide, Supply
10
10
  from engin._exceptions import AssemblyError
@@ -113,7 +113,7 @@ class Assembler:
113
113
  async def get(self, type_: type[T]) -> T:
114
114
  type_id = type_id_of(type_)
115
115
  if type_id in self._dependencies:
116
- return self._dependencies[type_id]
116
+ return cast(T, self._dependencies[type_id])
117
117
  if type_id.multi:
118
118
  out = []
119
119
  for provider in self._multiproviders[type_id]:
@@ -1,5 +1,6 @@
1
1
  import inspect
2
2
  from collections.abc import Iterable, Iterator
3
+ from typing import ClassVar
3
4
 
4
5
  from engin._dependency import Func, Invoke, Provide
5
6
 
@@ -15,11 +16,10 @@ def invoke(func: Func) -> Func:
15
16
 
16
17
 
17
18
  class Block(Iterable[Provide | Invoke]):
18
- _name: str
19
- _options: list[Provide | Invoke]
19
+ options: ClassVar[list[Provide | Invoke]] = []
20
20
 
21
21
  def __init__(self, /, block_name: str | None = None) -> None:
22
- self._options: list[Provide | Invoke] = []
22
+ self._options: list[Provide | Invoke] = self.options[:]
23
23
  self._name = block_name or f"{type(self).__name__}"
24
24
  for _, method in inspect.getmembers(self):
25
25
  if opt := getattr(method, "_opt", None):
@@ -25,7 +25,7 @@ Func: TypeAlias = (
25
25
  _SELF = object()
26
26
 
27
27
 
28
- def _noop(*args, **kwargs) -> None: ...
28
+ def _noop(*args: Any, **kwargs: Any) -> None: ...
29
29
 
30
30
 
31
31
  class Dependency(ABC, Generic[P, T]):
@@ -32,12 +32,14 @@ class Engin:
32
32
  def assembler(self) -> Assembler:
33
33
  return self._assembler
34
34
 
35
- async def run(self):
35
+ async def run(self) -> None:
36
36
  await self.start()
37
37
 
38
38
  # wait till stop signal recieved
39
39
  await self._stop_event.wait()
40
40
 
41
+ await self.stop()
42
+
41
43
  async def start(self) -> None:
42
44
  LOG.info("starting engin")
43
45
  assembled_invocations: list[AssembledDependency] = [
@@ -53,8 +55,10 @@ class Engin:
53
55
 
54
56
  async def stop(self) -> None:
55
57
  self._stop_event.set()
58
+ lifecycle = await self._assembler.get(Lifecycle)
59
+ await lifecycle.shutdown()
56
60
 
57
- def _destruct_options(self, options: Iterable[Option]):
61
+ def _destruct_options(self, options: Iterable[Option]) -> None:
58
62
  for opt in options:
59
63
  if isinstance(opt, Block):
60
64
  self._destruct_options(opt)
@@ -4,12 +4,12 @@ from contextlib import AbstractAsyncContextManager, AsyncExitStack
4
4
 
5
5
  class Lifecycle:
6
6
  def __init__(self) -> None:
7
- self._on_startup: list[Callable] = []
8
- self._on_shutdown: list[Callable] = []
7
+ self._on_startup: list[Callable[..., None]] = []
8
+ self._on_shutdown: list[Callable[..., None]] = []
9
9
  self._context_managers: list[AbstractAsyncContextManager] = []
10
10
  self._stack: AsyncExitStack = AsyncExitStack()
11
11
 
12
- def register_context(self, cm: AbstractAsyncContextManager):
12
+ def register_context(self, cm: AbstractAsyncContextManager) -> None:
13
13
  self._context_managers.append(cm)
14
14
 
15
15
  async def startup(self) -> None:
@@ -1,16 +1,16 @@
1
1
  import traceback
2
- import typing
3
- from typing import ClassVar, Protocol, TypeAlias
2
+ from collections.abc import Awaitable, Callable, MutableMapping
3
+ from typing import Any, ClassVar, Protocol, TypeAlias
4
4
 
5
5
  from engin import Engin, Option
6
6
 
7
7
  __all__ = ["ASGIEngin", "ASGIType"]
8
8
 
9
9
 
10
- _Scope: TypeAlias = typing.MutableMapping[str, typing.Any]
11
- _Message: TypeAlias = typing.MutableMapping[str, typing.Any]
12
- _Receive: TypeAlias = typing.Callable[[], typing.Awaitable[_Message]]
13
- _Send: TypeAlias = typing.Callable[[_Message], typing.Awaitable[None]]
10
+ _Scope: TypeAlias = MutableMapping[str, Any]
11
+ _Message: TypeAlias = MutableMapping[str, Any]
12
+ _Receive: TypeAlias = Callable[[], Awaitable[_Message]]
13
+ _Send: TypeAlias = Callable[[_Message], Awaitable[None]]
14
14
 
15
15
 
16
16
  class ASGIType(Protocol):
@@ -54,5 +54,5 @@ class _Rereceive:
54
54
  def __init__(self, message: _Message) -> None:
55
55
  self._message = message
56
56
 
57
- async def __call__(self, *args, **kwargs) -> _Message:
57
+ async def __call__(self, *args: Any, **kwargs: Any) -> _Message:
58
58
  return self._message
@@ -1,8 +1,11 @@
1
+ import asyncio
2
+ from contextlib import asynccontextmanager
1
3
  from datetime import datetime
4
+ from typing import Iterable
2
5
 
3
6
  import pytest
4
7
 
5
- from engin import AssemblyError, Engin, Entrypoint, Invoke, Provide
8
+ from engin import AssemblyError, Engin, Entrypoint, Invoke, Lifecycle, Provide
6
9
  from tests.deps import ABlock
7
10
 
8
11
 
@@ -73,3 +76,51 @@ async def test_engin_with_entrypoint():
73
76
  await engin.start()
74
77
 
75
78
  assert provider_called
79
+
80
+
81
+ async def test_engin_with_lifecycle():
82
+ state = 0
83
+
84
+ @asynccontextmanager
85
+ async def lifespan_task() -> Iterable[None]:
86
+ nonlocal state
87
+ state = 1
88
+ yield
89
+ state = 2
90
+
91
+ def foo(lifecycle: Lifecycle) -> None:
92
+ lifecycle.register_context(lifespan_task())
93
+
94
+ engin = Engin(Invoke(foo))
95
+
96
+ await engin.start()
97
+ assert state == 1
98
+
99
+ await engin.stop()
100
+ assert state == 2
101
+
102
+
103
+ async def test_engin_with_lifecycle_using_run():
104
+ state = 0
105
+
106
+ @asynccontextmanager
107
+ async def lifespan_task() -> Iterable[None]:
108
+ nonlocal state
109
+ state = 1
110
+ yield
111
+ state = 2
112
+
113
+ def foo(lifecycle: Lifecycle) -> None:
114
+ lifecycle.register_context(lifespan_task())
115
+
116
+ engin = Engin(Invoke(foo))
117
+
118
+ async def _stop_task():
119
+ await asyncio.sleep(0.25)
120
+ # lifecycle should have started by now
121
+ assert state == 1
122
+ await engin.stop()
123
+
124
+ await asyncio.gather(engin.run(), _stop_task())
125
+ # lifecycle should have stopped by now
126
+ assert state == 2
engin-0.0.2/CHANGELOG.md DELETED
@@ -1,19 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [0.0.2] - 2025-01-10
9
-
10
- ### Added
11
-
12
- - The `ext` sub-package is now explicitly exported in the package `__init__.py`
13
-
14
-
15
- ## [0.0.1] - 2024-12-12
16
-
17
- ### Added
18
-
19
- - Initial release
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