Skip to main content

Асинхронный фреймворк для MAX Bot API

Project description

PyPI version Python versions Monthly downloads License: MIT mypy strict

maxio

Асинхронный Python-фреймворк для MAX Bot API
с внедрением зависимостей по аннотациям типов.


Объявляйте в сигнатуре хэндлера то, что нужно, — фреймворк подставит из контекста апдейта сам:

@app.message(Command("help"))
async def help_cmd(message: Message) -> None:
    await message.answer("Это бот на фреймворке maxio!")

Никаких context["bot"], никакого middleware_data. Просто типы.

Установка

pip install maxio

Python 3.10+, зависимости: httpx + pydantic v2.

Быстрый старт

Получите токен у @MasterBot:

import os
from maxio import MaxBot, Message, Callback, Command, InlineKeyboard, Update, Bot
from maxio.keyboards import Button

app = MaxBot(token=os.environ["MAX_TOKEN"])


# Синяя кнопка Start шлёт событие bot_started (без message), а не /start.
@app.bot_started()
async def on_start(update: Update, bot: Bot):
    kb = InlineKeyboard().row(Button.callback("Нажми меня", "ping"))
    await bot.send_message("Привет! Я готов к работе.", chat_id=update.chat_id, keyboard=kb)


@app.message(Command("help"))
async def help_cmd(message: Message):
    await message.answer("Это бот на фреймворке maxio!")


@app.callback()
async def on_ping(callback: Callback):
    await callback.answer(notification="Понг!")        # всплывашка на кнопке
    if callback.message:
        await callback.message.answer("Ты нажал кнопку!")  # новое сообщение в чат


@app.message()
async def echo(message: Message):
    await message.reply(message.text or "")


if __name__ == "__main__":
    app.run()
MAX_TOKEN=<ваш_токен> python bot.py

Декораторы событий

Имена декораторов совпадают с типами событий MAX Bot API.

Декоратор Тип события Когда срабатывает
@app.message() message_created пришло новое сообщение от пользователя
@app.message_edited() message_edited пользователь отредактировал своё сообщение
@app.callback() message_callback нажата inline-кнопка (callback) под сообщением
@app.bot_started() bot_started пользователь запустил бота — синяя кнопка Start
@app.event(*types) любые подписка на произвольные типы апдейтов (или все — без аргументов)

Несколько хэндлеров — первый подходящий выигрывает (first-match-wins).

Чем message отличается от bot_started

Это частая путаница. Синяя кнопка Start в MAX не отправляет текст /start — она шлёт отдельное событие bot_started, в котором нет объекта Message (есть chat_id, user и опциональный payload из диплинка). Поэтому:

  • хэндлер @app.message(Command("start")) ловит только ручной ввод /start;
  • нажатие кнопки Start ловится через @app.bot_started().

Если нужно, чтобы и кнопка, и команда здоровались одинаково — заведите оба обработчика. Внутри bot_started нет Message, поэтому отвечать надо через Bot, указывая chat_id:

@app.bot_started()
async def on_start(update: Update, bot: Bot):
    await bot.send_message("Привет! Нажми кнопку ниже.", chat_id=update.chat_id)


@app.message(Command("start"))           # ручной ввод /start
async def start_cmd(message: Message):
    await message.answer("Привет! Нажми кнопку ниже.")

callback — нажатия inline-кнопок

Срабатывает, когда пользователь жмёт кнопку, созданную через Button.callback(text, payload). В обработчике доступен Callback; отвечать на нажатие нужно callback.answer(...), иначе у пользователя останется «крутилка» на кнопке. Исходное сообщение с кнопкой лежит в callback.message — у него есть привычные answer() / reply() для отправки в тот же чат.

@app.callback(CallbackPayload("buy"))    # фильтр по payload кнопки
async def buy(callback: Callback):
    await callback.answer(notification="Покупка оформлена")   # всплывашка на кнопке
    if callback.message:
        await callback.message.answer("Спасибо за заказ!")    # новое сообщение в чат

Фильтры

from maxio import Command, CallbackPayload

@app.message(Command("help"))
async def help_cmd(message: Message): ...

@app.callback(CallbackPayload("buy"))
async def buy(callback: Callback): ...

# Произвольный callable — тоже фильтр
@app.message(lambda u: u.message and len(u.message.text or "") > 100)
async def long_message(message: Message): ...

Inline-клавиатуры

from maxio import InlineKeyboard
from maxio.keyboards import Button

kb = (
    InlineKeyboard()
    .row(Button.callback("Да", "yes"), Button.callback("Нет", "no"))
    .row(Button.link("Сайт", "https://example.com"))
)
await message.answer("Выберите:", keyboard=kb)

