More docs

This commit is contained in:
Alex Root Junior 2023-07-29 03:42:04 +03:00
parent 48c4884877
commit 7fa3ebf8ba
No known key found for this signature in database
GPG key ID: 074C1D455EBEA4AC
17 changed files with 266 additions and 380 deletions

View file

@ -1,3 +1,5 @@
.. _enums:
#####
Enums
#####

View file

@ -53,6 +53,12 @@ DEFAULT_TIMEOUT: Final[float] = 60.0
class BaseSession(abc.ABC):
"""
This is base class for all HTTP sessions in aiogram.
If you want to create your own session, you must inherit from this class.
"""
def __init__(
self,
api: TelegramAPIServer = PRODUCTION,

View file

@ -53,7 +53,7 @@ class Router:
self.chat_member = TelegramEventObserver(router=self, event_name="chat_member")
self.chat_join_request = TelegramEventObserver(router=self, event_name="chat_join_request")
self.errors = TelegramEventObserver(router=self, event_name="error")
self.errors = self.error = TelegramEventObserver(router=self, event_name="error")
self.startup = EventObserver()
self.shutdown = EventObserver()
@ -184,6 +184,12 @@ class Router:
router.sub_routers.append(self)
def include_routers(self, *routers: Router) -> None:
"""
Attach multiple routers.
:param routers:
:return:
"""
if not routers:
raise ValueError("At least one router must be provided")
for router in routers:

View file

@ -6,10 +6,16 @@ from aiogram.utils.link import docs_url
class AiogramError(Exception):
pass
"""
Base exception for all aiogram errors.
"""
class DetailedAiogramError(AiogramError):
"""
Base exception for all aiogram errors with detailed message.
"""
url: Optional[str] = None
def __init__(self, message: str) -> None:
@ -26,14 +32,24 @@ class DetailedAiogramError(AiogramError):
class CallbackAnswerException(AiogramError):
pass
"""
Exception for callback answer.
"""
class UnsupportedKeywordArgument(DetailedAiogramError):
"""
Exception raised when a keyword argument is passed as filter.
"""
url = docs_url("migration_2_to_3.html", fragment_="filtering-events")
class TelegramAPIError(DetailedAiogramError):
"""
Base exception for all Telegram API errors.
"""
label: str = "Telegram server says"
def __init__(
@ -50,10 +66,18 @@ class TelegramAPIError(DetailedAiogramError):
class TelegramNetworkError(TelegramAPIError):
"""
Base exception for all Telegram network errors.
"""
label = "HTTP Client says"
class TelegramRetryAfter(TelegramAPIError):
"""
Exception raised when flood control exceeds.
"""
url = "https://core.telegram.org/bots/faq#my-bot-is-hitting-limits-how-do-i-avoid-this"
def __init__(
@ -73,6 +97,10 @@ class TelegramRetryAfter(TelegramAPIError):
class TelegramMigrateToChat(TelegramAPIError):
"""
Exception raised when chat has been migrated to a supergroup.
"""
url = "https://core.telegram.org/bots/api#responseparameters"
def __init__(
@ -90,38 +118,66 @@ class TelegramMigrateToChat(TelegramAPIError):
class TelegramBadRequest(TelegramAPIError):
pass
"""
Exception raised when request is malformed.
"""
class TelegramNotFound(TelegramAPIError):
pass
"""
Exception raised when chat, message, user, etc. not found.
"""
class TelegramConflictError(TelegramAPIError):
pass
"""
Exception raised when bot token is already used by another application in polling mode.
"""
class TelegramUnauthorizedError(TelegramAPIError):
pass
"""
Exception raised when bot token is invalid.
"""
class TelegramForbiddenError(TelegramAPIError):
pass
"""
Exception raised when bot is kicked from chat or etc.
"""
class TelegramServerError(TelegramAPIError):
pass
"""
Exception raised when Telegram server returns 5xx error.
"""
class RestartingTelegram(TelegramServerError):
pass
"""
Exception raised when Telegram server is restarting.
It seems like this error is not used by Telegram anymore,
but it's still here for backward compatibility.
Currently, you should expect that Telegram can raise RetryAfter (with timeout 5 seconds)
error instead of this one.
"""
class TelegramEntityTooLarge(TelegramNetworkError):
"""
Exception raised when you are trying to send a file that is too large.
"""
url = "https://core.telegram.org/bots/api#sending-files"
class ClientDecodeError(AiogramError):
"""
Exception raised when client can't decode response. (Malformed response, etc.)
"""
def __init__(self, message: str, original: Exception, data: Any) -> None:
self.message = message
self.original = original

View file

@ -1,3 +1,5 @@
.. _enums:
#####
Enums
#####

View file

@ -1,14 +1,14 @@
Use Custom API server
=====================
.. autoclass:: aiogram.client.telegram.TelegramAPIServer
:members:
For example, if you want to use self-hosted API server:
.. code-block:: python3
.. code-block:: python
session = AiohttpSession(
api=TelegramAPIServer.from_base('http://localhost:8082')
)
bot = Bot(..., session=session)
.. autoclass:: aiogram.client.telegram.TelegramAPIServer
:members:

View file

@ -8,3 +8,4 @@ Client sessions is used for interacting with API server.
custom_server
base
aiohttp
middleware

View file

@ -0,0 +1,75 @@
##########################
Client session middlewares
##########################
In some cases you may want to add some middlewares to the client session to customize the behavior of the client.
Some useful cases that is:
- Log the outgoing requests
- Customize the request parameters
- Handle rate limiting errors and retry the request
- others ...
So, you can do it using client session middlewares.
A client session middleware is a function (or callable class) that receives the request and the next middleware to call.
The middleware can modify the request and then call the next middleware to continue the request processing.
How to register client session middleware?
==========================================
Register using register method
------------------------------
.. code-block:: python
bot.session.middleware(RequestLogging(ignore_methods=[GetUpdates]))
Register using decorator
------------------------
.. code-block:: python
@bot.session.middleware()
async def my_middleware(
make_request: NextRequestMiddlewareType[TelegramType],
bot: "Bot",
method: TelegramMethod[TelegramType],
) -> Response[TelegramType]:
# do something with request
return await make_request(bot, method)
Example
=======
Class based session middleware
------------------------------
.. literalinclude:: ../../../aiogram/client/session/middlewares/request_logging.py
:lines: 16-
:language: python
:linenos:
.. note::
this middlewware is already implemented inside aiogram, so, if you want to use it you can
just import it :code:`from aiogram.client.session.middlewares.request_logging import RequestLogging`
Function based session middleware
---------------------------------
.. code-block:: python
async def __call__(
self,
make_request: NextRequestMiddlewareType[TelegramType],
bot: "Bot",
method: TelegramMethod[TelegramType],
) -> Response[TelegramType]:
try:
# do something with request
return await make_request(bot, method)
finally:
# do something after request

View file

@ -1,10 +0,0 @@
##########
ErrorEvent
##########
.. automodule:: aiogram.types.error_event
:members:
:member-order: bysource
:undoc-members: True
:exclude-members: model_config,model_fields

View file

@ -28,7 +28,6 @@ extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.ifconfig",
"sphinx.ext.intersphinx",
"sphinx-prompt",
"sphinx_substitution_extensions",
"sphinx_copybutton",
"sphinxcontrib.towncrier.ext",
@ -67,6 +66,6 @@ texinfo_documents = [
# add_module_names = False
towncrier_draft_autoversion_mode = 'draft'
towncrier_draft_autoversion_mode = "draft"
towncrier_draft_include_empty = False
towncrier_draft_working_directory = Path(__file__).parent.parent

View file

@ -0,0 +1,49 @@
######
Errors
######
Handling errors
===============
Is recommended way that you should use errors inside handlers using try-except block,
but in common cases you can use global errors handler at router or dispatcher level.
If you specify errors handler for router - it will be used for all handlers inside this router.
If you specify errors handler for dispatcher - it will be used for all handlers inside all routers.
.. code-block:: python
@router.error(ExceptionTypeFilter(MyCustomException), F.update.message.as_("message"))
async def handle_my_custom_exception(event: ErrorEvent, message: Message):
# do something with error
await message.answer("Oops, something went wrong!")
@router.error()
async def error_handler(event: ErrorEvent):
logger.critical("Critical error caused by %s", event.exception, exc_info=True)
# do something with error
...
.. _error-event:
ErrorEvent
==========
.. automodule:: aiogram.types.error_event
:members:
:member-order: bysource
:undoc-members: True
:exclude-members: model_config,model_fields
.. _error-types:
Error types
===========
.. automodule:: aiogram.exceptions
:members:
:member-order: bysource

View file

@ -17,7 +17,6 @@ Dispatcher is subclass of router and should be always is root router.
.. toctree::
observer
router
dispatcher
class_based_handlers/index
@ -25,3 +24,4 @@ Dispatcher is subclass of router and should be always is root router.
middlewares
finite_state_machine/index
flags
errors

View file

@ -1,26 +0,0 @@
########
Observer
########
Observer is used for filtering and handling different events. That is part of internal API with some public methods and is recommended to don't use methods is not listed here.
In `aiogram` framework is available two variants of observer:
- `EventObserver <#eventobserver>`__
- `TelegramEventObserver <#telegrameventobserver>`__
EventObserver
=============
.. autoclass:: aiogram.dispatcher.event.event.EventObserver
:members: register, trigger, __call__
:member-order: bysource
TelegramEventObserver
=====================
.. autoclass:: aiogram.dispatcher.event.telegram.TelegramEventObserver
:members: register, trigger, __call__, bind_filter, middleware, outer_middleware
:member-order: bysource

View file

@ -3,7 +3,7 @@ Router
######
.. autoclass:: aiogram.dispatcher.router.Router
:members: __init__, include_router
:members: __init__, include_router, include_routers, resolve_used_update_types
:show-inheritance:
@ -21,23 +21,6 @@ Here is the list of available observers and examples of how to register handlers
In these examples only decorator-style registering handlers are used, but if you don't like @decorators just use :obj:`<event type>.register(...)` method instead.
Update
------
.. code-block:: python
@dispatcher.update()
async def message_handler(update: types.Update) -> Any: pass
.. note::
By default Router already has an update handler which route all event types to another observers.
.. warning::
The only root Router (Dispatcher) can handle this type of event.
Message
-------
@ -149,10 +132,12 @@ Errors
@router.errors()
async def error_handler(exception: ErrorEvent) -> Any: pass
Is useful for handling errors from other handlers
Is useful for handling errors from other handlers, error event described :ref:`here <error-event>`
.. _Nested routers:
Nested routers
==============
@ -166,8 +151,8 @@ Nested routers
Example:
.. code-block:: python
:caption: module_2.py
:name: module_2
:caption: module_1.py
:name: module_1
router2 = Router()
@ -177,7 +162,7 @@ Example:
.. code-block:: python
:caption: module_2.py
:name: module_1
:name: module_2
from module_2 import router2
@ -186,6 +171,22 @@ Example:
router1.include_router(router2)
Update
------
.. code-block:: python
@dispatcher.update()
async def message_handler(update: types.Update) -> Any: pass
.. warning::
The only root Router (Dispatcher) can handle this type of event.
.. note::
Dispatcher already has default handler for this event type, so you can use it for handling all updates that are not handled by any other handlers.
How it works?
-------------

View file

@ -2,9 +2,6 @@
Installation
############
Stable (2.x)
============
From PyPI
---------
@ -35,10 +32,3 @@ From GitHub
.. code-block:: bash
pip install https://github.com/aiogram/aiogram/archive/refs/heads/dev-3.x.zip
From AUR
--------
.. code-block:: bash
yay -S python-aiogram3

View file

@ -7,12 +7,20 @@ Migration FAQ (2.x -> 3.0)
This guide is still in progress.
This version introduces much many breaking changes and architectural improvements,
helping to reduce global variables count in your code, provides mechanisms useful mechanisms
helping to reduce global variables count in your code, provides useful mechanisms
to separate your code to modules or just make sharable modules via packages on the PyPi,
makes middlewares and filters more controllable and others.
On this page you can read about points that changed corresponding to last stable 2.x version.
.. note::
This page is most like a detailed changelog than a migration guide,
but it will be updated in the future.
Feel free to contribute to this page, if you find something that is not mentioned here.
Dispatcher
==========
@ -22,7 +30,7 @@ Dispatcher
- :class:`Dispatcher` now can be extended with another Dispatcher-like
thing named :class:`Router` (:ref:`Read more » <Nested routers>`).
With routes you can easily separate your code to multiple modules
and maybe share this modules between projects.
and may be share this modules between projects.
- Removed the **_handler** suffix from all event handler decorators and registering methods.
(:ref:`Read more » <Event observers>`)
- Executor entirely removed, now you can use Dispatcher directly to start polling or webhook.
@ -31,24 +39,34 @@ Filtering events
================
- Keyword filters can no more be used, use filters explicitly. (`Read more » <https://github.com/aiogram/aiogram/issues/942>`_)
- In due to keyword filters was removed all enabled by default filters (state and content_type now is not enabled)
- In due to keyword filters was removed all enabled by default filters (state and content_type now is not enabled),
so you should specify them explicitly if you want to use.
For example instead of using :code:`@dp.message_handler(content_types=ContentType.PHOTO)`
you should use :code:`@router.message(F.photo)`
- Most of common filters is replaced by "magic filter". (:ref:`Read more » <magic-filters>`)
- Now by default message handler receives any content type, if you want specific one just add the filters (Magic or any other)
- State filter is not enabled by default, that's mean if you using :code:`state="*"` in v2
- Now by default message handler receives any content type,
if you want specific one just add the filters (Magic or any other)
- State filter now is not enabled by default, that's mean if you using :code:`state="*"` in v2
then you should not pass any state filter in v3, and vice versa,
if the state in v2 is not specified now you should specify the state.
- Added possibility to register per-router global filters, that helps to reduces
the number of repetitions in the code and makes easily way to control
for what each router will be used.
Bot API Methods
===============
Bot API
=======
- Now all API methods is classes with validation (via `pydantic <https://docs.pydantic.dev/>`_)
(all API calls is also available as methods in the Bot class)
- Increased pre-defined Enums count and moved into `aiogram.enums` sub-package.
(all API calls is also available as methods in the Bot class).
- Added more pre-defined Enums and moved into `aiogram.enums` sub-package. For example chat type enum now is
:class:`aiogram.enums.ChatType` instead of :class:`aiogram.types.chat.ChatType`.
(:ref:`Read more » <enums>`)
- Separated HTTP client session into container that can be reused between different
Bot instances in the application.
- API Exceptions is no more classified by specific message in due to Telegram has no documented error codes.
But all errors is classified by HTTP status code and for each method only one case can be caused with the same code,
so in most cases you should check that only error type (by status-code) without checking error message.
(:ref:`Read more » <error-types>`)
Middlewares
===========
@ -58,7 +76,7 @@ Middlewares
For example now you can easily pass some data into context inside middleware and
get it in the filters layer as the same way as in the handlers via keyword arguments.
- Added mechanism named **flags**, that helps to customize handler behavior
in conjunction with middlewares.
in conjunction with middlewares. (:ref:`Read more » <flags>`)
Keyboard Markup
===============
@ -80,6 +98,8 @@ Finite State machine
====================
- State filter will no more added to all handlers, you will need to specify state if you want
- Added possibility to change FSM strategy, for example if you want to control state
for each user in chat topics instead of user in chat you can specify it in the Dispatcher.
Sending Files
=============
@ -92,287 +112,3 @@ Webhook
- Simplified aiohttp web app configuration
- By default added possibility to upload files when you use reply into webhook
..
This part of document will be removed
.. code-block:: markdown
## Keyboards and filters (part 1)
- `(Reply|Inline)KeyboardMarkup` is no longer used for building keyboards via `add`/`insert`/`row`, use `(Reply|Inline)KeyboardBuilder` and `button` instead.
- `CallbackData` is now a base class, not a factory.
- Integrate `[magic-filter](https://pypi.org/project/magic-filter/)` into aiogram.
- Code for 2.x
```python
from secrets import token_urlsafe
from aiogram import Bot, Dispatcher
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from aiogram.utils.callback_data import CallbackData
dp = Dispatcher(Bot(TOKEN))
vote_cb = CallbackData("vote", "action", "id", sep="_")
votes = {} # For demo purposes only! Use database in real code!
@dp.message_handler(commands="start")
async def post(message: Message) -> None:
vote_id = token_urlsafe(8) # Lazy way to generate a random string
kb = (
InlineKeyboardMarkup(row_width=2)
.insert(InlineKeyboardButton(text="+1", callback_data=vote_cb.new(action="up", id=vote_id)))
.insert(InlineKeyboardButton(text="-1", callback_data=vote_cb.new(action="down", id=vote_id)))
.insert(InlineKeyboardButton(text="?", callback_data=vote_cb.new(action="count", id=vote_id)))
)
await message.reply("Vote on this post", reply_markup=kb)
@dp.callback_query_handler(vote_cb.filter(action="count"))
async def show_voters_count(query: CallbackQuery, callback_data: dict) -> None:
vote_id = int(callback_data["id"])
votes[vote_id] = votes.setdefault(vote_id, 0) + 1
await query.answer(votes[vote_id], cache_time=1)
@dp.callback_query_handler(vote_cb.filter()) # all other actions
async def vote(query: CallbackQuery, callback_data: dict) -> None:
if (action := callback_data["action"]) == "up":
d = 1
elif action == "down":
d = -1
else:
raise AssertionError(f"action action!r} is not implemented")
votes[int(callback_data["id"])] += d
await query.answer(f"{action.capitalize()}voted!")
```
- Code for 3.0
```python
from enum import Enum
from secrets import token_urlsafe
from aiogram import Dispatcher, F
from aiogram.types import CallbackQuery, Message
from aiogram.dispatcher.filters.callback_data import CallbackData
from aiogram.utils.keyboard import InlineKeyboardBuilder
dp = Dispatcher()
votes = {} # For demo purposes only! Use database in real code!
class VoteAction(Enum):
UP = "up"
DOWN = "down"
COUNT = "count"
class VoteCallback(CallbackData, prefix="vote", sep="_"):
action: VoteAction # Yes, it also supports `Enum`s
id: str
@dp.message(commands="start")
async def post(message: Message) -> None:
vote_id = token_urlsafe(8) # Lazy way to generate a random string
kb = (
InlineKeyboardBuilder()
.button(text="+1", callback_data=VoteCallback(action=VoteAction.UP, id=vote_id))
.button(text="-1", callback_data=VoteCallback(action=VoteAction.DOWN, id=vote_id))
.button(text="?", callback_data=VoteCallback(action=VoteAction.COUNT, id=vote_id))
.adjust(2) # row_width=2
)
await message.reply("Vote on this post", reply_markup=kb.as_markup())
# `F` is a `MagicFilter` instance, see docs for `magic-filter` for more info
@dp.callback_query(VoteCallback.filter(F.action == VoteAction.COUNT))
async def show_voters_count(
query: CallbackQuery,
callback_data: VoteCallback, # Now it is the class itself, not a mysterious `dict`
) -> None:
vote_id = callback_data.id
votes[vote_id] = votes.setdefault(vote_id, 0) + 1
await query.answer(votes[vote_id], cache_time=1)
@dp.callback_query(VoteCallback.filter()) # all other actions
async def vote(query: CallbackQuery, callback_data: VoteCallback) -> None:
if callback_data.action == VoteAction.UP:
d = 1
elif callback_data.action == VoteAction.DOWN:
d = -1
else:
raise AssertionError(f"action {callback_data.action!r} is not implemented")
votes[callback_data.id] += d
await query.answer(f"{action.capitalize()}voted!")
```
## Code style
- Allow the code to be split into several files in a convenient way with `Router`s.
- Make `Dispatcher` a router with some special abilities.
- Remove `<event>_handler` in favor of `<event>` (e.g. `dp.message()` instead of `dp.message_handler()`)
- Code for 2.x (one of possible ways)
```python
from aiogram import Bot, Dispatcher, executor
from mybot import handlers
dp = Dispatcher(Bot(TOKEN))
handlers.hello.setup(dp)
...
executor.start_polling(dp, ...)
```
```python
from aiogram import Dispatcher
from aiogram.types import Message
# No way to use decorators :(
async def hello(message: Message) -> None:
await message.reply("Hello!")
async def goodbye(message: Message) -> None:
await message.reply("Bye!")
def setup(dp: Dispatcher) -> None:
dp.register_message_handler(hello, commands=["hello", "hi"])
dp.register_message_handler(goodbye, commands=["goodbye", "bye"])
# This list can become huge in a time, may be inconvenient
```
- Code for 3.0
```python
from aiogram import Bot, Dispatcher
from mybot import handlers
dp = Dispatcher()
# Any router can include a sub-router
dp.include_router(handlers.hello.router) # `Dispatcher` is a `Router` too
...
dp.run_polling(Bot(TOKEN)) # But it's special, e.g. it can `run_polling`
```
```python
from aiogram import Router
from aiogram.types import Message
router = Router()
# Event handler decorator is an event type itself without `_handler` suffix
@router.message(commands=["hello", "hi"]) # Yay, decorators!
async def hello(message: Message) -> None:
await message.reply("Hello!")
async def goodbye(message: Message) -> None:
await message.reply("Bye!")
# If you still prefer registering handlers without decorators, use this
router.message.register(goodbye, commands=["goodbye", "bye"])
```
## Webhooks and API methods
- All methods are classes now.
- Allow using Reply into webhook with polling. *Check whether it worked in 2.x*
- Webhook setup is more flexible ~~and complicated xd~~
## Exceptions
- No more specific exceptions, only by status code.
- Code for 2.x [todo]
```python
from asyncio import sleep
from aiogram import Bot, Dispatcher
from aiogram.dispatcher.filters import Command
from aiogram.types import Message
from aiogram.utils.exceptions import (
BadRequest,
BotBlocked,
RestartingTelegram,
RetryAfter,
)
dp = Dispatcher(Bot(TOKEN))
chats = set()
async def broadcaster(bot: Bot, chat: int, text: str) -> bool:
"""Broadcasts a message and returns whether it was sent"""
while True:
try:
await bot.send_message(chat, text)
except BotBlocked:
chats.discard(chat)
log.warning("Remove chat %d because bot was blocked", chat)
return False
except RetryAfter as e:
log.info("Sleeping %d due to flood wait", e.retry_after)
await sleep(e.retry_after)
continue
except RestartingTelegram:
log.info("Telegram is restarting, sleeping for 1 sec")
await sleep(1)
continue
except BadRequest as e:
log.warning("Remove chat %d because of bad request", chat)
chats.discard(chat)
return False
else:
return True
@dp.message_handler(commands="broadcast")
async def broadcast(message: Message, command: Command.CommandObj) -> None:
# TODO ...
```
- Code for 3.x [todo]
```python
...
```
## Filters (part 2)
- Remove the majority of filters in favor of `MagicFilter` (aka `F`).
- Deprecate usage of bound filters in favor of classes, functions and `F`.
- Message handler defaults to any content type.
- Per-router filters.
## Middlewares and app state
- Rework middleware logic.
- Pass `**kwargs` from `start_polling` to handlers and filters.
- No more global `bot` and `message.bot`.
- `bot["foo"]``dp["foo"]`.
## FSM
- FSMStrategy.
- Default to any state.
- States are also callable filters.
- No more `next` and `proxy`.
- No state filtering is done by default:
[Default state is not None · Issue #954 · aiogram/aiogram](https://github.com/aiogram/aiogram/issues/954#issuecomment-1172967490)
## Misc
- No more unrelated attributes and methods in types.
- `get_args()`
- `get_(full_)command()`
- …?
- Add handler flags.
- ???

View file

@ -82,7 +82,6 @@ docs = [
"sphinx-autobuild~=2021.3.14",
"sphinx-copybutton~=0.5.2",
"furo~=2023.7.26",
"sphinx-prompt~=1.7.0",
"Sphinx-Substitution-Extensions~=2022.2.16",
"towncrier~=23.6.0",
"pygments~=2.15.1",