$ sudo teach IT
Модуль 7 · Продвинутые техники

Урок 7.4 — Роутеры

Изучаем систему роутеров aiosend: BaseRouter, PollingRouter, WebhookRouter, а также функции include_router() и include_routers() для модульной архитектуры.

Роутеры в aiosend — это механизм для организации обработчиков событий (invoice_paid, check_paid и т.д.) в отдельные модули. Вместо того чтобы регистрировать все хендлеры на одном объекте CryptoPay, ты можешь распределить их по роутерам, а затем подключить роутеры к клиенту. Это делает код чище и удобнее для больших проектов.

🗂️

BaseRouter — базовый роутер

BaseRouter — это базовый класс для всех роутеров. Он предоставляет те же декораторы для регистрации хендлеров, что и CryptoPay: @router.invoice_paid(), @router.check_paid() и другие.

Python · Создание роутера и регистрация хендлеров
from aiosend import CryptoPay, BaseRouter

# Создаём роутер
router = BaseRouter()

# Регистрируем хендлеры на роутере
@router.invoice_paid()
async def on_invoice_paid(invoice):
    print(f"Инвойс #{invoice.invoice_id} оплачен!")

@router.check_paid()
async def on_check_paid(check):
    print(f"Чек #{check.check_id} активирован!")

@router.invoice_expired()
async def on_invoice_expired(invoice):
    print(f"Инвойс #{invoice.invoice_id} истёк")

# Подключаем роутер к клиенту
cp = CryptoPay(token="1234:TOKEN")
cp.include_router(router)

# После include_router все хендлеры роутера активны
await cp.start_polling()

💡 BaseRouter — это «лёгкая» версия CryptoPay

BaseRouter не делает HTTP-запросов и не требует токена. Он только хранит зарегистрированные хендлеры. При подключении к клиенту через include_router() все хендлеры копируются в клиент.

Доступные декораторы BaseRouter:

Декоратор Событие
@router.invoice_paid() Инвойс оплачен
@router.invoice_expired() Инвойс просрочен
@router.check_paid() Чек активирован
@router.update() Любое обновление
🔄

PollingRouter — роутер для polling

PollingRouter расширяет BaseRouter и добавляет функциональность для работы с polling. По сути, это тот же BaseRouter, но с дополнительными методами для запуска/остановки polling, если ты хочешь сделать роутер самодостаточным.

Python · PollingRouter
from aiosend import CryptoPay, PollingRouter

# PollingRouter — наследник BaseRouter
router = PollingRouter()

@router.invoice_paid()
async def on_paid(invoice):
    print(f"Polling: инвойс #{invoice.invoice_id} оплачен!")

cp = CryptoPay(token="1234:TOKEN")
cp.include_router(router)

# Запускаем polling (хендлеры из роутера будут работать)
await cp.start_polling()

# PollingRouter можно использовать там же, где и BaseRouter.
# Разница только в том, что у PollingRouter есть доп. методы
# для самостоятельного управления polling.

⚠️ PollingRouter vs BaseRouter

В большинстве случаев тебе достаточно BaseRouter. PollingRouter нужен, если ты хочешь создать модуль, который сам управляет своим polling-циклом (например, для библиотек или плагинов).

🌐

WebhookRouter — роутер для вебхуков

WebhookRouter — роутер, предназначенный для работы с вебхуками. Он также расширяет BaseRouter.

Python · WebhookRouter с FastAPI
from aiosend import CryptoPay, WebhookRouter
from aiosend.webhook import FastAPIManager
from fastapi import FastAPI

app = FastAPI()
manager = FastAPIManager(app, path="/webhook")

# Создаём роутер для вебхуков
router = WebhookRouter()

@router.invoice_paid()
async def on_paid(invoice):
    print(f"Webhook: инвойс #{invoice.invoice_id} оплачен!")

cp = CryptoPay(
    token="1234:TOKEN",
    webhook_manager=manager,
)
cp.include_router(router)

# Все хендлеры из роутера будут обрабатывать вебхуки
# FastAPI сам запускается через uvicorn

🔍 Разница между роутерами

BaseRouter — универсальный роутер. PollingRouter — то же самое, но с доп. методами для запуска polling. WebhookRouter — для вебхуков. На практике BaseRouter покрывает 95% случаев.

🔗

include_router() и include_routers()

Методы include_router() и include_routers() подключают роутеры к клиенту. При подключении все хендлеры из роутера копируются в клиент.

include_router()

Подключает один роутер:

Python · include_router
from aiosend import CryptoPay, BaseRouter

