From 5b6ec599b1d5f725b7cdeff59299371d165cafeb Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Sun, 12 Apr 2020 20:27:32 +0300 Subject: [PATCH] Add middlewares (API + Docs + Tests) --- aiogram/__init__.py | 2 + aiogram/api/types/message.py | 2 + aiogram/dispatcher/event/observer.py | 62 +++- aiogram/dispatcher/middlewares/__init__.py | 0 aiogram/dispatcher/middlewares/abstract.py | 61 ++++ aiogram/dispatcher/middlewares/base.py | 300 ++++++++++++++++++ aiogram/dispatcher/middlewares/manager.py | 71 +++++ aiogram/dispatcher/middlewares/types.py | 34 ++ aiogram/dispatcher/router.py | 35 +- aiogram/loggers.py | 1 + docs/assets/images/basics_middleware.png | Bin 0 -> 32505 bytes docs/assets/images/middleware_pipeline.png | Bin 0 -> 16408 bytes .../images/middleware_pipeline_nested.png | Bin 0 -> 34635 bytes docs/dispatcher/middlewares/basics.md | 111 +++++++ docs/dispatcher/middlewares/index.md | 65 ++++ docs/index.md | 8 +- docs/todo.md | 5 +- mkdocs.yml | 5 +- tests/test_api/test_types/test_message.py | 13 + .../test_middlewares/__init__.py | 0 .../test_middlewares/test_base.py | 241 ++++++++++++++ .../test_middlewares/test_manager.py | 82 +++++ tests/test_dispatcher/test_router.py | 9 + tests/test_utils/test_markdown.py | 55 ++-- 24 files changed, 1120 insertions(+), 42 deletions(-) create mode 100644 aiogram/dispatcher/middlewares/__init__.py create mode 100644 aiogram/dispatcher/middlewares/abstract.py create mode 100644 aiogram/dispatcher/middlewares/base.py create mode 100644 aiogram/dispatcher/middlewares/manager.py create mode 100644 aiogram/dispatcher/middlewares/types.py create mode 100644 docs/assets/images/basics_middleware.png create mode 100644 docs/assets/images/middleware_pipeline.png create mode 100644 docs/assets/images/middleware_pipeline_nested.png create mode 100644 docs/dispatcher/middlewares/basics.md create mode 100644 docs/dispatcher/middlewares/index.md create mode 100644 tests/test_dispatcher/test_middlewares/__init__.py create mode 100644 tests/test_dispatcher/test_middlewares/test_base.py create mode 100644 tests/test_dispatcher/test_middlewares/test_manager.py diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 8975399c..816a1f57 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -3,6 +3,7 @@ from .api.client import session from .api.client.bot import Bot from .dispatcher import filters, handler from .dispatcher.dispatcher import Dispatcher +from .dispatcher.middlewares.base import BaseMiddleware from .dispatcher.router import Router try: @@ -22,6 +23,7 @@ __all__ = ( "session", "Dispatcher", "Router", + "BaseMiddleware", "filters", "handler", ) diff --git a/aiogram/api/types/message.py b/aiogram/api/types/message.py index 19d2b036..32894498 100644 --- a/aiogram/api/types/message.py +++ b/aiogram/api/types/message.py @@ -240,6 +240,8 @@ class Message(TelegramObject): return ContentType.PASSPORT_DATA if self.poll: return ContentType.POLL + if self.dice: + return ContentType.DICE return ContentType.UNKNOWN diff --git a/aiogram/dispatcher/event/observer.py b/aiogram/dispatcher/event/observer.py index 93f4aac6..756d57f2 100644 --- a/aiogram/dispatcher/event/observer.py +++ b/aiogram/dispatcher/event/observer.py @@ -1,21 +1,12 @@ from __future__ import annotations from itertools import chain -from typing import ( - TYPE_CHECKING, - Any, - AsyncGenerator, - Callable, - Dict, - Generator, - List, - Optional, - Type, -) +from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Dict, Generator, List, Type from pydantic import ValidationError from ..filters.base import BaseFilter +from ..middlewares.types import MiddlewareStep, UpdateType from .handler import CallbackType, FilterObject, FilterType, HandlerObject, HandlerType if TYPE_CHECKING: # pragma: no cover @@ -95,10 +86,8 @@ class TelegramEventObserver(EventObserver): """ registry: List[Type[BaseFilter]] = [] - router: Optional[Router] = self.router - while router: + for router in self.router.chain: observer = router.observers[self.event_name] - router = router.parent_router for filter_ in observer.filters: if filter_ in registry: @@ -133,6 +122,37 @@ class TelegramEventObserver(EventObserver): 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( self, callback: HandlerType, *filters: FilterType, **bound_filters: Any ) -> HandlerType: @@ -153,12 +173,24 @@ class TelegramEventObserver(EventObserver): Propagate event to handlers and stops propagation on first match. 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: result, data = await handler.check(*args, **kwargs) if result: kwargs.update(data) + await self.trigger_middleware( + step=MiddlewareStep.PROCESS, event=event, data=kwargs + ) 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: continue break diff --git a/aiogram/dispatcher/middlewares/__init__.py b/aiogram/dispatcher/middlewares/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aiogram/dispatcher/middlewares/abstract.py b/aiogram/dispatcher/middlewares/abstract.py new file mode 100644 index 00000000..eac16534 --- /dev/null +++ b/aiogram/dispatcher/middlewares/abstract.py @@ -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 diff --git a/aiogram/dispatcher/middlewares/base.py b/aiogram/dispatcher/middlewares/base.py new file mode 100644 index 00000000..2ec921b7 --- /dev/null +++ b/aiogram/dispatcher/middlewares/base.py @@ -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 + # ============================================================================================= + 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 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 . + # ============================================================================================= + 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 + """ diff --git a/aiogram/dispatcher/middlewares/manager.py b/aiogram/dispatcher/middlewares/manager.py new file mode 100644 index 00000000..39a6230d --- /dev/null +++ b/aiogram/dispatcher/middlewares/manager.py @@ -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 diff --git a/aiogram/dispatcher/middlewares/types.py b/aiogram/dispatcher/middlewares/types.py new file mode 100644 index 00000000..3d1da420 --- /dev/null +++ b/aiogram/dispatcher/middlewares/types.py @@ -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" diff --git a/aiogram/dispatcher/router.py b/aiogram/dispatcher/router.py index 699c82c3..888117be 100644 --- a/aiogram/dispatcher/router.py +++ b/aiogram/dispatcher/router.py @@ -1,13 +1,15 @@ from __future__ import annotations 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 ..utils.imports import import_module from ..utils.warnings import CodeHasNoEffect from .event.observer import EventObserver, SkipHandler, TelegramEventObserver from .filters import BUILTIN_FILTERS +from .middlewares.abstract import AbstractMiddleware +from .middlewares.manager import MiddlewareManager class Router: @@ -46,6 +48,7 @@ class Router: ) self.poll_handler = TelegramEventObserver(router=self, event_name="poll") self.poll_answer_handler = TelegramEventObserver(router=self, event_name="poll_answer") + self.middleware = MiddlewareManager(router=self) self.startup = EventObserver() self.shutdown = EventObserver() @@ -74,6 +77,36 @@ class Router: for builtin_filter in BUILTIN_FILTERS.get(name, ()): 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 def parent_router(self) -> Optional[Router]: return self._parent_router diff --git a/aiogram/loggers.py b/aiogram/loggers.py index 0352c0df..5b5a8eba 100644 --- a/aiogram/loggers.py +++ b/aiogram/loggers.py @@ -1,3 +1,4 @@ import logging dispatcher = logging.getLogger("aiogram.dispatcher") +middlewares = logging.getLogger("aiogram.middlewares") diff --git a/docs/assets/images/basics_middleware.png b/docs/assets/images/basics_middleware.png new file mode 100644 index 0000000000000000000000000000000000000000..a797fd384a1d630c1d2a631dc88a8856dc374313 GIT binary patch literal 32505 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 ZqWl}EBMTb=eA51tw77y;mB_b%{{x-X#IOJW literal 0 HcmV?d00001 diff --git a/docs/assets/images/middleware_pipeline.png b/docs/assets/images/middleware_pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb20d6f5caed16baa685b6dc07213f7e927a34b GIT binary patch literal 16408 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 T6xdxq2mU>fRFo(Z)ARZ-fB8-# literal 0 HcmV?d00001 diff --git a/docs/dispatcher/middlewares/basics.md b/docs/dispatcher/middlewares/basics.md new file mode 100644 index 00000000..973ffe98 --- /dev/null +++ b/docs/dispatcher/middlewares/basics.md @@ -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 diff --git a/docs/dispatcher/middlewares/index.md b/docs/dispatcher/middlewares/index.md new file mode 100644 index 00000000..114646d6 --- /dev/null +++ b/docs/dispatcher/middlewares/index.md @@ -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. + +[![middlewares](../../assets/images/basics_middleware.png)](../../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** middleware of specific type +1. Pass event over [filters](../filters/index.md) to detect specific handler +1. Call **process** 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** middleware +1. Call **post-process** update middleware in all routers tree +1. Emit response into webhook (when it needed) + +### Pipeline in pictures: + +#### Simple pipeline + +[![middlewares](../../assets/images/middleware_pipeline.png)](../../assets/images/middleware_pipeline.png) + +_(Click on image to zoom it)_ + +#### Nested routers pipeline + +[![middlewares](../../assets/images/middleware_pipeline_nested.png)](../../assets/images/middleware_pipeline_nested.png) + +_(Click on image to zoom it)_ + +## Read more + +- [Middleware Basics](basics.md) diff --git a/docs/index.md b/docs/index.md index 6961a181..57f6fa9b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,19 +15,19 @@ Documentation for version 3.0 [WIP] [^1] ## 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) - [Updates router](dispatcher/index.md) (Blueprints) - 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) !!! note Before start using **aiogram** is highly recommend to know how to work with [asyncio](https://docs.python.org/3/library/asyncio.html). - + Also if you has questions you can go to our community chats in Telegram: - + - [English language](https://t.me/aiogram) - [Russian language](https://t.me/aiogram_ru) diff --git a/docs/todo.md b/docs/todo.md index 02c99d9a..c06407f3 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -23,8 +23,8 @@ - [x] ContentTypes - [x] Text - [ ] ... - - [ ] Middlewares - - [ ] Engine + - [x] Middlewares + - [x] Engine - [ ] Builtin middlewares - [ ] ... - [ ] Webhook @@ -41,6 +41,7 @@ - [x] Dispatcher - [x] Router - [x] Observers + - [x] Middleware - [ ] Filters - [ ] Utils - [x] Helper diff --git a/mkdocs.yml b/mkdocs.yml index d64deb09..47631d6a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -17,7 +17,7 @@ theme: logo: 'assets/images/logo.png' extra: - version: 3.0.0a2 + version: 3.0.0a3 plugins: - search @@ -249,6 +249,9 @@ nav: - dispatcher/class_based_handlers/poll.md - dispatcher/class_based_handlers/pre_checkout_query.md - dispatcher/class_based_handlers/shipping_query.md + - Middlewares: + - dispatcher/middlewares/index.md + - dispatcher/middlewares/basics.md - todo.md - Build reports: - reports.md diff --git a/tests/test_api/test_types/test_message.py b/tests/test_api/test_types/test_message.py index 4b645e11..1254bd31 100644 --- a/tests/test_api/test_types/test_message.py +++ b/tests/test_api/test_types/test_message.py @@ -7,6 +7,7 @@ from aiogram.api.methods import ( SendAnimation, SendAudio, SendContact, + SendDice, SendDocument, SendGame, SendInvoice, @@ -26,6 +27,7 @@ from aiogram.api.types import ( Audio, Chat, Contact, + Dice, Document, EncryptedCredentials, Game, @@ -391,6 +393,16 @@ class TestMessage: ), 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_id=42, @@ -431,6 +443,7 @@ class TestMessage: ["", dict(text="test"), SendMessage], ["photo", dict(photo="photo"), SendPhoto], ["poll", dict(question="Q?", options=[]), SendPoll], + ["dice", dict(), SendDice], ["sticker", dict(sticker="sticker"), SendSticker], ["sticker", dict(sticker="sticker"), SendSticker], [ diff --git a/tests/test_dispatcher/test_middlewares/__init__.py b/tests/test_dispatcher/test_middlewares/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_dispatcher/test_middlewares/test_base.py b/tests/test_dispatcher/test_middlewares/test_base.py new file mode 100644 index 00000000..203028ec --- /dev/null +++ b/tests/test_dispatcher/test_middlewares/test_base.py @@ -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 diff --git a/tests/test_dispatcher/test_middlewares/test_manager.py b/tests/test_dispatcher/test_middlewares/test_manager.py new file mode 100644 index 00000000..0e23f1b2 --- /dev/null +++ b/tests/test_dispatcher/test_middlewares/test_manager.py @@ -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 diff --git a/tests/test_dispatcher/test_router.py b/tests/test_dispatcher/test_router.py index ca66c1ad..eacb8d0c 100644 --- a/tests/test_dispatcher/test_router.py +++ b/tests/test_dispatcher/test_router.py @@ -18,6 +18,7 @@ from aiogram.api.types import ( User, ) from aiogram.dispatcher.event.observer import SkipHandler +from aiogram.dispatcher.middlewares.base import BaseMiddleware from aiogram.dispatcher.router import Router from aiogram.utils.warnings import CodeHasNoEffect @@ -407,3 +408,11 @@ class TestRouter: await router1.emit_shutdown() 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 diff --git a/tests/test_utils/test_markdown.py b/tests/test_utils/test_markdown.py index b9da8f46..792c1bb4 100644 --- a/tests/test_utils/test_markdown.py +++ b/tests/test_utils/test_markdown.py @@ -2,37 +2,54 @@ from typing import Any, Callable, Optional, Tuple 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: @pytest.mark.parametrize( "func,args,sep,result", [ - [markdown.text, ("test", "test"), " ", "test test"], - [markdown.text, ("test", "test"), "\n", "test\ntest"], - [markdown.text, ("test", "test"), None, "test test"], - [markdown.bold, ("test", "test"), " ", "*test test*"], - [markdown.hbold, ("test", "test"), " ", "test test"], - [markdown.italic, ("test", "test"), " ", "_test test_\r"], - [markdown.hitalic, ("test", "test"), " ", "test test"], - [markdown.code, ("test", "test"), " ", "`test test`"], - [markdown.hcode, ("test", "test"), " ", "test test"], - [markdown.pre, ("test", "test"), " ", "```test test```"], - [markdown.hpre, ("test", "test"), " ", "
test test
"], - [markdown.underline, ("test", "test"), " ", "__test test__"], - [markdown.hunderline, ("test", "test"), " ", "test test"], - [markdown.strikethrough, ("test", "test"), " ", "~test test~"], - [markdown.hstrikethrough, ("test", "test"), " ", "test test"], - [markdown.link, ("test", "https://aiogram.dev"), None, "[test](https://aiogram.dev)"], + [text, ("test", "test"), " ", "test test"], + [text, ("test", "test"), "\n", "test\ntest"], + [text, ("test", "test"), None, "test test"], + [bold, ("test", "test"), " ", "*test test*"], + [hbold, ("test", "test"), " ", "test test"], + [italic, ("test", "test"), " ", "_test test_\r"], + [hitalic, ("test", "test"), " ", "test test"], + [code, ("test", "test"), " ", "`test test`"], + [hcode, ("test", "test"), " ", "test test"], + [pre, ("test", "test"), " ", "```test test```"], + [hpre, ("test", "test"), " ", "
test test
"], + [underline, ("test", "test"), " ", "__test test__"], + [hunderline, ("test", "test"), " ", "test test"], + [strikethrough, ("test", "test"), " ", "~test test~"], + [hstrikethrough, ("test", "test"), " ", "test test"], + [link, ("test", "https://aiogram.dev"), None, "[test](https://aiogram.dev)"], [ - markdown.hlink, + hlink, ("test", "https://aiogram.dev"), None, '
test', ], [ - markdown.hide_link, + hide_link, ("https://aiogram.dev",), None, '',