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
+ [![PyPI version](https://img.shields.io/pypi/v/modern-di-aiohttp.svg)](https://pypi.org/project/modern-di-aiohttp/)
37
+ [![Supported Python versions](https://img.shields.io/pypi/pyversions/modern-di-aiohttp.svg)](https://pypi.org/project/modern-di-aiohttp/)
38
+ [![Downloads](https://static.pepy.tech/badge/modern-di-aiohttp/month)](https://pepy.tech/projects/modern-di-aiohttp)
39
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/modern-python/modern-di-aiohttp/actions/workflows/ci.yml)
40
+ [![CI](https://github.com/modern-python/modern-di-aiohttp/actions/workflows/ci.yml/badge.svg)](https://github.com/modern-python/modern-di-aiohttp/actions/workflows/ci.yml)
41
+ [![License](https://img.shields.io/github/license/modern-python/modern-di-aiohttp.svg)](https://github.com/modern-python/modern-di-aiohttp/blob/main/LICENSE)
42
+ [![GitHub stars](https://img.shields.io/github/stars/modern-python/modern-di-aiohttp)](https://github.com/modern-python/modern-di-aiohttp/stargazers)
43
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
44
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
45
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.26
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any