Request API#

Маркер размещает поле операции. 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() оставляйте для вложенных документов и правил валидации, которыми действительно должна владеть модель.

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

задан вызывающим кодом

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 нельзя. Источником по умолчанию служит сам класс операции: в проекцию идут поля без маркера размещения, уже с применёнными значениями по умолчанию.

Ошибки 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 не поддерживаются.