Skip to content

Repository files navigation

image project

yookassax

Русский | English

Неофициальная библиотека. Проект не связан с ЮKassa и ЮMoney, ими не поддерживается и их продуктом не является. Официальный SDK лежит здесь. ЮKassa и ЮMoney — товарные знаки своих владельцев.

Неофициальный клиент ЮKassa для Python в двух режимах: синхронном и асинхронном. Типизированные модели, разбор уведомлений, идемпотентность и повторы из коробки.

pip install yookassax

Требуется Python 3.10 или новее. Единственная зависимость: httpx.

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

Синхронно:

from yookassax import YooKassa

with YooKassa(shop_id="123456", secret_key="live_...") as kassa:
    payment = kassa.payments.create({
        "amount": {"value": "100.00", "currency": "RUB"},
        "confirmation": {"type": "redirect", "return_url": "https://example.com/done"},
        "capture": True,
        "description": "Заказ 42",
    })
    print(payment.confirmation_url)

Асинхронно, то же самое:

from yookassax import AsyncYooKassa

async with AsyncYooKassa(shop_id="123456", secret_key="live_...") as kassa:
    payment = await kassa.payments.create({
        "amount": {"value": "100.00", "currency": "RUB"},
        "confirmation": {"type": "redirect", "return_url": "https://example.com/done"},
        "capture": True,
    })

Наборы методов у режимов одинаковые, это проверяется тестом. Переход с одного на другой сводится к добавлению await.

Примеры

Все сценарии из официальной документации, оба режима, два языка: русский, English.

Настройка клиента аутентификация, магазин, подписки
Платежи создание, подтверждение, отмена, списки
Возвраты полные и частичные
Чеки 54-ФЗ, маркированные товары
Сделки безопасная сделка целиком
Выплаты карта, СБП, кошелёк, самозанятые
Самозанятые регистрация и подтверждение
Персональные данные получатели выплат
Банки СБП справочник
Счета ссылка на оплату
Способы оплаты подписки и автоплатежи
Кассовые ссылки статические QR-коды
Уведомления FastAPI, Django, Flask
Ошибки и повторы идемпотентность, обрывы связи
Модели ответов все 73 модели и сверка со спецификацией
Партнёрская программа OAuth, работа от имени чужого магазина
Коды ответа HTTP что означает каждый и что делать
Тестовый магазин отладка, подмена HTTP, живой прогон
Логи logging, loguru, секреты в лог не уходят

Чем отличается от официального SDK

Ключи хранятся в экземпляре клиента. Официальный SDK держит их в Configuration на уровне класса. Если приложение работает с несколькими магазинами из одного процесса, два платежа могут переписать токен друг другу между настройкой и вызовом, и платёж уйдёт через чужой магазин. Здесь каждый клиент носит свои ключи, и такой гонки не существует.

Асинхронный режим настоящий. Официальный SDK синхронный, внутри requests. Вызов из асинхронного обработчика останавливает весь воркер: пока идёт обращение к API, процесс не обслуживает никого.

Ключ идемпотентности проставляется сам на всех изменяющих запросах. Повтор после обрыва связи идёт с тем же ключом, поэтому второго платежа не возникает.

Повторы на кодах 202, 429 и 500 с экспоненциальной паузой и дрожанием. Ошибки данных (400, 404) не повторяются: второй такой же запрос даст тот же ответ.

Способ оплаты разбирается в модель своего типа. Все 19 из спецификации: PaymentMethodBankCard, PaymentMethodSberLoan, PaymentMethodElectronicCertificate и так далее. Различать через isinstance, подробности.

Модели типизированы и терпимы к новым полям. ЮKassa добавляет поля в ответы; строгая модель превратила бы это в отказ обслуживать платежи. Всё неизвестное складывается в raw и доступно через extra. Но не молча: на каждое такое поле один раз выдаётся UnknownFieldWarning, иначе о новом поле никто и не узнает.

Билдеров нет. Тело запроса это обычный словарь: он принимает новые поля API сразу, а не после обновления библиотеки.

Работа с платежами

payment = kassa.payments.create({...})

payment.is_pending             # ждём оплату
payment.is_waiting_for_capture # деньги захолдированы, нужен capture или cancel
payment.is_succeeded           # деньги у магазина
payment.is_canceled            # деньги у плательщика

payment.amount.value           # Decimal("100.00"), не float
payment.created_at             # datetime с часовым поясом
payment.confirmation_url       # куда вести плательщика, либо None

kassa.payments.capture(payment.id)
kassa.payments.cancel(payment.id)

Списки

page = kassa.payments.list(status="succeeded", limit=50)
for payment in page:
    print(payment.id)

