engin 0.0.dev1__tar.gz → 0.0.2__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.2/.github/workflows/check.yaml +30 -0
- engin-0.0.2/CHANGELOG.md +19 -0
- engin-0.0.2/PKG-INFO +56 -0
- engin-0.0.2/README.md +48 -0
- engin-0.0.2/examples/fastapi/app.py +25 -0
- engin-0.0.2/examples/fastapi/main.py +14 -0
- engin-0.0.2/examples/fastapi/routes/cats/adapters/repository.py +18 -0
- engin-0.0.2/examples/fastapi/routes/cats/api.py +42 -0
- engin-0.0.2/examples/fastapi/routes/cats/block.py +16 -0
- engin-0.0.2/examples/fastapi/routes/cats/domain.py +18 -0
- engin-0.0.2/examples/fastapi/routes/cats/ports.py +14 -0
- {engin-0.0.dev1 → engin-0.0.2}/pyproject.toml +2 -1
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/__init__.py +3 -1
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/_assembler.py +8 -3
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/_engin.py +4 -5
- engin-0.0.2/src/engin/ext/__init__.py +0 -0
- {engin-0.0.dev1/src/engin/extensions → engin-0.0.2/src/engin/ext}/asgi.py +7 -4
- engin-0.0.2/src/engin/ext/fastapi.py +42 -0
- engin-0.0.2/src/engin/py.typed +0 -0
- engin-0.0.2/tests/__init__.py +0 -0
- engin-0.0.2/tests/conftest.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/tests/test_assembler.py +20 -0
- engin-0.0.2/uv.lock +482 -0
- engin-0.0.dev1/PKG-INFO +0 -6
- engin-0.0.dev1/uv.lock +0 -424
- {engin-0.0.dev1 → engin-0.0.2}/.github/workflows/publish.yaml +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/.gitignore +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/LICENSE +0 -0
- {engin-0.0.dev1/examples/asgi → engin-0.0.2/examples}/__init__.py +0 -0
- {engin-0.0.dev1/examples/asgi/common → engin-0.0.2/examples/asgi}/__init__.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/app.py +0 -0
- {engin-0.0.dev1/examples/asgi/common/db → engin-0.0.2/examples/asgi/common}/__init__.py +0 -0
- {engin-0.0.dev1/examples/asgi/common/db/adapaters → engin-0.0.2/examples/asgi/common/db}/__init__.py +0 -0
- {engin-0.0.dev1/examples/asgi/common/starlette → engin-0.0.2/examples/asgi/common/db/adapaters}/__init__.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/db/adapaters/memory.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/db/block.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/db/ports.py +0 -0
- {engin-0.0.dev1/examples/asgi/features → engin-0.0.2/examples/asgi/common/starlette}/__init__.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/common/starlette/endpoint.py +0 -0
- {engin-0.0.dev1/examples/asgi/features/cats → engin-0.0.2/examples/asgi/features}/__init__.py +0 -0
- {engin-0.0.dev1/examples/asgi/features/cats/api → engin-0.0.2/examples/asgi/features/cats}/__init__.py +0 -0
- {engin-0.0.dev1/examples/simple → engin-0.0.2/examples/asgi/features/cats/api}/__init__.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/api/get.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/api/post.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/block.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/features/cats/domain.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/asgi/main.py +0 -0
- {engin-0.0.dev1/src/engin/extensions → engin-0.0.2/examples/fastapi}/__init__.py +0 -0
- {engin-0.0.dev1/tests → engin-0.0.2/examples/fastapi/routes}/__init__.py +0 -0
- /engin-0.0.dev1/README.md → /engin-0.0.2/examples/fastapi/routes/cats/__init__.py +0 -0
- /engin-0.0.dev1/src/engin/py.typed → /engin-0.0.2/examples/fastapi/routes/cats/adapters/__init__.py +0 -0
- /engin-0.0.dev1/tests/conftest.py → /engin-0.0.2/examples/simple/__init__.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/examples/simple/main.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/_block.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/_dependency.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/_exceptions.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/_lifecycle.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/src/engin/_type_utils.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/tests/deps.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/tests/test_dependencies.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/tests/test_engin.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/tests/test_modules.py +0 -0
- {engin-0.0.dev1 → engin-0.0.2}/tests/test_utils.py +0 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
name: Check
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
|
6
|
+
jobs:
|
7
|
+
publish-to-pypi:
|
8
|
+
name: python
|
9
|
+
runs-on: ubuntu-latest
|
10
|
+
|
11
|
+
permissions:
|
12
|
+
id-token: write
|
13
|
+
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v4
|
16
|
+
|
17
|
+
- name: Install uv
|
18
|
+
uses: astral-sh/setup-uv@v3
|
19
|
+
|
20
|
+
- name: Set up Python
|
21
|
+
run: uv python install
|
22
|
+
|
23
|
+
- name: Ruff
|
24
|
+
run: uv run ruff check src tests
|
25
|
+
|
26
|
+
- name: Mypy
|
27
|
+
run: uv run mypy src
|
28
|
+
|
29
|
+
- name: Test
|
30
|
+
run: uv run pytest tests
|
engin-0.0.2/CHANGELOG.md
ADDED
@@ -0,0 +1,19 @@
|
|
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
|
engin-0.0.2/PKG-INFO
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: engin
|
3
|
+
Version: 0.0.2
|
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.2/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
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from fastapi import FastAPI
|
2
|
+
from pydantic_settings import BaseSettings
|
3
|
+
|
4
|
+
from engin import Block, invoke, provide
|
5
|
+
|
6
|
+
|
7
|
+
class AppConfig(BaseSettings):
|
8
|
+
debug: bool = False
|
9
|
+
|
10
|
+
|
11
|
+
class AppBlock(Block):
|
12
|
+
@provide
|
13
|
+
def app_factory(self, app_config: AppConfig) -> FastAPI:
|
14
|
+
return FastAPI(debug=app_config.debug)
|
15
|
+
|
16
|
+
@provide
|
17
|
+
def default_config(self) -> AppConfig:
|
18
|
+
return AppConfig()
|
19
|
+
|
20
|
+
@invoke
|
21
|
+
def add_health_endpoint(self, app: FastAPI) -> None:
|
22
|
+
async def health():
|
23
|
+
return {"ok": True}
|
24
|
+
|
25
|
+
app.add_api_route(path="/health", endpoint=health)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
import logging
|
2
|
+
|
3
|
+
import uvicorn
|
4
|
+
|
5
|
+
from engin import Supply
|
6
|
+
from engin.extensions.fastapi import FastAPIEngin
|
7
|
+
from examples.fastapi.app import AppBlock, AppConfig
|
8
|
+
from examples.fastapi.routes.cats.block import CatBlock
|
9
|
+
|
10
|
+
logging.basicConfig(level=logging.DEBUG)
|
11
|
+
|
12
|
+
app = FastAPIEngin(AppBlock(), CatBlock(), Supply(AppConfig(debug=True)))
|
13
|
+
|
14
|
+
uvicorn.run(app)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from examples.fastapi.routes.cats.domain import Cat
|
2
|
+
from examples.fastapi.routes.cats.ports import CatRepository
|
3
|
+
|
4
|
+
|
5
|
+
class InMemoryCatRepository(CatRepository):
|
6
|
+
def __init__(self) -> None:
|
7
|
+
self._cats: dict[int, Cat] = {}
|
8
|
+
|
9
|
+
def get(self, cat_id: int) -> Cat:
|
10
|
+
if cat_id in self._cats:
|
11
|
+
return self._cats[cat_id]
|
12
|
+
raise LookupError(f"No cat found for id: {cat_id}")
|
13
|
+
|
14
|
+
def set(self, cat: Cat) -> None:
|
15
|
+
self._cats[cat.id] = cat
|
16
|
+
|
17
|
+
def next_id(self) -> int:
|
18
|
+
return len(self._cats)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
from typing import Annotated
|
2
|
+
|
3
|
+
from fastapi import APIRouter
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
from engin.ext.fastapi import Inject
|
7
|
+
from examples.fastapi.routes.cats.domain import Cat, CatPersonality
|
8
|
+
from examples.fastapi.routes.cats.ports import CatRepository
|
9
|
+
|
10
|
+
router = APIRouter(prefix="/cats")
|
11
|
+
|
12
|
+
|
13
|
+
@router.get("/{cat_id}")
|
14
|
+
async def get_cat(
|
15
|
+
cat_id: int,
|
16
|
+
repository: Annotated[CatRepository, Inject(CatRepository)],
|
17
|
+
) -> Cat:
|
18
|
+
return repository.get(cat_id=cat_id)
|
19
|
+
|
20
|
+
|
21
|
+
class CatPostModel(BaseModel):
|
22
|
+
name: str
|
23
|
+
breed: str
|
24
|
+
age: float
|
25
|
+
personality: CatPersonality
|
26
|
+
|
27
|
+
|
28
|
+
@router.post("/")
|
29
|
+
async def get_cat(
|
30
|
+
cat: CatPostModel,
|
31
|
+
repository: Annotated[CatRepository, Inject(CatRepository)],
|
32
|
+
) -> int:
|
33
|
+
cat_id = repository.next_id()
|
34
|
+
cat = Cat(
|
35
|
+
id=cat_id,
|
36
|
+
name=cat.name,
|
37
|
+
personality=cat.personality,
|
38
|
+
age=cat.age,
|
39
|
+
breed=cat.breed,
|
40
|
+
)
|
41
|
+
repository.set(cat=cat)
|
42
|
+
return cat_id
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from fastapi import FastAPI
|
2
|
+
|
3
|
+
from engin import Block, invoke, provide
|
4
|
+
from examples.fastapi.routes.cats.adapters.repository import InMemoryCatRepository
|
5
|
+
from examples.fastapi.routes.cats.api import router
|
6
|
+
from examples.fastapi.routes.cats.ports import CatRepository
|
7
|
+
|
8
|
+
|
9
|
+
class CatBlock(Block):
|
10
|
+
@provide
|
11
|
+
def cat_repository(self) -> CatRepository:
|
12
|
+
return InMemoryCatRepository()
|
13
|
+
|
14
|
+
@invoke
|
15
|
+
def attach_router(self, app: FastAPI) -> None:
|
16
|
+
app.include_router(router)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
4
|
+
|
5
|
+
|
6
|
+
class CatPersonality(Enum):
|
7
|
+
CUTE = "CUTE"
|
8
|
+
EVIL = "EVIL"
|
9
|
+
|
10
|
+
|
11
|
+
class Cat(BaseModel):
|
12
|
+
model_config = ConfigDict(use_enum_values=True)
|
13
|
+
|
14
|
+
id: int
|
15
|
+
name: str
|
16
|
+
breed: str
|
17
|
+
age: float
|
18
|
+
personality: CatPersonality
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from abc import abstractmethod
|
2
|
+
|
3
|
+
from examples.fastapi.routes.cats.domain import Cat
|
4
|
+
|
5
|
+
|
6
|
+
class CatRepository:
|
7
|
+
@abstractmethod
|
8
|
+
def get(self, cat_id: int) -> Cat: ...
|
9
|
+
|
10
|
+
@abstractmethod
|
11
|
+
def set(self, cat: Cat) -> None: ...
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
def next_id(self) -> int: ...
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "engin"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.2"
|
4
4
|
description = "An async-first modular application framework"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
@@ -12,6 +12,7 @@ build-backend = "hatchling.build"
|
|
12
12
|
|
13
13
|
[tool.uv]
|
14
14
|
dev-dependencies = [
|
15
|
+
"fastapi>=0.115.6",
|
15
16
|
"httpx>=0.27.2",
|
16
17
|
"mypy>=1",
|
17
18
|
"pydantic-settings>=2.6.0",
|
@@ -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
|
@@ -8,14 +9,15 @@ from engin._lifecycle import Lifecycle
|
|
8
9
|
__all__ = [
|
9
10
|
"Assembler",
|
10
11
|
"AssemblyError",
|
12
|
+
"Block",
|
11
13
|
"Engin",
|
12
14
|
"Entrypoint",
|
13
15
|
"Invoke",
|
14
16
|
"Lifecycle",
|
15
|
-
"Block",
|
16
17
|
"Option",
|
17
18
|
"Provide",
|
18
19
|
"Supply",
|
20
|
+
"ext",
|
19
21
|
"invoke",
|
20
22
|
"provide",
|
21
23
|
]
|
@@ -49,7 +49,7 @@ class Assembler:
|
|
49
49
|
if not providers:
|
50
50
|
if type_id.multi:
|
51
51
|
LOG.warning(f"no provider for '{type_id}' defaulting to empty list")
|
52
|
-
providers = [(Supply([], type_hint=list[type_id.type]))]
|
52
|
+
providers = [(Supply([], type_hint=list[type_id.type]))] # type: ignore[name-defined]
|
53
53
|
else:
|
54
54
|
raise LookupError(f"No Provider registered for dependency '{type_id}'")
|
55
55
|
|
@@ -112,15 +112,20 @@ class Assembler:
|
|
112
112
|
|
113
113
|
async def get(self, type_: type[T]) -> T:
|
114
114
|
type_id = type_id_of(type_)
|
115
|
+
if type_id in self._dependencies:
|
116
|
+
return self._dependencies[type_id]
|
115
117
|
if type_id.multi:
|
116
118
|
out = []
|
117
119
|
for provider in self._multiproviders[type_id]:
|
118
120
|
assembled_dependency = await self.assemble(provider)
|
119
121
|
out.extend(await assembled_dependency())
|
120
|
-
|
122
|
+
self._dependencies[type_id] = out
|
123
|
+
return out # type: ignore[return-value]
|
121
124
|
else:
|
122
125
|
assembled_dependency = await self.assemble(self._providers[type_id])
|
123
|
-
|
126
|
+
value = await assembled_dependency()
|
127
|
+
self._dependencies[type_id] = value
|
128
|
+
return value # type: ignore[return-value]
|
124
129
|
|
125
130
|
def has(self, type_: type[T]) -> bool:
|
126
131
|
return type_id_of(type_) in self._providers
|
@@ -21,7 +21,7 @@ class Engin:
|
|
21
21
|
_LIB_OPTIONS: ClassVar[list[Option]] = [Provide(Lifecycle)]
|
22
22
|
|
23
23
|
def __init__(self, *options: Option) -> None:
|
24
|
-
self._providers: dict[TypeId, Provide] = {}
|
24
|
+
self._providers: dict[TypeId, Provide] = {TypeId.from_type(Engin): Provide(self._self)}
|
25
25
|
self._invokables: list[Invoke] = []
|
26
26
|
self._stop_event = Event()
|
27
27
|
|
@@ -35,13 +35,9 @@ class Engin:
|
|
35
35
|
async def run(self):
|
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
|
-
# lifecycle shutdown
|
44
|
-
|
45
41
|
async def start(self) -> None:
|
46
42
|
LOG.info("starting engin")
|
47
43
|
assembled_invocations: list[AssembledDependency] = [
|
@@ -87,3 +83,6 @@ class Engin:
|
|
87
83
|
LOG.debug(f"ENTRYPOINT {type_id!s:<35}")
|
88
84
|
elif isinstance(opt, Invoke):
|
89
85
|
LOG.debug(f"INVOKE {opt.name:<35}")
|
86
|
+
|
87
|
+
def _self(self) -> "Engin":
|
88
|
+
return self
|
File without changes
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import traceback
|
2
2
|
import typing
|
3
|
-
from typing import Protocol, TypeAlias
|
3
|
+
from typing import ClassVar, Protocol, TypeAlias
|
4
4
|
|
5
5
|
from engin import Engin, Option
|
6
6
|
|
@@ -18,13 +18,16 @@ class ASGIType(Protocol):
|
|
18
18
|
|
19
19
|
|
20
20
|
class ASGIEngin(Engin, ASGIType):
|
21
|
+
_asgi_type: ClassVar[type[ASGIType]] = ASGIType # type: ignore[type-abstract]
|
21
22
|
_asgi_app: ASGIType
|
22
23
|
|
23
24
|
def __init__(self, *options: Option) -> None:
|
24
25
|
super().__init__(*options)
|
25
26
|
|
26
|
-
if not self._assembler.has(
|
27
|
-
raise LookupError(
|
27
|
+
if not self._assembler.has(self._asgi_type):
|
28
|
+
raise LookupError(
|
29
|
+
f"A provider for `{self._asgi_type.__name__}` was expected, none found"
|
30
|
+
)
|
28
31
|
|
29
32
|
async def __call__(self, scope: _Scope, receive: _Receive, send: _Send) -> None:
|
30
33
|
if scope["type"] == "lifespan":
|
@@ -44,7 +47,7 @@ class ASGIEngin(Engin, ASGIType):
|
|
44
47
|
|
45
48
|
async def _startup(self) -> None:
|
46
49
|
await self.start()
|
47
|
-
self._asgi_app = await self._assembler.get(
|
50
|
+
self._asgi_app = await self._assembler.get(self._asgi_type)
|
48
51
|
|
49
52
|
|
50
53
|
class _Rereceive:
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import typing
|
2
|
+
from typing import ClassVar, TypeVar
|
3
|
+
|
4
|
+
from engin import Engin, Invoke, Option
|
5
|
+
from engin.ext.asgi import ASGIEngin
|
6
|
+
|
7
|
+
try:
|
8
|
+
from fastapi import FastAPI
|
9
|
+
from fastapi.params import Depends
|
10
|
+
from starlette.requests import HTTPConnection
|
11
|
+
except ImportError:
|
12
|
+
raise ImportError("fastapi must be installed to use the corresponding extension")
|
13
|
+
|
14
|
+
|
15
|
+
if typing.TYPE_CHECKING:
|
16
|
+
from fastapi import FastAPI
|
17
|
+
from fastapi.params import Depends
|
18
|
+
|
19
|
+
__all__ = ["FastAPIEngin", "Inject"]
|
20
|
+
|
21
|
+
|
22
|
+
def _attach_engin(
|
23
|
+
app: FastAPI,
|
24
|
+
engin: Engin,
|
25
|
+
) -> None:
|
26
|
+
app.state.engin = engin
|
27
|
+
|
28
|
+
|
29
|
+
class FastAPIEngin(ASGIEngin):
|
30
|
+
_LIB_OPTIONS: ClassVar[list[Option]] = [*ASGIEngin._LIB_OPTIONS, Invoke(_attach_engin)] # type: ignore[arg-type]
|
31
|
+
_asgi_type = FastAPI
|
32
|
+
|
33
|
+
|
34
|
+
T = TypeVar("T")
|
35
|
+
|
36
|
+
|
37
|
+
def Inject(interface: type[T]) -> Depends:
|
38
|
+
async def inner(conn: HTTPConnection) -> T:
|
39
|
+
engin: Engin = conn.app.state.engin
|
40
|
+
return await engin.assembler.get(interface)
|
41
|
+
|
42
|
+
return Depends(inner)
|
File without changes
|
File without changes
|
File without changes
|
@@ -25,3 +25,23 @@ async def test_assembler_with_multiproviders():
|
|
25
25
|
assembled_dependency = await assembler.assemble(Invoke(assert_all))
|
26
26
|
|
27
27
|
await assembled_dependency()
|
28
|
+
|
29
|
+
|
30
|
+
async def test_assembler_providers_only_called_once():
|
31
|
+
_count = 0
|
32
|
+
|
33
|
+
def count() -> int:
|
34
|
+
nonlocal _count
|
35
|
+
_count += 1
|
36
|
+
return _count
|
37
|
+
|
38
|
+
def assert_singleton(some: int) -> None:
|
39
|
+
assert some == 1
|
40
|
+
|
41
|
+
assembler = Assembler([Provide(count)])
|
42
|
+
|
43
|
+
assembled_dependency = await assembler.assemble(Invoke(assert_singleton))
|
44
|
+
await assembled_dependency()
|
45
|
+
|
46
|
+
assembled_dependency = await assembler.assemble(Invoke(assert_singleton))
|
47
|
+
await assembled_dependency()
|