How this library was generated

Every subpackage under action0.open_meteo — the clients, the operation classes, the enums, the models and their JSON converters — was generated by action0-client-openapi from Open-Meteo’s OpenAPI schemas, and is checked in like hand-written code. This page is the worked example of that generator: exactly what we ran, and what came out.

The schemas

Open-Meteo publishes one OpenAPI 3.1 document per service in the open-meteo/open-meteo repository. The nine documents are vendored under schemas/ (pinned to an upstream commit — schemas/README.md records which). The Geocoding API has no upstream schema, so schemas/geocoding.yml was written for this project against the API docs and live responses — authoring a small OpenAPI file and generating from it beats hand-writing a client.

The generation runs

tools/regenerate.sh runs the generator once per schema — the whole script is ten calls of this shape:

uv run action0-openapi schemas/forecast.yml \
    -o src/action0/open_meteo \
    --package-name forecast \
    --client-name ForecastClient \
    --force

--package-name and --client-name override the defaults derived from the schema’s info.title (which would have been the mouthful open_meteo_weather_forecast_api_client / OpenMeteoWeatherForecastApiClient). Everything else comes from the schema itself:

  • The base URL. Open-Meteo declares its servers per path, not at the document level; the generator falls back to them, so ForecastClient defaults to https://api.open-meteo.com and AirQualityClient to https://air-quality-api.open-meteo.com — no --base-url flags needed.

  • Enums for the weather variables. hourly=temperature_2m,rain-style parameters are enum arrays in the schema, so the generated fields are list[GetV1ForecastHourlyItem] and your IDE completes the legal values.

  • Typed dynamic keys. The ensemble and seasonal APIs answer with per-member keys (temperature_2m_member01, …) declared as additionalProperties; the generated models collect them in a typed catch-all field: additional_properties: dict[str, list[float]].

  • Dates. start_date/end_date parameters are format: date, so the fields are datetime.date and serialize to ISO 8601.

  • Comma-joined variable lists. The schemas declare explode: false on the array parameters, so the generated fields carry a serialize= join lambda — the request says hourly=temperature_2m,rain, Open-Meteo’s documented form.

  • Typed errors. The documented 400 answers ({"error": true, "reason": ...}) become an errors.py per package: a BadRequestError subclassing action0.client.APIError, raised by the operations’ generated check() with the parsed payload as .error.

Each run writes one package: client.py, operations.py, models.py, errors.py, __init__.py (re-exporting everything) and py.typed, each file headed by a generated by action0-client-openapi vX.Y.Z from forecast.yml do not edit line.

Living with generated code

The generated packages are treated exactly like hand-written code — reviewed in PRs, type-checked strictly (mypy --strict, pyright and ty run over them in CI) and linted (they come out of the generator already ruff format-clean). What is not done is editing them by hand: when Open-Meteo’s schemas change, we re-vendor them, run bash tools/regenerate.sh, and review the diff — hand edits would be lost on the next regeneration, which the do not edit headers are there to remind you of. Hand-written code (the tests, the examples, the docs) lives outside the generated packages.

This project also feeds back the other way: generating it surfaced three generator gaps — the additionalProperties catch-all, the path-level servers fallback and a naming fix (get_v_1_...get_v1_...) — all fixed in action0-client-openapi 0.1.1 before this library was first generated. A second round followed the same day: honoring explode: false and generating typed error exceptions landed in 0.1.2, and this library picked both up by regenerating — the diff was reviewed like any other change.