engin 0.0.1__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.
- engin-0.0.3/CHANGELOG.md +34 -0
- engin-0.0.3/PKG-INFO +56 -0
- engin-0.0.3/README.md +48 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/app.py +2 -1
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/main.py +1 -1
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/cats/api.py +1 -1
- {engin-0.0.1 → engin-0.0.3}/pyproject.toml +7 -1
- {engin-0.0.1 → engin-0.0.3}/src/engin/__init__.py +2 -0
- {engin-0.0.1 → engin-0.0.3}/src/engin/_assembler.py +2 -2
- {engin-0.0.1 → engin-0.0.3}/src/engin/_block.py +3 -3
- {engin-0.0.1 → engin-0.0.3}/src/engin/_dependency.py +1 -1
- {engin-0.0.1 → engin-0.0.3}/src/engin/_engin.py +5 -5
- {engin-0.0.1 → engin-0.0.3}/src/engin/_lifecycle.py +3 -3
- {engin-0.0.1 → engin-0.0.3}/src/engin/ext/asgi.py +7 -7
- {engin-0.0.1 → engin-0.0.3}/tests/test_engin.py +52 -1
- engin-0.0.3/uv.lock +482 -0
- engin-0.0.1/CHANGELOG.md +0 -12
- engin-0.0.1/PKG-INFO +0 -5
- engin-0.0.1/README.md +0 -0
- engin-0.0.1/uv.lock +0 -478
- {engin-0.0.1 → engin-0.0.3}/.github/workflows/check.yaml +0 -0
- {engin-0.0.1 → engin-0.0.3}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.1 → engin-0.0.3}/.gitignore +0 -0
- {engin-0.0.1 → engin-0.0.3}/LICENSE +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/app.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/db/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/db/adapaters/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/starlette/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/features/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/features/cats/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/features/cats/api/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/asgi/main.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/cats/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/cats/adapters/repository.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/cats/block.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/cats/domain.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/fastapi/routes/cats/ports.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/simple/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/examples/simple/main.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/src/engin/_exceptions.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/src/engin/_type_utils.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/src/engin/ext/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/src/engin/ext/fastapi.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/src/engin/py.typed +0 -0
- {engin-0.0.1 → engin-0.0.3}/tests/__init__.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/tests/conftest.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/tests/deps.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/tests/test_assembler.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/tests/test_dependencies.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/tests/test_modules.py +0 -0
- {engin-0.0.1 → engin-0.0.3}/tests/test_utils.py +0 -0
engin-0.0.3/CHANGELOG.md
ADDED
@@ -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
|
engin-0.0.3/PKG-INFO
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: engin
|
3
|
+
Version: 0.0.3
|
4
|
+
Summary: An async-first modular application framework
|
5
|
+
License-File: LICENSE
|
6
|
+
Requires-Python: >=3.10
|
7
|
+
Description-Content-Type: text/markdown
|
8
|
+
|
9
|
+
# Engin 🏎️
|
10
|
+
|
11
|
+
Engin is a zero-dependency application framework for modern Python.
|
12
|
+
|
13
|
+
## Features ✨
|
14
|
+
|
15
|
+
- **Lightweight**: Engin has no dependencies.
|
16
|
+
- **Async First**: Engin provides first-class support for applications.
|
17
|
+
- **Dependency Injection**: Engin promotes a modular decoupled architecture in your application.
|
18
|
+
- **Lifecycle Management**: Engin provides an simple, portable approach for implememting
|
19
|
+
startup and shutdown tasks.
|
20
|
+
- **Ecosystem Compatability**: seamlessly integrate with frameworks such as FastAPI without
|
21
|
+
having to migrate your dependencies.
|
22
|
+
- **Code Reuse**: Engin's modular components work great as packages and distributions. Allowing
|
23
|
+
low boiler-plate code reuse within your Organisation.
|
24
|
+
|
25
|
+
## Installation
|
26
|
+
|
27
|
+
Engin is available on PyPI, install using your favourite dependency manager:
|
28
|
+
|
29
|
+
- **pip**:`pip install engin`
|
30
|
+
- **poetry**: `poetry add engin`
|
31
|
+
- **uv**: `uv add engin`
|
32
|
+
|
33
|
+
## Getting Started
|
34
|
+
|
35
|
+
A minimal example:
|
36
|
+
|
37
|
+
```python
|
38
|
+
import asyncio
|
39
|
+
|
40
|
+
from httpx import AsyncClient
|
41
|
+
|
42
|
+
from engin import Engin, Invoke, Provide
|
43
|
+
|
44
|
+
|
45
|
+
def httpx_client() -> AsyncClient:
|
46
|
+
return AsyncClient()
|
47
|
+
|
48
|
+
|
49
|
+
async def main(http_client: AsyncClient) -> None:
|
50
|
+
print(await http_client.get("https://httpbin.org/get"))
|
51
|
+
|
52
|
+
engin = Engin(Provide(httpx_client), Invoke(main))
|
53
|
+
|
54
|
+
asyncio.run(engin.run())
|
55
|
+
```
|
56
|
+
|
engin-0.0.3/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Engin 🏎️
|
2
|
+
|
3
|
+
Engin is a zero-dependency application framework for modern Python.
|
4
|
+
|
5
|
+
## Features ✨
|
6
|
+
|
7
|
+
- **Lightweight**: Engin has no dependencies.
|
8
|
+
- **Async First**: Engin provides first-class support for applications.
|
9
|
+
- **Dependency Injection**: Engin promotes a modular decoupled architecture in your application.
|
10
|
+
- **Lifecycle Management**: Engin provides an simple, portable approach for implememting
|
11
|
+
startup and shutdown tasks.
|
12
|
+
- **Ecosystem Compatability**: seamlessly integrate with frameworks such as FastAPI without
|
13
|
+
having to migrate your dependencies.
|
14
|
+
- **Code Reuse**: Engin's modular components work great as packages and distributions. Allowing
|
15
|
+
low boiler-plate code reuse within your Organisation.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Engin is available on PyPI, install using your favourite dependency manager:
|
20
|
+
|
21
|
+
- **pip**:`pip install engin`
|
22
|
+
- **poetry**: `poetry add engin`
|
23
|
+
- **uv**: `uv add engin`
|
24
|
+
|
25
|
+
## Getting Started
|
26
|
+
|
27
|
+
A minimal example:
|
28
|
+
|
29
|
+
```python
|
30
|
+
import asyncio
|
31
|
+
|
32
|
+
from httpx import AsyncClient
|
33
|
+
|
34
|
+
from engin import Engin, Invoke, Provide
|
35
|
+
|
36
|
+
|
37
|
+
def httpx_client() -> AsyncClient:
|
38
|
+
return AsyncClient()
|
39
|
+
|
40
|
+
|
41
|
+
async def main(http_client: AsyncClient) -> None:
|
42
|
+
print(await http_client.get("https://httpbin.org/get"))
|
43
|
+
|
44
|
+
engin = Engin(Provide(httpx_client), Invoke(main))
|
45
|
+
|
46
|
+
asyncio.run(engin.run())
|
47
|
+
```
|
48
|
+
|
@@ -3,7 +3,7 @@ import logging
|
|
3
3
|
import uvicorn
|
4
4
|
|
5
5
|
from engin import Supply
|
6
|
-
from engin.
|
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
|
|
@@ -3,7 +3,7 @@ from typing import Annotated
|
|
3
3
|
from fastapi import APIRouter
|
4
4
|
from pydantic import BaseModel
|
5
5
|
|
6
|
-
from engin.
|
6
|
+
from engin.ext.fastapi import Inject
|
7
7
|
from examples.fastapi.routes.cats.domain import Cat, CatPersonality
|
8
8
|
from examples.fastapi.routes.cats.ports import CatRepository
|
9
9
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "engin"
|
3
|
-
version = "0.0.
|
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
|
+
]
|
@@ -1,3 +1,4 @@
|
|
1
|
+
from engin import ext
|
1
2
|
from engin._assembler import Assembler
|
2
3
|
from engin._block import Block, invoke, provide
|
3
4
|
from engin._dependency import Entrypoint, Invoke, Provide, Supply
|
@@ -16,6 +17,7 @@ __all__ = [
|
|
16
17
|
"Option",
|
17
18
|
"Provide",
|
18
19
|
"Supply",
|
20
|
+
"ext",
|
19
21
|
"invoke",
|
20
22
|
"provide",
|
21
23
|
]
|
@@ -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
|
-
|
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):
|
@@ -32,15 +32,13 @@ 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
|
-
# lifecycle startup
|
39
|
-
|
40
38
|
# wait till stop signal recieved
|
41
39
|
await self._stop_event.wait()
|
42
40
|
|
43
|
-
|
41
|
+
await self.stop()
|
44
42
|
|
45
43
|
async def start(self) -> None:
|
46
44
|
LOG.info("starting engin")
|
@@ -57,8 +55,10 @@ class Engin:
|
|
57
55
|
|
58
56
|
async def stop(self) -> None:
|
59
57
|
self._stop_event.set()
|
58
|
+
lifecycle = await self._assembler.get(Lifecycle)
|
59
|
+
await lifecycle.shutdown()
|
60
60
|
|
61
|
-
def _destruct_options(self, options: Iterable[Option]):
|
61
|
+
def _destruct_options(self, options: Iterable[Option]) -> None:
|
62
62
|
for opt in options:
|
63
63
|
if isinstance(opt, Block):
|
64
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
|
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 =
|
11
|
-
_Message: TypeAlias =
|
12
|
-
_Receive: TypeAlias =
|
13
|
-
_Send: TypeAlias =
|
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
|