--- title: Clients description: Zapros-backed clients, root SDK lifecycle и ad-hoc HTTP. sources: - eazy_sdk.Client - eazy_sdk.AsyncClient - eazy_sdk.ClientConfig - eazy_sdk.clients.CallOptions api_fingerprint: "sha256:85da10e3f07784916d48408a937fe029c93cb120f5370f2170f18c2bdd19eefc" --- `Client` принимает Zapros `BaseHandler`, а `AsyncClient` принимает `AsyncBaseHandler`. Handler является единственной transport extension boundary. ```python 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 с ним говорит: ```python 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 видны только роутеры. ```python 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() ``` Группы создаются лениво. Адрес операции берётся из роутера, если он его объявил, иначе из клиента. См. {doc}`Мультисервисный 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 ```python 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 перечислены в {doc}`Extension SPI ` | | `eazy_sdk.compile.*`, executor stages и compiled records | Private | Не импортируйте и не сохраняйте их между версиями |