Clients#

Client принимает Zapros BaseHandler, а AsyncClient принимает AsyncBaseHandler. Handler является единственной transport extension boundary.

from zapros import AsyncStdNetworkHandler

from eazy_sdk import AsyncClient, ClientConfig, Resilience

client = AsyncClient(
    base_url="https://api.example",
    handler=AsyncStdNetworkHandler(),
    config=ClientConfig(resilience=Resilience(timeout=5.0)),
)

По умолчанию client владеет handler. Передайте owns_handler=False, если lifecycle handler остаётся у caller. Sync client отклоняет async handler и наоборот.

ClientConfig состоит из трёх групп по владельцу: resilience= (retry, auth_retries, max_redirects, client_redirects, timeout, rate_limiter), security= (guard/anti-bot; Security.of(...) понижает установочные guard’ы) и hooks= (middleware). Аутентификация, ключи подписи и DI живут в Identity; библиотека моделей находится в Serialization на корне. Плоских полей у конфига больше нет.

Рядом с группами находятся две карты, ограниченные хостом: crypto= для шифрования по адресу и errors= для ошибки, которыми хост отвечает независимо от того, какой SDK с ним говорит:

config = ClientConfig(errors={"books.example": {429: RateLimited, 503: AccessBlocked}})

Ключом служит точный хост без порта. Эти варианты образуют самый внешний слой: при совпадении статуса выигрывает кейс операции, затем её сервиса, и только потом клиентский.

CallOptions управляет timeout, retry/redirect budgets и replay одного вызова. Auth, middleware, retry, redirect, rate limit и protection остаются в общем executor; каждый attempt создаёт новый Zapros Request.

Root SDK convenience#

SyncRoot и AsyncRoot составляют композицию роутеров. Корень не объявляет операций и не выдаёт клиентов наружу: в публичной поверхности SDK видны только роутеры.

from eazy_sdk import AsyncClient, AsyncRoot, api_group

class StoreSdk(AsyncRoot):
    users = api_group(UsersApi)
    orders = api_group(OrdersApi)

client = AsyncClient.httpx(base_url="https://api.example")
async with StoreSdk(client) as sdk:
    user = await sdk.users.get_user(user_id=42)
await client.aclose()

Группы создаются лениво. Адрес операции берётся из роутера, если он его объявил, иначе из клиента. См. Мультисервисный SDK. Владение задаётся явно:

Конструктор

Кто закрывает ресурсы

Root(client)

client остаётся у caller

Root.from_handler(..., owns_handler=True)

root владеет созданным client, а client владеет handler

Root.from_handler(..., owns_handler=False)

root закрывает созданный client, caller сохраняет handler

close()/aclose() закрывает только те клиенты, которые корень создал сам, и безопасен при повторном вызове.

Аутентификация клиенту не принадлежит: сессия, ключи подписи и DI объявляются в Identity(...) и передаются корню (ShopSdk(client, identity=…)) или отдельному роутеру (UsersApi(client, identity=…)). Конструктор того, кому передан identity=, регистрирует scoped SDK factory для auth-lifecycle. Scoped auth facades никогда не владеют parent resources.

Root и API groups не сериализуют пользовательские вызовы: независимые calls можно выполнять конкурентно в пределах concurrency-контракта выбранного handler. Встроенные auth/protection caches сами обеспечивают заявленный single-flight. Не запускайте новые calls одновременно с close()/aclose(); закрытие выполняется после завершения использующих root задач.

Синхронный клиент и event loop#

Client не создаёт event loop вообще. Синхронный драйвер шагает ту же спецификацию исполнения на вызывающем потоке: на синхронном транспорте в конвейере ничто не приостанавливается, поэтому цикл не нужен. Отсюда два следствия. Первое: Client можно вызывать из кода, где loop уже работает, например в Jupyter или обработчике asyncio-сервера. Установленный вызывающим цикл событий не меняется. Второе: то, что действительно ждёт (backoff, задержка rate limiter, занятый protection-лок), блокирует поток, а не крутит loop.

Синхронный драйвер не исполняет настоящий асинхронный ввод-вывод внутри колбэка. Solver, middleware или auth service, который сам уходит в сеть через await, приостанавливается, и тогда поднимается SynchronousSuspensionError: используйте AsyncClient или сделайте этот колбэк синхронным. Несколько потоков могут делить один Client; single-flight protection-локи не привязаны к loop.

Client configuration#

ClientConfig включает resilience= (retry, auth-replay, redirects, timeout, rate limiter), security= (ProtectionBundle), hooks= (middleware) и host-scoped crypto. First-party preset, Guard или custom challenge_guard() устанавливайте через ClientConfig(security=Security.of(...)) либо эквивалентный immutable config.with_protection(guard); client.invalidate_protection(*guards_or_names) сбрасывает закэшированные решения без доступа к runtime. ProtectionBundle объединяет operation_protections, before_call_policies, challenge_policies и solver_bindings; config.bundle возвращает установленную (или пустую) группу.

Managed protection state принадлежит одному client/handler session и очищается при закрытии client. Proxy, User-Agent и impersonation задаются при construction handler-а: фабрики Client.httpx(proxy=...), Client.requests(proxy=...), Client.curl_cffi(proxy=..., impersonate=...) и их async-варианты настраивают создаваемую сессию и объявляют результат в HandlerProfile.proxy и HandlerProfile.impersonation (для чужой сессии proxy= служит только декларация того, что она уже использует). Именно объявленный proxy входит в transport identity managed clearance. Для rotation создайте новый handler и client; per-attempt transport identity provider отсутствует.

Ad-hoc HTTP#

response = await client.request(
    "PATCH",
    "/experimental",
    params={"dry_run": "true"},
    json={"enabled": True},
    options=CallOptions(timeout=3.0),
)

Единственный метод прямого вызова клиента: request(method, url, *, params, headers, cookies, json, content, options); он возвращает NormalizedResponse. Отдельных verb-методов (get, post, …) у Client и AsyncClient нет. HTTP-метод передаётся первым аргументом. Повторяющиеся имена query запрещены; collection требует явного scalar codec.

Stability и extension boundaries#

Поверхность

Статус

Правило расширения

SyncApi, AsyncApi, SyncRoot, AsyncRoot, api_group, bind, Client, AsyncClient

Public

Используйте напрямую; ownership всегда задаётся конструктором

api.*, request markers, Responses, ResponseParser

Public authoring/SPI

Реализуйте документированные protocol-ы, не наследуйтесь от internal compiler types

Zapros BaseHandler / AsyncBaseHandler

Transport SPI

Единственная граница собственной сетевой интеграции

eazy_sdk.testing

Public test support

Наблюдает фактический Zapros request, не заменяет preparation

eazy_sdk.ext

Alpha implementation SPI

Используйте только имена из __all__; полный контракт и removed internals перечислены в Extension SPI

eazy_sdk.compile.*, executor stages и compiled records

Private

Не импортируйте и не сохраняйте их между версиями