Extension SPI#

eazy_sdk.ext задаёт узкую границу реализации для авторов расширений. Это не сокращённый импорт всего SDK и не доступ к compiler/runtime. Декларативные объекты импортируются из eazy_sdk.request, eazy_sdk.response, eazy_sdk.protection и eazy_sdk.handlers.

Предупреждение

Alpha-контракт Проект находится в alpha. Все имена из eazy_sdk.ext.__all__ публичны в пределах конкретной версии, но могут измениться в следующем alpha-релизе без compatibility alias. Всё, чего нет в __all__, а также eazy_sdk.compile, стадии исполнителя и записи подготовки или компиляции остаются закрытыми.

Выберите семейство#

Задача

Реализуйте

Получите от runtime

Верните

Особые bytes request body

BodyCodec

EncodeContext

bytes

Особое scalar-представление

ScalarCodec

ScalarEncodeContext

str

Один parser для нескольких response models

ResponseParser и BoundResponseParser

response context при bind

ParseAttempt[T]

Извлечение primitives из особого response format

ResponseExtractor и BoundResponseExtractor

response context при bind

ParseAttempt[object]

Особая canonical base

CustomCanonicalizer

SigningInput

bytes

Полностью особая подпись

CustomRequestSigner

SigningInput, optional key

SignatureResult

Scope/predicate, зависящий от запроса

RequestScope или чтение ScopeContext

method, host, path и OperationIdentity

match decision

Scope и predicate context#

RequestScope фильтрует правила по scheme, host, path prefix, HTTP method и operation id. ScopeContext содержит фактический контекст запроса для проверки. Его поле operation содержит OperationIdentity; сравнивайте operation.operation_id, не identity Python-объекта.

from eazy_sdk.ext import RequestScope

PAYMENTS = RequestScope(
    hosts=frozenset({"api.example.test"}),
    path_prefixes=("/payments/",),
    methods=frozenset({"POST"}),
)

Обычно ScopeContext и OperationIdentity создаёт runtime. Конструировать их вручную нужно только в unit-тесте собственного predicate. RequestScope.to_dict() и from_dict() предназначены для конфигурации, но не сериализуют callback или другие extension implementations.

Request codecs#

BodyCodec имеет name, optional media_type и метод encode(value, EncodeContext) -> bytes. ScalarCodec имеет name и encode(value, ScalarEncodeContext) -> str. Context передаёт operation id; scalar context также передаёт location и wire name.

import json
from dataclasses import dataclass
from typing import Annotated

from eazy_sdk import Http, HttpOperation, SyncApi, op
from eazy_sdk.ext import EncodeContext, ScalarEncodeContext

class CanonicalJson:
    name = "canonical-json"
    media_type = "application/vnd.example+json"

    def encode(self, value: object, context: EncodeContext) -> bytes:
        return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()

class PipeSeparated:
    name = "pipe-separated"

    def encode(self, value: object, context: ScalarEncodeContext) -> str:
        if not isinstance(value, list):
            raise TypeError("PipeSeparated expects a list")
        return "|".join(str(item) for item in value)

@dataclass(frozen=True, slots=True, kw_only=True)
class Upload(HttpOperation[object]):
    __http__ = Http.post("/uploads", success=None, raw_response=True)

    body: Annotated[dict[str, object], CanonicalJson()]

class UploadsApi(SyncApi):
    upload = op(Upload)

EncodeContext и ScalarEncodeContext immutable. Codec не получает client, transport или prepared request и не должен выполнять network I/O. Для встроенных codecs и их настройки используйте eazy_sdk.codecs или eazy_sdk.request.

Response parser и extractor#

ResponseParser.bind(context) возвращает BoundResponseParser для одного ответа. try_parse(Model) может вызываться для нескольких candidate models. Он возвращает ровно один из:

  • ParsedValue(value): модель распознана;

  • NoMatch(): парсер не применим к модели или ответу;

  • Malformed(cause, details=None): формат подходил, но данные повреждены.

Union этих трёх результатов называется ParseAttempt[T]. Не бросайте parse exception наружу: заверните его в Malformed. Для callback, заранее связанного с одной model, используйте eazy_sdk.response.callable_parser; собственный ResponseParser нужен для multi-model parser.

import json
from dataclasses import dataclass
from typing import cast

from eazy_sdk.ext import (
    BoundResponseParser,
    Malformed,
    NoMatch,
    ParseAttempt,
    ParsedValue,
)
from eazy_sdk.response import Parsed, ResponseContext, Responses, Success

@dataclass
class User:
    name: str

class BoundVendorParser:
    def __init__(self, context: ResponseContext[object]) -> None:
        self.context = context

    def try_parse[T](self, model: type[T]) -> ParseAttempt[T]:
        if model is not User:
            return NoMatch()
        try:
            document = json.loads(self.context.bytes)
            user = User(name=str(document["display_name"]))
        except (KeyError, TypeError, ValueError) as error:
            return Malformed(error)
        return cast(ParseAttempt[T], ParsedValue(user))

class VendorParser:
    def bind(self, context: ResponseContext[object]) -> BoundResponseParser:
        return BoundVendorParser(context)

RESPONSES = Responses(
    success=(Success(200, Parsed(User, using=VendorParser())),),
)

ResponseExtractor отличается тем, что возвращает semantic primitives для последующей загрузки модель через реестр. Связанная часть вызывается как BoundResponseExtractor.extract(model). Оба протокола публичны также из eazy_sdk.response, потому что Extracted(...) является обычной response декларацией.

Procedural signing#

Для стандартной HMAC используйте декларативные компоненты из eazy_sdk.request. Extension SPI нужен только для особой canonical base или полностью нестандартного алгоритма.

CustomCanonicalizer.build(SigningInput) -> bytes читает immutable prepared view. custom_base(...) связывает implementation с объявленным read set. read_set(...) строит read set из декларативных signature components; whole_prepared_request() выбирает весь prepared request.

CustomRequestSigner.sign(SigningInput, SigningKey | None) -> SignatureResult возвращает mapping только для заранее объявленных outputs. custom_signature(...) связывает signer, reads, outputs и optional key requirement.

import hashlib

from eazy_sdk.ext import (
    SignatureResult,
    SigningInput,
    custom_signature,
    whole_prepared_request,
)
from eazy_sdk.request import SigningKey, header_output

SIGNATURE_HEADER = header_output("X-Vendor-Signature")

class VendorSigner:
    def sign(self, signing: SigningInput, key: SigningKey | None) -> SignatureResult:
        request = signing.request
        digest = hashlib.sha256(request.method + b"\n" + request.target).hexdigest()
        return SignatureResult({SIGNATURE_HEADER: digest})

VENDOR_SIGNATURE = custom_signature(
    signer=VendorSigner(),
    reads=whole_prepared_request(),
    outputs=(SIGNATURE_HEADER,),
    name="vendor-sha256",
)

Передайте VENDOR_SIGNATURE в signing=(VENDOR_SIGNATURE,) объявления операции. Runtime проверяет undeclared/missing outputs до отправки. SigningInput не даёт мутировать prepared request; SignatureResult не может добавлять новую цель, которую не зарезервировал компилятор.

Матрица стабильности#

Слой

Пространство имён

Статус

Что это значит

Публичный API

eazy_sdk, eazy_sdk.request, eazy_sdk.response, eazy_sdk.protection, eazy_sdk.auth, eazy_sdk.clients, eazy_sdk.policies, eazy_sdk.middleware, eazy_sdk.websocket, eazy_sdk.crypto

Стабилен в пределах альфы

Ломающие изменения происходят только между альфа-релизами и записываются в CHANGELOG

SPI расширений

eazy_sdk.ext

Стабилен в пределах альфы

Расширения используют только 23 имени из таблицы ниже

Расширенный SPI защит

eazy_sdk.protection.advanced

Предварительный

Точный список контрактов понижения для пресетов и генераторов может меняться вместе с исполнителем

Ядро

eazy_sdk.core

Предварительный

План, ошибки и чистое ядро без зависимостей используются ext и плагинами, но не входят в API автора SDK

Компилятор

eazy_sdk.compile

Закрытый

Не импортируйте из приложений и плагинов; absence_audit запрещает такие импорты плагинам

Внутренности клиентов

eazy_sdk.clients._core, executor, _http_stages, websocket._client_*

Закрытые

Могут меняться в любом релизе

Полный экспорт пространства имён#

Семейство

Все имена в eazy_sdk.ext.__all__

Область и предикат

OperationIdentity, RequestScope, ScopeContext

Кодеки

BodyCodec, EncodeContext, ScalarCodec, ScalarEncodeContext

Ответ

BoundResponseExtractor, BoundResponseParser, Malformed, NoMatch, ParseAttempt, ParsedValue, ResponseExtractor, ResponseParser

Подпись

CustomCanonicalizer, CustomRequestSigner, SignatureResult, SigningInput, custom_base, custom_signature, read_set, whole_prepared_request

ExecutionCore, ExecutionRuntime, PreparedRequest, RequestPreparer, кеши и понижение зависимостей, разрешение авторизации, хранение сессии и функции compile_*/select_*/sign_prepared не являются SPI. Тесты SDK, которые намеренно проверяют эти стадии, импортируют их из внутренних модулей-владельцев и принимают риск поломки на любом коммите.