Response API#
Одна успешная case#
Тип результата операции и есть успешный случай:
from dataclasses import dataclass
from pydantic import BaseModel
from eazy_sdk import AsyncApi, Http, HttpOperation, Path, op
class User(BaseModel):
id: int
@dataclass(frozen=True, slots=True, kw_only=True)
class GetUser(HttpOperation[User]):
__http__ = Http.get("/users/{user_id}")
user_id: Path[int]
class UsersApi(AsyncApi):
get_user = op(GetUser)
Семья читается из модели: модель с селекторами CSS, XPath или Regex означает документ,
модель, у которой каждое поле читается из заголовков ответа, читается без тела, любая другая
модель означает JSON, str означает текст, bytes означает байты, а None означает пустое тело.
Явные Json(status=201), Html(...), HeaderModel(...), Text()
нужны, когда важны статус, медиа-тип или условие when=/accept=.
Несколько случаев#
success=, errors= и fallback= принимают мэппинг селектора на случай:
__http__ = Http.get(
"/users/{user_id}",
success={200: User, 202: PendingUser},
errors={404: UserNotFound, "5xx": ServiceUnavailable},
fallback=Problem,
)
Ключом служит статус, диапазон ("4xx", "5xx") или DEFAULT; DEFAULT допустим только в fallback=.
Значение может быть моделью, представлением, классом ApiError[Model], парой (Model, factory) или их списком.
Эквивалентная запись через Responses(success=(Success(...),), errors=(Error(...),)) остаётся
доступной: это то же значение, собранное вручную.
Representation |
Результат |
|---|---|
|
dataclass, Pydantic, msgspec или custom model adapter |
|
CSS/XPath extraction с последующим model adapter |
|
Модель из |
|
Custom primitive extractor с последующим model adapter |
|
Custom response parser |
|
|
|
|
|
|
Status selector может быть int, StatusRange(start, end) или DEFAULT.
Источники поля вне тела#
Маркер поля |
Откуда значение |
Пропуск обязательного поля |
|---|---|---|
|
одна строка заголовка ответа |
|
|
кука из |
|
|
полный адрес из |
случай не выбран |
|
значение параметра адреса |
случай не выбран |
Location на поле, тип которого не допускает None, считается критерием случая наравне с
when=, accept= и Const(...). Тот же объект передаётся в when= как условие. Случай на
статусе 3xx без критерия отклоняется при объявлении операции.
Редирект, на статус которого операция объявила случай, читает операция: клиент ему не следует.
Несколько источников одного поля#
A | B на маркерах источников даёт FirstOf: источники пробуются слева направо, поле получает
первое найденное значение.
class LoginPage(BaseModel):
csrf: Annotated[str, Regex(r'"csrf":"([^"]+)"') | FromCookie("act")]
В цепочку входят селекторы документа (CSS, XPath, Regex), FromHeader, FromCookie и
Location.query(...). Обязательное поле, которое не нашлось ни в одном источнике, даёт
MalformedResponseError; если в цепочке есть Location.query(...), случай не выбран. Шаблон
Location(...) в цепочку не входит: он стоит на поле один.
Теги поля#
Тег |
Утверждение |
|---|---|
|
поле равно значению |
|
поле равно одному из значений |
|
поле есть и не равно значению |
|
поле есть и не равно ни одному из значений |
Поле объявляет один тег. Значения сравниваются вместе с типом. Тег читается одинаково в
success= и в errors=. Два случая, чьи теги подошли одному ответу, дают
AmbiguousResponseError: Not(200, 401, 403) рядом с OneOf(401, 403) не пересекаются,
Not(200) рядом с ним пересекается.
Payload[T] работает в обобщённой pydantic-модели: success={200: Ok[Account]} возвращает
Account. Модель без параметра отклоняется при объявлении.
Ошибка, означающая устаревшую сессию#
class TokenRejected(ApiError[TokenRefused]):
reauth = True
Ответ, который выбрал такой случай, обновляет сессию и повторяет запрос: тот же бюджет
Resilience.auth_retries и то же требование идемпотентности, что у HTTP 401. Исключение
поднимается, когда обновлять больше нечем. Полная форма: Error(..., reauth=True). На операции
без security такой случай отклоняется при объявлении.
Typed callable parser#
Для одного custom model используйте фабрику, связывающую callback с типом заранее:
from eazy_sdk.ext import Malformed, NoMatch, ParseAttempt, ParsedValue
from eazy_sdk.response import ResponseContext, callable_parser
def parse_challenge(
context: ResponseContext[object],
) -> ParseAttempt[Challenge]:
if context.response.status_code != 403:
return NoMatch()
return ParsedValue(Challenge(context.text.value or ""))
CHALLENGE_PARSER = callable_parser(Challenge, parse_challenge)
Runtime не передаёт callback-у type[object]. Несовместимый return type отклоняют mypy и
Pyright. Для multi-model parser реализуйте advanced ResponseParser SPI. Старого
CallableParser и compatibility alias нет.
Результаты и ошибки#
Обычный вызов даёт значение success case. method.with_response(...) возвращает
ResponseEnvelope: value, status_code, headers и normalized raw response.
В ApiError[T] доступны typed .error и безопасный .context. Свойство .summary всегда доступно
и даёт ErrorSummary с operation_id, статусом, media type, номером попытки, методом и
редактированным target. Именно она переживает pickle: сериализованное исключение не несёт ни
тела ответа, ни его заголовков, а восстановленный .context содержит ErrorSummary.
Ошибки классификации разделены:
UnexpectedResponseError: подходящей case нет; если ответ пришёл документной медиа, а все кандидаты структурные, сообщение это называет;MalformedResponseError: body не разобран выбранным parser/extractor;AmbiguousResponseError: совпало больше одной одинаково специфичной case. При сравнении наличие заявленного критерия важнее его отсутствия, точный статус важнее диапазона, а диапазон важнееDEFAULT. Явная медиа важнее wildcard и отсутствия медиа; кейс операции важнее кейса сервиса, а кейс сервиса важнее кейса клиента. Критерием служитwhen=,accept=или тег, который объявила модель.
Parsing session создаётся один раз на response attempt, поэтому custom parser или extractor может кэшировать decoded document по identity.