Урок 7.1 — Dependency Injection в aiosend
Изучаем механизм внедрения зависимостей (DI) в aiosend: передача произвольных kwargs в CryptoPay, чтение значений в хендлерах и передача параметров через invoice.poll().
Dependency Injection (DI) — это паттерн, при котором зависимости не создаются внутри объекта, а передаются извне. В aiosend DI реализован через механизм произвольных аргументов: ты можешь передать любые **kwargs в конструктор CryptoPay() и затем читать их в хендлерах или передавать через invoice.poll(). Это мощный инструмент для организации кода в больших проектах.
Что такое Dependency Injection
В традиционном подходе объект сам создаёт свои зависимости. Например, хендлер сам создаёт подключение к базе данных. При DI зависимости передаются в объект извне — это делает код более гибким, тестируемым и слабо связанным.
В aiosend DI работает через словарь cp["key"] = value. Ты можешь сохранить в клиенте любые данные: пул соединений к БД, экземпляр бота, конфигурацию, сервисы — и затем получить к ним доступ в любом хендлере через cp["key"].
💡 Основная идея
Вместо того чтобы передавать зависимости через глобальные переменные или создавать их внутри хендлеров, ты регистрируешь всё нужное в клиенте при старте приложения. Хендлеры просто обращаются к cp["db"], cp["bot"] и т.д. Это делает код чище и упрощает тестирование.
❌ Без DI (плохо)
db = Database()
@cp.invoice_paid()
async def handler(inv):
# зависимость создаётся вне
# или через глобальную переменную
await db.save_payment(inv)
✅ С DI (хорошо)
cp["db"] = Database()
@cp.invoice_paid()
async def handler(inv, cp):
await cp["db"].save_payment(inv)
Передача произвольных kwargs в CryptoPay()
Конструктор CryptoPay() принимает не только документированные параметры, но и любые дополнительные **kwargs. Все они сохраняются во внутреннем словаре и становятся доступными через cp["key"].
from aiosend import CryptoPay
# Допустим, у нас есть сервисы
class Database:
async def save_payment(self, invoice):
print(f"Сохранён платёж #{invoice.invoice_id}")
class MailService:
async def send_receipt(self, invoice):
print(f"Отправлен чек для #{invoice.invoice_id}")
# Создаём клиент и передаём зависимости
cp = CryptoPay(
token="1234:TOKEN",
db=Database(), # произвольный kwargs
mail=MailService(), # ещё один
config={"lang": "ru"}, # можно и простые значения
)
# Все переданные kwargs доступны через cp["key"]
print(cp["db"]) #
print(cp["mail"]) #
print(cp["config"]) # {"lang": "ru"}
🔍 Как это работает внутри?
Класс CryptoPay наследуется от миксина DIMixin, который реализует методы __setitem__, __getitem__ и __contains__ для работы в стиле словаря. При передаче **kwargs в конструктор, все они сохраняются в self._di_store.
Ты также можешь добавлять и изменять зависимости после создания клиента:
cp = CryptoPay(token="1234:TOKEN")
# Добавляем зависимости после создания
cp["db"] = Database()
cp["mail"] = MailService()
cp["started_at"] = "2026-01-15T10:00:00"
# Проверяем наличие ключа
if "db" in cp:
print("База данных зарегистрирована")
# Удаляем зависимость
del cp["mail"]
⚠️ Важно: конфликты имён
Не используй ключи, совпадающие с именами атрибутов CryptoPay (например, session, network, polling_config). Они уже заняты внутренними механизмами. Используй уникальные имена: "my_db", "user_service", "app_config".
Доступ к зависимостям в хендлерах
Самая мощная особенность DI в aiosend — это автоматическая передача клиента cp в хендлеры. Когда ты регистрируешь хендлер через декоратор @cp.invoice_paid(), aiosend может передать в него объект клиента, если хендлер принимает параметр cp.
import asyncio
from aiosend import CryptoPay
# Сервисы
class PaymentService:
async def process(self, invoice):
print(f"Обработка платежа #{invoice.invoice_id}")
class Logger:
async def log(self, msg):
print(f"[LOG] {msg}")
async def main():
cp = CryptoPay(
token="1234:TOKEN",
payment_service=PaymentService(),
logger=Logger(),
)
@cp.invoice_paid()
async def on_paid(invoice, cp): # cp передаётся автоматически
logger = cp["logger"]
svc = cp["payment_service"]
await logger.log(f"Получен платёж #{invoice.invoice_id}")
await svc.process(invoice)
await cp.start_polling()
asyncio.run(main())
💡 Как aiosend передаёт cp в хендлер?
Когда хендлер вызывается, aiosend проверяет его сигнатуру с помощью inspect.signature(). Если среди параметров есть cp, aiosend автоматически подставляет объект клиента. Если параметра cp нет — он просто не передаётся. Это безопасно и не ломает существующие хендлеры.
Проверка наличия ключа
Всегда проверяй наличие ключа перед использованием, чтобы избежать KeyError:
@cp.invoice_paid()
async def safe_handler(invoice, cp):
# Проверяем наличие ключа
if "db" not in cp:
print("База данных не подключена!")
return
db = cp["db"]
# Используем .get() со значением по умолчанию
config = cp.get("config", {})
lang = config.get("lang", "en")
await db.save_payment(invoice)
print(f"Сохранено. Язык: {lang}")
Передача параметров через invoice.poll()
Метод invoice.poll() позволяет ожидать оплату конкретного инвойса. Через него тоже можно передать произвольные kwargs, которые будут доступны в хендлере on_paid.
Это особенно полезно, когда нужно привязать к платежу дополнительные данные:
- user_id — кому зачислить товар
- product_id — какой товар куплен
- chat_id — куда отправить уведомление
- callback — функцию для вызова после оплаты
import asyncio
from aiosend import CryptoPay
async def main():
cp = CryptoPay(token="1234:TOKEN")
# Создаём инвойс для конкретного пользователя
invoice = await cp.create_invoice(
amount=10,
asset="USDT",
payload="product_123",
)
# Ожидаем оплату, передавая user_id
paid_invoice = await invoice.poll(
on_paid=on_paid, # хендлер
user_id=12345, # произвольный kwargs
product_name="Премиум",
)
async def on_paid(invoice, **kwargs):
user_id = kwargs.get("user_id")
product = kwargs.get("product_name")
print(f"Пользователь {user_id} купил {product}!")
print(f"Инвойс #{invoice.invoice_id} оплачен")
asyncio.run(main())
Передача callback-функции
Ты можешь передать не только данные, но и функции обратного вызова:
async def send_goods(invoice, user_id):
print(f"Отправляем товар пользователю {user_id}")
print(f"Товар по инвойсу #{invoice.invoice_id}")
async def main():
cp = CryptoPay(token="1234:TOKEN")
invoice = await cp.create_invoice(amount=5, asset="USDT")
# Передаём callback через kwargs
paid = await invoice.poll(
on_paid=on_paid,
user_id=42,
callback=send_goods,
chat_id=-1001234567,
)
async def on_paid(invoice, **kwargs):
callback = kwargs.get("callback")
user_id = kwargs.get("user_id")
if callback:
await callback(invoice, user_id)
print(f"Чат для уведомления: {kwargs.get('chat_id')}")
Практический пример: магазин с DI
Давай соберём всё вместе и создадим полноценный пример магазина с использованием DI. У нас будут сервисы для работы с БД, отправки уведомлений и обработки платежей — все они зарегистрированы через DI.
import asyncio
import random
from aiosend import CryptoPay
class Database:
"""Сервис базы данных."""
async def create_order(self, user_id, product, amount):
order_id = random.randint(1000, 9999)
print(f"[DB] Заказ #{order_id}: user={user_id}, product={product}, amount={amount}")
return order_id
async def confirm_payment(self, invoice_id, order_id):
print(f"[DB] Платёж #{invoice_id} подтверждён для заказа #{order_id}")
class Notifier:
"""Сервис уведомлений."""
async def notify_user(self, user_id, message):
print(f"[NOTIFY] user={user_id}: {message}")
async def notify_admin(self, message):
print(f"[ADMIN] {message}")
class PaymentHandler:
"""Сервис обработки платежей."""
async def on_successful_payment(self, invoice, db, notifier, user_id):
await db.confirm_payment(invoice.invoice_id, invoice.payload)
await notifier.notify_user(
user_id,
f"Оплата #{invoice.invoice_id} на {invoice.amount} {invoice.asset} прошла!",
)
async def main():
# Регистрируем все зависимости в клиенте
cp = CryptoPay(
token="1234:YOUR_TOKEN",
db=Database(),
notifier=Notifier(),
payment_handler=PaymentHandler(),
)
# Хендлер с доступом к DI
@cp.invoice_paid()
async def on_paid(invoice, cp):
db = cp["db"]
notifier = cp["notifier"]
handler = cp["payment_handler"]
# Получаем user_id из payload
user_id = invoice.payload or "unknown"
await handler.on_successful_payment(invoice, db, notifier, user_id)
await notifier.notify_admin(
f"Поступила оплата #{invoice.invoice_id}: {invoice.amount} {invoice.asset}"
)
# Создаём тестовый инвойс
invoice = await cp.create_invoice(
amount=25,
asset="USDT",
description="Премиум-подписка",
payload="user_42",
)
print(f"Инвойс создан: {invoice.bot_invoice_url}")
# Запускаем polling
await cp.start_polling()
asyncio.run(main())
🔍 Что здесь происходит?
Мы создали три сервиса и зарегистрировали их в клиенте через kwargs. Хендлер on_paid получает cp и через него обращается к сервисам. Все сервисы легко заменить на моки для тестирования — достаточно передать другие объекты при создании клиента.
DI при использовании polling
При использовании cp.start_polling() все зарегистрированные зависимости доступны в хендлерах точно так же. Это работает и для обработчиков, назначенных через @cp.invoice_paid(), @cp.check_paid() и других декораторов.
import asyncio
from aiosend import CryptoPay
async def main():
cp = CryptoPay(
token="1234:TOKEN",
my_config={"admin_id": 12345, "lang": "ru"},
)
@cp.invoice_paid()
async def on_paid(invoice, cp):
config = cp["my_config"]
print(f"Уведомить админа {config['admin_id']}")
print(f"Язык уведомления: {config['lang']}")
@cp.check_paid()
async def on_check_paid(check, cp):
config = cp["my_config"]
print(f"Чек {check.check_id} оплачен!")
print(f"Админ для уведомления: {config['admin_id']}")
await cp.start_polling()
asyncio.run(main())
💡 Совет: типизация зависимостей
Для удобства можно создать датакласс с типизированными полями и передавать его как единую зависимость. Это даст автодополнение в IDE:
from dataclasses import dataclass
@dataclass
class AppServices:
db: Database
notifier: Notifier
config: dict
services = AppServices(db=Database(), notifier=Notifier(), config={})
cp = CryptoPay(token="TOKEN", services=services)
# В хендлере:
@cp.invoice_paid()
async def handler(invoice, cp):
svc: AppServices = cp["services"]
await svc.db.save_payment(invoice)
Что важно запомнить
cp["key"].cp, aiosend автоматически передаёт объект клиента.**kwargs.CryptoPay.Практическое задание
Задание: Реализуй систему DI для магазина
Создай скрипт, который:
- Определяет три сервиса:
Database,TelegramNotifier,Analytics - Регистрирует их в
CryptoPayчерез kwargs - Создаёт хендлер
@cp.invoice_paid(), который через DI получает все три сервиса - При получении оплаты: сохраняет в БД, отправляет уведомление в Telegram, записывает аналитику
- Использует
cp.get()для безопасного доступа
Подсказка:
import asyncio
from aiosend import CryptoPay
class Database:
async def save(self, invoice):
print(f"Сохранено: #{invoice.invoice_id}")
class TelegramNotifier:
async def send(self, user_id, text):
print(f"TG -> {user_id}: {text}")
class Analytics:
async def track(self, event, data):
print(f"Analytics: {event} {data}")
async def main():
cp = CryptoPay(
token="YOUR_TOKEN",
db=Database(),
tg=TelegramNotifier(),
analytics=Analytics(),
)
@cp.invoice_paid()
async def on_paid(invoice, cp):
# Используй cp.get() для безопасного доступа
db = cp.get("db")
tg = cp.get("tg")
analytics = cp.get("analytics")
if db:
await db.save(invoice)
if tg:
await tg.send(
invoice.payload or "unknown",
f"Оплачено: {invoice.amount} {invoice.asset}",
)
if analytics:
await analytics.track("payment", {
"id": invoice.invoice_id,
"amount": invoice.amount,
})
await cp.start_polling()
asyncio.run(main())
Урок 7.1: Dependency Injection
5 вопросов