Swagger binding subsystem¶
Swagger binding subsystem связывает локальный OpenAPI corpus с публичной поверхностью SDK. Его задача — доказуемо ответить на два вопроса:
- какая upstream Swagger operation покрыта каким публичным SDK-методом;
- как contract-test runner должен вызвать этот SDK-метод без реального HTTP.
Swagger/OpenAPI-файлы в docs/avito/api/*.json остаются единственным источником истины по HTTP-контракту: method, path, parameters, request body, content type, statuses, schemas и operation-level deprecated. Binding-и не дублируют эти данные. Они хранят только адресацию между SDK и Swagger.
Основные компоненты¶
| Компонент | Файл | Ответственность |
|---|---|---|
| Swagger downloader | scripts/download_avito_api_specs.py |
Скачивает свежий upstream OpenAPI catalog в docs/avito/api/ и удаляет stale specs |
| Binding decorator | avito/core/swagger.py |
Записывает metadata на публичный SDK-метод |
| Swagger registry | avito/core/swagger_registry.py |
Загружает docs/avito/api/*.json, нормализует операции и проверяет базовую валидность specs |
| Binding discovery | avito/core/swagger_discovery.py |
Находит decorated public domain methods без создания AvitoClient и без HTTP |
| Linter | avito/core/swagger_linter.py, scripts/lint_swagger_bindings.py |
Проверяет, что binding-и полные, уникальные и соответствуют Swagger |
| Report | avito/core/swagger_report.py |
Формирует JSON report для docs/reference и coverage |
| Factory map | avito/core/swagger_factory_map.py |
Даёт вспомогательную, неканоническую карту AvitoClient factory -> domain class -> spec candidates |
| Contract runner | avito/testing/swagger_fake_transport.py |
Строит SDK-вызовы по binding metadata и валидирует фактический request/response через Swagger |
Каноническая карта покрытия строится только из Swagger operation key -> discovered binding. Markdown inventory не участвует в coverage и не является источником истины.
Binding metadata¶
Публичный декоратор:
@swagger_operation(
method: str,
path: str,
*,
spec: str | None = None,
operation_id: str | None = None,
factory: str | None = None,
factory_args: Mapping[str, str] | None = None,
method_args: Mapping[str, str] | None = None,
deprecated: bool = False,
legacy: bool = False,
)
Class-level metadata на domain object задаёт defaults:
__swagger_domain__: str
__swagger_spec__: str
__sdk_factory__: str
__sdk_factory_args__: Mapping[str, str]
Приоритет значений:
- Значения из
@swagger_operation(...). - Значения из class-level metadata.
- Auto-resolve через registry, только если
method + normalized_pathсовпадает ровно с одной Swagger operation во всём corpus.
Decorator записывает metadata в func.__swagger_binding__. Он не меняет поведение метода и не читает Swagger-файлы на import time. Повторная разметка того же SDK method запрещена, а legacy metadata __swagger_bindings__ считается ошибкой совместимости.
Operation identity¶
Primary key операции:
Нормализация:
methodприводится к uppercase;- trailing slash удаляется, кроме
/; - path хранится в Swagger format:
/path/{param}; - path parameter aliases, отличающиеся только стилем записи (
userId/user_id), нормализуются к имени описанного Swagger parameter; - path остаётся case-sensitive;
- syntax path parameter кроме
{name}запрещён.
operation_id является дополнительной проверкой. Он помогает поймать ошибочный binding, но не является primary identity.
Expression mappings¶
factory_args и method_args описывают, как generated contract data превращается в вызов публичного SDK:
| Expression | Источник |
|---|---|
path.<name> |
path parameter Swagger operation |
query.<name> |
query parameter Swagger operation |
header.<name> |
header parameter Swagger operation |
body |
весь request body |
body.<field> |
поле request body |
constant.<name> |
контролируемая тестовая константа |
Expressions не являются Python-кодом. Произвольные callables, dotted paths вне whitelist и transport/request DTO запрещены.
Текущая реализация валидирует path.*, query.*, header.*, наличие requestBody для body, field-level body.<field> против top-level request body schema properties и наличие constant.* в test constants registry. Для Swagger properties с camelCase/Pascal acronym naming registry также хранит SDK-style snake_case aliases, чтобы binding мог ссылаться на публичные Python-имена без потери schema-aware проверки.
Discovery¶
Discovery импортирует пакет avito, но не создаёт AvitoClient, не читает обязательные env vars и не делает сетевых вызовов. Сканируются публичные domain classes из avito/<domain>/domain.py и заранее описанные non-domain exceptions, например low-level auth token bindings.
Игнорируются:
- private methods;
- internal helpers;
- summary/helper methods на
AvitoClient, если они не соответствуют одной конкретной upstream operation; - section clients как canonical target, кроме явно задокументированных legacy/non-domain exceptions.
Linter modes¶
Основные команды:
poetry run python scripts/lint_swagger_bindings.py
poetry run python scripts/lint_swagger_bindings.py --strict
poetry run python scripts/lint_swagger_bindings.py --json --strict --output swagger-bindings-report.json
make swagger-update
make swagger-lint
make swagger-coverage
Non-strict mode валидирует specs и уже найденные bindings. Strict mode дополнительно требует, чтобы каждая Swagger operation имела ровно один binding. make swagger-lint сначала скачивает свежие Swagger/OpenAPI files через make swagger-update, затем запускает strict validation. make swagger-coverage дополнительно запускает полный Swagger contract suite и входит в make check.
JSON report используется как стабильный machine-readable API для generated reference и coverage:
{
"summary": {
"specs": 23,
"operations_total": 204,
"deprecated_operations": 7,
"bound": 204,
"unbound": 0,
"duplicate": 0,
"ambiguous": 0
},
"operations": [],
"bindings": [],
"factory_mapping": {},
"errors": []
}
Deprecated and legacy policy¶
Operation-level deprecated: true in Swagger requires:
deprecated=Trueon binding;legacy=Trueon binding;- runtime
DeprecationWarningon the public SDK method throughdeprecated_method(...).
legacy=True on a non-deprecated operation is forbidden unless a separate allowlist entry exists with a reason and removal date. Deprecated schema fields, properties and enum values do not create operation-level legacy requirements.
Multi-operation SDK methods¶
The strict invariant is:
each Swagger operation -> exactly one discovered binding
each discovered SDK method -> exactly one Swagger operation
One SDK method must not have multiple Swagger bindings. When a user-facing scenario has several upstream modes, the canonical bindings belong to separate documented SDK methods; compatibility wrappers may delegate to those methods but must not carry additional bindings.
Contract tests¶
SwaggerFakeTransport uses discovered binding metadata to:
- Build an
AvitoClientwith fake transport. - Create the correct domain object through
AvitoClientfactory andfactory_args. - Call the public SDK method with
method_args. - Match the actual HTTP request against Swagger method/path.
- Validate required path/query/header parameters and request body/content type.
- Return declared Swagger response statuses only.
- Let normal SDK mapping and exception mapping run.
Contract tests must stay network-free. They are not a replacement for domain tests, but they catch binding drift: a method can be present in docs yet still fail contract invocation if factory args, method args, path, body or status handling are wrong.
The contract suite is exhaustive over the Swagger binding map:
- one request-contract case per discovered binding;
- one error-contract case per numeric Swagger error response;
- deprecated operation bindings are included in the request set and additionally checked for runtime
DeprecationWarning.
SwaggerFakeTransport provides deterministic generated SDK arguments and success payloads. The default success payload is the minimal JSON object accepted by most SDK mappers; operations whose mappers require a domain-specific response shape are listed in the controlled payload registry in avito/testing/swagger_fake_transport.py. Missing generated arguments or unsupported payload shapes are contract failures, not allowlisted gaps.
API method change checklist¶
When adding or changing a public API method that corresponds to Avito API:
- Confirm the upstream operation in
docs/avito/api/*.json. - Add or update the domain method, section client call, mapper and public models.
- Add
@swagger_operation(...)on the public domain method without schemas/statuses/content types in the decorator. - Add or update class-level metadata if the domain class is new.
- Document the public method through docstring so generated reference explains arguments, return model, pagination/dry-run/idempotency behavior and common exceptions.
- Add focused domain tests with
FakeTransport. - Add or adjust mapper/model tests when response or serialization changes.
- Ensure the binding is exercised by strict
make swagger-lintand the exhaustiveSwaggerFakeTransportcontract tests. - Update user-facing docs when the method creates a new workflow, changes behavior, or introduces a non-obvious contract.
Minimum local verification for API-surface changes:
make swagger-lint
poetry run pytest tests/core/test_swagger*.py tests/contracts/test_swagger_contracts.py
poetry run pytest tests/domains/<domain>/
poetry run mypy avito
poetry run ruff check .
Before merging a complete API change, run: