diff --git a/aiogram/__init__.py b/aiogram/__init__.py index 8ab84aa6..3df77008 100644 --- a/aiogram/__init__.py +++ b/aiogram/__init__.py @@ -5,6 +5,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: @@ -24,6 +25,7 @@ __all__ = ( "session", "Dispatcher", "Router", + "BaseMiddleware", "filters", "handler", ) diff --git a/docs/assets/images/basics_middleware.png b/docs/assets/images/basics_middleware.png index a797fd38..b4165e2e 100644 Binary files a/docs/assets/images/basics_middleware.png and b/docs/assets/images/basics_middleware.png differ diff --git a/docs/dispatcher/dispatcher.md b/docs/dispatcher/dispatcher.md index 462d0748..74f018e4 100644 --- a/docs/dispatcher/dispatcher.md +++ b/docs/dispatcher/dispatcher.md @@ -39,7 +39,7 @@ dp.include_router(router1) ## Handling updates All updates can be propagated to the dispatcher by `feed_update` method: -``` +```python3 bot = Bot(...) dp = Dispathcher() diff --git a/docs/dispatcher/middlewares.md b/docs/dispatcher/middlewares.md new file mode 100644 index 00000000..a0649ce5 --- /dev/null +++ b/docs/dispatcher/middlewares.md @@ -0,0 +1,95 @@ +# Middlewares + +**aiogram** provides powerful mechanism for customizing event handlers via middlewares. + +Middlewares in bot framework seems like Middlewares mechanism in 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 two layers of middlewares (before and after filters). + +!!! 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 in many places of pipeline. + +## Basics + +Middleware instance can be applied for every type of Telegram Event (Update, Message, etc.) in two places + +1. Outer scope - before processing filters (`#!python ..outer_middleware(...)`) +2. Inner scope - after processing filters but before handler (`#!python ..middleware(...)`) + +[![middlewares](../assets/images/basics_middleware.png)](../assets/images/basics_middleware.png) + +_(Click on image to zoom it)_ + +!!! warning + + Middleware should be subclass of `BaseMiddleware` (`#!python3 from aiogram import BaseMiddleware`) or any async callable + +## Arguments specification +| Argument | Type | Description | +| - | - | - | +| `handler` | `#!python Callable[[T, Dict[str, Any]], Awaitable[Any]]` | Wrapped handler in middlewares chain | +| `event` | `#!python T` | Incoming event (Subclass of `TelegramObject`) | +| `data` | `#!python Dict[str, Any]` | Contextual data. Will be mapped to handler arguments | + +## Examples + +!!! danger + + Middleware should always call `#!python await handler(event, data)` to propagate event for next middleware/handler + +### Class-based +```python3 +from aiogram import BaseMiddleware +from aiogram.api.types import Message + + +class CounterMiddleware(BaseMiddleware[Message]): + def __init__(self) -> None: + self.counter = 0 + + async def __call__( + self, + handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]], + event: Message, + data: Dict[str, Any] + ) -> Any: + self.counter += 1 + data['counter'] = self.counter + return await handler(event, data) +``` +and then +```python3 +router = Router() +router.message.middleware(CounterMiddleware()) +``` + +### Function-based +```python3 +@dispatcher.update.outer_middleware() +async def database_transaction_middleware( + handler: Callable[[Update, Dict[str, Any]], Awaitable[Any]], + event: Update, + data: Dict[str, Any] +) -> Any: + async with database.transaction(): + return await handler(event, data) +``` + +## Facts + +1. Middlewares from outer scope will be called on every incoming event +1. Middlewares from inner scope will be called only when filters pass +1. Inner middlewares is always calls for `Update` event type in due to all incoming updates going to specific event type handler through built in update handler + diff --git a/docs/dispatcher/middlewares/basics.md b/docs/dispatcher/middlewares/basics.md deleted file mode 100644 index 51ef8b4f..00000000 --- a/docs/dispatcher/middlewares/basics.md +++ /dev/null @@ -1,117 +0,0 @@ -# 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` - - `#!python3 error` - -## 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 -- `#!python3 on_pre_process_error` - will be triggered on **pre process** `#!python3 error` event -- `#!python3 on_process_error` - will be triggered on **process** `#!python3 error` event -- `#!python3 on_post_process_error` - will be triggered on **post process** `#!python3 error` event diff --git a/docs/dispatcher/middlewares/index.md b/docs/dispatcher/middlewares/index.md deleted file mode 100644 index 7752b5d1..00000000 --- a/docs/dispatcher/middlewares/index.md +++ /dev/null @@ -1,77 +0,0 @@ -# Overview - -**aiogram** provides powerful mechanism for customizing event handlers via middlewares. - -Middlewares in bot framework seems like Middlewares mechanism in 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 two 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) - -!!! warning - When filters does not match any handler with this event the `#!python3 process` - step will not be called. - -!!! warning - When exception will be caused in handlers pipeline will be stopped immediately - and then start processing error via errors handler and it own middleware callbacks. - -!!! warning - Middlewares for updates will be called for all routers in tree but callbacks for events - will be called only for specific branch of routers. - -### 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 9529d0ac..5f6b5bce 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,7 +20,7 @@ Documentation for version 3.0 [WIP] [^1] - [Supports Telegram Bot API v{!_api_version.md!}](api/index.md) - [Updates router](dispatcher/index.md) (Blueprints) - Finite State Machine -- [Middlewares](dispatcher/middlewares/index.md) +- [Middlewares](dispatcher/middlewares.md) - [Replies into Webhook](https://core.telegram.org/bots/faq#how-can-i-make-requests-in-response-to-updates) diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css new file mode 100644 index 00000000..b35d291c --- /dev/null +++ b/docs/stylesheets/extra.css @@ -0,0 +1,12 @@ +@font-face { + font-family: 'JetBrainsMono'; + src: url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2') format('woff2'), + url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff') format('woff'), + url('https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/ttf/JetBrainsMono-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} + +code, kbd, pre { + font-family: "JetBrainsMono", "Roboto Mono", "Courier New", Courier, monospace; +} diff --git a/mkdocs.yml b/mkdocs.yml index 527d0561..2c50c6db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,6 +16,9 @@ theme: favicon: 'assets/images/logo.png' logo: 'assets/images/logo.png' +extra_css: + - stylesheets/extra.css + extra: version: 3.0.0a3 @@ -255,9 +258,8 @@ nav: - dispatcher/class_based_handlers/pre_checkout_query.md - dispatcher/class_based_handlers/shipping_query.md - dispatcher/class_based_handlers/error.md - - Middlewares: - - dispatcher/middlewares/index.md - - dispatcher/middlewares/basics.md + - dispatcher/middlewares.md + - todo.md - Build reports: - reports.md diff --git a/poetry.lock b/poetry.lock index 3ccb6507..cdd78f22 100644 --- a/poetry.lock +++ b/poetry.lock @@ -434,14 +434,6 @@ html5 = ["html5lib"] htmlsoup = ["beautifulsoup4"] source = ["Cython (>=0.29.7)"] -[[package]] -category = "main" -description = "This package provides magic filter based on dynamic attribute getter" -name = "magic-filter" -optional = false -python-versions = ">=3.6.1,<4.0.0" -version = "0.1.2" - [[package]] category = "dev" description = "Python implementation of Markdown." @@ -963,7 +955,7 @@ python-versions = "*" version = "1.4.1" [[package]] -category = "main" +category = "dev" description = "Backported and Experimental Type Hints for Python 3.5+" name = "typing-extensions" optional = false @@ -1039,7 +1031,7 @@ fast = ["uvloop"] proxy = ["aiohttp-socks"] [metadata] -content-hash = "5c53527f09e65af097aa3d3a25e41646e8b8a0dda25e96445ceef969c19297e5" +content-hash = "768759359beca8b84811bfc21adac9649925cd22b87427a10608c9d1e16a0923" python-versions = "^3.7" [metadata.files] @@ -1256,10 +1248,6 @@ lxml = [ {file = "lxml-4.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3"}, {file = "lxml-4.5.0.tar.gz", hash = "sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60"}, ] -magic-filter = [ - {file = "magic-filter-0.1.2.tar.gz", hash = "sha256:dfd1a778493083ac1355791d1716c3beb6629598739f2c2ec078815952282c1d"}, - {file = "magic_filter-0.1.2-py3-none-any.whl", hash = "sha256:16d0c96584f0660fd7fa94b6cd16f92383616208a32568bf8f95a57fc1a69e9d"}, -] markdown = [ {file = "Markdown-3.2.1-py2.py3-none-any.whl", hash = "sha256:e4795399163109457d4c5af2183fbe6b60326c17cfdf25ce6e7474c6624f725d"}, {file = "Markdown-3.2.1.tar.gz", hash = "sha256:90fee683eeabe1a92e149f7ba74e5ccdc81cd397bd6c516d93a8da0ef90b6902"}, diff --git a/pyproject.toml b/pyproject.toml index 88878b56..807bab18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,8 +40,6 @@ aiofiles = "^0.4.0" uvloop = {version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'", optional = true} async_lru = "^1.0" aiohttp-socks = {version = "^0.3.8", optional = true} -typing-extensions = "^3.7.4" -magic-filter = "^0.1.2" [tool.poetry.dev-dependencies] uvloop = {version = "^0.14.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'"} @@ -69,6 +67,7 @@ markdown-include = "^0.5.1" aiohttp-socks = "^0.3.4" pre-commit = "^2.3.0" packaging = "^20.3" +typing-extensions = "^3.7.4" [tool.poetry.extras] fast = ["uvloop"]