Всем привет!

Вчера я выпустил крупное обновление 1.0.0 для своей библиотеки async_yookassa — неофициального клиента для асинхронного взаимодействия с API ЮKassa. О том, что изменилось, зачем я вообще взялся её писать и почему официальный SDK может "убить" вашего бота — в этой статье.

Кстати, я веду Telegram-канал Код на салфетке, где публикую заметки и гайды для новичков. Буду рад видеть вас там!


Предыстория

Давным-давно, в далёкой-далёкой... простите, не тот текст. Как-то раз передо мной стояла задача разработать Telegram-бота с возможностью приема платежей через ЮKassa. Дойдя до этапа интеграции оплаты, я, как порядочный разработчик, первым делом отправился в документацию. На странице с доступными инструментами красовалась ссылка на официальную библиотеку. Казалось бы, бери и пользуйся. Однако радость быстро сменилась разочарованием: официальная библиотека — синхронна.

За неимением выбора было принято решение реализовать необходимый минимум через прямые запросы к API. Но зерно безумной идеи уже было посеяно, и мысли о создании полноценного инструмента не давали мне покоя.

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

Сказано — сделано. Примерно за неделю первая версия была готова! Ну, как готова... Я пошёл путём наименьшего сопротивления: попытался просто "перегнать" функционал оригинальной библиотеки на асинхронные рельсы, не особо задумываясь об архитектурных проблемах и "детских болячках" такого подхода.

Почему официальная библиотека — не вариант?

Здесь стоит сделать небольшое лирическое, но важное отступление. Почему вообще возникла потребность в велосипеде, когда есть официальный SDK от Яндекса?

Проблема кроется в фундаменте. Официальная библиотека построена на requests и работает синхронно. В мире классических скриптов это нормально, но для асинхронных приложений (FastAPI, Aiogram 3.x) это критично.

Когда вы вызываете синхронный метод (например, создание платежа) внутри асинхронной функции, Python блокирует выполнение всего потока, ожидая ответа от сервера.

  • Синхронный подход: Пока бот ждёт ответа от ЮKassa (это может быть 0.5–2 секунды, в лучшем случае, а может занять сильно дольше времени при проблемах с соединением), он игнорирует всех остальных пользователей. 10 человек нажали "Купить" одновременно? Десятый будет ждать ответа 10–20 секунд, пока очередь дойдет до него.

  • Асинхронный подход (моя цель): Бот отправляет запрос и тут же переключается на другие задачи. Пока мы ждём ответ от банка, бот продолжает общаться с другими пользователями.

Чтобы заставить официальную библиотеку работать в асинхронном коде, приходится использовать "костыли" вроде run_in_executor, заворачивая вызовы в отдельные потоки. async_yookassa же изначально строилась на httpx и нативной асинхронности, чтобы решать эти проблемы "из коробки".


Свежий взгляд

С момента последнего коммита прошло 8 месяцев. Честно говоря, за это время я совершенно забыл о существовании библиотеки. Ну а что? Поводов использовать её в новых проектах не возникало, а баг-репорты в личку не прилетали. Тишина и спокойствие.

Идиллия нарушилась внезапно — буквально 1-го января. Мне написал один из пользователей с просьбой обновить библиотеку. И действительно: мало того, что релизов не было почти год, так ещё и в реальности произошли изменения (обновились ставки НДС и требования API), которые старая версия не учитывала. Пришлось отложить оливье и засучить рукава.

Открыв проект спустя столько времени, я первым делом задался вопросом: "Да кто, чёрт возьми, писал этот код?! Ах да, это же я...".

Беглого анализа архитектуры хватило, чтобы понять: косметическим ремонтом тут не обойтись. Передо мной встала дилемма из известного мультика, только на программистский лад: «Переписывать нельзя рефакторить». Предлагаю вам самим мысленно поставить запятую, но, спойлер: я решил не щадить прошлогоднего себя.


Решение проблем

Погрузившись в чтение (напомню, прошло 8 месяцев, так что я смотрел на код как в первый раз!), я обнаружил несколько фундаментальных проблем. Давайте разберём их по порядку.

Глобальное состояние (Global State)

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

Configuration.account_id = "..."
Configuration.secret_key = "..."

# или так:
Configuration.configure(
    account_id='...',
    secret_key='...'
)

Старая питонячья мудрость гласит: «Глобалы — зло!». И это чистая правда. Глобальное состояние — это не самый надёжный способ хранения чувствительных данных, не говоря уже о сложностях, которые возникают, если вам нужно оперировать несколькими магазинами (shop_id) в рамках одного проекта. В асинхронном коде, где контекст постоянно переключается, изменение глобальной переменной в одном месте может непредсказуемо повлиять на запрос, выполняющийся параллельно.

Решение этой проблемы было простым и очевидным — изоляция данных внутри конкретного объекта клиента.

Теперь инициализация выглядит так:

# Можно работать с сотней магазинов одновременно!
client_a = YooKassaClient(shop_id="SHOP_A", ...)
client_b = YooKassaClient(shop_id="SHOP_B", ...)

async with client_a as client:
    await client.payment.create(...)

async with client_b as client:
    await client.payment.create(...)

Такой подход позволяет не только изолировать данные магазина в конкретном объекте и работать с несколькими учётными записями, но и полностью устраняет риск состояния гонки (Race Condition). Вы больше не рискуете тем, что при высокой нагрузке ключи от одного магазина случайно "утекут" в запрос другого. Кроме того, код становится тестируемым: теперь клиент можно легко передавать как зависимость (Dependency Injection) или подменять его "фейковой" версией в тестах.

