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. Владение задаётся явно:
Конструктор |
Кто закрывает ресурсы |
|---|---|
|
client остаётся у caller |
|
root владеет созданным client, а client владеет handler |
|
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#
Поверхность |
Статус |
Правило расширения |
|---|---|---|
|
Public |
Используйте напрямую; ownership всегда задаётся конструктором |
|
Public authoring/SPI |
Реализуйте документированные protocol-ы, не наследуйтесь от internal compiler types |
Zapros |
Transport SPI |
Единственная граница собственной сетевой интеграции |
|
Public test support |
Наблюдает фактический Zapros request, не заменяет preparation |
|
Alpha implementation SPI |
Используйте только имена из |
|
Private |
Не импортируйте и не сохраняйте их между версиями |