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 |
|
|
|
Особое scalar-представление |
|
|
|
Один parser для нескольких response models |
|
response context при |
|
Извлечение primitives из особого response format |
|
response context при |
|
Особая canonical base |
|
|
|
Полностью особая подпись |
|
|
|
Scope/predicate, зависящий от запроса |
|
method, host, path и |
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 |
|
Стабилен в пределах альфы |
Ломающие изменения происходят только между альфа-релизами и записываются в CHANGELOG |
SPI расширений |
|
Стабилен в пределах альфы |
Расширения используют только 23 имени из таблицы ниже |
Расширенный SPI защит |
|
Предварительный |
Точный список контрактов понижения для пресетов и генераторов может меняться вместе с исполнителем |
Ядро |
|
Предварительный |
План, ошибки и чистое ядро без зависимостей используются |
Компилятор |
|
Закрытый |
Не импортируйте из приложений и плагинов; |
Внутренности клиентов |
|
Закрытые |
Могут меняться в любом релизе |
Полный экспорт пространства имён#
Семейство |
Все имена в |
|---|---|
Область и предикат |
|
Кодеки |
|
Ответ |
|
Подпись |
|
ExecutionCore, ExecutionRuntime, PreparedRequest, RequestPreparer, кеши и понижение
зависимостей, разрешение авторизации, хранение сессии и функции
compile_*/select_*/sign_prepared не являются SPI. Тесты SDK, которые намеренно проверяют эти
стадии, импортируют их из внутренних модулей-владельцев и принимают риск поломки на любом коммите.