modern-di-aiogram 2.0.0__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.
- modern_di_aiogram-2.0.0/PKG-INFO +62 -0
- modern_di_aiogram-2.0.0/README.md +35 -0
- modern_di_aiogram-2.0.0/modern_di_aiogram/__init__.py +18 -0
- modern_di_aiogram-2.0.0/modern_di_aiogram/main.py +131 -0
- modern_di_aiogram-2.0.0/modern_di_aiogram/py.typed +0 -0
- modern_di_aiogram-2.0.0/pyproject.toml +84 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modern-di-aiogram
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: modern-di integration for aiogram
|
|
5
|
+
Keywords: dependency-injection,di,ioc-container,modern-di,aiogram,telegram,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: aiogram>=3.2,<4
|
|
19
|
+
Requires-Dist: modern-di>=2.25,<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-aiogram
|
|
24
|
+
Project-URL: Issues, https://github.com/modern-python/modern-di-aiogram/issues
|
|
25
|
+
Project-URL: Changelog, https://github.com/modern-python/modern-di-aiogram/releases
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# modern-di-aiogram
|
|
29
|
+
|
|
30
|
+
[Modern-DI](https://github.com/modern-python/modern-di) integration for [aiogram](https://docs.aiogram.dev) 3.x.
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import typing
|
|
36
|
+
|
|
37
|
+
from aiogram import Bot, Dispatcher
|
|
38
|
+
from aiogram.types import Message
|
|
39
|
+
from modern_di import Container, Group, Scope, providers
|
|
40
|
+
from modern_di_aiogram import FromDI, inject, setup_di
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Settings:
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self.greeting = "hello"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Dependencies(Group):
|
|
49
|
+
settings = providers.Factory(scope=Scope.APP, creator=Settings)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
dispatcher = Dispatcher()
|
|
53
|
+
setup_di(dispatcher, Container(groups=[Dependencies], validate=True))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dispatcher.message()
|
|
57
|
+
@inject
|
|
58
|
+
async def greet(message: Message, settings: typing.Annotated[Settings, FromDI(Dependencies.settings)]) -> None:
|
|
59
|
+
await message.answer(f"{settings.greeting}, {message.from_user.first_name}")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See the [documentation](https://modern-di.modern-python.org) for the full guide, including `auto_inject`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# modern-di-aiogram
|
|
2
|
+
|
|
3
|
+
[Modern-DI](https://github.com/modern-python/modern-di) integration for [aiogram](https://docs.aiogram.dev) 3.x.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
from aiogram import Bot, Dispatcher
|
|
11
|
+
from aiogram.types import Message
|
|
12
|
+
from modern_di import Container, Group, Scope, providers
|
|
13
|
+
from modern_di_aiogram import FromDI, inject, setup_di
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Settings:
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self.greeting = "hello"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Dependencies(Group):
|
|
22
|
+
settings = providers.Factory(scope=Scope.APP, creator=Settings)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
dispatcher = Dispatcher()
|
|
26
|
+
setup_di(dispatcher, Container(groups=[Dependencies], validate=True))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dispatcher.message()
|
|
30
|
+
@inject
|
|
31
|
+
async def greet(message: Message, settings: typing.Annotated[Settings, FromDI(Dependencies.settings)]) -> None:
|
|
32
|
+
await message.answer(f"{settings.greeting}, {message.from_user.first_name}")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
See the [documentation](https://modern-di.modern-python.org) for the full guide, including `auto_inject`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from modern_di_aiogram.main import (
|
|
2
|
+
FromDI,
|
|
3
|
+
aiogram_event_provider,
|
|
4
|
+
aiogram_update_provider,
|
|
5
|
+
fetch_di_container,
|
|
6
|
+
inject,
|
|
7
|
+
setup_di,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"FromDI",
|
|
13
|
+
"aiogram_event_provider",
|
|
14
|
+
"aiogram_update_provider",
|
|
15
|
+
"fetch_di_container",
|
|
16
|
+
"inject",
|
|
17
|
+
"setup_di",
|
|
18
|
+
]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import functools
|
|
3
|
+
import inspect
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import aiogram
|
|
7
|
+
from aiogram import BaseMiddleware, Dispatcher
|
|
8
|
+
from aiogram.dispatcher.event.handler import HandlerObject
|
|
9
|
+
from aiogram.types import TelegramObject, Update
|
|
10
|
+
from modern_di import Container, Scope, providers
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
aiogram_update_provider = providers.ContextProvider(Update, scope=Scope.REQUEST)
|
|
14
|
+
aiogram_event_provider = providers.ContextProvider(TelegramObject, scope=Scope.REQUEST)
|
|
15
|
+
_CONNECTION_PROVIDERS = (aiogram_update_provider, aiogram_event_provider)
|
|
16
|
+
|
|
17
|
+
# Root container in the dispatcher's workflow-data; per-update child in the
|
|
18
|
+
# per-update ``data`` dict (also the keyword-only param name ``inject`` adds).
|
|
19
|
+
# Named constants keep writer and reader in provable agreement.
|
|
20
|
+
_ROOT_CONTAINER_KEY = "modern_di_root_container"
|
|
21
|
+
_CHILD_CONTAINER_KEY = "modern_di_container"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _DiMiddleware(BaseMiddleware):
|
|
25
|
+
def __init__(self, container: Container) -> None:
|
|
26
|
+
self.container = container
|
|
27
|
+
|
|
28
|
+
async def __call__(
|
|
29
|
+
self,
|
|
30
|
+
handler: typing.Callable[[TelegramObject, dict[str, typing.Any]], typing.Awaitable[typing.Any]],
|
|
31
|
+
event: TelegramObject,
|
|
32
|
+
data: dict[str, typing.Any],
|
|
33
|
+
) -> typing.Any: # noqa: ANN401
|
|
34
|
+
child_container = self.container.build_child_container(
|
|
35
|
+
scope=Scope.REQUEST,
|
|
36
|
+
context={Update: event, TelegramObject: typing.cast("Update", event).event},
|
|
37
|
+
)
|
|
38
|
+
data[_CHILD_CONTAINER_KEY] = child_container
|
|
39
|
+
try:
|
|
40
|
+
return await handler(event, data)
|
|
41
|
+
finally:
|
|
42
|
+
await child_container.close_async()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def setup_di(dispatcher: Dispatcher, container: Container, *, auto_inject: bool = False) -> Container:
|
|
46
|
+
dispatcher[_ROOT_CONTAINER_KEY] = container
|
|
47
|
+
container.add_providers(*_CONNECTION_PROVIDERS)
|
|
48
|
+
dispatcher.startup.register(container.open)
|
|
49
|
+
dispatcher.shutdown.register(container.close_async)
|
|
50
|
+
dispatcher.update.outer_middleware(_DiMiddleware(container))
|
|
51
|
+
if auto_inject:
|
|
52
|
+
dispatcher.startup.register(functools.partial(_inject_router, dispatcher))
|
|
53
|
+
return container
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def fetch_di_container(dispatcher: Dispatcher) -> Container:
|
|
57
|
+
return typing.cast(Container, dispatcher[_ROOT_CONTAINER_KEY])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
T = typing.TypeVar("T")
|
|
61
|
+
T_co = typing.TypeVar("T_co", covariant=True)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclasses.dataclass(slots=True, frozen=True)
|
|
65
|
+
class _FromDI(typing.Generic[T_co]):
|
|
66
|
+
dependency: providers.AbstractProvider[T_co] | type[T_co]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def FromDI(dependency: providers.AbstractProvider[T] | type[T]) -> T: # noqa: N802
|
|
70
|
+
return typing.cast(T, _FromDI(dependency))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_inject_params(func: typing.Callable[..., typing.Any]) -> dict[str, _FromDI[typing.Any]]:
|
|
74
|
+
hints = typing.get_type_hints(func, include_extras=True)
|
|
75
|
+
di_params: dict[str, _FromDI[typing.Any]] = {}
|
|
76
|
+
for name, hint in hints.items():
|
|
77
|
+
if name == "return":
|
|
78
|
+
continue
|
|
79
|
+
if typing.get_origin(hint) is typing.Annotated:
|
|
80
|
+
for meta in typing.get_args(hint)[1:]:
|
|
81
|
+
if isinstance(meta, _FromDI):
|
|
82
|
+
di_params[name] = meta
|
|
83
|
+
break
|
|
84
|
+
return di_params
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def inject(func: typing.Callable[..., typing.Awaitable[T]]) -> typing.Callable[..., typing.Awaitable[T]]:
|
|
88
|
+
di_params = _parse_inject_params(func)
|
|
89
|
+
if not di_params:
|
|
90
|
+
func.__modern_di_injected__ = True # ty: ignore[unresolved-attribute]
|
|
91
|
+
return func
|
|
92
|
+
|
|
93
|
+
original_signature = inspect.signature(func)
|
|
94
|
+
visible_params = [p for name, p in original_signature.parameters.items() if name not in di_params]
|
|
95
|
+
container_param_injected = _CHILD_CONTAINER_KEY not in original_signature.parameters
|
|
96
|
+
if container_param_injected:
|
|
97
|
+
visible_params.append(
|
|
98
|
+
inspect.Parameter(_CHILD_CONTAINER_KEY, kind=inspect.Parameter.KEYWORD_ONLY, annotation=Container),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def wrapper(*args: typing.Any, **kwargs: typing.Any) -> T: # noqa: ANN401
|
|
102
|
+
container: Container = (
|
|
103
|
+
kwargs.pop(_CHILD_CONTAINER_KEY) if container_param_injected else kwargs[_CHILD_CONTAINER_KEY]
|
|
104
|
+
)
|
|
105
|
+
resolved = {name: container.resolve_dependency(marker.dependency) for name, marker in di_params.items()}
|
|
106
|
+
return await func(*args, **kwargs, **resolved)
|
|
107
|
+
|
|
108
|
+
# NOT functools.wraps: aiogram unwraps __wrapped__ and would defeat __signature__.
|
|
109
|
+
wrapper.__name__ = func.__name__ # ty: ignore[unresolved-attribute]
|
|
110
|
+
wrapper.__qualname__ = func.__qualname__ # ty: ignore[unresolved-attribute]
|
|
111
|
+
wrapper.__doc__ = func.__doc__
|
|
112
|
+
wrapper.__module__ = func.__module__
|
|
113
|
+
wrapper.__signature__ = original_signature.replace(parameters=visible_params) # ty: ignore[unresolved-attribute]
|
|
114
|
+
wrapper.__modern_di_injected__ = True # ty: ignore[unresolved-attribute]
|
|
115
|
+
return wrapper
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _inject_router(router: aiogram.Router) -> None:
|
|
119
|
+
for sub_router in router.chain_tail:
|
|
120
|
+
for observer in sub_router.observers.values():
|
|
121
|
+
if observer.event_name == "update":
|
|
122
|
+
continue
|
|
123
|
+
for handler in observer.handlers:
|
|
124
|
+
if not getattr(handler.callback, "__modern_di_injected__", False):
|
|
125
|
+
wrapped = HandlerObject(
|
|
126
|
+
callback=inject(handler.callback),
|
|
127
|
+
filters=handler.filters,
|
|
128
|
+
flags=handler.flags,
|
|
129
|
+
)
|
|
130
|
+
handler.callback = wrapped.callback
|
|
131
|
+
handler.params = wrapped.params
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "modern-di-aiogram"
|
|
3
|
+
description = "modern-di integration for aiogram"
|
|
4
|
+
authors = [{ name = "Artur Shiriev", email = "me@shiriev.ru" }]
|
|
5
|
+
requires-python = ">=3.10,<4"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
keywords = ["dependency-injection", "di", "ioc-container", "modern-di", "aiogram", "telegram", "python"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Programming Language :: Python :: 3.10",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
"Topic :: Software Development :: Libraries",
|
|
19
|
+
]
|
|
20
|
+
dependencies = ["aiogram>=3.2,<4", "modern-di>=2.25,<3"]
|
|
21
|
+
version = "2.0.0"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://modern-di.modern-python.org"
|
|
25
|
+
Documentation = "https://modern-di.modern-python.org"
|
|
26
|
+
Repository = "https://github.com/modern-python/modern-di-aiogram"
|
|
27
|
+
Issues = "https://github.com/modern-python/modern-di-aiogram/issues"
|
|
28
|
+
Changelog = "https://github.com/modern-python/modern-di-aiogram/releases"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest",
|
|
33
|
+
"pytest-cov",
|
|
34
|
+
"pytest-asyncio",
|
|
35
|
+
]
|
|
36
|
+
lint = [
|
|
37
|
+
"ty",
|
|
38
|
+
"ruff",
|
|
39
|
+
"eof-fixer",
|
|
40
|
+
"typing-extensions",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.11,<1.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[tool.uv.build-backend]
|
|
48
|
+
module-name = "modern_di_aiogram"
|
|
49
|
+
module-root = ""
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
fix = false
|
|
53
|
+
unsafe-fixes = true
|
|
54
|
+
line-length = 120
|
|
55
|
+
target-version = "py310"
|
|
56
|
+
|
|
57
|
+
[tool.ruff.format]
|
|
58
|
+
docstring-code-format = true
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["ALL"]
|
|
62
|
+
ignore = [
|
|
63
|
+
"D1", # allow missing docstrings
|
|
64
|
+
"S101", # allow asserts
|
|
65
|
+
"TCH", # ignore flake8-type-checking
|
|
66
|
+
"FBT", # allow boolean args
|
|
67
|
+
"D203", # "one-blank-line-before-class" conflicting with D211
|
|
68
|
+
"D213", # "multi-line-summary-second-line" conflicting with D212
|
|
69
|
+
"COM812", # flake8-commas "Trailing comma missing"
|
|
70
|
+
"ISC001", # flake8-implicit-str-concat
|
|
71
|
+
"G004", # allow f-strings in logging
|
|
72
|
+
]
|
|
73
|
+
isort.lines-after-imports = 2
|
|
74
|
+
isort.no-lines-before = ["standard-library", "local-folder"]
|
|
75
|
+
|
|
76
|
+
[tool.pytest.ini_options]
|
|
77
|
+
addopts = ""
|
|
78
|
+
asyncio_mode = "auto"
|
|
79
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
80
|
+
|
|
81
|
+
[tool.coverage.report]
|
|
82
|
+
exclude_also = [
|
|
83
|
+
"if typing.TYPE_CHECKING:",
|
|
84
|
+
]
|