Управление ресурсами

В прошлых версиях вопросу ресурсов уделялось преступно мало внимания. Например, при старом подходе инициализация работы с платежами (класс Payment) часто создавала новый экземпляр httpx клиента под капотом. Если происходила ошибка или метод просто завершался, клиент не всегда закрывался корректно.

В масштабах одного запроса это незаметно, но под нагрузкой это приводило к утечке файловых дескрипторов (незакрытых сокетов) и назойливым предупреждениям Unclosed client session в логах. В худшем случае сервер просто переставал открывать новые соединения.

Написав универсальный класс YooKassaClient, я перешел на использование асинхронного контекстного менеджера (async with).

Это решает сразу две задачи:

  1. Гарантированная очистка: Даже если внутри блока кода произойдет критическая ошибка, метод aexit автоматически закроет сессию и освободит ресурсы.

  2. Connection Pooling (Переиспользование соединений): Это то, о чем часто забывают. Пока вы находитесь внутри контекста, библиотека не тратит время на установку нового SSL-соединения для каждого запроса (Handshake), а использует уже открытое. Это дает ощутимый прирост производительности при серии запросов.

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

Раньше модули были "государствами в государстве". Мы отдельно создавали экземпляры классов Payment, Receipt и других, что приводило к дублированию кода и сложностям в поддержке.

Чтобы навести порядок, я внедрил Сервисный слой. Теперь архитектура строится вокруг базового абстрактного класса, который берет на себя всю грязную работу:

class BaseService(ABC):
    """
    Базовый класс для всех сервисов YooKassa API.

    Обеспечивает базовую функциональность для выполнения HTTP запросов,
    работы с ключами идемпотентности и сериализации данных.
    """

    BASE_PATH: str = ""
    CMS_NAME: str = "async_yookassa_python"

    def __init__(self, http_client: HttpClient) -> None:
        self._http = http_client

    async def _get(
        self,
        path: str,
        query_params: dict[str, str] | None = None,
    ) -> dict[str, Any]:
        """
        Выполнение GET запроса.

        :param path: Путь запроса
        :param query_params: Query параметры
        :return: JSON ответ
        """
        ...

    async def _post(
        self,
        path: str,
        body: dict[str, Any] | None = None,
        idempotency_key: uuid.UUID | None = None,
    ) -> dict[str, Any]:
        """
        Выполнение POST запроса с поддержкой идемпотентности.

        :param path: Путь запроса
        :param body: Тело запроса
        :param idempotency_key: Ключ идемпотентности (если не передан, генерируется новый)
        :return: JSON ответ
        """
        ...

    @staticmethod
    def _get_idempotency_headers(
        idempotency_key: uuid.UUID | None,
    ) -> dict[str, str] | None:
        """
        Формирует заголовок Idempotence-Key.

        Если ключ не передан, генерирует новый UUIDv4.
        """
        ...

    @staticmethod
    def _serialize_request(request: BaseModel) -> dict[str, Any]:
        """
        Сериализует Pydantic модель запроса в словарь.

        Исключает поля со значением None.
        """
        ...

Все разделы API (Платежи, Чеки, Возвраты) теперь наследуются от этого класса. Это позволяет гибко управлять модулями: добавлять новые методы буквально в пару строк кода, не задумываясь о том, как там "под капотом" формируются HTTP-заголовки или обрабатывается JSON.

Прочие изменения

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

  • Переезд на uv: Проект мигрировал с poetry на uv — современный и невероятно быстрый менеджер пакетов. Это упрощает установку и управление зависимостями для разработчиков.

  • Python 3.11+: Минимальная версия Python поднята до 3.11. Это позволило использовать новые фичи типизации и получить бесплатный прирост производительности самого интерпретатора.

  • Актуализация моделей: Все Pydantic-модели приведены в полное соответствие с документацией ЮKassa на начало 2026 года.

  • Строгая валидация: Теперь библиотека "ругается" на неверные типы данных еще до отправки запроса, экономя ваше время на отладку.


Планы и заключение

Грандиозных планов по захвату мира пока нет — функционально библиотека делает всё, что от неё требуется. Однако, есть одно "но".

Ахиллесова пята проекта прямо сейчас — это тесты. У меня, к сожалению, катастрофически не хватает времени покрыть всё это дело качественными автотестами. Поэтому, если среди читателей есть энтузиасты, желающие внести свой вклад в Open Source (и получить плюсик в карму или строчку в резюме) — милости просим в наш репозиторий! Pull Request’ы с тестами будут приняты с распростёртыми объятиями.

Я искренне надеюсь, что больше не оставлю проект на такой долгий срок без внимания. Но тут есть прямая зависимость: ваша активность — мое топливо. Прошлый "застой" случился во многом из-за того, что я не получал обратной связи и думал, что библиотекой никто не пользуется. Поэтому не стесняйтесь: ставьте звёздочки на GitHub, заводите Issues, если нашли баг, или просто пишите "спасибо", если всё работает. Контакты есть на странице PyPI и в репозитории.

Ссылки:

В ближайших планах — постучаться в поддержку ЮKassa. Хочу предложить им добавить async_yookassa на страницу документации как рекомендованное решение для асинхронных проектов. А заодно узнать, существует ли у них нормальная рассылка об изменениях API, чтобы не узнавать про новые изменения постфактум от пользователей 1-го января.

Буду рад вашим отзывам, критике кода и предложениям!

P.S. Также я веду Telegram-канал Код на салфетке, в котором публикую статьи, заметки и гайды для новичков. Заходите, у нас уютно! Буду рад видеть вас там!