Урок 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() и другие.
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, если ты хочешь сделать роутер самодостаточным.
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.
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()
Подключает один роутер:
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()
Подключает несколько роутеров за раз:
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
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
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
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 к одному роутеру, но сами делятся на подгруппы.
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 будут активны
💡 Когда использовать вложенные роутеры?
Представь, что у тебя есть роутер для магазина, внутри которого роутеры для разных отделов: электроника, одежда, цифровые товары. Вложенные роутеры помогают сохранить иерархию.
Полный пример: многофайловый проект
# ============= 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())
Что важно запомнить
cp для доступа к зависимостям.Практическое задание
Задание: Организуй многофайловый проект с роутерами
Создай структуру из трёх файлов (можно в одном скрипте, разделив комментариями):
- Роутер A — обрабатывает invoice_paid, логирует оплату
- Роутер B — обрабатывает check_paid, отправляет уведомление
- main — создаёт клиент с DI (db, logger), подключает оба роутера через include_routers(), запускает polling
Подсказка:
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 вопросов