aiogram-webhook 2.0.1__tar.gz → 3.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.
Files changed (63) hide show
  1. aiogram_webhook-3.0.0/PKG-INFO +94 -0
  2. aiogram_webhook-3.0.0/README.md +55 -0
  3. {aiogram_webhook-2.0.1 → aiogram_webhook-3.0.0}/pyproject.toml +9 -6
  4. aiogram_webhook-3.0.0/src/aiogram_webhook/__init__.py +16 -0
  5. {aiogram_webhook-2.0.1/src/aiogram_webhook/config → aiogram_webhook-3.0.0/src/aiogram_webhook/configs}/bot.py +1 -1
  6. {aiogram_webhook-2.0.1/src/aiogram_webhook/config → aiogram_webhook-3.0.0/src/aiogram_webhook/configs}/webhook.py +11 -9
  7. aiogram_webhook-3.0.0/src/aiogram_webhook/engines/base.py +145 -0
  8. aiogram_webhook-3.0.0/src/aiogram_webhook/engines/errors.py +77 -0
  9. aiogram_webhook-3.0.0/src/aiogram_webhook/engines/multi.py +76 -0
  10. aiogram_webhook-3.0.0/src/aiogram_webhook/engines/single.py +73 -0
  11. aiogram_webhook-3.0.0/src/aiogram_webhook/engines/target.py +7 -0
  12. aiogram_webhook-3.0.0/src/aiogram_webhook/engines/token.py +133 -0
  13. aiogram_webhook-3.0.0/src/aiogram_webhook/errors.py +16 -0
  14. aiogram_webhook-3.0.0/src/aiogram_webhook/logs.py +20 -0
  15. aiogram_webhook-3.0.0/src/aiogram_webhook/route/__init__.py +5 -0
  16. aiogram_webhook-3.0.0/src/aiogram_webhook/route/config.py +29 -0
  17. aiogram_webhook-3.0.0/src/aiogram_webhook/route/errors.py +213 -0
  18. aiogram_webhook-3.0.0/src/aiogram_webhook/route/params.py +43 -0
  19. aiogram_webhook-3.0.0/src/aiogram_webhook/route/path.py +87 -0
  20. aiogram_webhook-3.0.0/src/aiogram_webhook/route/query.py +148 -0
  21. aiogram_webhook-3.0.0/src/aiogram_webhook/route/route.py +98 -0
  22. aiogram_webhook-3.0.0/src/aiogram_webhook/route/url.py +18 -0
  23. aiogram_webhook-3.0.0/src/aiogram_webhook/security/checks/check.py +20 -0
  24. aiogram_webhook-3.0.0/src/aiogram_webhook/security/checks/ip.py +83 -0
  25. aiogram_webhook-3.0.0/src/aiogram_webhook/security/errors.py +42 -0
  26. aiogram_webhook-3.0.0/src/aiogram_webhook/security/secret_token.py +64 -0
  27. aiogram_webhook-3.0.0/src/aiogram_webhook/security/security.py +38 -0
  28. aiogram_webhook-3.0.0/src/aiogram_webhook/tasks.py +58 -0
  29. aiogram_webhook-3.0.0/src/aiogram_webhook/utils/_payload.py +36 -0
  30. aiogram_webhook-3.0.0/src/aiogram_webhook/utils/config.py +52 -0
  31. aiogram_webhook-3.0.0/src/aiogram_webhook/web/__init__.py +3 -0
  32. aiogram_webhook-3.0.0/src/aiogram_webhook/web/_starlette.py +55 -0
  33. aiogram_webhook-3.0.0/src/aiogram_webhook/web/aiohttp.py +89 -0
  34. aiogram_webhook-3.0.0/src/aiogram_webhook/web/base.py +79 -0
  35. aiogram_webhook-3.0.0/src/aiogram_webhook/web/fastapi.py +99 -0
  36. aiogram_webhook-2.0.1/PKG-INFO +0 -396
  37. aiogram_webhook-2.0.1/README.md +0 -361
  38. aiogram_webhook-2.0.1/src/aiogram_webhook/__init__.py +0 -21
  39. aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/aiohttp/adapter.py +0 -56
  40. aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/aiohttp/mapping.py +0 -15
  41. aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/base_adapter.py +0 -86
  42. aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/base_mapping.py +0 -38
  43. aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/fastapi/adapter.py +0 -49
  44. aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/fastapi/mapping.py +0 -15
  45. aiogram_webhook-2.0.1/src/aiogram_webhook/engines/__init__.py +0 -5
  46. aiogram_webhook-2.0.1/src/aiogram_webhook/engines/base.py +0 -158
  47. aiogram_webhook-2.0.1/src/aiogram_webhook/engines/simple.py +0 -101
  48. aiogram_webhook-2.0.1/src/aiogram_webhook/engines/token.py +0 -126
  49. aiogram_webhook-2.0.1/src/aiogram_webhook/routing/__init__.py +0 -6
  50. aiogram_webhook-2.0.1/src/aiogram_webhook/routing/base.py +0 -38
  51. aiogram_webhook-2.0.1/src/aiogram_webhook/routing/path.py +0 -28
  52. aiogram_webhook-2.0.1/src/aiogram_webhook/routing/query.py +0 -18
  53. aiogram_webhook-2.0.1/src/aiogram_webhook/routing/static.py +0 -12
  54. aiogram_webhook-2.0.1/src/aiogram_webhook/security/checks/check.py +0 -17
  55. aiogram_webhook-2.0.1/src/aiogram_webhook/security/checks/ip.py +0 -83
  56. aiogram_webhook-2.0.1/src/aiogram_webhook/security/secret_token.py +0 -54
  57. aiogram_webhook-2.0.1/src/aiogram_webhook/security/security.py +0 -44
  58. {aiogram_webhook-2.0.1/src/aiogram_webhook/adapters → aiogram_webhook-3.0.0/src/aiogram_webhook/configs}/__init__.py +0 -0
  59. {aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/aiohttp → aiogram_webhook-3.0.0/src/aiogram_webhook/engines}/__init__.py +0 -0
  60. /aiogram_webhook-2.0.1/src/aiogram_webhook/adapters/fastapi/__init__.py → /aiogram_webhook-3.0.0/src/aiogram_webhook/py.typed +0 -0
  61. {aiogram_webhook-2.0.1 → aiogram_webhook-3.0.0}/src/aiogram_webhook/security/__init__.py +0 -0
  62. {aiogram_webhook-2.0.1/src/aiogram_webhook/config → aiogram_webhook-3.0.0/src/aiogram_webhook/security/checks}/__init__.py +0 -0
  63. {aiogram_webhook-2.0.1/src/aiogram_webhook/security/checks → aiogram_webhook-3.0.0/src/aiogram_webhook/utils}/__init__.py +0 -0
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.3
2
+ Name: aiogram-webhook
3
+ Version: 3.0.0
4
+ Summary: A python library for integrating webhook support with multiple web frameworks in aiogram. Organizes bot operation via webhooks for both single and multi-bot setups.
5
+ Keywords: aiogram,multibot,telegram,webhook,fastapi,aiohttp
6
+ Author: m-xim
7
+ Author-email: m-xim <i@m-xim.ru>
8
+ License: MIT
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Typing :: Typed
18
+ Requires-Dist: aiogram>=3.14.0
19
+ Requires-Dist: yarl>=1.17.0
20
+ Requires-Dist: multidict>=6.7.1
21
+ Requires-Dist: aiohttp>=3.9.0
22
+ Requires-Dist: aiohttp>=3.9.0 ; extra == 'aiohttp'
23
+ Requires-Dist: ruff ; extra == 'dev'
24
+ Requires-Dist: ty ; extra == 'dev'
25
+ Requires-Dist: pytest ; extra == 'dev'
26
+ Requires-Dist: pytest-asyncio ; extra == 'dev'
27
+ Requires-Dist: pytest-cov ; extra == 'dev'
28
+ Requires-Dist: httpx2 ; extra == 'dev'
29
+ Requires-Dist: fastapi>=0.128.0 ; extra == 'fastapi'
30
+ Requires-Python: >=3.10
31
+ Project-URL: Homepage, https://github.com/m-xim/aiogram-webhook
32
+ Project-URL: Repository, https://github.com/m-xim/aiogram-webhook
33
+ Project-URL: Issues, https://github.com/m-xim/aiogram-webhook/issues
34
+ Project-URL: Documentation, https://aiogram-webhook.m-xim.ru
35
+ Provides-Extra: aiohttp
36
+ Provides-Extra: dev
37
+ Provides-Extra: fastapi
38
+ Description-Content-Type: text/markdown
39
+
40
+ ![PyPI version](docs/_assets/brand/banner.png)
41
+
42
+ # aiogram-webhook
43
+ [![PyPI version](https://img.shields.io/pypi/v/aiogram-webhook?color=blue)](https://pypi.org/project/aiogram-webhook)
44
+ [![codecov](https://codecov.io/github/m-xim/aiogram-webhook/graph/badge.svg?token=H21MX17Y7D)](https://codecov.io/github/m-xim/aiogram-webhook)
45
+ [![Tests Status](https://github.com/m-xim/aiogram-webhook/actions/workflows/tests.yml/badge.svg)](https://github.com/m-xim/aiogram-webhook/actions)
46
+ [![License](https://img.shields.io/github/license/m-xim/aiogram-webhook.svg)](/LICENSE)
47
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/m-xim/aiogram-webhook)
48
+ [![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)
49
+ [![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)
50
+
51
+ `aiogram-webhook` is a modular Python library for webhook integration in aiogram.
52
+ It supports single-bot and token-based multi-bot setups, with route building, optional request checks, and adapters for FastAPI and aiohttp.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install aiogram-webhook
58
+ pip install "aiogram-webhook[fastapi]"
59
+ pip install "aiogram-webhook[aiohttp]"
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ ```python
65
+ from aiogram import Bot, Dispatcher
66
+ from fastapi import FastAPI
67
+
68
+ from aiogram_webhook import FastAPIAdapter, SingleBotEngine
69
+ from aiogram_webhook.route import Route
70
+
71
+ dispatcher = Dispatcher()
72
+ bot = Bot("BOT_TOKEN")
73
+
74
+ engine = SingleBotEngine(
75
+ dispatcher,
76
+ bot,
77
+ web=FastAPIAdapter(),
78
+ route=Route(base_url="https://example.com", path="/webhook"),
79
+ )
80
+
81
+ app = FastAPI()
82
+ engine.register(app)
83
+ ```
84
+
85
+ Call `await engine.set_webhook()` during your application startup to register the public webhook URL in Telegram.
86
+ For production, pass `security=Security(...)` to verify Telegram requests.
87
+
88
+ ## Documentation
89
+
90
+ The full documentation is in [`docs`](https://aiogram-webhook.m-xim.ru). It covers installation, FastAPI and aiohttp setup, routing, security, lifecycle behavior, and the public API.
91
+
92
+ ## Contributing
93
+
94
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development setup, branch naming, commit conventions, and PR guidelines.
@@ -0,0 +1,55 @@
1
+ ![PyPI version](docs/_assets/brand/banner.png)
2
+
3
+ # aiogram-webhook
4
+ [![PyPI version](https://img.shields.io/pypi/v/aiogram-webhook?color=blue)](https://pypi.org/project/aiogram-webhook)
5
+ [![codecov](https://codecov.io/github/m-xim/aiogram-webhook/graph/badge.svg?token=H21MX17Y7D)](https://codecov.io/github/m-xim/aiogram-webhook)
6
+ [![Tests Status](https://github.com/m-xim/aiogram-webhook/actions/workflows/tests.yml/badge.svg)](https://github.com/m-xim/aiogram-webhook/actions)
7
+ [![License](https://img.shields.io/github/license/m-xim/aiogram-webhook.svg)](/LICENSE)
8
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/m-xim/aiogram-webhook)
9
+ [![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)
10
+ [![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)
11
+
12
+ `aiogram-webhook` is a modular Python library for webhook integration in aiogram.
13
+ It supports single-bot and token-based multi-bot setups, with route building, optional request checks, and adapters for FastAPI and aiohttp.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install aiogram-webhook
19
+ pip install "aiogram-webhook[fastapi]"
20
+ pip install "aiogram-webhook[aiohttp]"
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```python
26
+ from aiogram import Bot, Dispatcher
27
+ from fastapi import FastAPI
28
+
29
+ from aiogram_webhook import FastAPIAdapter, SingleBotEngine
30
+ from aiogram_webhook.route import Route
31
+
32
+ dispatcher = Dispatcher()
33
+ bot = Bot("BOT_TOKEN")
34
+
35
+ engine = SingleBotEngine(
36
+ dispatcher,
37
+ bot,
38
+ web=FastAPIAdapter(),
39
+ route=Route(base_url="https://example.com", path="/webhook"),
40
+ )
41
+
42
+ app = FastAPI()
43
+ engine.register(app)
44
+ ```
45
+
46
+ Call `await engine.set_webhook()` during your application startup to register the public webhook URL in Telegram.
47
+ For production, pass `security=Security(...)` to verify Telegram requests.
48
+
49
+ ## Documentation
50
+
51
+ The full documentation is in [`docs`](https://aiogram-webhook.m-xim.ru). It covers installation, FastAPI and aiohttp setup, routing, security, lifecycle behavior, and the public API.
52
+
53
+ ## Contributing
54
+
55
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for development setup, branch naming, commit conventions, and PR guidelines.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aiogram-webhook"
3
- version = "2.0.1"
3
+ version = "3.0.0"
4
4
  description = "A python library for integrating webhook support with multiple web frameworks in aiogram. Organizes bot operation via webhooks for both single and multi-bot setups."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -9,15 +9,17 @@ requires-python = ">=3.10"
9
9
  dependencies = [
10
10
  "aiogram>=3.14.0",
11
11
  "yarl>=1.17.0",
12
+ "multidict>=6.7.1",
13
+ "aiohttp>=3.9.0",
12
14
  ]
13
15
 
14
16
  keywords = [
15
17
  "aiogram",
18
+ "multibot",
16
19
  "telegram",
17
20
  "webhook",
18
21
  "fastapi",
19
22
  "aiohttp",
20
- "multibot",
21
23
  ]
22
24
  classifiers = [
23
25
  "Programming Language :: Python :: 3",
@@ -28,13 +30,14 @@ classifiers = [
28
30
  "Programming Language :: Python :: 3.14",
29
31
  "Topic :: Software Development :: Libraries",
30
32
  "Operating System :: OS Independent",
33
+ "Typing :: Typed",
31
34
  ]
32
35
 
33
36
  [project.urls]
34
37
  Homepage = "https://github.com/m-xim/aiogram-webhook"
35
38
  Repository = "https://github.com/m-xim/aiogram-webhook"
36
39
  Issues = "https://github.com/m-xim/aiogram-webhook/issues"
37
- Documentation = "https://github.com/m-xim/aiogram-webhook#readme"
40
+ Documentation = "https://aiogram-webhook.m-xim.ru"
38
41
 
39
42
  [project.optional-dependencies]
40
43
  fastapi = [
@@ -48,17 +51,17 @@ dev = [
48
51
  "ty",
49
52
  "pytest",
50
53
  "pytest-asyncio",
51
- "httpx"
54
+ "pytest-cov",
55
+ "httpx2",
52
56
  ]
53
57
 
54
58
  [build-system]
55
- requires = ["uv_build"]
59
+ requires = ["uv_build<0.12"]
56
60
  build-backend = "uv_build"
57
61
 
58
62
  [tool.uv]
59
63
  package = true
60
64
 
61
-
62
65
  [tool.semantic_release]
63
66
  version_source = "pyproject"
64
67
  commit_message = "chore(release): {version}"
@@ -0,0 +1,16 @@
1
+ from aiogram_webhook.configs.bot import BotConfig
2
+ from aiogram_webhook.configs.webhook import WebhookConfig
3
+ from aiogram_webhook.engines.single import SingleBotEngine
4
+ from aiogram_webhook.engines.token import TokenEngine
5
+ from aiogram_webhook.web.aiohttp import AiohttpAdapter
6
+
7
+ __all__ = ["AiohttpAdapter", "BotConfig", "SingleBotEngine", "TokenEngine", "WebhookConfig"]
8
+
9
+
10
+ try:
11
+ from aiogram_webhook.web.fastapi import FastAPIAdapter # noqa: F401
12
+
13
+ __all__.insert(2, "FastAPIAdapter")
14
+ except ModuleNotFoundError as exc:
15
+ if exc.name != "fastapi":
16
+ raise
@@ -4,7 +4,7 @@ from aiogram.client.default import DefaultBotProperties
4
4
  from aiogram.client.session.base import BaseSession
5
5
 
6
6
 
7
- @dataclass
7
+ @dataclass(slots=True)
8
8
  class BotConfig:
9
9
  session: BaseSession | None = None
10
10
  """HTTP Client session (For example AiohttpSession). If not specified it will be automatically created."""
@@ -1,19 +1,21 @@
1
+ from dataclasses import dataclass
2
+
1
3
  from aiogram.types import InputFile
2
- from pydantic import BaseModel, ConfigDict, Field
3
4
 
5
+ from aiogram_webhook.utils.config import OMITTED, Omittable
4
6
 
5
- class WebhookConfig(BaseModel):
6
- """Webhook configuration for setWebhook API parameters."""
7
7
 
8
- model_config = ConfigDict(arbitrary_types_allowed=True)
8
+ @dataclass(frozen=True, slots=True)
9
+ class WebhookConfig:
10
+ """Webhook configuration for setWebhook API parameters."""
9
11
 
10
- certificate: InputFile | None = None
12
+ certificate: Omittable[InputFile | None] = OMITTED
11
13
  """Upload your public key certificate so that the root certificate in use can be checked. See our `self-signed guide <https://core.telegram.org/bots/self-signed>`_ for details."""
12
- ip_address: str | None = None
14
+ ip_address: Omittable[str | None] = OMITTED
13
15
  """The fixed IP address which will be used to send webhook requests instead of the IP address resolved through DNS"""
14
- max_connections: int | None = Field(default=None, ge=1, le=100)
16
+ max_connections: Omittable[int | None] = OMITTED
15
17
  """The maximum allowed number of simultaneous HTTPS connections to the webhook for update delivery, 1-100. Defaults to *40*. Use lower values to limit the load on your bot's server, and higher values to increase your bot's throughput."""
16
- allowed_updates: list[str] | None = None
18
+ allowed_updates: Omittable[list[str] | None] = OMITTED
17
19
  """A JSON-serialized list of the update types you want your bot to receive. For example, specify :code:`["message", "edited_channel_post", "callback_query"]` to only receive updates of these types. See :class:`aiogram.types.update.Update` for a complete list of available update types. Specify an empty list to receive all update types except *chat_member*, *message_reaction*, and *message_reaction_count* (default). If not specified, the previous setting will be used."""
18
- drop_pending_updates: bool | None = None
20
+ drop_pending_updates: Omittable[bool | None] = OMITTED
19
21
  """Pass :code:`True` to drop all pending updates"""
@@ -0,0 +1,145 @@
1
+ import warnings
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Generic, TypeVar
4
+
5
+ from aiogram import Bot
6
+ from aiogram.methods import TelegramMethod
7
+
8
+ from aiogram_webhook.engines.errors import (
9
+ BotNotFoundError,
10
+ InvalidJsonError,
11
+ RequestHandlingStoppedError,
12
+ TargetNotFoundError,
13
+ )
14
+ from aiogram_webhook.engines.target import Target
15
+ from aiogram_webhook.errors import AiogramWebhookError
16
+ from aiogram_webhook.logs import get_logger, log_webhook_error
17
+ from aiogram_webhook.route import Route
18
+ from aiogram_webhook.route.params import RouteParams
19
+ from aiogram_webhook.security import Security
20
+ from aiogram_webhook.tasks import TaskTracker
21
+ from aiogram_webhook.utils._payload import build_webhook_payload
22
+ from aiogram_webhook.web.base import WebAdapter, WebRequest
23
+
24
+ logger = get_logger("engines")
25
+
26
+ AppT = TypeVar("AppT")
27
+ RawRequestT = TypeVar("RawRequestT")
28
+ FrameworkResponseT = TypeVar("FrameworkResponseT")
29
+
30
+
31
+ class BaseWebhookEngine(ABC, Generic[AppT, RawRequestT, FrameworkResponseT]):
32
+ def __init__(
33
+ self,
34
+ dispatcher,
35
+ /,
36
+ *,
37
+ web: WebAdapter[AppT, RawRequestT, FrameworkResponseT],
38
+ route: Route,
39
+ security: Security | None = None,
40
+ handle_in_background: bool = True,
41
+ shutdown_timeout: float = 10.0,
42
+ ) -> None:
43
+ self.dispatcher = dispatcher
44
+ self.web = web
45
+ self.route = route
46
+ self.security = security
47
+ self.handle_in_background = handle_in_background
48
+
49
+ self.shutdown_timeout = shutdown_timeout
50
+ self._is_shutting_down = False
51
+
52
+ if self.security is None:
53
+ warnings.warn(
54
+ f"Security is not configured for {type(self).__name__}. Pass security=... to verify webhook requests.",
55
+ UserWarning,
56
+ stacklevel=2,
57
+ )
58
+
59
+ def register(self, app: AppT) -> None:
60
+ logger.info("Registering webhook path %s via %s", self.route.path, self.web.__class__.__name__)
61
+ self.web.register(
62
+ app=app,
63
+ path=self.route.path,
64
+ handler=self.handle_request,
65
+ on_startup=self.on_startup,
66
+ on_shutdown=self.on_shutdown,
67
+ )
68
+
69
+ async def handle_request(self, request: WebRequest[RawRequestT]) -> FrameworkResponseT:
70
+ try:
71
+ if self._is_shutting_down:
72
+ raise RequestHandlingStoppedError
73
+
74
+ route_params = await self.route.match(request)
75
+
76
+ target = await self._resolve_target(request=request, route_params=route_params)
77
+ if target is None:
78
+ raise TargetNotFoundError(route_param_names=route_params.keys())
79
+
80
+ if self.security is not None:
81
+ await self.security.verify(target=target, request=request, route_params=route_params)
82
+
83
+ bot = await self._resolve_bot(target=target)
84
+ if bot is None:
85
+ raise BotNotFoundError(target_bot_id=target.bot_id, target_type=target.__class__.__name__)
86
+
87
+ try:
88
+ update = await request.json()
89
+ except ValueError as exc:
90
+ raise InvalidJsonError(original_error=exc) from exc
91
+
92
+ if self.handle_in_background:
93
+ self._get_task_tracker(bot).spawn(self._background_feed(bot, update))
94
+ else:
95
+ result = await self.dispatcher.feed_webhook_update(bot=bot, update=update)
96
+ if isinstance(result, TelegramMethod):
97
+ return self.web.payload_response(status_code=200, payload=build_webhook_payload(bot, result))
98
+
99
+ return self.web.json_response(status_code=200, data={})
100
+
101
+ except AiogramWebhookError as exc:
102
+ log_webhook_error(logger, exc)
103
+
104
+ return self.web.json_response(status_code=exc.status_code, data=exc.response_payload())
105
+
106
+ async def on_startup(self, app: AppT, *args: Any, **kwargs: Any) -> None:
107
+ await self._on_startup(app, *args, **kwargs)
108
+ self._is_shutting_down = False
109
+
110
+ async def on_shutdown(self, app: AppT, *args: Any, **kwargs: Any) -> None:
111
+ self._is_shutting_down = True
112
+ await self._on_shutdown(app, *args, **kwargs)
113
+
114
+ @abstractmethod
115
+ async def _on_startup(self, app: AppT, *args: Any, **kwargs: Any) -> None:
116
+ raise NotImplementedError
117
+
118
+ @abstractmethod
119
+ async def _on_shutdown(self, app: AppT, *args: Any, **kwargs: Any) -> None:
120
+ raise NotImplementedError
121
+
122
+ @abstractmethod
123
+ async def _resolve_target(self, request: WebRequest[RawRequestT], route_params: RouteParams) -> Target | None: ...
124
+
125
+ @abstractmethod
126
+ async def _resolve_bot(self, target: Target) -> Bot | None: ...
127
+
128
+ @abstractmethod
129
+ def _get_task_tracker(self, bot: Bot) -> TaskTracker:
130
+ raise NotImplementedError
131
+
132
+ async def _background_feed(self, bot: Bot, update: dict[str, Any]) -> None:
133
+ result = await self.dispatcher.feed_raw_update(bot=bot, update=update)
134
+
135
+ if isinstance(result, TelegramMethod):
136
+ await self.dispatcher.silent_call_request(bot=bot, result=result)
137
+
138
+ def _build_lifecycle_data(self, *, app: AppT, **kwargs) -> dict[str, Any]:
139
+ return {
140
+ "dispatcher": self.dispatcher,
141
+ **self.dispatcher.workflow_data,
142
+ "app": app,
143
+ "webhook_engine": self,
144
+ **kwargs,
145
+ }
@@ -0,0 +1,77 @@
1
+ import logging
2
+ from collections.abc import Iterable
3
+
4
+ from aiogram_webhook.errors import AiogramWebhookError
5
+
6
+
7
+ def format_names(names: Iterable[str]) -> str:
8
+ return ", ".join(repr(name) for name in sorted(names))
9
+
10
+
11
+ class EngineError(AiogramWebhookError):
12
+ code = "engine_error"
13
+
14
+
15
+ class TargetNotFoundError(EngineError):
16
+ code = "engine_target_not_found"
17
+ status_code = 404
18
+ public_detail = "Not found"
19
+ log_level = logging.INFO
20
+
21
+ def __init__(self, *, route_param_names: Iterable[str]) -> None:
22
+ self.route_param_names = tuple(sorted(route_param_names))
23
+
24
+ super().__init__(f"Webhook target was not found. Route param names: {format_names(self.route_param_names)}.")
25
+
26
+
27
+ class BotNotFoundError(EngineError):
28
+ code = "engine_bot_not_found"
29
+ status_code = 404
30
+ public_detail = "Not found"
31
+ log_level = logging.ERROR
32
+
33
+ def __init__(
34
+ self,
35
+ *,
36
+ target_bot_id: int | None = None,
37
+ target_type: str | None = None,
38
+ ) -> None:
39
+ self.target_bot_id = target_bot_id
40
+ self.target_type = target_type
41
+
42
+ message = "Webhook bot was not found."
43
+
44
+ if target_bot_id is not None:
45
+ message += f" Target bot id: {target_bot_id}."
46
+
47
+ if target_type is not None:
48
+ message += f" Target type: {target_type!r}."
49
+
50
+ super().__init__(message)
51
+
52
+
53
+ class InvalidJsonError(EngineError):
54
+ code = "engine_invalid_json"
55
+ status_code = 400
56
+ public_detail = "Bad request"
57
+ log_level = logging.ERROR
58
+
59
+ def __init__(self, *, original_error: BaseException | None = None) -> None:
60
+ self.original_error_type = type(original_error).__name__ if original_error is not None else None
61
+
62
+ message = "Invalid webhook JSON payload."
63
+
64
+ if self.original_error_type is not None:
65
+ message += f" Original error type: {self.original_error_type}."
66
+
67
+ super().__init__(message)
68
+
69
+
70
+ class RequestHandlingStoppedError(EngineError):
71
+ code = "engine_request_handling_stopped"
72
+ status_code = 503
73
+ public_detail = "Service unavailable"
74
+ log_level = logging.DEBUG
75
+
76
+ def __init__(self) -> None:
77
+ super().__init__("Webhook engine is shutting down and no longer accepts requests.")
@@ -0,0 +1,76 @@
1
+ from abc import ABC
2
+ from collections.abc import Iterable, Mapping
3
+ from types import MappingProxyType
4
+ from typing import Any, Generic
5
+
6
+ from aiogram import Bot
7
+
8
+ from aiogram_webhook import WebhookConfig
9
+ from aiogram_webhook.engines.base import AppT, BaseWebhookEngine, FrameworkResponseT, RawRequestT, logger
10
+ from aiogram_webhook.engines.target import Target
11
+ from aiogram_webhook.route import Route
12
+ from aiogram_webhook.security import Security
13
+ from aiogram_webhook.tasks import TaskTracker
14
+ from aiogram_webhook.utils.config import dataclass_config_to_kwargs
15
+ from aiogram_webhook.web.base import WebAdapter
16
+
17
+
18
+ class BaseMultiBotEngine(
19
+ BaseWebhookEngine[AppT, RawRequestT, FrameworkResponseT], ABC, Generic[AppT, RawRequestT, FrameworkResponseT]
20
+ ):
21
+ def __init__(
22
+ self,
23
+ dispatcher,
24
+ /,
25
+ *,
26
+ web: WebAdapter[AppT, RawRequestT, FrameworkResponseT],
27
+ route: Route,
28
+ security: Security | None = None,
29
+ webhook_config: WebhookConfig | None = None,
30
+ handle_in_background: bool = True,
31
+ shutdown_timeout: float = 10.0,
32
+ ) -> None:
33
+ self._task_trackers: dict[int, TaskTracker] = {}
34
+ self._bots: dict[int, Bot] = {}
35
+ self.webhook_config = webhook_config or WebhookConfig()
36
+ super().__init__(
37
+ dispatcher,
38
+ web=web,
39
+ route=route,
40
+ security=security,
41
+ handle_in_background=handle_in_background,
42
+ shutdown_timeout=shutdown_timeout,
43
+ )
44
+
45
+ async def _build_webhook_kwargs(
46
+ self, target: Target, webhook_config: WebhookConfig | None = None
47
+ ) -> dict[str, Any]:
48
+ kwargs = dataclass_config_to_kwargs(self.webhook_config, webhook_config)
49
+ if self.security is not None:
50
+ secret_token = await self.security.secret_token(target)
51
+ if secret_token is not None:
52
+ kwargs["secret_token"] = secret_token
53
+ return kwargs
54
+
55
+ @property
56
+ def bots(self) -> Mapping[int, Bot]:
57
+ return MappingProxyType(self._bots)
58
+
59
+ async def _on_startup(self, app: AppT, *args: Any, bots: Iterable[Bot] | None = None, **kwargs: Any) -> None: # noqa: ARG002
60
+ all_bots = set(self.bots.values())
61
+
62
+ if bots is not None:
63
+ all_bots |= set(bots)
64
+
65
+ logger.info("Starting multi-bot webhook engine with %s bot(s)", len(all_bots))
66
+ lifecycle_data = self._build_lifecycle_data(app=app, bots=all_bots, **kwargs)
67
+ await self.dispatcher.emit_startup(**lifecycle_data)
68
+
69
+ def _get_task_tracker(self, bot: Bot) -> TaskTracker:
70
+ tracker = self._task_trackers.get(bot.id)
71
+
72
+ if tracker is None:
73
+ tracker = TaskTracker()
74
+ self._task_trackers[bot.id] = tracker
75
+
76
+ return tracker
@@ -0,0 +1,73 @@
1
+ from typing import Generic
2
+
3
+ from aiogram import Bot
4
+
5
+ from aiogram_webhook.configs.webhook import WebhookConfig
6
+ from aiogram_webhook.engines.base import AppT, BaseWebhookEngine, FrameworkResponseT, RawRequestT, logger
7
+ from aiogram_webhook.engines.target import Target
8
+ from aiogram_webhook.route import Route
9
+ from aiogram_webhook.route.params import RouteParams
10
+ from aiogram_webhook.tasks import TaskTracker
11
+ from aiogram_webhook.utils.config import dataclass_config_to_kwargs
12
+ from aiogram_webhook.web.base import WebAdapter, WebRequest
13
+
14
+
15
+ class SingleBotEngine(
16
+ BaseWebhookEngine[AppT, RawRequestT, FrameworkResponseT], Generic[AppT, RawRequestT, FrameworkResponseT]
17
+ ):
18
+ def __init__(
19
+ self,
20
+ dispatcher,
21
+ bot: Bot,
22
+ /,
23
+ *,
24
+ web: WebAdapter[AppT, RawRequestT, FrameworkResponseT],
25
+ route: Route,
26
+ security=None,
27
+ handle_in_background: bool = True,
28
+ shutdown_timeout: float = 10.0,
29
+ ) -> None:
30
+ self.bot = bot
31
+ self._task_tracker = TaskTracker()
32
+
33
+ super().__init__(
34
+ dispatcher,
35
+ web=web,
36
+ route=route,
37
+ security=security,
38
+ handle_in_background=handle_in_background,
39
+ shutdown_timeout=shutdown_timeout,
40
+ )
41
+
42
+ async def _resolve_target(self, request: WebRequest[RawRequestT], route_params: RouteParams) -> Target | None: # noqa: ARG002
43
+ return Target(bot_id=self.bot.id, bot_token=self.bot.token)
44
+
45
+ async def _resolve_bot(self, target: Target) -> Bot: # noqa: ARG002
46
+ return self.bot
47
+
48
+ def _get_task_tracker(self, bot: Bot) -> TaskTracker: # noqa: ARG002
49
+ return self._task_tracker
50
+
51
+ async def set_webhook(self, webhook_config: WebhookConfig | None = None) -> bool:
52
+ target = Target(bot_id=self.bot.id, bot_token=self.bot.token)
53
+ kwargs = dataclass_config_to_kwargs(webhook_config or WebhookConfig())
54
+ if self.security is not None:
55
+ secret_token = await self.security.secret_token(target)
56
+ if secret_token is not None:
57
+ kwargs["secret_token"] = secret_token
58
+ return await self.bot.set_webhook(url=await self.route.build_url(target=target), **kwargs)
59
+
60
+ async def _on_startup(self, app: AppT, *args, **kwargs) -> None: # noqa: ARG002
61
+ logger.info("Starting single-bot webhook engine for bot %s", self.bot.id)
62
+ lifecycle_data = self._build_lifecycle_data(app=app, bot=self.bot, **kwargs)
63
+ await self.dispatcher.emit_startup(**lifecycle_data)
64
+
65
+ async def _on_shutdown(self, app: AppT, *args, **kwargs) -> None: # noqa: ARG002
66
+ logger.info("Stopping single-bot webhook engine for bot %s", self.bot.id)
67
+ await self._task_tracker.close(timeout=self.shutdown_timeout)
68
+
69
+ lifecycle_data = self._build_lifecycle_data(app=app, bot=self.bot, **kwargs)
70
+ await self.dispatcher.emit_shutdown(**lifecycle_data)
71
+
72
+ if self.bot.session is not None:
73
+ await self.bot.session.close()
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True, slots=True)
5
+ class Target:
6
+ bot_id: int
7
+ bot_token: str