mirror of
https://github.com/aiogram/aiogram.git
synced 2025-12-11 09:55:21 +00:00
Add middlewares (API + Docs + Tests)
This commit is contained in:
parent
e4cd4c1763
commit
5b6ec599b1
24 changed files with 1120 additions and 42 deletions
|
|
@ -3,6 +3,7 @@ from .api.client import session
|
||||||
from .api.client.bot import Bot
|
from .api.client.bot import Bot
|
||||||
from .dispatcher import filters, handler
|
from .dispatcher import filters, handler
|
||||||
from .dispatcher.dispatcher import Dispatcher
|
from .dispatcher.dispatcher import Dispatcher
|
||||||
|
from .dispatcher.middlewares.base import BaseMiddleware
|
||||||
from .dispatcher.router import Router
|
from .dispatcher.router import Router
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -22,6 +23,7 @@ __all__ = (
|
||||||
"session",
|
"session",
|
||||||
"Dispatcher",
|
"Dispatcher",
|
||||||
"Router",
|
"Router",
|
||||||
|
"BaseMiddleware",
|
||||||
"filters",
|
"filters",
|
||||||
"handler",
|
"handler",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,8 @@ class Message(TelegramObject):
|
||||||
return ContentType.PASSPORT_DATA
|
return ContentType.PASSPORT_DATA
|
||||||
if self.poll:
|
if self.poll:
|
||||||
return ContentType.POLL
|
return ContentType.POLL
|
||||||
|
if self.dice:
|
||||||
|
return ContentType.DICE
|
||||||
|
|
||||||
return ContentType.UNKNOWN
|
return ContentType.UNKNOWN
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import (
|
from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Dict, Generator, List, Type
|
||||||
TYPE_CHECKING,
|
|
||||||
Any,
|
|
||||||
AsyncGenerator,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Generator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Type,
|
|
||||||
)
|
|
||||||
|
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from ..filters.base import BaseFilter
|
from ..filters.base import BaseFilter
|
||||||
|
from ..middlewares.types import MiddlewareStep, UpdateType
|
||||||
from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType
|
from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
|
@ -95,10 +86,8 @@ class TelegramEventObserver(EventObserver):
|
||||||
"""
|
"""
|
||||||
registry: List[Type[BaseFilter]] = []
|
registry: List[Type[BaseFilter]] = []
|
||||||
|
|
||||||
router: Optional[Router] = self.router
|
for router in self.router.chain:
|
||||||
while router:
|
|
||||||
observer = router.observers[self.event_name]
|
observer = router.observers[self.event_name]
|
||||||
router = router.parent_router
|
|
||||||
|
|
||||||
for filter_ in observer.filters:
|
for filter_ in observer.filters:
|
||||||
if filter_ in registry:
|
if filter_ in registry:
|
||||||
|
|
@ -133,6 +122,37 @@ class TelegramEventObserver(EventObserver):
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
async def trigger_middleware(
|
||||||
|
self, step: MiddlewareStep, event: UpdateType, data: Dict[str, Any], result: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Trigger middlewares chain
|
||||||
|
|
||||||
|
:param step:
|
||||||
|
:param event:
|
||||||
|
:param data:
|
||||||
|
:param result:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
reverse = step == MiddlewareStep.POST_PROCESS
|
||||||
|
recursive = self.event_name == "update" or step == MiddlewareStep.PROCESS
|
||||||
|
|
||||||
|
if self.event_name == "update":
|
||||||
|
routers = self.router.chain
|
||||||
|
else:
|
||||||
|
routers = self.router.chain_head
|
||||||
|
for router in routers:
|
||||||
|
await router.middleware.trigger(
|
||||||
|
step=step,
|
||||||
|
event_name=self.event_name,
|
||||||
|
event=event,
|
||||||
|
data=data,
|
||||||
|
result=result,
|
||||||
|
reverse=reverse,
|
||||||
|
)
|
||||||
|
if not recursive:
|
||||||
|
break
|
||||||
|
|
||||||
def register(
|
def register(
|
||||||
self, callback: HandlerType, *filters: FilterType, **bound_filters: Any
|
self, callback: HandlerType, *filters: FilterType, **bound_filters: Any
|
||||||
) -> HandlerType:
|
) -> HandlerType:
|
||||||
|
|
@ -153,12 +173,24 @@ class TelegramEventObserver(EventObserver):
|
||||||
Propagate event to handlers and stops propagation on first match.
|
Propagate event to handlers and stops propagation on first match.
|
||||||
Handler will be called when all its filters is pass.
|
Handler will be called when all its filters is pass.
|
||||||
"""
|
"""
|
||||||
|
event = args[0]
|
||||||
|
await self.trigger_middleware(step=MiddlewareStep.PRE_PROCESS, event=event, data=kwargs)
|
||||||
for handler in self.handlers:
|
for handler in self.handlers:
|
||||||
result, data = await handler.check(*args, **kwargs)
|
result, data = await handler.check(*args, **kwargs)
|
||||||
if result:
|
if result:
|
||||||
kwargs.update(data)
|
kwargs.update(data)
|
||||||
|
await self.trigger_middleware(
|
||||||
|
step=MiddlewareStep.PROCESS, event=event, data=kwargs
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
yield await handler.call(*args, **kwargs)
|
response = await handler.call(*args, **kwargs)
|
||||||
|
await self.trigger_middleware(
|
||||||
|
step=MiddlewareStep.POST_PROCESS,
|
||||||
|
event=event,
|
||||||
|
data=kwargs,
|
||||||
|
result=response,
|
||||||
|
)
|
||||||
|
yield response
|
||||||
except SkipHandler:
|
except SkipHandler:
|
||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
|
|
|
||||||
0
aiogram/dispatcher/middlewares/__init__.py
Normal file
0
aiogram/dispatcher/middlewares/__init__.py
Normal file
61
aiogram/dispatcher/middlewares/abstract.py
Normal file
61
aiogram/dispatcher/middlewares/abstract.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||||
|
|
||||||
|
from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from aiogram.dispatcher.middlewares.manager import MiddlewareManager
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractMiddleware(ABC):
|
||||||
|
"""
|
||||||
|
Abstract class for middleware.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._manager: Optional[MiddlewareManager] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manager(self) -> MiddlewareManager:
|
||||||
|
"""
|
||||||
|
Instance of MiddlewareManager
|
||||||
|
"""
|
||||||
|
if self._manager is None:
|
||||||
|
raise RuntimeError("Middleware is not configured!")
|
||||||
|
return self._manager
|
||||||
|
|
||||||
|
def setup(self, manager: MiddlewareManager, _stack_level: int = 1) -> AbstractMiddleware:
|
||||||
|
"""
|
||||||
|
Mark middleware as configured
|
||||||
|
|
||||||
|
:param manager:
|
||||||
|
:param _stack_level:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self.configured:
|
||||||
|
return manager.setup(self, _stack_level=_stack_level + 1)
|
||||||
|
|
||||||
|
self._manager = manager
|
||||||
|
return self
|
||||||
|
|
||||||
|
@property
|
||||||
|
def configured(self) -> bool:
|
||||||
|
"""
|
||||||
|
Check middleware is configured
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return bool(self._manager)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def trigger(
|
||||||
|
self,
|
||||||
|
step: MiddlewareStep,
|
||||||
|
event_name: str,
|
||||||
|
event: UpdateType,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
result: Any = None,
|
||||||
|
) -> Any: # pragma: no cover
|
||||||
|
pass
|
||||||
300
aiogram/dispatcher/middlewares/base.py
Normal file
300
aiogram/dispatcher/middlewares/base.py
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict
|
||||||
|
|
||||||
|
from aiogram.dispatcher.middlewares.abstract import AbstractMiddleware
|
||||||
|
from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from aiogram.api.types import (
|
||||||
|
CallbackQuery,
|
||||||
|
ChosenInlineResult,
|
||||||
|
InlineQuery,
|
||||||
|
Message,
|
||||||
|
Poll,
|
||||||
|
PollAnswer,
|
||||||
|
PreCheckoutQuery,
|
||||||
|
ShippingQuery,
|
||||||
|
Update,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMiddleware(AbstractMiddleware):
|
||||||
|
"""
|
||||||
|
Base class for middleware.
|
||||||
|
|
||||||
|
All methods on the middle always must be coroutines and name starts with "on_" like "on_process_message".
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def trigger(
|
||||||
|
self,
|
||||||
|
step: MiddlewareStep,
|
||||||
|
event_name: str,
|
||||||
|
event: UpdateType,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
result: Any = None,
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Trigger action.
|
||||||
|
|
||||||
|
:param step:
|
||||||
|
:param event_name:
|
||||||
|
:param event:
|
||||||
|
:param data:
|
||||||
|
:param result:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
handler_name = f"on_{step.value}_{event_name}"
|
||||||
|
handler = getattr(self, handler_name, None)
|
||||||
|
if not handler:
|
||||||
|
return None
|
||||||
|
args = (event, result, data) if step == MiddlewareStep.POST_PROCESS else (event, data)
|
||||||
|
return await handler(*args)
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
# =============================================================================================
|
||||||
|
# Event that triggers before process <event>
|
||||||
|
# =============================================================================================
|
||||||
|
async def on_pre_process_update(self, update: Update, data: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process update
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_message(self, message: Message, data: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process message
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_edited_message(
|
||||||
|
self, edited_message: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process edited_message
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_channel_post(
|
||||||
|
self, channel_post: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process channel_post
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_edited_channel_post(
|
||||||
|
self, edited_channel_post: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process edited_channel_post
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_inline_query(
|
||||||
|
self, inline_query: InlineQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process inline_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_chosen_inline_result(
|
||||||
|
self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process chosen_inline_result
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_callback_query(
|
||||||
|
self, callback_query: CallbackQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process callback_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_shipping_query(
|
||||||
|
self, shipping_query: ShippingQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process shipping_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_pre_checkout_query(
|
||||||
|
self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process pre_checkout_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process poll
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_pre_process_poll_answer(
|
||||||
|
self, poll_answer: PollAnswer, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers before process poll_answer
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =============================================================================================
|
||||||
|
# Event that triggers on process <event> after filters.
|
||||||
|
# =============================================================================================
|
||||||
|
async def on_process_update(self, update: Update, data: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process update
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_message(self, message: Message, data: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process message
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_edited_message(
|
||||||
|
self, edited_message: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process edited_message
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_channel_post(
|
||||||
|
self, channel_post: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process channel_post
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_edited_channel_post(
|
||||||
|
self, edited_channel_post: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process edited_channel_post
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_inline_query(
|
||||||
|
self, inline_query: InlineQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process inline_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_chosen_inline_result(
|
||||||
|
self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process chosen_inline_result
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_callback_query(
|
||||||
|
self, callback_query: CallbackQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process callback_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_shipping_query(
|
||||||
|
self, shipping_query: ShippingQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process shipping_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_pre_checkout_query(
|
||||||
|
self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process pre_checkout_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process poll
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_process_poll_answer(
|
||||||
|
self, poll_answer: PollAnswer, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers on process poll_answer
|
||||||
|
"""
|
||||||
|
|
||||||
|
# =============================================================================================
|
||||||
|
# Event that triggers after process <event>.
|
||||||
|
# =============================================================================================
|
||||||
|
async def on_post_process_update(
|
||||||
|
self, update: Update, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing update
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_message(
|
||||||
|
self, message: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing message
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_edited_message(
|
||||||
|
self, edited_message: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing edited_message
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_channel_post(
|
||||||
|
self, channel_post: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing channel_post
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_edited_channel_post(
|
||||||
|
self, edited_channel_post: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing edited_channel_post
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_inline_query(
|
||||||
|
self, inline_query: InlineQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing inline_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_chosen_inline_result(
|
||||||
|
self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing chosen_inline_result
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_callback_query(
|
||||||
|
self, callback_query: CallbackQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing callback_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_shipping_query(
|
||||||
|
self, shipping_query: ShippingQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing shipping_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_pre_checkout_query(
|
||||||
|
self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing pre_checkout_query
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_poll(self, poll: Poll, data: Dict[str, Any], result: Any) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing poll
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def on_post_process_poll_answer(
|
||||||
|
self, poll_answer: PollAnswer, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Event that triggers after processing poll_answer
|
||||||
|
"""
|
||||||
71
aiogram/dispatcher/middlewares/manager.py
Normal file
71
aiogram/dispatcher/middlewares/manager.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from .abstract import AbstractMiddleware
|
||||||
|
from .types import MiddlewareStep, UpdateType
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from aiogram.dispatcher.router import Router
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareManager:
|
||||||
|
"""
|
||||||
|
Middleware manager.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, router: Router) -> None:
|
||||||
|
self.router = router
|
||||||
|
self.middlewares: List[AbstractMiddleware] = []
|
||||||
|
|
||||||
|
def setup(self, middleware: AbstractMiddleware, _stack_level: int = 1) -> AbstractMiddleware:
|
||||||
|
"""
|
||||||
|
Setup middleware
|
||||||
|
|
||||||
|
:param middleware:
|
||||||
|
:param _stack_level:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not isinstance(middleware, AbstractMiddleware):
|
||||||
|
raise TypeError(
|
||||||
|
f"`middleware` should be instance of BaseMiddleware, not {type(middleware)}"
|
||||||
|
)
|
||||||
|
if middleware.configured:
|
||||||
|
if middleware.manager is self:
|
||||||
|
warn(
|
||||||
|
f"Middleware {middleware} is already configured for this Router "
|
||||||
|
"That's mean re-installing of this middleware has no effect.",
|
||||||
|
category=RuntimeWarning,
|
||||||
|
stacklevel=_stack_level + 1,
|
||||||
|
)
|
||||||
|
return middleware
|
||||||
|
raise ValueError(
|
||||||
|
f"Middleware is already configured for another manager {middleware.manager} "
|
||||||
|
f"in router {middleware.manager.router}!"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.middlewares.append(middleware)
|
||||||
|
middleware.setup(self)
|
||||||
|
return middleware
|
||||||
|
|
||||||
|
async def trigger(
|
||||||
|
self,
|
||||||
|
step: MiddlewareStep,
|
||||||
|
event_name: str,
|
||||||
|
event: UpdateType,
|
||||||
|
data: Dict[str, Any],
|
||||||
|
result: Any = None,
|
||||||
|
reverse: bool = False,
|
||||||
|
) -> Any:
|
||||||
|
"""
|
||||||
|
Call action to middlewares with args lilt.
|
||||||
|
"""
|
||||||
|
middlewares = reversed(self.middlewares) if reverse else self.middlewares
|
||||||
|
for middleware in middlewares:
|
||||||
|
await middleware.trigger(
|
||||||
|
step=step, event_name=event_name, event=event, data=data, result=result
|
||||||
|
)
|
||||||
|
|
||||||
|
def __contains__(self, item: AbstractMiddleware) -> bool:
|
||||||
|
return item in self.middlewares
|
||||||
34
aiogram/dispatcher/middlewares/types.py
Normal file
34
aiogram/dispatcher/middlewares/types.py
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from aiogram.api.types import (
|
||||||
|
CallbackQuery,
|
||||||
|
ChosenInlineResult,
|
||||||
|
InlineQuery,
|
||||||
|
Message,
|
||||||
|
Poll,
|
||||||
|
PollAnswer,
|
||||||
|
PreCheckoutQuery,
|
||||||
|
ShippingQuery,
|
||||||
|
Update,
|
||||||
|
)
|
||||||
|
|
||||||
|
UpdateType = Union[
|
||||||
|
CallbackQuery,
|
||||||
|
ChosenInlineResult,
|
||||||
|
InlineQuery,
|
||||||
|
Message,
|
||||||
|
Poll,
|
||||||
|
PollAnswer,
|
||||||
|
PreCheckoutQuery,
|
||||||
|
ShippingQuery,
|
||||||
|
Update,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MiddlewareStep(Enum):
|
||||||
|
PRE_PROCESS = "pre_process"
|
||||||
|
PROCESS = "process"
|
||||||
|
POST_PROCESS = "post_process"
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, Generator, List, Optional, Union
|
||||||
|
|
||||||
from ..api.types import Chat, TelegramObject, Update, User
|
from ..api.types import Chat, TelegramObject, Update, User
|
||||||
from ..utils.imports import import_module
|
from ..utils.imports import import_module
|
||||||
from ..utils.warnings import CodeHasNoEffect
|
from ..utils.warnings import CodeHasNoEffect
|
||||||
from .event.observer import EventObserver, SkipHandler, TelegramEventObserver
|
from .event.observer import EventObserver, SkipHandler, TelegramEventObserver
|
||||||
from .filters import BUILTIN_FILTERS
|
from .filters import BUILTIN_FILTERS
|
||||||
|
from .middlewares.abstract import AbstractMiddleware
|
||||||
|
from .middlewares.manager import MiddlewareManager
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
class Router:
|
||||||
|
|
@ -46,6 +48,7 @@ class Router:
|
||||||
)
|
)
|
||||||
self.poll_handler = TelegramEventObserver(router=self, event_name="poll")
|
self.poll_handler = TelegramEventObserver(router=self, event_name="poll")
|
||||||
self.poll_answer_handler = TelegramEventObserver(router=self, event_name="poll_answer")
|
self.poll_answer_handler = TelegramEventObserver(router=self, event_name="poll_answer")
|
||||||
|
self.middleware = MiddlewareManager(router=self)
|
||||||
|
|
||||||
self.startup = EventObserver()
|
self.startup = EventObserver()
|
||||||
self.shutdown = EventObserver()
|
self.shutdown = EventObserver()
|
||||||
|
|
@ -74,6 +77,36 @@ class Router:
|
||||||
for builtin_filter in BUILTIN_FILTERS.get(name, ()):
|
for builtin_filter in BUILTIN_FILTERS.get(name, ()):
|
||||||
observer.bind_filter(builtin_filter)
|
observer.bind_filter(builtin_filter)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chain_head(self) -> Generator[Router, None, None]:
|
||||||
|
router: Optional[Router] = self
|
||||||
|
while router:
|
||||||
|
yield router
|
||||||
|
router = router.parent_router
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chain_tail(self) -> Generator[Router, None, None]:
|
||||||
|
yield self
|
||||||
|
for router in self.sub_routers:
|
||||||
|
yield from router.chain_tail
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chain(self) -> Generator[Router, None, None]:
|
||||||
|
yield from self.chain_head
|
||||||
|
tail = self.chain_tail
|
||||||
|
next(tail) # Skip self
|
||||||
|
yield from tail
|
||||||
|
|
||||||
|
def use(self, middleware: AbstractMiddleware, _stack_level: int = 1) -> AbstractMiddleware:
|
||||||
|
"""
|
||||||
|
Use middleware
|
||||||
|
|
||||||
|
:param middleware:
|
||||||
|
:param _stack_level:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.middleware.setup(middleware, _stack_level=_stack_level + 1)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent_router(self) -> Optional[Router]:
|
def parent_router(self) -> Optional[Router]:
|
||||||
return self._parent_router
|
return self._parent_router
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
dispatcher = logging.getLogger("aiogram.dispatcher")
|
dispatcher = logging.getLogger("aiogram.dispatcher")
|
||||||
|
middlewares = logging.getLogger("aiogram.middlewares")
|
||||||
|
|
|
||||||
BIN
docs/assets/images/basics_middleware.png
Normal file
BIN
docs/assets/images/basics_middleware.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/assets/images/middleware_pipeline.png
Normal file
BIN
docs/assets/images/middleware_pipeline.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/assets/images/middleware_pipeline_nested.png
Normal file
BIN
docs/assets/images/middleware_pipeline_nested.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
111
docs/dispatcher/middlewares/basics.md
Normal file
111
docs/dispatcher/middlewares/basics.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Basics
|
||||||
|
|
||||||
|
All middlewares should be made with `BaseMiddleware` (`#!python3 from aiogram import BaseMiddleware`) as base class.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```python3
|
||||||
|
class MyMiddleware(BaseMiddleware): ...
|
||||||
|
```
|
||||||
|
|
||||||
|
And then use next pattern in naming callback functions in middleware: `on_{step}_{event}`
|
||||||
|
|
||||||
|
Where is:
|
||||||
|
|
||||||
|
- `#!python3 step`:
|
||||||
|
- `#!python3 pre_process`
|
||||||
|
- `#!python3 process`
|
||||||
|
- `#!python3 post_process`
|
||||||
|
- `#!python3 event`:
|
||||||
|
- `#!python3 update`
|
||||||
|
- `#!python3 message`
|
||||||
|
- `#!python3 edited_message`
|
||||||
|
- `#!python3 channel_post`
|
||||||
|
- `#!python3 edited_channel_post`
|
||||||
|
- `#!python3 inline_query`
|
||||||
|
- `#!python3 chosen_inline_result`
|
||||||
|
- `#!python3 callback_query`
|
||||||
|
- `#!python3 shipping_query`
|
||||||
|
- `#!python3 pre_checkout_query`
|
||||||
|
- `#!python3 poll`
|
||||||
|
- `#!python3 poll_answer`
|
||||||
|
|
||||||
|
## Connecting middleware with router
|
||||||
|
|
||||||
|
Middlewares can be connected with router by next ways:
|
||||||
|
|
||||||
|
1. `#!python3 router.use(MyMiddleware())` (**recommended**)
|
||||||
|
1. `#!python3 router.middleware.setup(MyMiddleware())`
|
||||||
|
1. `#!python3 MyMiddleware().setup(router.middleware)` (**not recommended**)
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
One instance of middleware **can't** be registered twice in single or many middleware managers
|
||||||
|
|
||||||
|
## The specification of step callbacks
|
||||||
|
|
||||||
|
### Pre-process step
|
||||||
|
|
||||||
|
| Argument | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| event name | Any of event type (Update, Message and etc.) | Event |
|
||||||
|
| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) |
|
||||||
|
|
||||||
|
Returns `#!python3 Any`
|
||||||
|
|
||||||
|
### Process step
|
||||||
|
|
||||||
|
| Argument | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| event name | Any of event type (Update, Message and etc.) | Event |
|
||||||
|
| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) |
|
||||||
|
|
||||||
|
Returns `#!python3 Any`
|
||||||
|
|
||||||
|
### Post-Process step
|
||||||
|
|
||||||
|
| Argument | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| event name | Any of event type (Update, Message and etc.) | Event |
|
||||||
|
| `#!python3 data` | `#!python3 Dict[str, Any]` | Contextual data (Will be mapped to handler arguments) |
|
||||||
|
| `#!python3 result` | `#!python3 Dict[str, Any]` | Response from handlers |
|
||||||
|
|
||||||
|
Returns `#!python3 Any`
|
||||||
|
|
||||||
|
## Full list of available callbacks
|
||||||
|
|
||||||
|
- `#!python3 on_pre_process_update` - will be triggered on **pre process** `#!python3 update` event
|
||||||
|
- `#!python3 on_process_update` - will be triggered on **process** `#!python3 update` event
|
||||||
|
- `#!python3 on_post_process_update` - will be triggered on **post process** `#!python3 update` event
|
||||||
|
- `#!python3 on_pre_process_message` - will be triggered on **pre process** `#!python3 message` event
|
||||||
|
- `#!python3 on_process_message` - will be triggered on **process** `#!python3 message` event
|
||||||
|
- `#!python3 on_post_process_message` - will be triggered on **post process** `#!python3 message` event
|
||||||
|
- `#!python3 on_pre_process_edited_message` - will be triggered on **pre process** `#!python3 edited_message` event
|
||||||
|
- `#!python3 on_process_edited_message` - will be triggered on **process** `#!python3 edited_message` event
|
||||||
|
- `#!python3 on_post_process_edited_message` - will be triggered on **post process** `#!python3 edited_message` event
|
||||||
|
- `#!python3 on_pre_process_channel_post` - will be triggered on **pre process** `#!python3 channel_post` event
|
||||||
|
- `#!python3 on_process_channel_post` - will be triggered on **process** `#!python3 channel_post` event
|
||||||
|
- `#!python3 on_post_process_channel_post` - will be triggered on **post process** `#!python3 channel_post` event
|
||||||
|
- `#!python3 on_pre_process_edited_channel_post` - will be triggered on **pre process** `#!python3 edited_channel_post` event
|
||||||
|
- `#!python3 on_process_edited_channel_post` - will be triggered on **process** `#!python3 edited_channel_post` event
|
||||||
|
- `#!python3 on_post_process_edited_channel_post` - will be triggered on **post process** `#!python3 edited_channel_post` event
|
||||||
|
- `#!python3 on_pre_process_inline_query` - will be triggered on **pre process** `#!python3 inline_query` event
|
||||||
|
- `#!python3 on_process_inline_query` - will be triggered on **process** `#!python3 inline_query` event
|
||||||
|
- `#!python3 on_post_process_inline_query` - will be triggered on **post process** `#!python3 inline_query` event
|
||||||
|
- `#!python3 on_pre_process_chosen_inline_result` - will be triggered on **pre process** `#!python3 chosen_inline_result` event
|
||||||
|
- `#!python3 on_process_chosen_inline_result` - will be triggered on **process** `#!python3 chosen_inline_result` event
|
||||||
|
- `#!python3 on_post_process_chosen_inline_result` - will be triggered on **post process** `#!python3 chosen_inline_result` event
|
||||||
|
- `#!python3 on_pre_process_callback_query` - will be triggered on **pre process** `#!python3 callback_query` event
|
||||||
|
- `#!python3 on_process_callback_query` - will be triggered on **process** `#!python3 callback_query` event
|
||||||
|
- `#!python3 on_post_process_callback_query` - will be triggered on **post process** `#!python3 callback_query` event
|
||||||
|
- `#!python3 on_pre_process_shipping_query` - will be triggered on **pre process** `#!python3 shipping_query` event
|
||||||
|
- `#!python3 on_process_shipping_query` - will be triggered on **process** `#!python3 shipping_query` event
|
||||||
|
- `#!python3 on_post_process_shipping_query` - will be triggered on **post process** `#!python3 shipping_query` event
|
||||||
|
- `#!python3 on_pre_process_pre_checkout_query` - will be triggered on **pre process** `#!python3 pre_checkout_query` event
|
||||||
|
- `#!python3 on_process_pre_checkout_query` - will be triggered on **process** `#!python3 pre_checkout_query` event
|
||||||
|
- `#!python3 on_post_process_pre_checkout_query` - will be triggered on **post process** `#!python3 pre_checkout_query` event
|
||||||
|
- `#!python3 on_pre_process_poll` - will be triggered on **pre process** `#!python3 poll` event
|
||||||
|
- `#!python3 on_process_poll` - will be triggered on **process** `#!python3 poll` event
|
||||||
|
- `#!python3 on_post_process_poll` - will be triggered on **post process** `#!python3 poll` event
|
||||||
|
- `#!python3 on_pre_process_poll_answer` - will be triggered on **pre process** `#!python3 poll_answer` event
|
||||||
|
- `#!python3 on_process_poll_answer` - will be triggered on **process** `#!python3 poll_answer` event
|
||||||
|
- `#!python3 on_post_process_poll_answer` - will be triggered on **post process** `#!python3 poll_answer` event
|
||||||
65
docs/dispatcher/middlewares/index.md
Normal file
65
docs/dispatcher/middlewares/index.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
**aiogram**'s provides powerful mechanism for customizing event handlers via middlewares.
|
||||||
|
|
||||||
|
Middlewares in bot framework seems like Middlewares mechanism in powerful web-frameworks
|
||||||
|
(like [aiohttp](https://docs.aiohttp.org/en/stable/web_advanced.html#aiohttp-web-middlewares),
|
||||||
|
[fastapi](https://fastapi.tiangolo.com/tutorial/middleware/),
|
||||||
|
[Django](https://docs.djangoproject.com/en/3.0/topics/http/middleware/) or etc.)
|
||||||
|
with small difference - here is implemented many layers of processing
|
||||||
|
(named as [pipeline](#event-pipeline)).
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Middleware is function that triggered on every event received from
|
||||||
|
Telegram Bot API in many points on processing pipeline.
|
||||||
|
|
||||||
|
## Base theory
|
||||||
|
|
||||||
|
As many books and other literature in internet says:
|
||||||
|
> Middleware is reusable software that leverages patterns and frameworks to bridge
|
||||||
|
>the gap between the functional requirements of applications and the underlying operating systems,
|
||||||
|
> network protocol stacks, and databases.
|
||||||
|
|
||||||
|
Middleware can modify, extend or reject processing event before-,
|
||||||
|
on- or after- processing of that event.
|
||||||
|
|
||||||
|
[](../../assets/images/basics_middleware.png)
|
||||||
|
|
||||||
|
_(Click on image to zoom it)_
|
||||||
|
|
||||||
|
## Event pipeline
|
||||||
|
|
||||||
|
As described below middleware an interact with event in many stages of pipeline.
|
||||||
|
|
||||||
|
Simple workflow:
|
||||||
|
|
||||||
|
1. Dispatcher receive an [Update](../../api/types/update.md)
|
||||||
|
1. Call **pre-process** update middleware in all routers tree
|
||||||
|
1. Filter Update over handlers
|
||||||
|
1. Call **process** update middleware in all routers tree
|
||||||
|
1. Router detects event type (Message, Callback query, etc.)
|
||||||
|
1. Router triggers **pre-process** <event> middleware of specific type
|
||||||
|
1. Pass event over [filters](../filters/index.md) to detect specific handler
|
||||||
|
1. Call **process** <event> middleware for specific type (only when handler for this event exists)
|
||||||
|
1. *Do magick*. Call handler (Read more [Event observers](../router.md#event-observers))
|
||||||
|
1. Call **post-process** <event> middleware
|
||||||
|
1. Call **post-process** update middleware in all routers tree
|
||||||
|
1. Emit response into webhook (when it needed)
|
||||||
|
|
||||||
|
### Pipeline in pictures:
|
||||||
|
|
||||||
|
#### Simple pipeline
|
||||||
|
|
||||||
|
[](../../assets/images/middleware_pipeline.png)
|
||||||
|
|
||||||
|
_(Click on image to zoom it)_
|
||||||
|
|
||||||
|
#### Nested routers pipeline
|
||||||
|
|
||||||
|
[](../../assets/images/middleware_pipeline_nested.png)
|
||||||
|
|
||||||
|
_(Click on image to zoom it)_
|
||||||
|
|
||||||
|
## Read more
|
||||||
|
|
||||||
|
- [Middleware Basics](basics.md)
|
||||||
|
|
@ -15,11 +15,11 @@ Documentation for version 3.0 [WIP] [^1]
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Asynchronous
|
- Asynchronous ([asyncio docs](https://docs.python.org/3/library/asyncio.html), [PEP-492](https://www.python.org/dev/peps/pep-0492/))
|
||||||
- [Supports Telegram Bot API v{!_api_version.md!}](api/index.md)
|
- [Supports Telegram Bot API v{!_api_version.md!}](api/index.md)
|
||||||
- [Updates router](dispatcher/index.md) (Blueprints)
|
- [Updates router](dispatcher/index.md) (Blueprints)
|
||||||
- Finite State Machine
|
- Finite State Machine
|
||||||
- Middlewares
|
- [Middlewares](dispatcher/middlewares/index.md)
|
||||||
- [Replies into Webhook](https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates)
|
- [Replies into Webhook](https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@
|
||||||
- [x] ContentTypes
|
- [x] ContentTypes
|
||||||
- [x] Text
|
- [x] Text
|
||||||
- [ ] ...
|
- [ ] ...
|
||||||
- [ ] Middlewares
|
- [x] Middlewares
|
||||||
- [ ] Engine
|
- [x] Engine
|
||||||
- [ ] Builtin middlewares
|
- [ ] Builtin middlewares
|
||||||
- [ ] ...
|
- [ ] ...
|
||||||
- [ ] Webhook
|
- [ ] Webhook
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
- [x] Dispatcher
|
- [x] Dispatcher
|
||||||
- [x] Router
|
- [x] Router
|
||||||
- [x] Observers
|
- [x] Observers
|
||||||
|
- [x] Middleware
|
||||||
- [ ] Filters
|
- [ ] Filters
|
||||||
- [ ] Utils
|
- [ ] Utils
|
||||||
- [x] Helper
|
- [x] Helper
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ theme:
|
||||||
logo: 'assets/images/logo.png'
|
logo: 'assets/images/logo.png'
|
||||||
|
|
||||||
extra:
|
extra:
|
||||||
version: 3.0.0a2
|
version: 3.0.0a3
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- search
|
- search
|
||||||
|
|
@ -249,6 +249,9 @@ nav:
|
||||||
- dispatcher/class_based_handlers/poll.md
|
- dispatcher/class_based_handlers/poll.md
|
||||||
- dispatcher/class_based_handlers/pre_checkout_query.md
|
- dispatcher/class_based_handlers/pre_checkout_query.md
|
||||||
- dispatcher/class_based_handlers/shipping_query.md
|
- dispatcher/class_based_handlers/shipping_query.md
|
||||||
|
- Middlewares:
|
||||||
|
- dispatcher/middlewares/index.md
|
||||||
|
- dispatcher/middlewares/basics.md
|
||||||
- todo.md
|
- todo.md
|
||||||
- Build reports:
|
- Build reports:
|
||||||
- reports.md
|
- reports.md
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from aiogram.api.methods import (
|
||||||
SendAnimation,
|
SendAnimation,
|
||||||
SendAudio,
|
SendAudio,
|
||||||
SendContact,
|
SendContact,
|
||||||
|
SendDice,
|
||||||
SendDocument,
|
SendDocument,
|
||||||
SendGame,
|
SendGame,
|
||||||
SendInvoice,
|
SendInvoice,
|
||||||
|
|
@ -26,6 +27,7 @@ from aiogram.api.types import (
|
||||||
Audio,
|
Audio,
|
||||||
Chat,
|
Chat,
|
||||||
Contact,
|
Contact,
|
||||||
|
Dice,
|
||||||
Document,
|
Document,
|
||||||
EncryptedCredentials,
|
EncryptedCredentials,
|
||||||
Game,
|
Game,
|
||||||
|
|
@ -391,6 +393,16 @@ class TestMessage:
|
||||||
),
|
),
|
||||||
ContentType.POLL,
|
ContentType.POLL,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
Message(
|
||||||
|
message_id=42,
|
||||||
|
date=datetime.datetime.now(),
|
||||||
|
chat=Chat(id=42, type="private"),
|
||||||
|
dice=Dice(value=6),
|
||||||
|
from_user=User(id=42, is_bot=False, first_name="Test"),
|
||||||
|
),
|
||||||
|
ContentType.DICE,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
Message(
|
Message(
|
||||||
message_id=42,
|
message_id=42,
|
||||||
|
|
@ -431,6 +443,7 @@ class TestMessage:
|
||||||
["", dict(text="test"), SendMessage],
|
["", dict(text="test"), SendMessage],
|
||||||
["photo", dict(photo="photo"), SendPhoto],
|
["photo", dict(photo="photo"), SendPhoto],
|
||||||
["poll", dict(question="Q?", options=[]), SendPoll],
|
["poll", dict(question="Q?", options=[]), SendPoll],
|
||||||
|
["dice", dict(), SendDice],
|
||||||
["sticker", dict(sticker="sticker"), SendSticker],
|
["sticker", dict(sticker="sticker"), SendSticker],
|
||||||
["sticker", dict(sticker="sticker"), SendSticker],
|
["sticker", dict(sticker="sticker"), SendSticker],
|
||||||
[
|
[
|
||||||
|
|
|
||||||
0
tests/test_dispatcher/test_middlewares/__init__.py
Normal file
0
tests/test_dispatcher/test_middlewares/__init__.py
Normal file
241
tests/test_dispatcher/test_middlewares/test_base.py
Normal file
241
tests/test_dispatcher/test_middlewares/test_base.py
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
import datetime
|
||||||
|
from typing import Any, Dict, Type
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiogram.api.types import (
|
||||||
|
CallbackQuery,
|
||||||
|
Chat,
|
||||||
|
ChosenInlineResult,
|
||||||
|
InlineQuery,
|
||||||
|
Message,
|
||||||
|
Poll,
|
||||||
|
PollAnswer,
|
||||||
|
PreCheckoutQuery,
|
||||||
|
ShippingQuery,
|
||||||
|
Update,
|
||||||
|
User,
|
||||||
|
)
|
||||||
|
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||||
|
from aiogram.dispatcher.middlewares.types import MiddlewareStep, UpdateType
|
||||||
|
|
||||||
|
try:
|
||||||
|
from asynctest import CoroutineMock, patch
|
||||||
|
except ImportError:
|
||||||
|
from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
class MyMiddleware(BaseMiddleware):
|
||||||
|
async def on_pre_process_update(self, update: Update, data: Dict[str, Any]) -> Any:
|
||||||
|
return "update"
|
||||||
|
|
||||||
|
async def on_pre_process_message(self, message: Message, data: Dict[str, Any]) -> Any:
|
||||||
|
return "message"
|
||||||
|
|
||||||
|
async def on_pre_process_edited_message(
|
||||||
|
self, edited_message: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "edited_message"
|
||||||
|
|
||||||
|
async def on_pre_process_channel_post(
|
||||||
|
self, channel_post: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "channel_post"
|
||||||
|
|
||||||
|
async def on_pre_process_edited_channel_post(
|
||||||
|
self, edited_channel_post: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "edited_channel_post"
|
||||||
|
|
||||||
|
async def on_pre_process_inline_query(
|
||||||
|
self, inline_query: InlineQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "inline_query"
|
||||||
|
|
||||||
|
async def on_pre_process_chosen_inline_result(
|
||||||
|
self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "chosen_inline_result"
|
||||||
|
|
||||||
|
async def on_pre_process_callback_query(
|
||||||
|
self, callback_query: CallbackQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "callback_query"
|
||||||
|
|
||||||
|
async def on_pre_process_shipping_query(
|
||||||
|
self, shipping_query: ShippingQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "shipping_query"
|
||||||
|
|
||||||
|
async def on_pre_process_pre_checkout_query(
|
||||||
|
self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "pre_checkout_query"
|
||||||
|
|
||||||
|
async def on_pre_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any:
|
||||||
|
return "poll"
|
||||||
|
|
||||||
|
async def on_pre_process_poll_answer(
|
||||||
|
self, poll_answer: PollAnswer, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "poll_answer"
|
||||||
|
|
||||||
|
async def on_process_update(self, update: Update, data: Dict[str, Any]) -> Any:
|
||||||
|
return "update"
|
||||||
|
|
||||||
|
async def on_process_message(self, message: Message, data: Dict[str, Any]) -> Any:
|
||||||
|
return "message"
|
||||||
|
|
||||||
|
async def on_process_edited_message(
|
||||||
|
self, edited_message: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "edited_message"
|
||||||
|
|
||||||
|
async def on_process_channel_post(self, channel_post: Message, data: Dict[str, Any]) -> Any:
|
||||||
|
return "channel_post"
|
||||||
|
|
||||||
|
async def on_process_edited_channel_post(
|
||||||
|
self, edited_channel_post: Message, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "edited_channel_post"
|
||||||
|
|
||||||
|
async def on_process_inline_query(
|
||||||
|
self, inline_query: InlineQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "inline_query"
|
||||||
|
|
||||||
|
async def on_process_chosen_inline_result(
|
||||||
|
self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "chosen_inline_result"
|
||||||
|
|
||||||
|
async def on_process_callback_query(
|
||||||
|
self, callback_query: CallbackQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "callback_query"
|
||||||
|
|
||||||
|
async def on_process_shipping_query(
|
||||||
|
self, shipping_query: ShippingQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "shipping_query"
|
||||||
|
|
||||||
|
async def on_process_pre_checkout_query(
|
||||||
|
self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any]
|
||||||
|
) -> Any:
|
||||||
|
return "pre_checkout_query"
|
||||||
|
|
||||||
|
async def on_process_poll(self, poll: Poll, data: Dict[str, Any]) -> Any:
|
||||||
|
return "poll"
|
||||||
|
|
||||||
|
async def on_process_poll_answer(self, poll_answer: PollAnswer, data: Dict[str, Any]) -> Any:
|
||||||
|
return "poll_answer"
|
||||||
|
|
||||||
|
async def on_post_process_update(
|
||||||
|
self, update: Update, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "update"
|
||||||
|
|
||||||
|
async def on_post_process_message(
|
||||||
|
self, message: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "message"
|
||||||
|
|
||||||
|
async def on_post_process_edited_message(
|
||||||
|
self, edited_message: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "edited_message"
|
||||||
|
|
||||||
|
async def on_post_process_channel_post(
|
||||||
|
self, channel_post: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "channel_post"
|
||||||
|
|
||||||
|
async def on_post_process_edited_channel_post(
|
||||||
|
self, edited_channel_post: Message, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "edited_channel_post"
|
||||||
|
|
||||||
|
async def on_post_process_inline_query(
|
||||||
|
self, inline_query: InlineQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "inline_query"
|
||||||
|
|
||||||
|
async def on_post_process_chosen_inline_result(
|
||||||
|
self, chosen_inline_result: ChosenInlineResult, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "chosen_inline_result"
|
||||||
|
|
||||||
|
async def on_post_process_callback_query(
|
||||||
|
self, callback_query: CallbackQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "callback_query"
|
||||||
|
|
||||||
|
async def on_post_process_shipping_query(
|
||||||
|
self, shipping_query: ShippingQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "shipping_query"
|
||||||
|
|
||||||
|
async def on_post_process_pre_checkout_query(
|
||||||
|
self, pre_checkout_query: PreCheckoutQuery, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "pre_checkout_query"
|
||||||
|
|
||||||
|
async def on_post_process_poll(self, poll: Poll, data: Dict[str, Any], result: Any) -> Any:
|
||||||
|
return "poll"
|
||||||
|
|
||||||
|
async def on_post_process_poll_answer(
|
||||||
|
self, poll_answer: PollAnswer, data: Dict[str, Any], result: Any
|
||||||
|
) -> Any:
|
||||||
|
return "poll_answer"
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE = Update(update_id=42)
|
||||||
|
MESSAGE = Message(message_id=42, date=datetime.datetime.now(), chat=Chat(id=42, type="private"))
|
||||||
|
POLL_ANSWER = PollAnswer(
|
||||||
|
poll_id="poll", user=User(id=42, is_bot=False, first_name="Test"), option_ids=[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBaseMiddleware:
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"middleware_cls,should_be_awaited", [[MyMiddleware, True], [BaseMiddleware, False]]
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"step", [MiddlewareStep.PRE_PROCESS, MiddlewareStep.PROCESS, MiddlewareStep.POST_PROCESS]
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"event_name,event",
|
||||||
|
[["update", UPDATE], ["message", MESSAGE], ["poll_answer", POLL_ANSWER],],
|
||||||
|
)
|
||||||
|
async def test_trigger(
|
||||||
|
self,
|
||||||
|
step: MiddlewareStep,
|
||||||
|
event_name: str,
|
||||||
|
event: UpdateType,
|
||||||
|
middleware_cls: Type[BaseMiddleware],
|
||||||
|
should_be_awaited: bool,
|
||||||
|
):
|
||||||
|
middleware = middleware_cls()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
f"tests.test_dispatcher.test_middlewares.test_base."
|
||||||
|
f"MyMiddleware.on_{step.value}_{event_name}",
|
||||||
|
new_callable=CoroutineMock,
|
||||||
|
) as mocked_call:
|
||||||
|
response = await middleware.trigger(
|
||||||
|
step=step, event_name=event_name, event=event, data={}
|
||||||
|
)
|
||||||
|
if should_be_awaited:
|
||||||
|
mocked_call.assert_awaited()
|
||||||
|
assert response is not None
|
||||||
|
else:
|
||||||
|
mocked_call.assert_not_awaited()
|
||||||
|
assert response is None
|
||||||
|
|
||||||
|
def test_not_configured(self):
|
||||||
|
middleware = BaseMiddleware()
|
||||||
|
assert not middleware.configured
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
manager = middleware.manager
|
||||||
82
tests/test_dispatcher/test_middlewares/test_manager.py
Normal file
82
tests/test_dispatcher/test_middlewares/test_manager.py
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiogram import Router
|
||||||
|
from aiogram.api.types import Update
|
||||||
|
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||||
|
from aiogram.dispatcher.middlewares.manager import MiddlewareManager
|
||||||
|
from aiogram.dispatcher.middlewares.types import MiddlewareStep
|
||||||
|
|
||||||
|
try:
|
||||||
|
from asynctest import CoroutineMock, patch
|
||||||
|
except ImportError:
|
||||||
|
from unittest.mock import AsyncMock as CoroutineMock, patch # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture("function")
|
||||||
|
def router():
|
||||||
|
return Router()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture("function")
|
||||||
|
def manager(router: Router):
|
||||||
|
return MiddlewareManager(router)
|
||||||
|
|
||||||
|
|
||||||
|
class TestManager:
|
||||||
|
def test_setup(self, manager: MiddlewareManager):
|
||||||
|
middleware = BaseMiddleware()
|
||||||
|
returned = manager.setup(middleware)
|
||||||
|
assert returned is middleware
|
||||||
|
assert middleware.configured
|
||||||
|
assert middleware.manager is manager
|
||||||
|
assert middleware in manager
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("obj", [object, object(), None, BaseMiddleware])
|
||||||
|
def test_setup_invalid_type(self, manager: MiddlewareManager, obj):
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
assert manager.setup(obj)
|
||||||
|
|
||||||
|
def test_configure_twice_different_managers(self, manager: MiddlewareManager, router: Router):
|
||||||
|
middleware = BaseMiddleware()
|
||||||
|
manager.setup(middleware)
|
||||||
|
|
||||||
|
assert middleware.configured
|
||||||
|
|
||||||
|
new_manager = MiddlewareManager(router)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
new_manager.setup(middleware)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
middleware.setup(new_manager)
|
||||||
|
|
||||||
|
def test_configure_twice(self, manager: MiddlewareManager):
|
||||||
|
middleware = BaseMiddleware()
|
||||||
|
manager.setup(middleware)
|
||||||
|
|
||||||
|
assert middleware.configured
|
||||||
|
|
||||||
|
with pytest.warns(RuntimeWarning, match="is already configured for this Router"):
|
||||||
|
manager.setup(middleware)
|
||||||
|
|
||||||
|
with pytest.warns(RuntimeWarning, match="is already configured for this Router"):
|
||||||
|
middleware.setup(manager)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("count", range(5))
|
||||||
|
async def test_trigger(self, manager: MiddlewareManager, count: int):
|
||||||
|
for _ in range(count):
|
||||||
|
manager.setup(BaseMiddleware())
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"aiogram.dispatcher.middlewares.base.BaseMiddleware.trigger",
|
||||||
|
new_callable=CoroutineMock,
|
||||||
|
) as mocked_call:
|
||||||
|
await manager.trigger(
|
||||||
|
step=MiddlewareStep.PROCESS,
|
||||||
|
event_name="update",
|
||||||
|
event=Update(update_id=42),
|
||||||
|
data={},
|
||||||
|
result=None,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mocked_call.await_count == count
|
||||||
|
|
@ -18,6 +18,7 @@ from aiogram.api.types import (
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from aiogram.dispatcher.event.observer import SkipHandler
|
from aiogram.dispatcher.event.observer import SkipHandler
|
||||||
|
from aiogram.dispatcher.middlewares.base import BaseMiddleware
|
||||||
from aiogram.dispatcher.router import Router
|
from aiogram.dispatcher.router import Router
|
||||||
from aiogram.utils.warnings import CodeHasNoEffect
|
from aiogram.utils.warnings import CodeHasNoEffect
|
||||||
|
|
||||||
|
|
@ -407,3 +408,11 @@ class TestRouter:
|
||||||
|
|
||||||
await router1.emit_shutdown()
|
await router1.emit_shutdown()
|
||||||
assert results == [2, 1, 2]
|
assert results == [2, 1, 2]
|
||||||
|
|
||||||
|
def test_use(self):
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
middleware = router.use(BaseMiddleware())
|
||||||
|
assert isinstance(middleware, BaseMiddleware)
|
||||||
|
assert middleware.configured
|
||||||
|
assert middleware.manager == router.middleware
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,54 @@ from typing import Any, Callable, Optional, Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from aiogram.utils import markdown
|
from aiogram.utils.markdown import (
|
||||||
|
bold,
|
||||||
|
code,
|
||||||
|
hbold,
|
||||||
|
hcode,
|
||||||
|
hide_link,
|
||||||
|
hitalic,
|
||||||
|
hlink,
|
||||||
|
hpre,
|
||||||
|
hstrikethrough,
|
||||||
|
hunderline,
|
||||||
|
italic,
|
||||||
|
link,
|
||||||
|
pre,
|
||||||
|
strikethrough,
|
||||||
|
text,
|
||||||
|
underline,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestMarkdown:
|
class TestMarkdown:
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"func,args,sep,result",
|
"func,args,sep,result",
|
||||||
[
|
[
|
||||||
[markdown.text, ("test", "test"), " ", "test test"],
|
[text, ("test", "test"), " ", "test test"],
|
||||||
[markdown.text, ("test", "test"), "\n", "test\ntest"],
|
[text, ("test", "test"), "\n", "test\ntest"],
|
||||||
[markdown.text, ("test", "test"), None, "test test"],
|
[text, ("test", "test"), None, "test test"],
|
||||||
[markdown.bold, ("test", "test"), " ", "*test test*"],
|
[bold, ("test", "test"), " ", "*test test*"],
|
||||||
[markdown.hbold, ("test", "test"), " ", "<b>test test</b>"],
|
[hbold, ("test", "test"), " ", "<b>test test</b>"],
|
||||||
[markdown.italic, ("test", "test"), " ", "_test test_\r"],
|
[italic, ("test", "test"), " ", "_test test_\r"],
|
||||||
[markdown.hitalic, ("test", "test"), " ", "<i>test test</i>"],
|
[hitalic, ("test", "test"), " ", "<i>test test</i>"],
|
||||||
[markdown.code, ("test", "test"), " ", "`test test`"],
|
[code, ("test", "test"), " ", "`test test`"],
|
||||||
[markdown.hcode, ("test", "test"), " ", "<code>test test</code>"],
|
[hcode, ("test", "test"), " ", "<code>test test</code>"],
|
||||||
[markdown.pre, ("test", "test"), " ", "```test test```"],
|
[pre, ("test", "test"), " ", "```test test```"],
|
||||||
[markdown.hpre, ("test", "test"), " ", "<pre>test test</pre>"],
|
[hpre, ("test", "test"), " ", "<pre>test test</pre>"],
|
||||||
[markdown.underline, ("test", "test"), " ", "__test test__"],
|
[underline, ("test", "test"), " ", "__test test__"],
|
||||||
[markdown.hunderline, ("test", "test"), " ", "<u>test test</u>"],
|
[hunderline, ("test", "test"), " ", "<u>test test</u>"],
|
||||||
[markdown.strikethrough, ("test", "test"), " ", "~test test~"],
|
[strikethrough, ("test", "test"), " ", "~test test~"],
|
||||||
[markdown.hstrikethrough, ("test", "test"), " ", "<s>test test</s>"],
|
[hstrikethrough, ("test", "test"), " ", "<s>test test</s>"],
|
||||||
[markdown.link, ("test", "https://aiogram.dev"), None, "[test](https://aiogram.dev)"],
|
[link, ("test", "https://aiogram.dev"), None, "[test](https://aiogram.dev)"],
|
||||||
[
|
[
|
||||||
markdown.hlink,
|
hlink,
|
||||||
("test", "https://aiogram.dev"),
|
("test", "https://aiogram.dev"),
|
||||||
None,
|
None,
|
||||||
'<a href="https://aiogram.dev">test</a>',
|
'<a href="https://aiogram.dev">test</a>',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
markdown.hide_link,
|
hide_link,
|
||||||
("https://aiogram.dev",),
|
("https://aiogram.dev",),
|
||||||
None,
|
None,
|
||||||
'<a href="https://aiogram.dev">​</a>',
|
'<a href="https://aiogram.dev">​</a>',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue