Skip to content

Architecture ​

A one-page tour of what's actually in the main repo, so nothing about the generated clients feels magic.

The pipeline ​

download-specs.py          →  swaggers/*.yaml         (raw upstream, checksummed)
process-specs.py           →  swaggers/processed/     (13 passes)
generate-clients.py <ver>  →  clients/<lang>/…        (openapi-generator-cli, 8 langs)
    ├── inject-secrets.py          (SecretString wrapper per lang)
    ├── {black|prettier|gofmt|spotless|php-cs-fixer|rustfmt}   (canonicalize formatting)
    └── generate-readmes.py        (per-language README.md)
     ↓ committed at v<version>, single tag covers all 8 languages
publish.yml                →  PyPI / npm / Go tag / Maven Central / Packagist / hub.oscript.io / NuGet / crates.io
generate-docs.py           →  wb-api-client-docs      (API reference: operation pages,
                                                        call snippets and data structures
                                                        in 8 languages, RU/EN)

Every step is a plain Python file, so nothing is hidden behind opaque tooling.

Why generate? ​

WB ships OpenAPI specs. Hand-writing 13 clients × 8 languages would drift within a week. The upside of generating from the spec is that every field, every enum, every response wrapper is exactly what WB documents — and re-syncing takes exactly zero effort once the pipeline exists.

The downside is that generator output is generator output — sometimes ugly, sometimes carrying artifacts from spec quirks. Which is why there's a post-processing layer.

process-specs.py — 13 passes ​

  1. inject_bearer_auth — drops WB's HeaderApiKey scheme (duplicated with the real bearer JWT semantics) and installs a single BearerAuth scheme instead. Every generated client then exposes exactly one auth code path.
  2. fix_untyped_arrays — adds items: {type: string} to arrays missing their inner type (Go / TS would otherwise emit []any).
  3. widen_integer_formats — sets format: int64 on integer fields with no format or with uint32/uint64/Unix-timestamp formats: WB uses those for IDs and millisecond timestamps that overflow a signed 32-bit int. Explicit int32 and currency-code formats are left alone.
  4. inline_top_level_arrays — inlines every $ref to a type: array component and collapses allOf: [array, description-only] wrappers (openapi-generator mishandles arrays-at-root). Runs to fixed point because inlined subtrees can contain further refs.
  5. rename_digit_prefixed_schemas — 409SupplyDeliverError → Http409SupplyDeliverError (invalid Go identifier otherwise).
  6. name_inline_response_schemas — hoists anonymous inline 4xx/5xx bodies to <OperationId>Response<code> (again, valid Go identifiers).
  7. drop_response_required — WB marks nearly every response field required, but the live API omits fields at will, and every generated language then throws on deserialization. The pass deletes required from every schema reachable from a response; request bodies keep their required lists, so input validation is unaffected.
  8. sanitize_non_ascii_enums — adds x-enum-varnames for Russian enum members like "Склад WB" (empty JS/Go/Java identifiers otherwise).
  9. rename_tags_to_module — WB tags are Russian (Финансовые отчёты), which openapi-generator sanitizes to an empty class-name stem, so every language fell back to Api/DefaultApi classes. The pass rewrites every operation's tags to the module name (Finances, Reports, …), giving each module a single well-named API class (FinancesApi, …), and stashes the original tags in x-original-tags for the docs generator.
  10. drop_date_formats — WB returns "" for unset date fields (fixTariffDateFrom, utdUcdDate), which no strict date parser accepts. The pass strips format: date / format: date-time from string schemas so clients type these as plain strings.
  11. htmlize_descriptions_to_markdown — every description field is passed through markdownify so HTML in docstrings renders as Markdown in generated docs.
  12. absolutize_description_links — relative […](/openapi/…) links get https://dev.wildberries.ru prepended so links in generated docstrings still work.
  13. add_docs_links — appends a Library doc: <url> line to every operation's description, pointing at the operation's page on this site. Generators copy description into docstrings/Javadoc, so every client method links to its rendered docs. generate-docs.py strips the line back out, so the docs site itself is unaffected.

generate-clients.py ​

  • Runs openapi-generator-cli (pinned Docker image) per spec per language, into scratch directories.
  • Splices each per-spec SDK into a unified per-language tree — Python packages nested under wb_api_client.<slug>, TypeScript subpath exports, Go sub-packages, Java sub-packages, PHP sub-namespaces, for OneScript a class-name prefix (the language has no namespaces), C# namespaces, and Rust modules of a single crate (wb_api_client::<slug>).
  • Runs inject-secrets.py to patch every generated Configuration / ApiClient with the language-native secret-string wrapper.
  • Runs the pinned formatter per language: black, prettier, gofmt, spotless (google-java-format), php-cs-fixer, dotnet format, cargo fmt (rustfmt). All inside Docker — no host runtime needed.
  • Substitutes __VERSION__ in top-level manifests (pyproject.toml, package.json, go.mod, pom.xml, composer.json, Cargo.toml) with the passed version.
  • Runs generate-readmes.py to emit a per-language README.md.

The result: clients/{python,typescript,go,java,php,onescript,csharp,rust}/ — eight ready-to-publish packages.

Determinism ​

The daily job would flap constantly if generation weren't deterministic. To keep it stable:

  • openapi-generator-cli Docker tag is pinned.
  • Every language's openapi-generator config sets hideGenerationTimestamp: true.
  • Every formatter is pinned to a specific version.
  • The PR-check workflow regenerates from scratch and fails the PR if git diff swaggers/ clients/ is non-empty. This catches any change that would make subsequent runs produce drift.

Publishing ​

daily-check.yml computes the version, regenerates, commits, tags. Then it fires 8 workflow_dispatch events at publish.yml — one per language — with the tag as the ref input. Each language branch of publish.yml:

  • Python — OIDC → PyPI trusted publishing (pypa/gh-action-pypi-publish).
  • TypeScript — OIDC → npm trusted publishing (npm publish --provenance).
  • Go — the tag is pushed to wb-api-client-go (see below); the job itself is a no-op because proxy.golang.org fetches directly from the tag.
  • Java — GPG-signed deploy to Maven Central via central-publishing-maven-plugin. Maven is pinned to 3.9.x (stCarolas/setup-maven): the plugin builds its bundle through a Maven-2 stager, and Maven 3.10's Resolver drops a stray maven-metadata-local.xml into it, so Central rejects the bundle ("Bundle has content that does NOT have a .pom file").
  • PHP — ping the Packagist update-package API.
  • OneScript — opm build then opm push to hub.oscript.io.
  • C# — dotnet pack then dotnet nuget push to NuGet.
  • Rust — cargo publish to crates.io (GitHub environment crates-io, toolchain pinned at 1.98).

Separately, daily-check regenerates the API reference (generate-docs.py — operation pages, call snippets, and request/response data structures per language, with links to the WB specification) and pushes it to wb-api-client-docs, which deploys to GitHub Pages.

Reusable workflows (workflow_call) would be cleaner, but PyPI + npm trusted publishing don't support them — both the OIDC token's job_workflow_ref (callee) and the Sigstore attestation cert's workflow_ref (caller) have to point at the same file, which is impossible with workflow_call. Hence workflow_dispatch.

The sibling repositories ​

Four languages live in their own repos, mounted here as git submodules:

  • ValeryVerkhoturov/wb-api-client-php → clients/php. Reason: Packagist requires composer.json at the ROOT of the crawled repo.
  • ValeryVerkhoturov/wb-api-client-1c → clients/onescript. So 1C users can clone just the client and install it from a tag.
  • ValeryVerkhoturov/wb-api-client-go → clients/go. Reason: Go versions modules with repository tags, so the module must sit at a repo root (go get github.com/ValeryVerkhoturov/wb-api-client-go) — a subdirectory module would need clients/go/-prefixed tags and go get would fetch the whole monorepo.
  • ValeryVerkhoturov/wb-api-client-rust → clients/rust. Reason: crates.io links the crate to a dedicated repo, and git dependencies (wb-api-client = { git = "…", tag = "vX.Y.Z" }) resolve from the repo root.

Every daily run commits and tags all four siblings BEFORE the main repo — otherwise the submodule pointers recorded in the release commit would reference commits their remotes have never seen, and a fresh git clone --recurse-submodules would fail.

A fifth sibling is ValeryVerkhoturov/wb-api-client-docs — this site. Not a submodule: daily-check clones it, runs generate-docs.py, and pushes the result back.

The OneScript generator ​

openapi-generator ships no OneScript target, so it lives in a separate repo — ValeryVerkhoturov/onescript-openapi-generator. It is an ordinary plugin: the class extends DefaultCodegen, registers through META-INF/services, and is loaded next to openapi-generator-cli.jar, so -g onescript resolves like a built-in generator. The main repo pins it by commit — the OneScript equivalent of pinning the Docker image tag for the other languages.

What's not generated ​

The hand-written parts of the main repo:

  • scripts/ — the pipeline itself, organized by bounded context under scripts/pipeline/: core/ (shared kernel: repo paths, spec/module/language naming, YAML IO, external-tool runners (subprocess, docker, git, python, macOS codesign)), specs/ (download, postprocess), generation/ (generate, secrets), onescript/ (toolchain, manifests, verify), docs/ (reference, readmes). The flat scripts/*.py files are thin CLI shims over those modules, named verb-object by what they produce: download-specs, process-specs, generate-clients, generate-readmes, generate-docs, generate-onescript-manifests, inject-secrets, verify-onescript. House rule: no # comments and no docstrings under scripts/ — enforced by make scripts-check and the PR check.
  • generator-configs/*.yaml — one openapi-generator config per language.
  • templates/* — top-level manifests with __VERSION__ placeholders.
  • .github/workflows/* — daily-check, per-language publish, PR drift check.
  • Makefile, README.md, CLAUDE.md.

Everything under clients/ is generated output — never hand-edit it, regenerate.

Full pipeline docs ​

The main-repo CLAUDE.md is a working, machine-readable version of the same content — kept up to date because CI itself reads it.

wb-api-client mascot