diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b27b2514..bd638785 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
exclude: ^(.*\.md|.*\.yml|.*\.yaml)
diff --git a/Makefile b/Makefile
index 4bb4be64..5f91257c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,11 @@
SHELL = /bin/bash
+.PHONY: upgrade
+upgrade:
+ uv lock --upgrade
+
+
.PHONY: run-test
run-test:
@echo "Running linters and tests in parallel (uv run)..."
diff --git a/README.md b/README.md
index 2c450b8f..23644bd6 100644
--- a/README.md
+++ b/README.md
@@ -95,14 +95,15 @@ logging.basicConfig(level=logging.INFO)
bot = Bot()
dp = Dispatcher()
+
# Ответ бота при нажатии на кнопку "Начать"
@dp.bot_started()
async def bot_started(event: BotStarted):
await bot.send_message(
- chat_id=event.chat_id,
- text='Привет! Отправь мне /start'
+ chat_id=event.chat_id, text="Привет! Отправь мне /start"
)
+
# Ответ бота на команду /start
@dp.message_created(CommandStart())
async def hello(event: MessageCreated):
@@ -118,7 +119,7 @@ async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -167,12 +168,12 @@ async def hello(event: MessageCreated):
async def main():
await dp.handle_webhook(
bot=bot,
- host='0.0.0.0',
+ host="0.0.0.0",
port=8080,
)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -201,11 +202,15 @@ import uvicorn
from fastapi import FastAPI
from maxapi.webhook.fastapi import FastAPIMaxWebhook
+
async def main():
webhook = FastAPIMaxWebhook(dp=dp, bot=bot)
app = FastAPI(lifespan=webhook.lifespan)
- webhook.setup(app, path='/webhook')
- await uvicorn.Server(uvicorn.Config(app, host='0.0.0.0', port=8080)).serve()
+ webhook.setup(app, path="/webhook")
+ await uvicorn.Server(
+ uvicorn.Config(app, host="0.0.0.0", port=8080)
+ ).serve()
+
asyncio.run(main())
```
@@ -217,10 +222,14 @@ import asyncio
import uvicorn
from maxapi.webhook.litestar import LitestarMaxWebhook
+
async def main():
webhook = LitestarMaxWebhook(dp=dp, bot=bot)
- app = webhook.create_app(path='/webhook')
- await uvicorn.Server(uvicorn.Config(app, host='0.0.0.0', port=8080)).serve()
+ app = webhook.create_app(path="/webhook")
+ await uvicorn.Server(
+ uvicorn.Config(app, host="0.0.0.0", port=8080)
+ ).serve()
+
asyncio.run(main())
```
diff --git a/doc/router_inner_middleware.md b/doc/router_inner_middleware.md
index 9a53cbe9..a0e48fe7 100644
--- a/doc/router_inner_middleware.md
+++ b/doc/router_inner_middleware.md
@@ -13,16 +13,22 @@ closes #132
```python
admin_router = Router(router_id="admin")
-admin_router.middleware(BroadcastLockMiddleware()) # хочется: lock только когда broadcast реально начнётся
+admin_router.middleware(
+ BroadcastLockMiddleware()
+) # хочется: lock только когда broadcast реально начнётся
+
@admin_router.message_created(IsAdmin(), Command("broadcast"))
async def handle_broadcast(event): ...
+
fallback_router = Router(router_id="fallback")
+
@fallback_router.message_created()
async def handle_fallback(event): ...
+
dp.include_routers(admin_router, fallback_router)
```
@@ -183,8 +189,8 @@ handler.mw_chain = accumulated_inner_middlewares + handler.middlewares → call_
### 1. `Dispatcher.__init__` — переименование и новый атрибут
```python
-self.outer_middlewares: list[BaseMiddleware] = [] # было: self.middlewares
-self.inner_middlewares: list[BaseMiddleware] = [] # новый
+self.outer_middlewares: list[BaseMiddleware] = [] # было: self.middlewares
+self.inner_middlewares: list[BaseMiddleware] = [] # новый
```
Атрибут `middlewares` сохраняется как `@property`-алиас с
@@ -223,6 +229,7 @@ def register_outer_middleware(self, middleware: BaseMiddleware) -> None:
"""
self.outer_middlewares.append(middleware)
+
def register_inner_middleware(self, middleware: BaseMiddleware) -> None:
"""Inner middleware (после фильтров handler).
@@ -233,6 +240,7 @@ def register_inner_middleware(self, middleware: BaseMiddleware) -> None:
"""
self.inner_middlewares.append(middleware)
+
def register_middleware(self, middleware: BaseMiddleware) -> None:
"""
.. deprecated::
@@ -266,7 +274,7 @@ def _iter_routers(
self,
routers,
parent_middlewares=None,
- parent_inner_middlewares=None, # новый параметр
+ parent_inner_middlewares=None, # новый параметр
parent_filters=None,
parent_base_filters=None,
path=None,
@@ -280,12 +288,14 @@ def _iter_routers(
if router is self:
accumulated_inner_middlewares = inner_middlewares
else:
- accumulated_inner_middlewares = inner_middlewares + router.inner_middlewares
+ accumulated_inner_middlewares = (
+ inner_middlewares + router.inner_middlewares
+ )
yield (
router,
accumulated_middlewares,
- accumulated_inner_middlewares, # ← новый 5-й элемент
+ accumulated_inner_middlewares, # ← новый 5-й элемент
accumulated_filters,
accumulated_base_filters,
)
@@ -317,17 +327,15 @@ def _prepare_handlers(self, bot: Bot) -> None:
# Порядок: global inner → router inner (накопленный) → handler mw
# Внешний слой — global, внутренний — handler.
all_inner = (
- global_inner_mw
- + accumulated_inner_mw
- + handler.middlewares
+ global_inner_mw + accumulated_inner_mw + handler.middlewares
)
handler.mw_chain = self.build_middleware_chain(
all_inner,
functools.partial(self.call_handler, handler),
)
- router.handlers_by_type.setdefault(
- handler.update_type, []
- ).append(handler)
+ router.handlers_by_type.setdefault(handler.update_type, []).append(
+ handler
+ )
self._cached_router_entries = list(self._iter_unique_routers(self.routers))
```
@@ -457,9 +465,12 @@ fallback_router:
```python
from maxapi import Router, Dispatcher, Bot
from myapp.middleware import (
- RequestIdMiddleware, LoggingMiddleware,
- DbTransactionMiddleware, HandlerLatencyMetricMiddleware,
- AdminAccessLogMiddleware, BroadcastLockMiddleware,
+ RequestIdMiddleware,
+ LoggingMiddleware,
+ DbTransactionMiddleware,
+ HandlerLatencyMetricMiddleware,
+ AdminAccessLogMiddleware,
+ BroadcastLockMiddleware,
)
from myapp.filters import IsAdmin
from maxapi.filters import Command
@@ -470,14 +481,18 @@ dp = Dispatcher()
# Срабатывает для каждого события, даже для тех, что в итоге будут
# проигнорированы. Подходит всё, что должно «жить» в контексте
# любого входящего апдейта.
-dp.register_outer_middleware(RequestIdMiddleware()) # request_id для трейсинга
-dp.register_outer_middleware(LoggingMiddleware()) # лог всех updates
+dp.register_outer_middleware(RequestIdMiddleware()) # request_id для трейсинга
+dp.register_outer_middleware(LoggingMiddleware()) # лог всех updates
# ─── Глобальный inner ─────────────────────────────────────────────
# Срабатывает только если какой-то handler реально будет выполнен.
# Подходит то, что бессмысленно делать «вхолостую».
-dp.register_inner_middleware(DbTransactionMiddleware()) # транзакция только под handler
-dp.register_inner_middleware(HandlerLatencyMetricMiddleware()) # метрика времени handler
+dp.register_inner_middleware(
+ DbTransactionMiddleware()
+) # транзакция только под handler
+dp.register_inner_middleware(
+ HandlerLatencyMetricMiddleware()
+) # метрика времени handler
admin_router = Router(router_id="admin")
@@ -493,9 +508,11 @@ admin_router.register_outer_middleware(AdminAccessLogMiddleware())
# исключительно перед началом тяжёлой операции.
admin_router.register_inner_middleware(BroadcastLockMiddleware())
+
@admin_router.message_created(IsAdmin(), Command("broadcast"))
async def broadcast(event): ...
+
dp.include_routers(admin_router)
```
@@ -544,10 +561,14 @@ outer middleware и фильтры:
```python
parent = Router(router_id="parent")
-parent.register_inner_middleware(DbTransactionMiddleware()) # на любой handler в parent + детях
+parent.register_inner_middleware(
+ DbTransactionMiddleware() # на любой handler в parent + детях
+)
child = Router(router_id="child")
-child.register_inner_middleware(ChildAuditMiddleware()) # только на handler внутри child
+child.register_inner_middleware(
+ ChildAuditMiddleware() # только на handler внутри child
+)
parent.include_routers(child)
```
@@ -600,10 +621,14 @@ handler.mw_chain = [DbTransactionMiddleware] + handler.middlewares → call_hand
# aiogram
router = Router()
-router.message.outer_middleware(LoggingMiddleware()) # outer для message-обзёрвера
-router.message.middleware(AuthMiddleware()) # inner для message-обзёрвера
+router.message.outer_middleware(
+ LoggingMiddleware()
+) # outer для message-обзёрвера
+router.message.middleware(AuthMiddleware()) # inner для message-обзёрвера
-router.callback_query.middleware(RateLimitMiddleware()) # только для callback_query
+router.callback_query.middleware(
+ RateLimitMiddleware()
+) # только для callback_query
# Дополнительно: middleware на уровне Router.update — ловит ВСЕ типы
router.update.outer_middleware(TracingMiddleware())
@@ -679,14 +704,26 @@ warning):
```python
# Глобальный уровень
-dp.register_outer_middleware(LoggingMW()) # ✅ канонично: outer (для каждого события)
-dp.register_inner_middleware(MetricsMW()) # ✅ канонично: inner (только когда handler сработал)
-dp.middleware(SomeMW()) # ⚠️ DeprecationWarning → используйте .register_outer_middleware()
+dp.register_outer_middleware(
+ LoggingMW()
+) # ✅ канонично: outer (для каждого события)
+dp.register_inner_middleware(
+ MetricsMW()
+) # ✅ канонично: inner (только когда handler сработал)
+dp.middleware(
+ SomeMW()
+) # ⚠️ DeprecationWarning → используйте .register_outer_middleware()
# Роутерный уровень
-router.register_outer_middleware(TracingMW()) # ✅ канонично: outer (до filters handler)
-router.register_inner_middleware(AuthMW()) # ✅ канонично: inner (после filters handler)
-router.middleware(AuditMW()) # ⚠️ DeprecationWarning → используйте .register_outer_middleware()
+router.register_outer_middleware(
+ TracingMW()
+) # ✅ канонично: outer (до filters handler)
+router.register_inner_middleware(
+ AuthMW()
+) # ✅ канонично: inner (после filters handler)
+router.middleware(
+ AuditMW()
+) # ⚠️ DeprecationWarning → используйте .register_outer_middleware()
```
**Почему это лучше, чем в aiogram**: в aiogram `router.middleware()` без
@@ -755,6 +792,7 @@ execution order). Причины:
message_router = Router()
message_router.register_inner_middleware(MyMW())
+
@message_router.message_created()
async def handler(event): ...
```
diff --git a/docs/examples.md b/docs/examples.md
index 245b7a9e..8d8b65bb 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -28,7 +28,7 @@ async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -42,23 +42,23 @@ import logging
from maxapi import Bot, Dispatcher
from maxapi.types import (
- BotStarted,
- Command,
- MessageCreated,
- CallbackButton,
- MessageCallback,
- BotAdded,
- ChatTitleChanged,
- MessageEdited,
- MessageRemoved,
- UserAdded,
+ BotStarted,
+ Command,
+ MessageCreated,
+ CallbackButton,
+ MessageCallback,
+ BotAdded,
+ ChatTitleChanged,
+ MessageEdited,
+ MessageRemoved,
+ UserAdded,
UserRemoved,
BotStopped,
DialogCleared,
DialogMuted,
DialogUnmuted,
ChatButton, # deprecated: 0.9.14
- MessageChatCreated # deprecated: 0.9.14
+ MessageChatCreated, # deprecated: 0.9.14
)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
@@ -68,32 +68,32 @@ bot = Bot()
dp = Dispatcher()
-@dp.message_created(Command('start'))
+@dp.message_created(Command("start"))
async def hello(event: MessageCreated):
builder = InlineKeyboardBuilder()
builder.row(
CallbackButton(
- text='Кнопка 1',
- payload='btn_1'
+ text="Кнопка 1",
+ payload="btn_1",
),
CallbackButton(
- text='Кнопка 2',
- payload='btn_2',
- )
+ text="Кнопка 2",
+ payload="btn_2",
+ ),
)
builder.add(
ChatButton( # deprecated: 0.9.14
- text='Создать чат',
- chat_title='Тест чат'
+ text="Создать чат",
+ chat_title="Тест чат",
)
)
await event.message.answer(
- text='Привет!',
+ text="Привет!",
attachments=[
builder.as_markup(),
- ]
+ ],
)
@@ -101,12 +101,13 @@ async def hello(event: MessageCreated):
async def bot_added(event: BotAdded):
chat = await event.fetch_chat()
if chat is None:
- logging.info('Не удалось получить chat, возможно отключен auto_requests!')
+ logging.info(
+ "Не удалось получить chat, возможно отключен auto_requests!"
+ )
return
-
+
await bot.send_message(
- chat_id=event.chat_id,
- text=f'Привет чат {chat.title}!'
+ chat_id=event.chat_id, text=f"Привет чат {chat.title}!"
)
@@ -114,7 +115,7 @@ async def bot_added(event: BotAdded):
async def message_removed(event: MessageRemoved):
await bot.send_message(
chat_id=event.chat_id,
- text='Я всё видел!'
+ text="Я всё видел!",
)
@@ -122,7 +123,7 @@ async def message_removed(event: MessageRemoved):
async def bot_started(event: BotStarted):
await bot.send_message(
chat_id=event.chat_id,
- text='Привет! Отправь мне /start'
+ text="Привет! Отправь мне /start",
)
@@ -130,22 +131,20 @@ async def bot_started(event: BotStarted):
async def chat_title_changed(event: ChatTitleChanged):
await bot.send_message(
chat_id=event.chat_id,
- text=f'Крутое новое название "{event.title}"!'
+ text=f'Крутое новое название "{event.title}"!',
)
@dp.message_callback()
async def message_callback(event: MessageCallback):
await event.answer(
- new_text=f'Вы нажали на кнопку {event.callback.payload}!'
+ new_text=f"Вы нажали на кнопку {event.callback.payload}!"
)
@dp.message_edited()
async def message_edited(event: MessageEdited):
- await event.message.answer(
- text='Вы отредактировали сообщение!'
- )
+ await event.message.answer(text="Вы отредактировали сообщение!")
@dp.user_removed()
@@ -154,12 +153,12 @@ async def user_removed(event: UserRemoved):
if from_user is None:
return await bot.send_message(
chat_id=event.chat_id,
- text=f'Неизвестный кикнул {event.user.first_name} 😢'
+ text=f"Неизвестный кикнул {event.user.first_name} 😢",
)
-
+
await bot.send_message(
chat_id=event.chat_id,
- text=f'{from_user.first_name} кикнул {event.user.first_name} 😢'
+ text=f"{from_user.first_name} кикнул {event.user.first_name} 😢",
)
@@ -169,12 +168,12 @@ async def user_added(event: UserAdded):
if chat is None:
return await bot.send_message(
chat_id=event.chat_id,
- text=f'Чат приветствует вас, {event.user.first_name}!'
+ text=f"Чат приветствует вас, {event.user.first_name}!",
)
-
+
await bot.send_message(
chat_id=event.chat_id,
- text=f'Чат "{chat.title}" приветствует вас, {event.user.first_name}!'
+ text=f'Чат "{chat.title}" приветствует вас, {event.user.first_name}!',
)
@@ -182,7 +181,7 @@ async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -222,41 +221,41 @@ bot = Bot()
dp = Dispatcher()
-@dp.message_created(F.message.body.text == 'привет')
+@dp.message_created(F.message.body.text == "привет")
async def on_hello(event: MessageCreated):
- await event.message.answer('Привет!')
+ await event.message.answer("Привет!")
-@dp.message_created(F.message.body.text.lower().contains('помощь'))
+@dp.message_created(F.message.body.text.lower().contains("помощь"))
async def on_help(event: MessageCreated):
- await event.message.answer('Чем могу помочь?')
+ await event.message.answer("Чем могу помочь?")
-@dp.message_created(F.message.body.text.regexp(r'^\d{4}$'))
+@dp.message_created(F.message.body.text.regexp(r"^\d{4}$"))
async def on_code(event: MessageCreated):
- await event.message.answer('Принят 4-значный код')
+ await event.message.answer("Принят 4-значный код")
@dp.message_created(F.message.body.attachments)
async def on_attachment(event: MessageCreated):
- await event.message.answer('Получено вложение')
+ await event.message.answer("Получено вложение")
@dp.message_created(F.message.body.text.len() > 20)
async def on_long_text(event: MessageCreated):
- await event.message.answer('Слишком длинное сообщение')
+ await event.message.answer("Слишком длинное сообщение")
@dp.message_created(F.message.body.text.len() > 0)
async def on_non_empty(event: MessageCreated):
- await event.message.answer('Вы что-то написали.')
+ await event.message.answer("Вы что-то написали.")
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -271,18 +270,18 @@ import logging
from maxapi import Bot, Dispatcher
from maxapi.types import (
ChatButton, # deprecated: 0.9.14
- LinkButton,
- CallbackButton,
- RequestGeoLocationButton,
- MessageButton,
+ LinkButton,
+ CallbackButton,
+ RequestGeoLocationButton,
+ MessageButton,
ButtonsPayload,
- RequestContactButton,
+ RequestContactButton,
OpenAppButton,
- MessageCreated,
- MessageCallback,
- MessageChatCreated, # deprecated: 0.9.14
- CommandStart,
- Command
+ MessageCreated,
+ MessageCallback,
+ MessageChatCreated, # deprecated: 0.9.14
+ CommandStart,
+ Command,
)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
@@ -296,70 +295,68 @@ dp = Dispatcher()
async def echo(event: MessageCreated):
await event.message.answer(
(
- 'Привет! Мои команды:\n\n'
-
- '/builder - Клавиатура из InlineKeyboardBuilder\n'
- '/payload - Клавиатура из pydantic моделей\n'
+ "Привет! Мои команды:\n\n"
+ "/builder - Клавиатура из InlineKeyboardBuilder\n"
+ "/payload - Клавиатура из pydantic моделей\n"
)
)
-
-
-@dp.message_created(Command('builder'))
+
+
+@dp.message_created(Command("builder"))
async def builder(event: MessageCreated):
builder = InlineKeyboardBuilder()
-
+
builder.row(
ChatButton( # deprecated: 0.9.14
- text="Создать чат",
- chat_title='Test',
- chat_description='Test desc'
+ text="Создать чат",
+ chat_title="Test",
+ chat_description="Test desc",
),
LinkButton(
- text="Документация MAX",
- url="https://dev.max.ru/docs"
+ text="Документация MAX",
+ url="https://dev.max.ru/docs",
),
)
-
+
builder.row(
RequestGeoLocationButton(text="Геолокация"),
MessageButton(text="Сообщение"),
)
-
+
builder.row(
RequestContactButton(text="Контакт"),
OpenAppButton(
- text="Приложение",
- web_app=event.bot.me.username,
- contact_id=event.bot.me.user_id
+ text="Приложение",
+ web_app=event.bot.me.username,
+ contact_id=event.bot.me.user_id,
),
)
-
+
builder.row(
CallbackButton(
- text='Callback',
- payload='test',
+ text="Callback",
+ payload="test",
)
)
-
+
await event.message.answer(
- text='Клавиатура из InlineKeyboardBuilder',
- attachments=[
- builder.as_markup()
- ])
-
-
-@dp.message_created(Command('payload'))
+ text="Клавиатура из InlineKeyboardBuilder",
+ attachments=[builder.as_markup()],
+ )
+
+
+@dp.message_created(Command("payload"))
async def payload(event: MessageCreated):
buttons = [
[
ChatButton( # deprecated: 0.9.14
- text="Создать чат",
- chat_title='Test',
- chat_description='Test desc'
+ text="Создать чат",
+ chat_title="Test",
+ chat_description="Test desc",
),
LinkButton(
- text="Документация MAX",
- url="https://dev.max.ru/docs"
+ text="Документация MAX",
+ url="https://dev.max.ru/docs",
),
],
[
@@ -369,46 +366,45 @@ async def payload(event: MessageCreated):
[
RequestContactButton(text="Контакт"),
OpenAppButton(
- text="Приложение",
- web_app=event.bot.me.username,
- contact_id=event.bot.me.user_id
+ text="Приложение",
+ web_app=event.bot.me.username,
+ contact_id=event.bot.me.user_id,
),
],
[
CallbackButton(
- text='Callback',
- payload='test',
+ text="Callback",
+ payload="test",
)
- ]
+ ],
]
-
+
buttons_payload = ButtonsPayload(buttons=buttons).pack()
-
+
await event.message.answer(
- text='Клавиатура из pydantic моделей',
- attachments=[
- buttons_payload
- ])
-
-
+ text="Клавиатура из pydantic моделей",
+ attachments=[buttons_payload],
+ )
+
+
@dp.message_chat_created() # deprecated: 0.9.14
async def message_chat_created(obj: MessageChatCreated):
await obj.bot.send_message(
chat_id=obj.chat.chat_id,
- text=f'Чат создан! Ссылка: {obj.chat.link}'
+ text=f"Чат создан! Ссылка: {obj.chat.link}",
)
-
+
@dp.message_callback()
async def message_callback(callback: MessageCallback):
- await callback.message.answer('Вы нажали на Callback!')
+ await callback.message.answer("Вы нажали на Callback!")
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -496,16 +492,15 @@ bot = Bot()
dp = Dispatcher()
-@dp.message_created(F.message.link.type == 'forward')
+@dp.message_created(F.message.link.type == "forward")
async def get_ids_from_forward(event: MessageCreated):
text = (
- 'Информация о пересланном сообщении:\n\n'
-
- f'Из чата: {event.message.link.chat_id}\n'
- f'От пользователя: {event.message.link.sender.user_id}'
+ "Информация о пересланном сообщении:\n\n"
+ f"Из чата: {event.message.link.chat_id}\n"
+ f"От пользователя: {event.message.link.sender.user_id}"
)
await event.message.reply(text)
-
+
@dp.message_created()
async def get_ids(event: MessageCreated):
@@ -516,8 +511,8 @@ async def get_ids(event: MessageCreated):
return
text = (
- f'Ваш ID: {from_user.user_id}\n'
- f'ID этого чата: {chat.chat_id}'
+ f"Ваш ID: {from_user.user_id}\n"
+ f"ID этого чата: {chat.chat_id}"
)
await event.message.answer(text, format=Format.HTML)
@@ -526,7 +521,7 @@ async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -549,30 +544,29 @@ dp = Dispatcher()
class FilterChat(BaseFilter):
-
"""
Фильтр, который срабатывает только в чате с названием `Test`
"""
-
+
async def __call__(self, event: UpdateUnion):
chat = await event.fetch_chat()
if chat is None:
return False
- return chat.title == 'Test'
+ return chat.title == "Test"
@dp.message_created(CommandStart(), FilterChat())
async def custom_data(event: MessageCreated):
- await event.message.answer('Привет!')
+ await event.message.answer("Привет!")
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -600,12 +594,12 @@ bot = Bot()
dp = Dispatcher()
-class MyPayload(CallbackPayload, prefix='mypayload'):
+class MyPayload(CallbackPayload, prefix="mypayload"):
foo: str
action: str
-class AnotherPayload(CallbackPayload, prefix='another'):
+class AnotherPayload(CallbackPayload, prefix="another"):
bar: str
value: int
@@ -614,33 +608,37 @@ class AnotherPayload(CallbackPayload, prefix='another'):
async def show_keyboard(event: MessageCreated):
kb = InlineKeyboardBuilder()
kb.row(
- CallbackButton(
- text='Первая кнопка',
- payload=MyPayload(foo='123', action='edit').pack(),
- ),
CallbackButton(
- text='Вторая кнопка',
- payload=AnotherPayload(bar='abc', value=42).pack(),
+ text="Первая кнопка",
+ payload=MyPayload(foo="123", action="edit").pack(),
+ ),
+ CallbackButton(
+ text="Вторая кнопка",
+ payload=AnotherPayload(bar="abc", value=42).pack(),
),
)
- await event.message.answer('Нажми кнопку!', attachments=[kb.as_markup()])
+ await event.message.answer("Нажми кнопку!", attachments=[kb.as_markup()])
-@dp.message_callback(MyPayload.filter(F.foo == '123'))
+@dp.message_callback(MyPayload.filter(F.foo == "123"))
async def on_first_callback(event: MessageCallback, payload: MyPayload):
- await event.answer(new_text=f'Первая кнопка: foo={payload.foo}, action={payload.action}')
+ await event.answer(
+ new_text=f"Первая кнопка: foo={payload.foo}, action={payload.action}"
+ )
@dp.message_callback(AnotherPayload.filter())
async def on_second_callback(event: MessageCallback, payload: AnotherPayload):
- await event.answer(new_text=f'Вторая кнопка: bar={payload.bar}, value={payload.value}')
+ await event.answer(
+ new_text=f"Вторая кнопка: bar={payload.bar}, value={payload.value}"
+ )
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -671,8 +669,8 @@ class CheckChatTitleMiddleware(BaseMiddleware):
event_object: UpdateUnion,
data: Dict[str, Any],
) -> Any:
-
- if event_object.chat.title == 'MAXApi':
+
+ if event_object.chat.title == "MAXApi":
return await handler(event_object, data)
@@ -683,33 +681,43 @@ class CustomDataMiddleware(BaseMiddleware):
event_object: UpdateUnion,
data: Dict[str, Any],
) -> Any:
-
- data['custom_data'] = f'Это ID того кто вызвал команду: {event_object.from_user.user_id}'
-
+
+ data["custom_data"] = (
+ f"Это ID того кто вызвал команду: {event_object.from_user.user_id}"
+ )
+
await handler(event_object, data)
-@dp.message_created(Command('start'), CheckChatTitleMiddleware())
+@dp.message_created(Command("start"), CheckChatTitleMiddleware())
async def start(event: MessageCreated):
- await event.message.answer('Это сообщение было отправлено, так как ваш чат называется "MAXApi"!')
+ await event.message.answer(
+ 'Это сообщение было отправлено, так как ваш чат называется "MAXApi"!'
+ )
-
-@dp.message_created(Command('custom_data'), CustomDataMiddleware())
+
+@dp.message_created(Command("custom_data"), CustomDataMiddleware())
async def custom_data(event: MessageCreated, custom_data: str):
await event.message.answer(custom_data)
-
-
-@dp.message_created(Command('many_middlewares'), CheckChatTitleMiddleware(), CustomDataMiddleware())
+
+
+@dp.message_created(
+ Command("many_middlewares"),
+ CheckChatTitleMiddleware(),
+ CustomDataMiddleware(),
+)
async def many_middlewares(event: MessageCreated, custom_data: str):
- await event.message.answer('Это сообщение было отправлено, так как ваш чат называется "MAXApi"!')
+ await event.message.answer(
+ 'Это сообщение было отправлено, так как ваш чат называется "MAXApi"!'
+ )
await event.message.answer(custom_data)
-
+
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -740,24 +748,26 @@ class CustomDataForRouterMiddleware(BaseMiddleware):
event_object: UpdateUnion,
data: Dict[str, Any],
) -> Any:
-
- data['custom_data'] = f'Это ID того кто вызвал команду: {event_object.from_user.user_id}'
+
+ data["custom_data"] = (
+ f"Это ID того кто вызвал команду: {event_object.from_user.user_id}"
+ )
result = await handler(event_object, data)
return result
-
-@dp.message_created(Command('custom_data'))
+
+@dp.message_created(Command("custom_data"))
async def custom_data(event: MessageCreated, custom_data: str):
await event.message.answer(custom_data)
-
-
+
+
async def main():
dp.register_outer_middleware(CustomDataForRouterMiddleware())
-
+
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -771,7 +781,14 @@ import logging
from maxapi import Bot, Dispatcher, F
from maxapi.context import MemoryContext, State, StatesGroup
-from maxapi.types import BotStarted, Command, MessageCreated, CallbackButton, MessageCallback, BotCommand
+from maxapi.types import (
+ BotStarted,
+ Command,
+ MessageCreated,
+ CallbackButton,
+ MessageCallback,
+ BotCommand,
+)
from maxapi.utils.inline_keyboard import InlineKeyboardBuilder
from router import router
@@ -784,14 +801,14 @@ dp = Dispatcher()
dp.include_routers(router)
-start_text = '''Пример чат-бота для MAX 💙
+start_text = """Пример чат-бота для MAX 💙
Мои команды:
/clear очищает ваш контекст
/state или /context показывают ваше контекстное состояние
/data показывает вашу контекстную память
-'''
+"""
class Form(StatesGroup):
@@ -801,83 +818,72 @@ class Form(StatesGroup):
@dp.on_started()
async def _():
- logging.info('Бот стартовал!')
+ logging.info("Бот стартовал!")
@dp.bot_started()
async def bot_started(event: BotStarted):
await event.bot.send_message(
chat_id=event.chat_id,
- text='Привет! Отправь мне /start'
+ text="Привет! Отправь мне /start",
)
-@dp.message_created(Command('clear'))
+@dp.message_created(Command("clear"))
async def hello(event: MessageCreated, context: MemoryContext):
await context.clear()
await event.message.answer(f"Ваш контекст был очищен!")
-@dp.message_created(Command('data'))
+@dp.message_created(Command("data"))
async def hello(event: MessageCreated, context: MemoryContext):
data = await context.get_data()
await event.message.answer(f"Ваша контекстная память: {str(data)}")
-@dp.message_created(Command('context'))
-@dp.message_created(Command('state'))
+@dp.message_created(Command("context"))
+@dp.message_created(Command("state"))
async def hello(event: MessageCreated, context: MemoryContext):
data = await context.get_state()
await event.message.answer(f"Ваше контекстное состояние: {str(data)}")
-@dp.message_created(Command('start'))
+@dp.message_created(Command("start"))
async def hello(event: MessageCreated):
builder = InlineKeyboardBuilder()
builder.row(
- CallbackButton(
- text='Ввести свое имя',
- payload='btn_1'
- ),
- CallbackButton(
- text='Ввести свой возраст',
- payload='btn_2'
- )
- )
- builder.row(
- CallbackButton(
- text='Не хочу',
- payload='btn_3'
- )
+ CallbackButton(text="Ввести свое имя", payload="btn_1"),
+ CallbackButton(text="Ввести свой возраст", payload="btn_2"),
)
+ builder.row(CallbackButton(text="Не хочу", payload="btn_3"))
await event.message.answer(
- text=start_text,
+ text=start_text,
attachments=[
builder.as_markup(),
- ]
+ ],
)
-
-@dp.message_callback(F.callback.payload == 'btn_1')
+
+@dp.message_callback(F.callback.payload == "btn_1")
async def hello(event: MessageCallback, context: MemoryContext):
await context.set_state(Form.name)
await event.message.delete()
- await event.message.answer(f'Отправьте свое имя:')
+ await event.message.answer(f"Отправьте свое имя:")
-@dp.message_callback(F.callback.payload == 'btn_2')
+@dp.message_callback(F.callback.payload == "btn_2")
async def hello(event: MessageCallback, context: MemoryContext):
await context.set_state(Form.age)
await event.message.delete()
- await event.message.answer(f'Отправьте ваш возраст:')
+ await event.message.answer(f"Отправьте ваш возраст:")
-@dp.message_callback(F.callback.payload == 'btn_3')
+@dp.message_callback(F.callback.payload == "btn_3")
async def hello(event: MessageCallback, context: MemoryContext):
await event.message.delete()
- await event.message.answer(f'Ну ладно 🥲')
+ await event.message.answer(f"Ну ладно 🥲")
@dp.message_created(F.message.body.text, Form.name)
@@ -886,8 +892,10 @@ async def hello(event: MessageCreated, context: MemoryContext):
data = await context.get_data()
- await event.message.answer(f"Приятно познакомиться, {data['name'].title()}!")
-
+ await event.message.answer(
+ f"Приятно познакомиться, {data['name'].title()}!"
+ )
+
@dp.message_created(F.message.body.text, Form.age)
async def hello(event: MessageCreated, context: MemoryContext):
@@ -898,31 +906,23 @@ async def hello(event: MessageCreated, context: MemoryContext):
async def main():
await bot.set_my_commands(
+ BotCommand(name="/start", description="Перезапустить бота"),
+ BotCommand(name="/clear", description="Очищает ваш контекст"),
BotCommand(
- name='/start',
- description='Перезапустить бота'
+ name="/state", description="Показывают ваше контекстное состояние"
),
BotCommand(
- name='/clear',
- description='Очищает ваш контекст'
+ name="/data", description="Показывает вашу контекстную память"
),
BotCommand(
- name='/state',
- description='Показывают ваше контекстное состояние'
+ name="/context",
+ description="Показывают ваше контекстное состояние",
),
- BotCommand(
- name='/data',
- description='Показывает вашу контекстную память'
- ),
- BotCommand(
- name='/context',
- description='Показывают ваше контекстное состояние'
- )
)
await dp.start_polling(bot)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -934,23 +934,21 @@ from maxapi.types import Command, MessageCreated
from maxapi.types import InputMedia
router = Router()
-file = __file__.split('\\')[-1]
+file = __file__.split("\\")[-1]
-@router.message_created(Command('router'))
+@router.message_created(Command("router"))
async def hello(obj: MessageCreated):
await obj.message.answer(f"Пишу тебе из роута {file}")
-
-# новая команда для примера, /media,
+
+# новая команда для примера, /media,
# пример использования: /media image.png (медиафайл берется указанному пути)
-@router.message_created(Command('media'))
+@router.message_created(Command("media"))
async def hello(event: MessageCreated):
await event.message.answer(
attachments=[
- InputMedia(
- path=event.message.body.text.replace('/media ', '')
- )
+ InputMedia(path=event.message.body.text.replace("/media ", ""))
]
)
```
@@ -1014,12 +1012,12 @@ dp = Dispatcher()
@dp.message_created()
async def handle_message(event: MessageCreated):
- await event.message.answer('Бот работает через вебхук!')
+ await event.message.answer("Бот работает через вебхук!")
async def main():
- webhook_url = 'https://ваш-домен.рф/webhook' # <-- укажите свой
- webhook_secret = 'my-secret-token' # <-- укажите свой (5–256 символов)
+ webhook_url = "https://ваш-домен.рф/webhook" # <-- укажите свой
+ webhook_secret = "my-secret-token" # <-- укажите свой (5–256 символов)
# Регистрируем вебхук на стороне MAX — платформа будет отправлять
# заголовок X-Max-Bot-Api-Secret с каждым запросом.
@@ -1030,13 +1028,13 @@ async def main():
# или не совпадает (защита от посторонних запросов).
await dp.handle_webhook(
bot=bot,
- host='0.0.0.0',
+ host="0.0.0.0",
port=8080,
secret=webhook_secret,
)
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -1069,12 +1067,12 @@ dp = Dispatcher()
@dp.message_created()
async def handle_message(event: MessageCreated):
- await event.message.answer('Бот работает через вебхук!')
+ await event.message.answer("Бот работает через вебхук!")
async def main():
- webhook_url = 'https://ваш-домен.рф/webhook' # <-- укажите свой
- webhook_secret = 'my-secret-token' # <-- укажите свой (5–256 символов)
+ webhook_url = "https://ваш-домен.рф/webhook" # <-- укажите свой
+ webhook_secret = "my-secret-token" # <-- укажите свой (5–256 символов)
# Передаём secret в конструктор — он сохраняется в webhook.secret.
# Фреймворк будет автоматически проверять заголовок X-Max-Bot-Api-Secret
@@ -1085,24 +1083,24 @@ async def main():
app = FastAPI(lifespan=webhook.lifespan)
# Собственные маршруты — например, healthcheck
- @app.get('/health')
+ @app.get("/health")
async def health():
- return {'status': 'ok'}
+ return {"status": "ok"}
# Подключаем MAX webhook-обработчик к нашему приложению
- webhook.setup(app, path='/webhook')
+ webhook.setup(app, path="/webhook")
# Подписываемся на webhook — передаём тот же secret,
# чтобы платформа MAX добавляла X-Max-Bot-Api-Secret в каждый запрос.
await bot.subscribe_webhook(url=webhook_url, secret=webhook_secret)
# Запускаем сервер uvicorn
- config = uvicorn.Config(app=app, host='0.0.0.0', port=8080)
+ config = uvicorn.Config(app=app, host="0.0.0.0", port=8080)
server = uvicorn.Server(config)
await server.serve()
-if __name__ == '__main__':
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -1128,14 +1126,17 @@ connection_props = DefaultConnectionProperties(proxy=proxy_url)
bot = Bot(default_connection=connection_props)
dp = Dispatcher()
-@dp.message_created(Command('start'))
+
+@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated):
await event.message.answer("Привет!")
+
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -1155,14 +1156,17 @@ bot = Bot(
)
dp = Dispatcher()
-@dp.message_created(Command('start'))
+
+@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated):
await event.message.answer("Привет!")
+
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+
+if __name__ == "__main__":
asyncio.run(main())
```
diff --git a/docs/guides/context.md b/docs/guides/context.md
index dc9293cb..b650d813 100644
--- a/docs/guides/context.md
+++ b/docs/guides/context.md
@@ -8,21 +8,25 @@
from maxapi.context import MemoryContext, StatesGroup, State
from maxapi.types import MessageCreated, Command
+
class Form(StatesGroup):
name = State()
age = State()
-@dp.message_created(Command('start'))
+
+@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated, context: MemoryContext):
await context.set_state(Form.name)
await event.message.answer("Как вас зовут?")
+
@dp.message_created(Form.name)
async def name_handler(event: MessageCreated, context: MemoryContext):
await context.update_data(name=event.message.body.text)
await context.set_state(Form.age)
await event.message.answer("Сколько вам лет?")
+
@dp.message_created(Form.age)
async def age_handler(event: MessageCreated, context: MemoryContext):
data = await context.get_data()
@@ -111,7 +115,7 @@ dp = Dispatcher(
```python
class Form(StatesGroup):
name = State() # Автоматически получит имя 'Form:name'
- age = State() # Автоматически получит имя 'Form:age'
+ age = State() # Автоматически получит имя 'Form:age'
```
## Фильтрация по состояниям
@@ -121,18 +125,17 @@ class Form(StatesGroup):
```python
# Только в состоянии Form.name
@dp.message_created(Form.name)
-async def name_handler(event: MessageCreated, context: MemoryContext):
- ...
+async def name_handler(event: MessageCreated, context: MemoryContext): ...
+
# Только когда НЕТ активного состояния
@dp.message_created(None)
-async def no_state_handler(event: MessageCreated):
- ...
+async def no_state_handler(event: MessageCreated): ...
+
# В любом из перечисленных состояний
@dp.message_created(Form.name, Form.age)
-async def multi_state_handler(event: MessageCreated):
- ...
+async def multi_state_handler(event: MessageCreated): ...
```
## Хранение в Redis
@@ -153,13 +156,13 @@ from maxapi import Dispatcher
from maxapi.context import RedisContext
# Инициализация клиента Redis
-redis_client = redis.Redis(host='localhost', port=6379, db=0)
+redis_client = redis.Redis(host="localhost", port=6379, db=0)
# Передача RedisContext в Диспетчер
dp = Dispatcher(
storage=RedisContext,
redis_client=redis_client,
- key_prefix="my_bot"
+ key_prefix="my_bot",
)
```
@@ -223,6 +226,7 @@ bot = Bot()
dp = Dispatcher()
dp.register_outer_middleware(SaveMarkerMiddleware())
+
async def main() -> None:
marker = await load_marker() # str | None
diff --git a/docs/guides/documentation_handlers.md b/docs/guides/documentation_handlers.md
index 97949f85..012b9aee 100644
--- a/docs/guides/documentation_handlers.md
+++ b/docs/guides/documentation_handlers.md
@@ -11,11 +11,11 @@
В docstring обработчика необходимо указать маркер `commands_info:` с описанием команды:
```python
-@dp.message_created(Command('start'))
+@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated):
"""
Обработчик команды /start
-
+
commands_info: Запускает бота и показывает приветственное сообщение
"""
await event.message.answer("Привет! Добро пожаловать!")
@@ -32,24 +32,27 @@ from maxapi.types import MessageCreated, Command
bot = Bot()
dp = Dispatcher()
-@dp.message_created(Command('help'))
+
+@dp.message_created(Command("help"))
async def help_handler(event: MessageCreated):
"""
Обработчик команды помощи
-
+
commands_info: Показывает список доступных команд и их описание
"""
- await event.message.answer("Доступные команды:\n/start - Начать работу\n/help - Помощь")
+ await event.message.answer(
+ "Доступные команды:\n/start - Начать работу\n/help - Помощь"
+ )
```
### Несколько команд с одним описанием
```python
-@dp.message_created(Command(['start', 'begin', 'go']))
+@dp.message_created(Command(["start", "begin", "go"]))
async def start_handler(event: MessageCreated):
"""
Обработчик команд запуска
-
+
commands_info: Инициализирует бота и начинает диалог с пользователем
"""
await event.message.answer("Бот запущен!")
@@ -58,11 +61,11 @@ async def start_handler(event: MessageCreated):
### Многострочное описание
```python
-@dp.message_created(Command('settings'))
+@dp.message_created(Command("settings"))
async def settings_handler(event: MessageCreated):
"""
Обработчик настроек
-
+
commands_info: Открывает меню настроек бота.
Позволяет изменить язык, уведомления и другие параметры.
"""
@@ -98,8 +101,8 @@ from maxapi.filters.command import CommandsInfo
# Пример использования
cmd_info = CommandsInfo(
- commands=['start', 'begin'],
- info='Запускает бота'
+ commands=["start", "begin"],
+ info="Запускает бота",
)
```
@@ -112,6 +115,7 @@ import logging
logger = logging.getLogger(__name__)
+
@dp.on_started()
async def log_all_commands():
"""Логирует все зарегистрированные команды"""
@@ -143,47 +147,53 @@ logging.basicConfig(level=logging.INFO)
bot = Bot()
dp = Dispatcher()
-@dp.message_created(Command('start'))
+
+@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated):
"""
Обработчик команды /start
-
+
commands_info: Запускает бота и показывает приветствие
"""
await event.message.answer("Привет!")
-@dp.message_created(Command('help'))
+
+@dp.message_created(Command("help"))
async def help_handler(event: MessageCreated):
"""
Обработчик команды /help
-
+
commands_info: Показывает справку по использованию бота
"""
await event.message.answer("Справка по командам...")
-@dp.message_created(Command('settings'))
+
+@dp.message_created(Command("settings"))
async def settings_handler(event: MessageCreated):
"""
Обработчик команды /settings
-
+
commands_info: Открывает меню настроек
"""
await event.message.answer("Настройки...")
+
@dp.on_started()
async def log_all_commands():
"""Логирует все зарегистрированные команды"""
logger = logging.getLogger(__name__)
-
+
logger.info("Зарегистрированные команды:")
for cmd_info in bot.handlers_commands:
commands_str = ", ".join([f"/{cmd}" for cmd in cmd_info.commands])
info_str = f" - {cmd_info.info}" if cmd_info.info else ""
logger.info(f" {commands_str}{info_str}")
+
async def main():
await dp.start_polling(bot)
-if __name__ == '__main__':
+
+if __name__ == "__main__":
asyncio.run(main())
```
diff --git a/docs/guides/filters.md b/docs/guides/filters.md
index e0ce49f9..90020a41 100644
--- a/docs/guides/filters.md
+++ b/docs/guides/filters.md
@@ -9,23 +9,26 @@
```python
from maxapi import F
+
# Только текстовые сообщения
@dp.message_created(F.message.body.text)
-async def text_handler(event: MessageCreated):
- ...
+async def text_handler(event: MessageCreated): ...
+
# Сообщения с вложениями
@dp.message_created(F.message.body.attachments)
-async def attachment_handler(event: MessageCreated):
- ...
+async def attachment_handler(event: MessageCreated): ...
+
# Комбинация условий
from maxapi.enums.chat_type import ChatType
+
# ⚠️ Скобки обязательны: & и | имеют более высокий приоритет, чем ==
-@dp.message_created(F.message.body.text & (F.message.chat.type == ChatType.DIALOG))
-async def dialog_text_handler(event: MessageCreated):
- ...
+@dp.message_created(
+ F.message.body.text & (F.message.chat.type == ChatType.DIALOG)
+)
+async def dialog_text_handler(event: MessageCreated): ...
```
Для личных сообщений используйте `ChatType.DIALOG`.
@@ -35,15 +38,15 @@ async def dialog_text_handler(event: MessageCreated):
```python
from maxapi.types import Command
+
# Одна команда
-@dp.message_created(Command('start'))
-async def start_handler(event: MessageCreated):
- ...
+@dp.message_created(Command("start"))
+async def start_handler(event: MessageCreated): ...
+
# Несколько команд
-@dp.message_created(Command(['start', 'help', 'info']))
-async def commands_handler(event: MessageCreated):
- ...
+@dp.message_created(Command(["start", "help", "info"]))
+async def commands_handler(event: MessageCreated): ...
```
## Callback Payload фильтр
@@ -51,23 +54,26 @@ async def commands_handler(event: MessageCreated):
```python
from maxapi.filters.callback_payload import CallbackPayload
+
# Простой payload (строка)
-@dp.message_callback(F.callback.payload == 'button_click')
-async def callback_handler(event: MessageCallback):
- ...
+@dp.message_callback(F.callback.payload == "button_click")
+async def callback_handler(event: MessageCallback): ...
+
# Структурированный payload (класс)
-class MyPayload(CallbackPayload, prefix='mypayload'):
+class MyPayload(CallbackPayload, prefix="mypayload"):
action: str
value: int
+
# Без дополнительных условий
@dp.message_callback(MyPayload.filter())
async def callback_handler(event: MessageCallback, payload: MyPayload):
await event.answer(f"Action: {payload.action}, Value: {payload.value}")
+
# С дополнительным фильтром
-@dp.message_callback(MyPayload.filter(F.action == 'edit'))
+@dp.message_callback(MyPayload.filter(F.action == "edit"))
async def callback_handler(event: MessageCallback, payload: MyPayload):
await event.answer(f"Edit action: {payload.value}")
```
@@ -140,10 +146,10 @@ F.message.body.text | F.message.body.attachments
# Отрицание (NOT)
~F.message.body.text
+
# Несколько фильтров в декораторе (все объединяются через AND)
-@dp.message_created(F.message.body.text, Command('start'), Form.name)
-async def handler(event: MessageCreated):
- ...
+@dp.message_created(F.message.body.text, Command("start"), Form.name)
+async def handler(event: MessageCreated): ...
```
## Базовые фильтры (BaseFilter)
@@ -153,6 +159,7 @@ async def handler(event: MessageCreated):
```python
from maxapi.filters.filter import BaseFilter
+
class MyFilter(BaseFilter):
async def __call__(self, event):
# Возвращает True/False или dict с данными
diff --git a/docs/guides/handlers.md b/docs/guides/handlers.md
index 82eada3c..bf11f3f9 100644
--- a/docs/guides/handlers.md
+++ b/docs/guides/handlers.md
@@ -28,7 +28,8 @@ dp.message_created.register(my_handler, <фильтры>)
```python
from maxapi.types import MessageCreated, Command
-@dp.message_created(Command('start'))
+
+@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated):
await event.message.answer("Привет!")
```
@@ -38,9 +39,11 @@ async def start_handler(event: MessageCreated):
Если вы хотите, чтобы хендлер срабатывал только тогда, когда у пользователя нет активного состояния в FSM, используйте `None`:
```python
-@dp.message_created(None, Command('help'))
+@dp.message_created(None, Command("help"))
async def help_no_state(event: MessageCreated):
- await event.message.answer("Вы запросили помощь вне контекста заполнения формы.")
+ await event.message.answer(
+ "Вы запросили помощь вне контекста заполнения формы."
+ )
```
### Обработка с фильтром
@@ -48,6 +51,7 @@ async def help_no_state(event: MessageCreated):
```python
from maxapi import F
+
@dp.message_created(F.message.body.text)
async def text_handler(event: MessageCreated):
await event.message.answer(f"Вы написали: {event.message.body.text}")
@@ -66,9 +70,11 @@ async def any_message(event: MessageCreated):
```python
from maxapi.context import State, StatesGroup
+
class Form(StatesGroup):
name = State()
+
@dp.message_created(F.message.body.text, Form.name)
async def name_handler(event: MessageCreated, context: MemoryContext):
await context.update_data(name=event.message.body.text)
@@ -78,7 +84,7 @@ async def name_handler(event: MessageCreated, context: MemoryContext):
### Обработка с контекстом
```python
-@dp.message_created(Command('data'))
+@dp.message_created(Command("data"))
async def data_handler(event: MessageCreated, context: MemoryContext):
data = await context.get_data()
await event.message.answer(f"Данные: {data}")
@@ -92,17 +98,18 @@ async def data_handler(event: MessageCreated, context: MemoryContext):
from maxapi.types.attachments.upload import AttachmentUpload, AttachmentPayload
from maxapi.enums.upload_type import UploadType
-@dp.message_created(Command('send_photo'))
+
+@dp.message_created(Command("send_photo"))
async def send_photo_by_token(event: MessageCreated):
# Создаем вложение, используя существующий токен
attachment = AttachmentUpload(
type=UploadType.IMAGE,
- payload=AttachmentPayload(token="ВАШ_ТОКЕН_ЗДЕСЬ")
+ payload=AttachmentPayload(token="ВАШ_ТОКЕН_ЗДЕСЬ"),
)
-
+
await event.message.answer(
text="Вот ваше фото по токену",
- attachments=[attachment]
+ attachments=[attachment],
)
```
diff --git a/docs/guides/keyboards.md b/docs/guides/keyboards.md
index c49e1e3d..4b68b913 100644
--- a/docs/guides/keyboards.md
+++ b/docs/guides/keyboards.md
@@ -22,7 +22,7 @@ builder.row(CallbackButton(text="Ещё кнопка", payload="more"))
await event.message.answer(
text="Выберите действие:",
- attachments=[builder.as_markup()]
+ attachments=[builder.as_markup()],
)
```
@@ -41,13 +41,13 @@ from maxapi.types.attachments.buttons import (
buttons = [
[LinkButton(text="Сайт", url="https://example.com")],
[ClipboardButton(text="Скопировать код", payload="ABC-123")],
- [CallbackButton(text="Callback", payload="data")]
+ [CallbackButton(text="Callback", payload="data")],
]
payload = ButtonsPayload(buttons=buttons).pack()
await event.message.answer(
text="Клавиатура",
- attachments=[payload]
+ attachments=[payload],
)
```
diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md
index e958579c..261f9f28 100644
--- a/docs/guides/middleware.md
+++ b/docs/guides/middleware.md
@@ -8,6 +8,7 @@ Middleware позволяет обрабатывать события до и п
from maxapi.filters.middleware import BaseMiddleware
from typing import Any, Awaitable, Callable, Dict
+
class LoggingMiddleware(BaseMiddleware):
async def __call__(
self,
@@ -35,7 +36,7 @@ class LoggingMiddleware(BaseMiddleware):
захвата распределённых блокировок и т.п.
```python
-dp.register_outer_middleware(LoggingMiddleware()) # каждый update
+dp.register_outer_middleware(LoggingMiddleware()) # каждый update
dp.register_inner_middleware(DbTransactionMiddleware()) # только под handler
```
@@ -74,6 +75,7 @@ from maxapi.types.updates.message_created import MessageCreated
# ── Фильтры ──────────────────────────────────────────────────────────────────
+
class IsAdmin(BaseFilter):
"""Пропускает только пользователей из списка администраторов."""
@@ -85,11 +87,13 @@ class IsAdmin(BaseFilter):
# ── Middleware ────────────────────────────────────────────────────────────────
+
class RequestIdMiddleware(BaseMiddleware):
"""Outer-global: проставляет уникальный request-id на каждое событие."""
async def __call__(self, handler, event, data):
import uuid
+
data["request_id"] = str(uuid.uuid4())
return await handler(event, data)
@@ -194,7 +198,7 @@ dp.register_outer_middleware → RequestIdMiddleware
## Middleware в обработчике
```python
-@dp.message_created(Command('start'), LoggingMiddleware())
+@dp.message_created(Command("start"), LoggingMiddleware())
async def start_handler(event: MessageCreated):
await event.message.answer("Привет!")
```
@@ -204,10 +208,11 @@ async def start_handler(event: MessageCreated):
```python
class CustomDataMiddleware(BaseMiddleware):
async def __call__(self, handler, event_object, data):
- data['custom_data'] = f'User ID: {event_object.from_user.user_id}'
+ data["custom_data"] = f"User ID: {event_object.from_user.user_id}"
return await handler(event_object, data)
-@dp.message_created(Command('data'), CustomDataMiddleware())
+
+@dp.message_created(Command("data"), CustomDataMiddleware())
async def handler(event: MessageCreated, custom_data: str):
await event.message.answer(custom_data)
```
@@ -300,8 +305,7 @@ async def value_error_handler(event: ErrorEvent):
@dp.errors(ExceptionTypeFilter(RuntimeError))
-async def runtime_error_handler(event: ErrorEvent):
- ...
+async def runtime_error_handler(event: ErrorEvent): ...
```
В декоратор можно передавать:
diff --git a/docs/guides/routers.md b/docs/guides/routers.md
index 0b79e51f..c8e05e7c 100644
--- a/docs/guides/routers.md
+++ b/docs/guides/routers.md
@@ -10,7 +10,8 @@ from maxapi.types import MessageCreated, Command
router = Router(router_id="my_router")
-@router.message_created(Command('help'))
+
+@router.message_created(Command("help"))
async def help_handler(event: MessageCreated):
await event.message.answer("Помощь")
```
@@ -40,10 +41,12 @@ router.filters.append(F.chat.type == ChatType.DIALOG) # Личный диало
```python
from maxapi.filters.middleware import BaseMiddleware
+
class RouterMiddleware(BaseMiddleware):
async def __call__(self, handler, event_object, data):
# Логика только для этого роутера
return await handler(event_object, data)
+
router.register_outer_middleware(RouterMiddleware())
```
diff --git a/docs/guides/webhook_vs_polling.md b/docs/guides/webhook_vs_polling.md
index 9dcc1b33..454397fe 100644
--- a/docs/guides/webhook_vs_polling.md
+++ b/docs/guides/webhook_vs_polling.md
@@ -42,9 +42,9 @@ async def main():
```python
async def main():
- webhook_url = 'https://ваш-домен.рф/webhook' # <-- укажите свой
+ webhook_url = "https://ваш-домен.рф/webhook" # <-- укажите свой
await bot.subscribe_webhook(url=webhook_url)
- await dp.handle_webhook(bot, host='0.0.0.0', port=8080, path='/webhook')
+ await dp.handle_webhook(bot, host="0.0.0.0", port=8080, path="/webhook")
```
**Параметры:**
diff --git a/docs/index.md b/docs/index.md
index 53740c39..930803e8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -39,22 +39,26 @@ logging.basicConfig(level=logging.INFO)
bot = Bot()
dp = Dispatcher()
+
# Обработчик команды /start
-@dp.message_created(Command('start'))
+@dp.message_created(Command("start"))
async def start_handler(event: MessageCreated):
await event.message.answer("Привет! 👋\nЯ простой бот на MaxAPI.")
+
# Обработчик всех текстовых сообщений
@dp.message_created()
async def echo_handler(event: MessageCreated):
if event.message.body.text:
await event.message.answer(f"Вы написали: {event.message.body.text}")
+
async def main():
# Запуск бота в режиме polling
await dp.start_polling(bot)
-if __name__ == '__main__':
+
+if __name__ == "__main__":
asyncio.run(main())
```
@@ -80,12 +84,14 @@ from maxapi.types import MessageCreated
bot = Bot()
dp = Dispatcher()
+
# Обработчик только текстовых сообщений
@dp.message_created(F.message.body.text)
async def text_handler(event: MessageCreated):
text = event.message.body.text
await event.message.answer(f"Длина вашего сообщения: {len(text)} символов")
+
# Обработчик сообщений с вложениями
@dp.message_created(F.message.attachments)
async def attachment_handler(event: MessageCreated):
diff --git a/examples/README.md b/examples/README.md
index bf3cf0fc..28ffa54a 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -46,6 +46,7 @@ echo 'MAX_BOT_TOKEN=ваш_токен' > .env
Затем в коде бота перед созданием `Bot()`:
```python
from dotenv import load_dotenv
+
load_dotenv() # Загружает переменные из .env в os.environ
bot = Bot() # Автоматически берёт токен из MAX_BOT_TOKEN
@@ -380,18 +381,20 @@ python examples/01_echo_bot.py
from maxapi import Bot, Dispatcher
# Инициализация
-bot = Bot() # Токен из MAX_BOT_TOKEN
+bot = Bot() # Токен из MAX_BOT_TOKEN
dp = Dispatcher()
+
# Обработчики
@dp.message_created(...)
-async def handler(event):
- ...
+async def handler(event): ...
+
# Запуск
async def main():
await dp.start_polling(bot)
+
if __name__ == "__main__":
asyncio.run(main())
```
@@ -413,6 +416,7 @@ if __name__ == "__main__":
**Как включить debug-логирование?**
```python
import logging
+
logging.basicConfig(level=logging.DEBUG)
```
Логгеры библиотеки: `bot`, `dispatcher`, `connection`.
@@ -424,6 +428,7 @@ logging.basicConfig(level=logging.DEBUG)
Используйте `RedisContext` вместо `MemoryContext`:
```python
import redis.asyncio as redis
+
dp = Dispatcher(
storage=RedisContext,
redis_client=redis.from_url("redis://localhost"),
diff --git a/maxapi/filters/state.py b/maxapi/filters/state.py
index 16ff1d61..95c11fcc 100644
--- a/maxapi/filters/state.py
+++ b/maxapi/filters/state.py
@@ -10,7 +10,7 @@
if TYPE_CHECKING:
from ..types.updates import UpdateUnion
-StateType: TypeAlias = State | str | None | StatesGroup | type[StatesGroup]
+StateType: TypeAlias = State | str | StatesGroup | type[StatesGroup] | None
StateInput: TypeAlias = StateType | Iterable[StateType]
_UNSET = object()
diff --git a/maxapi/types/fetchable.py b/maxapi/types/fetchable.py
index c2bb535d..2a6ff2cd 100644
--- a/maxapi/types/fetchable.py
+++ b/maxapi/types/fetchable.py
@@ -43,7 +43,7 @@ def __init__(
self._fetcher = fetcher
self._setter = setter
self._description = description
- self._resolved: ResolvedValue | None | object = _UNSET
+ self._resolved: ResolvedValue | object | None = _UNSET
self._fetch_lock = asyncio.Lock()
async def fetch(self) -> ResolvedValue | None:
diff --git a/maxapi/types/message.py b/maxapi/types/message.py
index f35515c9..a5906364 100644
--- a/maxapi/types/message.py
+++ b/maxapi/types/message.py
@@ -200,7 +200,7 @@ def _utf16_to_py_index(utf16_pos: int) -> int:
}
char_styles: list[
- list[tuple[TextStyle, str | None | tuple[str, int]]]
+ list[tuple[TextStyle, str | tuple[str, int] | None]]
] = []
for i in range(len(text)):
utf16_i = utf16_offsets[i] if i < len(utf16_offsets) else 0
@@ -244,7 +244,7 @@ def _utf16_to_py_index(utf16_pos: int) -> int:
def wrap_chunk(
chunk: str,
- tags: list[tuple[TextStyle, str | None | tuple[str, int]]],
+ tags: list[tuple[TextStyle, str | tuple[str, int] | None]],
) -> object:
node: object = chunk
for style, val in reversed(tags):
diff --git a/pyproject.toml b/pyproject.toml
index 9bd0f296..0c7c4348 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -137,6 +137,7 @@ ignore = [
"ARG",
"ANN",
"COM812",
+ "CPY",
"D",
"DTZ",
"EXE",