Методы API

@app.message()
async def handler(message: Message, bot: Bot):
    me = await bot.get_me()
    await bot.send_message(chat_id=message.recipient.chat_id, text=f"Я — {me.name}")
    await bot.edit_message(message_id=message.message_id, text="Обновлено")
    await bot.delete_message(message_id=message.message_id)
    chats = await bot.get_chats()

Роутеры

Разбивайте хэндлеры по файлам через Router:

from maxio import Router

admin = Router()
users = Router()

app.include_routers(admin, users)

@admin.message(Command("ban"))
async def ban(message: Message): ...

Порядок проверки: app → роутеры в порядке include_routers. Middleware, зарегистрированная на роутере, срабатывает только если хэндлер принадлежит этому роутеру.

Middleware

Middleware — callable-объект или функция, прикрепляется к MaxBot или Router:

from maxio.middleware import CallNextOuter

class TimingMiddleware:
    async def __call__(self, 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(TimingMiddleware())         # на все апдейты
app.outer_middleware(log_fn, UpdateType.MESSAGE_CREATED)  # только на нужный тип

Порядок: app.outer → router.outer → app.inner → router.inner → handler.

inner_middleware вызывается после выбора хэндлера и получает (handler_fn, kwargs, call_next) — можно читать и изменять уже резолвленные аргументы.

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} лет — записал!")

FSMContext инжектируется в хэндлер по типу аннотации — никакого state.get_state() вручную. По умолчанию состояния хранятся в памяти (MemoryStorage). Своё хранилище: MaxBot(token, storage=MyRedisStorage()).

Медиа — загрузка и получение файлов

from maxio import HasMedia
from maxio import 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)])

# Принять картинку от пользователя
@app.message(HasMedia("image"))
async def got_photo(message: Message) -> None:
    for photo in message.photos:   # список PhotoAttachmentPayload
        await message.answer(f"URL: {photo.url}")

# Принять любой файл
@app.message(HasMedia("file"))
async def got_file(message: Message) -> None:
    for f in message.files:        # список FileAttachmentPayload
        await message.answer(f"Файл: {f.filename} ({f.size} байт)")

Bot.upload принимает bytes, IO[bytes] или Path; поддерживаемые типы: IMAGE, VIDEO, AUDIO, FILE.

DI — внедрение зависимостей

Тип Когда доступен
Message message_created, message_edited, callback
Callback message_callback
User отправитель/инициатор события
Chat message_chat_created
FSMContext всегда (текущий контекст FSM)
Update всегда (сырой апдейт)
Bot всегда (HTTP-клиент, прямой доступ к API)

Несовместимый тип → понятная ошибка MaxError с описанием проблемы.

Возможности

  • Long polling с автоматическим переподключением
  • Роутеры (Router) и include_routers для разбивки хэндлеров
  • Middleware: outer / inner, на MaxBot и на Router, по типам апдейтов
  • FSM: StatesGroup, State, FSMContext, StateFilter, MemoryStorage
  • Медиа: Bot.upload(), media.image/video/audio/file(), HasMedia фильтр
  • HTTP-клиент: get_me, send_message, edit_message, delete_message, get_messages, get_chats, answer_callback, get_updates
  • Декораторы: message, message_edited, callback, bot_started, event
  • DI по аннотациям типов — без Depends()
  • Фильтры: Command, CallbackPayload, HasMedia, любой Callable
  • Inline-клавиатуры: callback / link / 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 — деплой в один клик, мониторинг, автозапуск.

Лицензия

MIT

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

maxio-0.3.0.tar.gz (32.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

maxio-0.3.0-py3-none-any.whl (29.4 kB view details)

Uploaded Python 3

File details

Details for the file maxio-0.3.0.tar.gz.

File metadata

  • Download URL: maxio-0.3.0.tar.gz
  • Upload date:
  • Size: 32.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for maxio-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e8ccb2adbbdb0ff65a26c554b9bf3de27852b214887a897a1003d07d97d43e57
MD5 ae73e42c64220cc08362b034b09d1001
BLAKE2b-256 270359f3c7dfb0a89c1ff3cdb27a84097cf42d1f2f7a86c1923d0887e079eb7b

See more details on using hashes here.

File details

Details for the file maxio-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: maxio-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 29.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for maxio-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2608cf91ec59035f71731dc436ceb4a89ff84fcea3bf350630da12f3fadae9e9
MD5 df1b131ede55afb4c5485de463b4ecc3
BLAKE2b-256 324e0b5d407dfb996585bc8de225e3808ff9ac91c65216e745aee75577d851a9

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page