--- title: Response API description: Singular responses, advanced cases, typed parsers и envelopes. sources: - eazy_sdk.response api_fingerprint: "sha256:fb98b1c877125947271dbd1c0d92c4e1d3b6a21c115fbd0014385c8bb12ad30d" --- ## Одна успешная case Тип результата операции и есть успешный случай: ```python 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=` принимают мэппинг селектора на случай: ```python __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 | Результат | |---|---| | `Json(Model, media_type=...)` | dataclass, Pydantic, msgspec или custom model adapter | | `Html(Model, media_type=...)` | CSS/XPath extraction с последующим model adapter | | `HeaderModel(Model)` | Модель из `Location`, `FromHeader` и `FromCookie`; тело не читается | | `Extracted(Model, using=...)` | Custom primitive extractor с последующим model adapter | | `Parsed(Model, using=...)` | Custom response parser | | `Text(media_type=...)` | `str` | | `Bytes(media_type=...)` | `bytes` | | `Empty(media_type=...)` | `None`, только для пустого body | Status selector может быть `int`, `StatusRange(start, end)` или `DEFAULT`. ## Источники поля вне тела | Маркер поля | Откуда значение | Пропуск обязательного поля | |---|---|---| | `FromHeader("X-Request-Id")` | одна строка заголовка ответа | `MalformedResponseError` | | `FromCookie("sid")` | кука из `Set-Cookie` этого ответа, последняя из одноимённых | `MalformedResponseError` | | `Location(host=..., path=..., query=..., contains=...)` | полный адрес из `Location` | случай не выбран | | `Location.query("fail")` | значение параметра адреса | случай не выбран | `Location` на поле, тип которого не допускает `None`, считается критерием случая наравне с `when=`, `accept=` и `Const(...)`. Тот же объект передаётся в `when=` как условие. Случай на статусе `3xx` без критерия отклоняется при объявлении операции. Редирект, на статус которого операция объявила случай, читает операция: клиент ему не следует. ### Несколько источников одного поля `A | B` на маркерах источников даёт `FirstOf`: источники пробуются слева направо, поле получает первое найденное значение. ```python class LoginPage(BaseModel): csrf: Annotated[str, Regex(r'"csrf":"([^"]+)"') | FromCookie("act")] ``` В цепочку входят селекторы документа (`CSS`, `XPath`, `Regex`), `FromHeader`, `FromCookie` и `Location.query(...)`. Обязательное поле, которое не нашлось ни в одном источнике, даёт `MalformedResponseError`; если в цепочке есть `Location.query(...)`, случай не выбран. Шаблон `Location(...)` в цепочку не входит: он стоит на поле один. ## Теги поля | Тег | Утверждение | |---|---| | `Const(200)` | поле равно значению | | `OneOf(401, 403)` | поле равно одному из значений | | `Not(200)` | поле есть и не равно значению | | `Not(200, 401, 403)` | поле есть и не равно ни одному из значений | Поле объявляет один тег. Значения сравниваются вместе с типом. Тег читается одинаково в `success=` и в `errors=`. Два случая, чьи теги подошли одному ответу, дают `AmbiguousResponseError`: `Not(200, 401, 403)` рядом с `OneOf(401, 403)` не пересекаются, `Not(200)` рядом с ним пересекается. `Payload[T]` работает в обобщённой pydantic-модели: `success={200: Ok[Account]}` возвращает `Account`. Модель без параметра отклоняется при объявлении. ## Ошибка, означающая устаревшую сессию ```python class TokenRejected(ApiError[TokenRefused]): reauth = True ``` Ответ, который выбрал такой случай, обновляет сессию и повторяет запрос: тот же бюджет `Resilience.auth_retries` и то же требование идемпотентности, что у HTTP `401`. Исключение поднимается, когда обновлять больше нечем. Полная форма: `Error(..., reauth=True)`. На операции без `security` такой случай отклоняется при объявлении. ## Typed callable parser Для одного custom model используйте фабрику, связывающую callback с типом заранее: ```python 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.