Generating a client

One command reads the schema and writes the package:

action0-openapi petstore.json -o src/
src/petstore_client/__init__.py
src/petstore_client/client.py
src/petstore_client/errors.py
src/petstore_client/models.py
src/petstore_client/operations.py
src/petstore_client/py.typed

The package name (petstore_client) and the client class name (PetstoreClient) are derived from the schema’s info.title; the client’s default base URL comes from the schema’s first servers entry (server variables at their defaults). A document without top-level servers falls back to the servers declared on its paths and operations — Open-Meteo’s specs are shaped like that — as long as they all agree on their first URL; several distinct URLs leave the client without a default (a warning names them), so callers pass base_url themselves. All of it can be overridden:

action0-openapi petstore.yaml -o src/ \
    --package-name zoo \
    --client-name ZooClient \
    --base-url https://zoo.example.com/v1

YAML schemas need the yaml extra (Installation). Schemas split over several files ($ref: './components/geo.yaml#/...') are loaded and bundled automatically — Supported schemas describes the merge rules. Existing files are never overwritten unless you pass --force.

The schema can also be an http(s) URL. The URL you name is fetched directly, but files it references download only with your consent — one [y/N] prompt per file, or all of them with --download:

action0-openapi https://example.com/api/openapi.yaml -o src/ --download
action0-openapi: downloading https://example.com/api/components/geo.yaml
src/example_client/__init__.py
...

For large APIs, --split-by-tag puts each OpenAPI tag’s operations into a module of its own (operations_pets.py, operations_auth.py, …; untagged operations stay in operations.py). The package root re-exports everything either way, so user imports — from petstore_client import ListPets — do not depend on the layout.

Constructs the generator flattens or skips (an unsupported security scheme, several request media types, …) are reported as warnings on stderr; constructs outside the supported subset stop the run with an error naming the schema location — Supported schemas lists both categories.

The generated code is meant to be checked in like hand-written code: it is readable, fully typed (mypy strict, pyright and ty pass on it), ruff-clean, and it depends only on action0-client — not on this package. Regenerate with --force when the schema changes, and review the diff like any other change.

The same pipeline is available as a library — one function per stage:

from pathlib import Path

from action0.openapi import bundle_documents, generate_package, load_documents
from action0.openapi import parse_api, write_package

document, warnings = bundle_documents(load_documents("petstore.json"))
api = parse_api(document)
files = generate_package(api, client_name="PetstoreClient", schema_name="petstore.json")
write_package(files, Path("src/petstore_client"))

(For a schema that is known to be a single file, load_schema reads it without following references, and its result can go straight into parse_api.)