Quickstart

Install the package together with the HTTP library your backend needs — the extras mirror action0-client’s backends (requests, httpx, aiohttp, urllib3, twisted; the urllib and thread-pool backends are stdlib-only and need no extra):

uv add "action0-open-meteo-api[httpx]"

From place name to forecast

Each Open-Meteo service has a subpackage of its own with a client preset to the service’s base URL. Turn a place name into coordinates with the geocoding service, then ask the forecast service for the weather:

from action0.client.backends.requests import RequestsBackend
from action0.open_meteo.forecast import ForecastClient, GetV1Forecast, GetV1ForecastHourlyItem
from action0.open_meteo.geocoding import GeocodingClient, SearchLocations

with RequestsBackend() as backend:
    places = GeocodingClient(backend)  # https://geocoding-api.open-meteo.com
    found = places.send(SearchLocations(name="Vienna", count=1))
    assert found.results, "no place matched"
    vienna = found.results[0]  # Location(id=2761369, name="Vienna", ...)

    weather = ForecastClient(backend)  # https://api.open-meteo.com
    forecast = weather.send(
        GetV1Forecast(
            latitude=str(vienna.latitude),
            longitude=str(vienna.longitude),
            hourly=[GetV1ForecastHourlyItem.TEMPERATURE_2M, GetV1ForecastHourlyItem.RAIN],
            forecast_days=1,
        )
    )
    assert forecast.hourly is not None
    print(forecast.hourly.time)  # ["2026-08-15T00:00", "2026-08-15T01:00", ...]
    print(forecast.hourly.temperature_2m)  # [17.2, 16.8, ...]

The weather variables are generated enums, so your IDE completes the legal values; dates (start_date=, end_date=) are datetime.date; the responses are plain typed dataclasses. Fields you did not request come back as None.

Sync, async or Twisted — the backend decides

The clients are generic over the backend, so the execution model is your choice, with the static types following along:

from action0.client.backends.httpx import AsyncHttpxBackend

async with AsyncHttpxBackend() as backend:
    weather = ForecastClient(backend)
    forecast = await weather.send(GetV1Forecast(latitude="48.21", longitude="16.37"))

A rejected request raises a typed error: Open-Meteo’s documented 400 answer parses into the generated BadRequestError (a subclass of action0.client.APIError, so broad handlers keep working):

from action0.open_meteo.forecast import BadRequestError

try:
    forecast = weather.send(GetV1Forecast(latitude="91", longitude="16.37"))
except BadRequestError as error:
    print(error.error.reason)  # "Latitude must be in range of -90 to 90"

Any other non-2xx status raises the plain APIError with request and response attached; transport problems arrive as TransportError / TimeoutError — see the action0-client error guide.

No API key needed (usually)

The Open-Meteo APIs are free for non-commercial use without any key (terms). Commercial subscriptions pass their key in the operations’ apikey field and the customer--prefixed hosts as the client’s base_url:

weather = ForecastClient(backend, base_url="https://customer-api.open-meteo.com")
forecast = weather.send(GetV1Forecast(latitude="48.21", longitude="16.37", apikey="..."))

Testing your integration

The stub backends of action0.client.testing drive the clients without a server — exactly how this repository tests the generated packages (examples/vienna_forecast.py is a complete network-free demo):

from action0.client.testing import StubBackend
from action0.req import Response

backend = StubBackend(Response(200, body='{"elevation": [194.0]}'))
client = ElevationClient(backend)
print(client.send(GetV1Elevation(latitude="48.21", longitude="16.37")).elevation)  # [194.0]
print(backend.requests[0].url.as_str())  # the URL that would have been fetched