Асинхронный фреймворк для MAX Bot API
Project description
maxio
Асинхронный Python-фреймворк для MAX Bot API
с внедрением зависимостей по аннотациям типов.
Объявляйте в сигнатуре хэндлера только то, что нужно — фреймворк подставит из контекста сам:
@app.message(F.text == "привет")
async def greet(message: Message, bot: Bot) -> None:
await message.answer("Привет!")
Никаких context["bot"], никакого middleware_data. Просто типы.
Установка
pip install maxio
Python 3.10+, зависимости: httpx + pydantic v2.
Быстрый старт
Получите токен у @MasterBot:
import os
from maxio import Bot, Callback, F, InlineKeyboard, MaxBot, Message, Update
from maxio.keyboards import Button
app = MaxBot(os.environ["MAX_TOKEN"])
# Синяя кнопка Start шлёт событие bot_started (без message), а не /start.
@app.bot_started()
async def on_start(update: Update, bot: Bot) -> None:
kb = InlineKeyboard().row(Button.callback("Нажми меня", "ping"))
await bot.send_message("Привет!", chat_id=update.chat_id, keyboard=kb)
@app.message(F.text == "/help")
async def help_cmd(message: Message) -> None:
await message.answer("Это бот на фреймворке maxio!")
@app.callback(F.data == "ping")
async def on_ping(callback: Callback) -> None:
await callback.answer(notification="Понг!")
if callback.message:
await callback.message.answer("Ты нажал кнопку!")
@app.message()
async def echo(message: Message) -> None:
await message.reply(message.text or "")
if __name__ == "__main__":
app.run()
MAX_TOKEN=<ваш_токен> python bot.py
Декораторы событий
Имена совпадают с типами событий MAX Bot API. Все 11 типов покрыты.
Сообщения
| Декоратор | Тип события | Когда |
|---|---|---|
@app.message() |
message_created |
новое сообщение |
@app.message_edited() |
message_edited |
сообщение отредактировано |
@app.message_removed() |
message_removed |
сообщение удалено (update.message_id, chat_id) |
@app.callback() |
message_callback |
нажата inline-кнопка |
Чаты
| Декоратор | Тип события | Когда |
|---|---|---|
@app.chat_created() |
message_chat_created |
создан групповой чат с ботом |
@app.chat_title_changed() |
chat_title_changed |
изменён заголовок (update.title) |
Участники
| Декоратор | Тип события | Когда |
|---|---|---|
@app.user_added() |
user_added |
пользователь добавлен (update.inviter_id) |
@app.user_removed() |
user_removed |
пользователь удалён (update.admin_id) |
Бот
| Декоратор | Тип события | Когда |
|---|---|---|
@app.bot_started() |
bot_started |
нажата кнопка «Начать» (update.payload) |
@app.bot_added() |
bot_added |
бот добавлен в чат/канал (update.is_channel) |
@app.bot_removed() |
bot_removed |
бот удалён из чата/канала |
Низкоуровневый
| Декоратор | Когда |
|---|---|
@app.event(*types) |
подписка на произвольные типы (или все — без аргументов) |
Несколько хэндлеров — первый подходящий выигрывает (first-match-wins).
Чем bot_started отличается от /start
Синяя кнопка Start в MAX не отправляет текст /start — она шлёт отдельное событие
bot_started, в котором нет объекта Message. Отвечать нужно через Bot:
@app.bot_started()
async def on_start(update: Update, bot: Bot) -> None:
await bot.send_message("Привет!", chat_id=update.chat_id)
@app.message(Command("start")) # ручной ввод /start
async def start_cmd(message: Message) -> None:
await message.answer("Привет!")
F — Magic Filter
F — ленивый объект для построения фильтров. Читается как обычное выражение:
from maxio import F
@app.message(F.text == "да")
@app.message(F.text.startswith("/"))
@app.message(F.text.in_("стоп", "отмена"))
@app.message(F.photo) # есть фото-вложение
@app.callback(F.data == "buy")
@app.callback(F.data.in_("buy", "sell"))
Шорткаты — наиболее частые поля доступны напрямую:
| Выражение | Что проверяет |
|---|---|
F.text |
update.message.text |
F.data |
update.callback.payload |
F.payload |
update.payload (deep link в bot_started) |
F.photo / F.image |
есть вложение типа image |
F.video |
есть вложение типа video |
F.audio |
есть вложение типа audio |
F.file / F.document |
есть вложение типа file |
Полный путь тоже работает: F.message.sender.user_id == 5
Операторы:
F.text == "да" # равенство
F.text != "нет" # неравенство
F.text.startswith("/") # начинается с
F.text.endswith("!") # заканчивается на
F.text.contains("ключ") # содержит подстроку
F.data.in_("a", "b", "c") # входит в список
F.data.not_in_("x", "y") # не входит в список
~F.photo # NOT
F.text & F.photo # AND
F.text | F.data # OR
Старые фильтры Command, CallbackPayload, HasMedia тоже работают — F не замена, а дополнение.
Фильтры
Любой callable (Update) -> bool или объект с async def check(update) -> bool — фильтр.
Несколько фильтров в декораторе объединяются как AND.
from maxio import Command, HasMedia
@app.message(Command("help"))
async def help_cmd(message: Message) -> None: ...
# Функция как фильтр
def is_private(update: Update) -> bool:
return bool(update.message and update.message.recipient.chat_type == "dialog")
@app.message(is_private, F.text)
async def echo(message: Message) -> None: ...
# HasMedia — по типу вложения
@app.message(HasMedia("image"))
async def got_photo(message: Message) -> None: ...
DI — внедрение зависимостей
Объявляйте в сигнатуре хэндлера только нужное — фреймворк резолвит по типу:
@app.message()
async def handler(message: Message, bot: Bot, fsm: FSMContext) -> None: ...
Доступные типы:
| Тип | Когда доступен |
|---|---|
Update |
всегда |
Bot |
всегда |
FSMContext |
всегда |
Message |
message_created, message_edited, message_callback |
Callback |
message_callback |
User |
отправитель / инициатор события |
Chat |
message_chat_created |
Optional — безопасно, если тип недоступен для данного события:
@app.bot_started()
async def on_start(update: Update, message: Message | None) -> None:
# message будет None — bot_started не несёт объект Message
...
Несовместимый тип (не Optional и нет дефолта) → понятная ошибка MaxError.
Middleware
Middleware — callable-объект или функция, прикрепляется к MaxBot или Router.
Порядок: app.outer → router.outer → app.inner → router.inner → handler
Outer middleware
Оборачивает всю диспетчеризацию. Может прервать цепочку (не вызвать call_next).
Аргументы резолвятся по DI — объявляйте только нужное:
from maxio.middleware import CallNextOuter
async def timing(update: Update, call_next: CallNextOuter) -> bool:
t = time.monotonic()
result = await call_next()
print(f"{update.update_type} → {time.monotonic() - t:.3f}s")
return result
app.outer_middleware(timing) # все апдейты
app.outer_middleware(timing, UpdateType.MESSAGE_CREATED) # только нужный тип
DI в outer middleware: можно инжектировать Bot, User, Message | None и т.д.:
async def require_auth(call_next: CallNextOuter, user: User | None) -> bool:
if not user or user.user_id not in ALLOWED:
return False
return await call_next()
Inner middleware
Вызывается после выбора хэндлера. Получает CallNextInner и любые DI-типы.
HandlerKwargs — уже резолвленные аргументы, которые пойдут в хэндлер:
from maxio.middleware import CallNextInner, HandlerKwargs
async def log_args(call_next: CallNextInner, kwargs: HandlerKwargs) -> None:
print("хэндлер получит:", list(kwargs.keys()))
await call_next()
app.inner_middleware(log_args)
Роутеры
from maxio import Router
admin = Router()
users = Router()
app.include_routers(admin, users)
@admin.message(Command("ban"))
async def ban(message: Message) -> None: ...
Middleware на роутере срабатывает только если хэндлер принадлежит этому роутеру.
FSM — диалоги с состоянием
from maxio import StatesGroup, State, StateFilter, FSMContext
class Form(StatesGroup):
waiting_name = State()
waiting_age = State()
@app.message(Command("register"))
async def start_form(message: Message, fsm: FSMContext) -> None:
await fsm.set_state(Form.waiting_name)
await message.answer("Как тебя зовут?")
@app.message(StateFilter(Form.waiting_name))
async def got_name(message: Message, fsm: FSMContext) -> None:
await fsm.update_data(name=message.text)
await fsm.set_state(Form.waiting_age)
await message.answer("Сколько тебе лет?")
@app.message(StateFilter(Form.waiting_age))
async def got_age(message: Message, fsm: FSMContext) -> None:
data = await fsm.get_data()
await fsm.clear()
await message.answer(f"{data['name']}, {message.text} лет — записал!")
@app.message(Command("cancel"), StateFilter(Form.waiting_name, Form.waiting_age))
async def cancel(message: Message, fsm: FSMContext) -> None:
await fsm.clear()
await message.answer("Отменено.")
FSMContext инжектируется по типу. По умолчанию — MemoryStorage.
Своё хранилище: MaxBot(token, storage=MyStorage()).
Медиа
from maxio import HasMedia, F, media
from maxio.enums import UploadType
from pathlib import Path
# Отправить картинку из файла
@app.message(Command("photo"))
async def send_photo(message: Message, bot: Bot) -> None:
token = await bot.upload(Path("photo.jpg"), UploadType.IMAGE)
await message.answer("Держи!", attachments=[media.image(token)])
# Принять фото через F-фильтр
@app.message(F.photo)
async def got_photo(message: Message) -> None:
for photo in message.photos: # list[PhotoAttachmentPayload]
await message.answer(f"URL: {photo.url}")
# Принять файл
@app.message(F.file)
async def got_file(message: Message) -> None:
for f in message.files: # list[FileAttachmentPayload]
await message.answer(f"{f.filename} — {f.size} байт")
Bot.upload принимает bytes, IO[bytes] или Path.
Типы: IMAGE, VIDEO, AUDIO, FILE.
Inline-клавиатуры
from maxio import InlineKeyboard
from maxio.keyboards import Button
from maxio.enums import Intent
kb = (
InlineKeyboard()
.row(
Button.callback("✅ Ок", "ok", intent=Intent.POSITIVE),
Button.callback("❌ Отмена", "cancel", intent=Intent.NEGATIVE),
)
.row(Button.link("Сайт", "https://example.com"))
.row(Button.request_contact("📱 Поделиться номером"))
)
await message.answer("Выбери:", keyboard=kb)
MaxBot — параметры запуска
app = MaxBot(
token="...",
storage=MyStorage(), # FSM-хранилище (по умолч. MemoryStorage)
timeout=60.0, # таймаут HTTP-запросов в секундах (по умолч. 100.0)
mask_token_in_logs=True, # скрыть токен в логах httpx (по умолч. True)
)
app.run() # запуск polling, блокирующий
# или
await app.start_polling() # async-вариант
Методы Bot
await bot.get_me()
await bot.send_message(text, chat_id=..., user_id=..., keyboard=..., attachments=...)
await bot.edit_message(message_id, text=..., keyboard=..., attachments=...)
await bot.delete_message(message_id)
await bot.get_message(message_id)
await bot.get_messages(chat_id)
await bot.answer_callback(callback_id, notification=..., payload=...)
await bot.get_chats()
await bot.upload(file, UploadType.IMAGE)
Возможности
- Long polling с автоматическим переподключением
- Все 11 типов событий MAX Bot API — именованные декораторы для каждого
- F (MagicFilter) — ленивые фильтры-выражения:
F.text == "да",F.photo,F.data.in_(...) - Роутеры (
Router) иinclude_routersдля разбивки хэндлеров - DI в middleware —
CallNextOuter,CallNextInner,HandlerKwargsинжектируются по типу - Optional в DI —
Message | NoneподставляетNoneвместо ошибки - Middleware: outer / inner, на
MaxBotи наRouter, по типам апдейтов - FSM:
StatesGroup,State,FSMContext,StateFilter,MemoryStorage - Медиа:
Bot.upload(),media.image/video/audio/file(), фильтрыF.photo/HasMedia - Inline-клавиатуры: callback / link / request_contact / request_geo_location
- Сахар:
message.answer(),message.reply(),callback.answer(),callback.message.answer() - Pydantic v2,
py.typed,mypy --strict✅
Спонсоры
|
sudoteach.com
Генеральный спонсор. Онлайн-школа программирования — там есть курс «Боты на maxio»: от установки до продакшна. |
bothost.ru
Официальный партнёр. Хостинг для ботов MAX и Telegram — деплой в один клик, мониторинг, автозапуск. |
Лицензия
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file maxio-0.4.0.tar.gz.
File metadata
- Download URL: maxio-0.4.0.tar.gz
- Upload date:
- Size: 37.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fac33bf4dfac65e6d868c0a5a05cd340f277ff9fe6814f7af9aa8d9186d67c2d
|
|
| MD5 |
bd38490d57b200cb3c33b7bfc931b86a
|
|
| BLAKE2b-256 |
2d38a7b3509590b100acb3f6f48a0b72e8cf124a38d88697e8a2c9290378e0e4
|
File details
Details for the file maxio-0.4.0-py3-none-any.whl.
File metadata
- Download URL: maxio-0.4.0-py3-none-any.whl
- Upload date:
- Size: 32.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1f2a7a5bc8fb2267ee41f42560572012b1b13e28c750cf7e1e9e1d60db46fef
|
|
| MD5 |
c9c7d422a3eca3110afef17bb3a2fd90
|
|
| BLAKE2b-256 |
6a5bc61ad7c57626144de2dab338df3554091b557b5c60145f7d64ff4d01b7cf
|