--- title: Request API description: Маркеры размещения, тела и представление запроса на проводе. sources: - eazy_sdk.request - eazy_sdk.core.errors.OperationBindingError api_fingerprint: "sha256:117a654c795764aed18032f5138ba4ac9a52505957b3ab0cb6e5f97874e967af" --- Маркер размещает поле операции. `Path[T]` и `Annotated[T, markers.Path()]` являются двумя записями одной аннотации. Имя wire-поля передаётся маркеру явно (`markers.Query("perPage")`) либо объявляется моделью (`serialization_alias` у Pydantic, `msgspec.field(name=...)`); без этого используется Python-имя. Плоское object-shaped тело состоит из полей самой операции с маркером `JsonField`, `Form` или `Part`; готовые bytes помечаются `markers.BytesBody()`. Dataclass, Pydantic v2 и msgspec под `markers.JsonBody()` оставляйте для вложенных документов и правил валидации, которыми действительно должна владеть модель. ```python from dataclasses import dataclass from eazy_sdk import Http, HttpOperation, JsonField, SyncApi, op @dataclass(frozen=True, slots=True, kw_only=True) class CreateUser(HttpOperation[User]): __http__ = Http.post("/users") name: JsonField[str] class UsersApi(SyncApi): create = op(CreateUser) ``` Caller передаёт `create(name="Ada")`; type checker видит конструктор класса со всеми его значениями по умолчанию. Если публичные поля должны остаться плоскими, а сервер ждёт другой nested document, объявите `projection=BodyProjection(target=..., using=..., encoding=...)` в `__http__`. Источником служит сама операция: поля без маркера и есть вход mapper-а. Mapper строит private target на каждой attempt, target проходит model adapter validation, и только после этого выбранный encoding (`Body.json()`, form/multipart или custom `BodyCodec`) кодирует один semantic document. Mapper не получает client/transport state; если ему нужны значения зависимостей, объявите `requires=` и второй параметр `using(source, injected)`. | Задача | Форма | Поведение отсутствующего значения | |---|---|---| | Значение по умолчанию | `page: Query[int] = 1` | default материализуется до projection | | «Не отправлять вовсе» | `since: Query[Omittable[str]] = UNSET` | поле не попадает на провод | | Обязательное значение | Поле без default | `TypeError` конструктора до mapper и I/O | | Явно переданный `None` | Nullable-тип поля | значение присутствует и равно `None` | `Wire` фиксирует представление запроса на проводе, то есть всё, что влияет на байты, и объявляется там же, где остальной контракт операции. `CallOptions | None` служит зарезервированным keyword-only параметр управления одним вызовом. ## Placement markers | Marker | Default wire name | Основные настройки | |---|---|---| | `Path(name=None)` | имя поля | `style`, `explode`, `allow_reserved`, `codec` | | `Query(name=None)` | имя поля | `style`, `explode`, `allow_reserved`, `codec` | | `Header(name=None)` | имя поля | `style`, `explode`, `allow_reserved`, `codec` | | `Cookie(name=None)` | имя поля | `style`, `explode`, `allow_reserved`, `codec` | | `Url(hosts=(...))` | адрес запроса целиком | `hosts`: шаблоны хостов, куда может вести ссылка | `Url` стоит и на месте пути, и на поле: `Http.get(markers.Url(hosts=("*.example",)))` вместе с `url: Url[str]`. Адрес приходит аргументом вызова; ссылка на хост вне списка отклоняется до подготовки запроса, относительная резолвится против адреса сервиса. `Path`-полей у такой операции нет, а объявленное `Query`-поле делает запрос владельцем query. ## Body markers | Marker | Content type | Назначение | |---|---|---| | `JsonBody(...)` | `application/json` | Вложенная модель, scalar или array | | `JsonField(name=None)` | `application/json` | Одно JSON field | | `FormBody(...)` | form-urlencoded | Root, nested или model-owned document | | `Form(name=None)` | form-urlencoded | Одно form field | | `MultipartBody(...)` | multipart/form-data | Root, nested или model-owned document | | `Part(name=None)` | multipart/form-data | Одна part | | `BytesBody(...)` | заданный caller-ом | Buffered bytes | | `ReplayableStreamBody(...)` | заданный caller-ом | Повторно создаваемый stream | ## Кодирование тела Имена выше являются маркерами поля: `body: JsonBody[Payload]` размещает значение. Тот же контракт в `encoding=` у `BodyProjection` записывается через `Body`: один импорт на все форматы, как `Http` на все методы: | Кодирование | Дескриптор | Тип содержимого | |---|---|---| | `Body.json()` | `markers.JsonBody` | `application/json` | | `Body.form()` | `markers.FormBody` | form-urlencoded | | `Body.multipart(boundary=None)` | `markers.MultipartBody` | multipart/form-data | | `Body.raw(content_encoding=None)` | `markers.BytesBody` | задан вызывающим кодом | | `Body.stream()` | `markers.ReplayableStreamBody` | задан вызывающим кодом | ```python from eazy_sdk import Body, BodyProjection, Http, HttpOperation @dataclass(frozen=True, slots=True, kw_only=True) class RegisterUser(HttpOperation[RegisteredUser]): __http__ = Http.post( "/register", success={201: RegisteredUser}, projection=BodyProjection( target=RegisterUserWire, using=register_to_wire, encoding=Body.json(), ), ) ``` Каждый метод принимает `content_type=`, поэтому vendor media type объявляется на месте: `Body.json(content_type="application/vnd.api+json")`. `BodyProjection(...)` не является ещё одним body marker. Это связь публичных полей операции с private wire schema и одним из перечисленных encodings; смешивать projection с root/flat body нельзя. Источником по умолчанию служит сам класс операции: в проекцию идут поля без маркера размещения, уже с применёнными значениями по умолчанию. `QueryModel(target, using=None)` описывает query одной приватной моделью: каждое поле `target` становится одним параметром, поле операции без маркера размещения попадает в одноимённое поле модели. Передаётся в `__http__` как `query=`. Подробности описаны в руководстве {doc}`о представлении запроса `. ## Ошибки binding и projection Public binding, projection и preparation failures используют один `eazy_sdk.core.errors.OperationBindingError`. Tooling читает поля ошибки напрямую, не разбирает message: | Поле | Значение | |---|---| | `code` | Стабильный machine-readable вид ошибки | | `operation_id` | Public identity операции | | `field` | Caller field или nested path, если он известен | | `phase` | `bind`, `projection` или `prepare` | `error.as_dict()` возвращает только эти безопасные поля. Значение аргумента, private wire document, credentials и internal slot name в message, `repr` и mapping не входят. Для источника проекции три состояния не смешиваются. Поле, оставшееся `UNSET`, в input проекции не попадает. Явный `None` попадает. Отсутствие обязательного поля приводит к `TypeError` конструктора операции, до проекции, providers и handler. Проекция получает значения этой attempt, включая retry и redirect. Для плоского JSON, form или multipart document объявляйте поля операции маркерами `JsonField`, `Form` или `Part`. Не оборачивайте несколько scalar fields в Pydantic, dataclass или msgspec model. Root body markers нужны для scalar/array payload, вложенного document или случая, когда модель действительно владеет validation и serialization policy. `Wire` принимает `encrypted`, `order` (`FieldOrder(query=…, header=…, cookie=…, body=…)`), `exact`, `encoding` (`JsonPolicy`), `query` (`QueryCodec`) и `transport`; проекция объявляется отдельным параметром `projection=`. Она описывает представление, а не байты. Каждое поле `None` означает «наследовать», поэтому операция, объявившая только `encoding`, сохраняет шифрование своего сервиса. `exact` и `transport` проверяются как capability транспорта до resolver calls и сетевых side effects. Custom `BodyCodec` можно использовать вместо root body marker или как encoding `BodyProjection`: он получает уже адаптированные semantic primitives и `EncodeContext(operation_id=...)`, затем возвращает окончательные bytes. Model registry в context отсутствует. `ScalarCodec` в placement marker обязан вернуть одно wire value; повторяющиеся query names не поддерживаются.