---
title: "Architecture"
description: "A one-page tour of what's actually in the main repo, so nothing about the generated clients feels magic."
lang: en-US
source: https://valeryverkhoturov.github.io/wb-api-client-docs/en/guides/architecture
---

# Architecture

A one-page tour of what's actually in the [main repo](https://github.com/ValeryVerkhoturov/wb-api-client), 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 `[…](https://valeryverkhoturov.github.io/wb-api-client-docs/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`](https://github.com/ValeryVerkhoturov/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`](https://github.com/ValeryVerkhoturov/wb-api-client-php) → `clients/php`. Reason: Packagist requires `composer.json` at the ROOT of the crawled repo.
- [`ValeryVerkhoturov/wb-api-client-1c`](https://github.com/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`](https://github.com/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`](https://github.com/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`](https://github.com/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`](https://github.com/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`](https://github.com/ValeryVerkhoturov/wb-api-client/blob/main/CLAUDE.md) is a working, machine-readable version of the same content — kept up to date because CI itself reads it.

<p align="center">
  <img src="/pics/blueberry-academic-cap-ascii-animated.webp" style="height: 200px; width: auto; max-width: 100%; object-fit: contain" alt="wb-api-client mascot">
</p>