router = BaseRouter()

@router.invoice_paid()
async def handler(invoice):
    print(f"Оплачен: #{invoice.invoice_id}")

cp = CryptoPay(token="1234:TOKEN")
cp.include_router(router)  # подключаем один роутер

include_routers()

Подключает несколько роутеров за раз:

Python · include_routers
from aiosend import CryptoPay, BaseRouter

router1 = BaseRouter()
router2 = BaseRouter()
router3 = BaseRouter()

# Регистрируем хендлеры на каждом роутере
@router1.invoice_paid()
async def handler1(invoice):
    print(f"Хендлер 1: инвойс #{invoice.invoice_id}")

@router2.invoice_paid()
async def handler2(invoice):
    print(f"Хендлер 2: инвойс #{invoice.invoice_id}")

@router3.invoice_paid()
async def handler3(invoice):
    print(f"Хендлер 3: инвойс #{invoice.invoice_id}")

cp = CryptoPay(token="1234:TOKEN")

# Подключаем все три роутера одной строкой
cp.include_routers(router1, router2, router3)

# Все хендлеры из всех роутеров будут вызваны
# при оплате инвойса (один за другим)
await cp.start_polling()

💡 Порядок вызова хендлеров

Хендлеры из подключённых роутеров вызываются в порядке подключения. Сначала хендлеры из первого роутера, затем из второго и т.д. Если тебе важен порядок — подключай роутеры в нужной последовательности.

🏗️

Модульная архитектура с роутерами

Роутеры позволяют организовать код в многофайловую структуру. Каждый роутер — это отдельный файл (модуль) со своими хендлерами. Основной файл только подключает роутеры и запускает клиент.

Пример структуры проекта:

project/
├── main.py              # Точка входа
├── routers/
│   ├── __init__.py
│   ├── payments.py      # Роутер для платежей
│   ├── checks.py        # Роутер для чеков
│   └── admin.py         # Роутер для админ-событий
└── services/
    ├── __init__.py
    ├── database.py
    └── notifier.py

routers/payments.py

Python · Роутер платежей
from aiosend import BaseRouter

router = BaseRouter()

@router.invoice_paid()
async def on_invoice_paid(invoice, cp):
    """Обработка оплаты инвойса."""
    db = cp.get("db")
    if db:
        await db.save_payment(invoice)
    print(f"Платёж #{invoice.invoice_id} обработан")

@router.invoice_expired()
async def on_invoice_expired(invoice):
    """Обработка просроченного инвойса."""
    print(f"Инвойс #{invoice.invoice_id} просрочен")

routers/checks.py

Python · Роутер чеков
from aiosend import BaseRouter

router = BaseRouter()

@router.check_paid()
async def on_check_paid(check, cp):
    """Обработка активации чека."""
    notifier = cp.get("notifier")
    if notifier:
        await notifier.send_notification(
            f"Чек #{check.check_id} активирован!"
        )
    print(f"Чек #{check.check_id} активирован на {check.amount}")

main.py

Python · Точка входа
import asyncio
from aiosend import CryptoPay
from routers.payments import router as payments_router
from routers.checks import router as checks_router

async def main():
    # Создаём клиент с DI
    cp = CryptoPay(
        token="1234:TOKEN",
        db=Database(),
        notifier=Notifier(),
    )

    # Подключаем все роутеры
    cp.include_routers(
        payments_router,
        checks_router,
    )

    # Запускаем polling
    await cp.start_polling()

asyncio.run(main())

🔍 Преимущества модульной архитектуры

Каждый роутер — это отдельный файл с чёткой зоной ответственности. Хендлеры не свалены в одну кучу. DI позволяет передавать зависимости в роутеры без циклических импортов. Код легко тестировать и поддерживать.

🪆

Вложенные роутеры

Роутеры можно вкладывать друг в друга. Это полезно, когда у тебя есть группа хендлеров, которые логически belong к одному роутеру, но сами делятся на подгруппы.

Python · Вложенные роутеры
from aiosend import BaseRouter

# Создаём «подроутеры»
payment_router = BaseRouter()
check_router = BaseRouter()

@payment_router.invoice_paid()
async def on_paid(invoice):
    print(f"Оплачен инвойс #{invoice.invoice_id}")

@check_router.check_paid()
async def on_check(check):
    print(f"Активирован чек #{check.check_id}")

# Создаём главный роутер и вкладываем подроутеры
main_router = BaseRouter()
main_router.include_router(payment_router)
main_router.include_router(check_router)

# Подключаем главный роутер к клиенту
cp = CryptoPay(token="1234:TOKEN")
cp.include_router(main_router)
# Все хендлеры из payment_router и check_router будут активны

💡 Когда использовать вложенные роутеры?

Представь, что у тебя есть роутер для магазина, внутри которого роутеры для разных отделов: электроника, одежда, цифровые товары. Вложенные роутеры помогают сохранить иерархию.

🚀

Полный пример: многофайловый проект

Python · Структура и код
# ============= routers/__init__.py =============
from .payments import router as payments_router
from .checks import router as checks_router
from .admin import router as admin_router

# ============= routers/payments.py =============
from aiosend import BaseRouter

router = BaseRouter()

@router.invoice_paid()
async def payment_handler(invoice, cp):
    db = cp["db"]
    await db.save({
        "type": "invoice",
        "id": invoice.invoice_id,
        "amount": invoice.amount,
        "asset": invoice.asset,
    })

@router.invoice_expired()
async def expiry_handler(invoice):
    print(f"Просрочен: #{invoice.invoice_id}")

# ============= routers/checks.py =============
from aiosend import BaseRouter

router = BaseRouter()

@router.check_paid()
async def check_handler(check, cp):
    notifier = cp["notifier"]
    await notifier.send(f"Чек #{check.check_id} активирован")

# ============= routers/admin.py =============
from aiosend import BaseRouter

router = BaseRouter()

@router.invoice_paid()
@router.check_paid()
async def admin_logger(event, cp):
    """Логирует все события."""
    logger = cp["logger"]
    event_type = "invoice" if hasattr(event, "invoice_id") else "check"
    event_id = getattr(event, "invoice_id", getattr(event, "check_id", None))
    await logger.log(f"{event_type} #{event_id}")

# ============= main.py =============
import asyncio
from aiosend import CryptoPay
from routers import (
    payments_router,
    checks_router,
    admin_router,
)

class Database:
    async def save(self, data):
        print(f"[DB] Сохранено: {data}")

class Notifier:
    async def send(self, msg):
        print(f"[NOTIFY] {msg}")

class Logger:
    async def log(self, msg):
        print(f"[LOG] {msg}")

async def main():
    cp = CryptoPay(
        token="1234:TOKEN",
        db=Database(),
        notifier=Notifier(),
        logger=Logger(),
    )

    cp.include_routers(
        payments_router,
        checks_router,
        admin_router,
    )

    print("Запуск polling...")
    await cp.start_polling()

asyncio.run(main())
📌

Что важно запомнить

1️⃣
BaseRouter — базовый класс. Предоставляет декораторы invoice_paid, check_paid, invoice_expired, update.
2️⃣
PollingRouter и WebhookRouter — специализированные версии BaseRouter для polling и вебхуков.
3️⃣
include_router() — для одного, include_routers() — для нескольких. Хендлеры копируются в клиент при подключении.
4️⃣
Роутеры поддерживают DI. Хендлеры на роутерах могут принимать параметр cp для доступа к зависимостям.
5️⃣
Роутеры можно вкладывать. Вызов include_router() доступен и на самом роутере, создавая иерархию.
🎯

Практическое задание

Задание: Организуй многофайловый проект с роутерами

Создай структуру из трёх файлов (можно в одном скрипте, разделив комментариями):

  • Роутер A — обрабатывает invoice_paid, логирует оплату
  • Роутер B — обрабатывает check_paid, отправляет уведомление
  • main — создаёт клиент с DI (db, logger), подключает оба роутера через include_routers(), запускает polling

Подсказка:

Python · Шаблон решения
import asyncio
from aiosend import CryptoPay, BaseRouter

# Роутер A
router_a = BaseRouter()
@router_a.invoice_paid()
async def on_paid(invoice, cp):
    logger = cp.get("logger")
    if logger:
        await logger.log(f"Оплачен инвойс #{invoice.invoice_id}")

# Роутер B
router_b = BaseRouter()
@router_b.check_paid()
async def on_check(check, cp):
    notifier = cp.get("notifier")
    if notifier:
        await notifier.send(f"Активирован чек #{check.check_id}")

# Сервисы
class Logger:
    async def log(self, msg):
        print(f"[LOG] {msg}")

class Notifier:
    async def send(self, msg):
        print(f"[NOTIFY] {msg}")

# main
async def main():
    cp = CryptoPay(
        token="YOUR_TOKEN",
        logger=Logger(),
        notifier=Notifier(),
    )
    cp.include_routers(router_a, router_b)
    await cp.start_polling()

asyncio.run(main())

Урок 7.4: Роутеры

5 вопросов