if page.has_more:
    next_page = kassa.payments.list(status="succeeded", cursor=page.next_cursor)

Или без ручного перелистывания:

for payment in kassa.payments.iterate(status="succeeded"):
    print(payment.id)

В асинхронном режиме то же самое через async for.

Уведомления

Тело уведомления ЮKassa не подписывает, поэтому единственная встроенная проверка это адрес отправителя. Её недостаточно: решение о деньгах принимайте по ответу API, а не по телу уведомления.

from fastapi import Request, Response
from yookassax import webhooks

@app.post("/webhook")
async def handle(request: Request):
    if not webhooks.is_trusted_ip(request.headers.get("X-Real-IP", "")):
        return Response(status_code=403)

    notification = webhooks.parse(await request.json())

    if notification.is_payment_succeeded:
        payment = await kassa.payments.get(notification.object.id)
        if payment.is_succeeded:
            ...

    return {"ok": True}

Берите достоверный адрес отправителя. За обратным прокси это тот, который прокси проставляет сам, обычно X-Real-IP из nginx. Левый элемент X-Forwarded-For подставляет клиент, и проверка теряет смысл.

Отвечайте 200 быстро: иначе ЮKassa повторит доставку, и обработчик получит то же событие ещё раз.

Ошибки

from yookassax import BadRequest, Forbidden, TransportError, YooKassaError

try:
    payment = kassa.payments.create({...})
except Forbidden:
    # магазину не разрешена операция, частый случай: не подключён рекуррент
    ...
except BadRequest as error:
    print(error.code, error.description, error.parameter)
except TransportError:
    # ответа не было вообще, состояние платежа неизвестно
    ...
except YooKassaError:
    ...

TransportError стоит отдельно от остальных намеренно: если создание платежа упало с ним, неизвестно, создан платёж или нет.

Новые поля в ответах

Поле, которого нет в модели, разбор не роняет, но и не скрывает:

payment = kassa.payments.get(payment_id)
# UnknownFieldWarning: Payment: в ответе API есть поля, которых нет в модели:
# loyalty_bonus. Значения доступны через extra(), но, возможно, стоит обновить
# yookassax.

payment.extra("loyalty_bonus")

Предупреждение указывает на строку вашего кода и выдаётся один раз на пару "модель плюс поле" за жизнь процесса: страница из ста платежей даст одну строку, а не сто. Отключается штатным фильтром:

import warnings
from yookassax import UnknownFieldWarning

warnings.filterwarnings("ignore", category=UnknownFieldWarning)

Логи

Каждое обращение к API пишется стандартным logging в логгер yookassax. Приложениям на loguru отдельная поддержка не нужна: записи подхватывает InterceptHandler.

import logging

logging.getLogger("yookassax").setLevel(logging.INFO)
ЮKassa ответ: POST /payments -> 200 за 0.412 c, id: 3225ad37-000f-5001-8000-108ff2fd923d
ЮKassa повтор: POST /payments, попытка 2 через 0.503 c, причина: ServerError

Заголовок Authorization в лог не попадает никогда, тела запроса и ответа — только на DEBUG: там персональные данные плательщика.

Доступные ресурсы

payments, refunds, receipts, payouts, webhooks, settings, payment_methods, deals, invoices, personal_data, self_employed, pos_links, sbp_banks.

Покрыты все маршруты официальной спецификации OpenAPI. Полнота проверяется тестом.

OAuth

Для работы с чужими магазинами:

kassa = YooKassa(oauth_token="токен, выданный магазином")

Одновременно с shop_id и secret_key не задаётся: клиент откажется собираться, чтобы не выбирать за вас.

Как получить такой токен, подписаться на уведомления по API и что делать, когда магазин отозвал права, - в примерах по партнёрской программе.

Эндпоинт, которого ещё нет в библиотеке

from yookassax import Operation

operation = Operation(
    method="POST",
    path="/new_endpoint",
    body={"key": "value"},
    idempotent=True,
)
result = kassa.send(operation)

Для ИИ-ассистентов

В каталоге docs лежит llms.txt: полный справочник по библиотеке одним файлом, чтобы вставить в контекст модели. Английская версия: llms.en.txt.

Разработка

pip install -e ".[dev]"
pytest
ruff check .
mypy src

Тесты не ходят в сеть. Отдельно есть прогон по живому API, он требует ключи тестового магазина и без них пропускается:

export YOOKASSA_SHOP_ID=... YOOKASSA_SECRET_KEY=test_...
pytest tests/integration

Лицензия

MIT.

About

Sync and async YooKassa client for Python - typed models, webhooks, idempotency built in

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages