modern-di-aiohttp 2.0.0__py3-none-any.whl
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.
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from modern_di_aiohttp.main import (
|
|
2
|
+
FromDI,
|
|
3
|
+
aiohttp_request_provider,
|
|
4
|
+
aiohttp_websocket_provider,
|
|
5
|
+
fetch_di_container,
|
|
6
|
+
fetch_request_container,
|
|
7
|
+
inject,
|
|
8
|
+
setup_di,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FromDI",
|
|
14
|
+
"aiohttp_request_provider",
|
|
15
|
+
"aiohttp_websocket_provider",
|
|
16
|
+
"fetch_di_container",
|
|
17
|
+
"fetch_request_container",
|
|
18
|
+
"inject",
|
|
19
|
+
"setup_di",
|
|
20
|
+
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""modern-di integration for aiohttp."""
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import enum
|
|
5
|
+
import functools
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from aiohttp import web
|
|
9
|
+
from modern_di import Container, Scope, providers
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
T_co = typing.TypeVar("T_co", covariant=True)
|
|
13
|
+
T = typing.TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# aiohttp exposes only `web.Request` at middleware entry (a WebSocket is an
|
|
17
|
+
# upgraded HTTP request), so both connection providers bind `web.Request`. The
|
|
18
|
+
# providers registry rejects two providers of the same type, so the websocket
|
|
19
|
+
# provider is reference-only (`bound_type=None`): not registered by type, but
|
|
20
|
+
# resolvable via `FromDI(aiohttp_websocket_provider)` from the SESSION container.
|
|
21
|
+
aiohttp_request_provider = providers.ContextProvider(scope=Scope.REQUEST, context_type=web.Request)
|
|
22
|
+
aiohttp_websocket_provider = providers.ContextProvider(scope=Scope.SESSION, context_type=web.Request, bound_type=None)
|
|
23
|
+
_CONNECTION_PROVIDERS = (aiohttp_request_provider, aiohttp_websocket_provider)
|
|
24
|
+
|
|
25
|
+
# Root container on the aiohttp application (typed key, aiohttp >= 3.9).
|
|
26
|
+
_DI_CONTAINER_APP_KEY: "web.AppKey[Container]" = web.AppKey("modern_di_container", Container)
|
|
27
|
+
# Per-connection child container, stashed on the request's dict interface.
|
|
28
|
+
_CONTAINER_REQUEST_KEY = "modern_di_child_container"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def fetch_di_container(app: web.Application) -> Container:
|
|
32
|
+
return app[_DI_CONTAINER_APP_KEY]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def fetch_request_container(request: web.Request) -> Container:
|
|
36
|
+
"""Return the per-connection child container the middleware built for this request."""
|
|
37
|
+
try:
|
|
38
|
+
return request[_CONTAINER_REQUEST_KEY]
|
|
39
|
+
except KeyError:
|
|
40
|
+
msg = (
|
|
41
|
+
"No modern-di container found on the request. "
|
|
42
|
+
"Call setup_di(app, container) so requests pass through the modern-di middleware "
|
|
43
|
+
"before using @inject or fetch_request_container."
|
|
44
|
+
)
|
|
45
|
+
raise RuntimeError(msg) from None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _on_startup(app: web.Application) -> None:
|
|
49
|
+
# Reopen so a second run of the same container (reload, test re-entry) does
|
|
50
|
+
# not raise ContainerClosedError; reopening an open container is a no-op.
|
|
51
|
+
fetch_di_container(app).open()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def _on_cleanup(app: web.Application) -> None:
|
|
55
|
+
await fetch_di_container(app).close_async()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@web.middleware
|
|
59
|
+
async def _di_middleware(
|
|
60
|
+
request: web.Request,
|
|
61
|
+
handler: typing.Callable[[web.Request], typing.Awaitable[web.StreamResponse]],
|
|
62
|
+
) -> web.StreamResponse:
|
|
63
|
+
# `can_prepare` never raises and does not start the handshake; it only checks
|
|
64
|
+
# whether the request is a valid WebSocket upgrade.
|
|
65
|
+
connection_scope: enum.IntEnum = Scope.SESSION if web.WebSocketResponse().can_prepare(request).ok else Scope.REQUEST
|
|
66
|
+
child_container = fetch_di_container(request.app).build_child_container(
|
|
67
|
+
context={web.Request: request}, scope=connection_scope
|
|
68
|
+
)
|
|
69
|
+
request[_CONTAINER_REQUEST_KEY] = child_container
|
|
70
|
+
try:
|
|
71
|
+
return await handler(request)
|
|
72
|
+
finally:
|
|
73
|
+
await child_container.close_async()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def setup_di(app: web.Application, container: Container) -> Container:
|
|
77
|
+
app[_DI_CONTAINER_APP_KEY] = container
|
|
78
|
+
container.providers_registry.add_providers(*_CONNECTION_PROVIDERS)
|
|
79
|
+
app.on_startup.append(_on_startup)
|
|
80
|
+
app.on_cleanup.append(_on_cleanup)
|
|
81
|
+
app.middlewares.append(_di_middleware)
|
|
82
|
+
return container
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
86
|
+
class _FromDI(typing.Generic[T_co]):
|
|
87
|
+
dependency: providers.AbstractProvider[T_co] | type[T_co]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def FromDI(dependency: providers.AbstractProvider[T_co] | type[T_co]) -> T_co: # noqa: N802
|
|
91
|
+
return typing.cast(T_co, _FromDI(dependency))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_inject_params(func: typing.Callable[..., typing.Any]) -> dict[str, _FromDI[typing.Any]]:
|
|
95
|
+
hints = typing.get_type_hints(func, include_extras=True)
|
|
96
|
+
di_params: dict[str, _FromDI[typing.Any]] = {}
|
|
97
|
+
for name, hint in hints.items():
|
|
98
|
+
if name == "return":
|
|
99
|
+
continue
|
|
100
|
+
if typing.get_origin(hint) is typing.Annotated:
|
|
101
|
+
for meta in typing.get_args(hint)[1:]:
|
|
102
|
+
if isinstance(meta, _FromDI):
|
|
103
|
+
di_params[name] = meta
|
|
104
|
+
break
|
|
105
|
+
return di_params
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolve_di_params(container: Container, di_params: dict[str, _FromDI[typing.Any]]) -> dict[str, typing.Any]:
|
|
109
|
+
return {
|
|
110
|
+
name: (
|
|
111
|
+
container.resolve_provider(marker.dependency)
|
|
112
|
+
if isinstance(marker.dependency, providers.AbstractProvider)
|
|
113
|
+
else container.resolve(dependency_type=marker.dependency)
|
|
114
|
+
)
|
|
115
|
+
for name, marker in di_params.items()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def inject(func: typing.Callable[..., typing.Awaitable[T]]) -> typing.Callable[..., typing.Awaitable[T]]:
|
|
120
|
+
di_params = _parse_inject_params(func)
|
|
121
|
+
|
|
122
|
+
@functools.wraps(func)
|
|
123
|
+
async def wrapper(request: web.Request) -> T:
|
|
124
|
+
child_container = fetch_request_container(request)
|
|
125
|
+
return await func(request, **_resolve_di_params(child_container, di_params))
|
|
126
|
+
|
|
127
|
+
return wrapper
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modern-di-aiohttp
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: modern-di integration for aiohttp
|
|
5
|
+
Keywords: dependency-injection,di,ioc-container,modern-di,aiohttp,python
|
|
6
|
+
Author: Artur Shiriev
|
|
7
|
+
Author-email: Artur Shiriev <me@shiriev.ru>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
18
|
+
Requires-Dist: aiohttp>=3.9,<4
|
|
19
|
+
Requires-Dist: modern-di>=2.21.0,<3
|
|
20
|
+
Requires-Python: >=3.10, <4
|
|
21
|
+
Project-URL: Homepage, https://modern-di.modern-python.org
|
|
22
|
+
Project-URL: Documentation, https://modern-di.modern-python.org
|
|
23
|
+
Project-URL: Repository, https://github.com/modern-python/modern-di-aiohttp
|
|
24
|
+
Project-URL: Issues, https://github.com/modern-python/modern-di-aiohttp/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/modern-python/modern-di-aiohttp/releases
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
<p align="center">
|
|
29
|
+
<picture>
|
|
30
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/modern-python/.github/main/brand/projects/modern-di-aiohttp/lockup-dark.svg">
|
|
31
|
+
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/modern-python/.github/main/brand/projects/modern-di-aiohttp/lockup-light.svg">
|
|
32
|
+
<img alt="modern-di-aiohttp" src="https://raw.githubusercontent.com/modern-python/.github/main/brand/projects/modern-di-aiohttp/lockup.png" width="420">
|
|
33
|
+
</picture>
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/modern-di-aiohttp/)
|
|
37
|
+
[](https://pypi.org/project/modern-di-aiohttp/)
|
|
38
|
+
[](https://pepy.tech/projects/modern-di-aiohttp)
|
|
39
|
+
[](https://github.com/modern-python/modern-di-aiohttp/actions/workflows/ci.yml)
|
|
40
|
+
[](https://github.com/modern-python/modern-di-aiohttp/actions/workflows/ci.yml)
|
|
41
|
+
[](https://github.com/modern-python/modern-di-aiohttp/blob/main/LICENSE)
|
|
42
|
+
[](https://github.com/modern-python/modern-di-aiohttp/stargazers)
|
|
43
|
+
[](https://github.com/astral-sh/uv)
|
|
44
|
+
[](https://github.com/astral-sh/ruff)
|
|
45
|
+
[](https://github.com/astral-sh/ty)
|
|
46
|
+
|
|
47
|
+
[Modern-DI](https://github.com/modern-python/modern-di) integration for [aiohttp](https://docs.aiohttp.org).
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add modern-di-aiohttp # or: pip install modern-di-aiohttp
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
aiohttp has no dependency-injection system of its own, so `modern-di-aiohttp` pairs an `@inject` decorator with inert `FromDI` markers. `setup_di` stores the container on the app, opens it on startup and closes it on cleanup, and installs middleware that builds a per-connection child container automatically; `FromDI` resolves a provider (or type) into a handler parameter.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import dataclasses
|
|
61
|
+
import typing
|
|
62
|
+
|
|
63
|
+
from aiohttp import web
|
|
64
|
+
from modern_di import Container, Group, Scope, providers
|
|
65
|
+
from modern_di_aiohttp import FromDI, inject, setup_di
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclasses.dataclass(kw_only=True)
|
|
69
|
+
class Settings:
|
|
70
|
+
debug: bool = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclasses.dataclass(kw_only=True)
|
|
74
|
+
class UserService:
|
|
75
|
+
settings: Settings # auto-injected by type
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Dependencies(Group):
|
|
79
|
+
settings = providers.Factory(scope=Scope.APP, creator=Settings)
|
|
80
|
+
user_service = providers.Factory(scope=Scope.REQUEST, creator=UserService)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@inject
|
|
84
|
+
async def index(
|
|
85
|
+
request: web.Request,
|
|
86
|
+
user_service: typing.Annotated[UserService, FromDI(Dependencies.user_service)],
|
|
87
|
+
) -> web.Response:
|
|
88
|
+
return web.json_response({"debug": user_service.settings.debug})
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
app = web.Application()
|
|
92
|
+
app.router.add_get("/", index)
|
|
93
|
+
setup_di(app, Container(groups=[Dependencies], validate=True))
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
An HTTP request opens a `Scope.REQUEST` child container; a WebSocket connection opens a `Scope.SESSION` one. The connection `aiohttp.web.Request` is resolvable within DI: HTTP handlers and `REQUEST`-scoped factories inject it by type via `aiohttp_request_provider`, while WebSocket handlers read it via `FromDI(aiohttp_websocket_provider)`. For per-message work inside a WebSocket handler, open a nested `Scope.REQUEST` child of the session container fetched with `fetch_request_container`.
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
| Symbol | Description |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `setup_di(app, container)` | Stores the container on the app, wires `on_startup`/`on_cleanup` (reopen on startup, close on cleanup), registers the connection providers, and installs the per-connection middleware |
|
|
103
|
+
| `inject(handler)` | Decorator that resolves every `FromDI`-marked parameter from the request's child container and passes them to the handler |
|
|
104
|
+
| `FromDI(dependency)` | Inert `Annotated` marker resolved by `@inject`; accepts a provider or a type |
|
|
105
|
+
| `fetch_di_container(app)` | Returns the app-scoped root container |
|
|
106
|
+
| `fetch_request_container(request)` | Returns the per-connection child container the middleware built for this request |
|
|
107
|
+
| `aiohttp_request_provider` | `ContextProvider` for the current `aiohttp.web.Request` (`REQUEST` scope) |
|
|
108
|
+
| `aiohttp_websocket_provider` | `ContextProvider` for the connection `aiohttp.web.Request` at WebSocket `SESSION` scope |
|
|
109
|
+
|
|
110
|
+
## 📦 [PyPI](https://pypi.org/project/modern-di-aiohttp)
|
|
111
|
+
|
|
112
|
+
## 📝 [License](LICENSE)
|
|
113
|
+
|
|
114
|
+
## Part of `modern-python`
|
|
115
|
+
|
|
116
|
+
Built on [`modern-di`](https://github.com/modern-python/modern-di), a dependency-injection framework with IoC container and scopes.
|
|
117
|
+
|
|
118
|
+
Browse the full list of templates and libraries in
|
|
119
|
+
[`modern-python`](https://github.com/modern-python) — see the org profile for the categorized index.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
modern_di_aiohttp/__init__.py,sha256=8zUL7rz1gCf7MyLrH9Dga6fQLp-GizHkUa4FFhpGlTs,375
|
|
2
|
+
modern_di_aiohttp/main.py,sha256=05Wtg7OK5E2d-X3S8m13xvqL1ooqlfW5oLIwvtUhBgo,4919
|
|
3
|
+
modern_di_aiohttp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
modern_di_aiohttp-2.0.0.dist-info/WHEEL,sha256=uOqnPWqgFlbov4NeTCercq7cBQ2UN7xh5fiW55lOnAg,81
|
|
5
|
+
modern_di_aiohttp-2.0.0.dist-info/METADATA,sha256=SCdcDT27cYxR4nP41L9PZevET-y6zKUVjQ_oWfB6SqY,6524
|
|
6
|
+
modern_di_aiohttp-2.0.0.dist-info/RECORD,,
|