Skip to content

Configuration

The client and CLI both use a TOML configuration file that matches OmniWeatherConfig.

When the CLI has no explicit --config, it first checks platformdirs.user_config_path("omni-weather", appauthor=False) / "config.toml", then the legacy ~/.config/omni_weather_forecast_apis.toml. If neither exists, an interactive terminal starts omni-weather init; non-interactive use exits 2 with setup instructions. An explicitly supplied missing file never launches the wizard.

The platform defaults, with XDG and other platform overrides still honored, are:

Platform Configuration SQLite data
Linux ~/.config/omni-weather/config.toml ~/.local/share/omni-weather/forecasts.sqlite
macOS ~/Library/Application Support/omni-weather/config.toml ~/Library/Application Support/omni-weather/forecasts.sqlite
Windows %LOCALAPPDATA%\omni-weather\config.toml %LOCALAPPDATA%\omni-weather\forecasts.sqlite
latitude = 40.7128
longitude = -74.0060
sqlite = "forecasts.sqlite"
granularity = ["hourly", "daily"]
language = "en"
include_raw = false
debug = false
default_timeout_ms = 10000

[rate_limiting]
max_in_flight = 10
max_requests_per_second = 20

[retry]
max_attempts = 3
initial_backoff_ms = 500
max_backoff_ms = 8000
backoff_multiplier = 2.0
jitter = true

[http]
max_connections = 20
max_keepalive_connections = 10
connect_timeout_ms = 5000
cache_enabled = true
cache_max_entries = 256
raw_archive_enabled = true

[[providers]]
plugin_id = "open_meteo"
enabled = true
config = { models = ["best_match", "ecmwf_ifs025"] }

[[providers]]
plugin_id = "openweather"
config = { api_key = "${OPENWEATHER_API_KEY}" }
rate_limit_rps = 1.0
timeout_ms = 8000
max_requests_per_day = 900

Top-level options

Key Default Description
providers List of provider registrations (required)
latitude / longitude None Default coordinates for the CLI
sqlite None Default SQLite output path for the CLI; persistence is skipped when unset
granularity ["hourly", "daily"] Granularities to request
language "en" Provider language preference
include_raw false Persist raw provider payloads
default_timeout_ms 10000 Per-provider fetch timeout
debug false Verbose CLI logging

Retry policy — [retry]

Transient failures — network errors, timeouts, and HTTP 429 rate limits — are retried with exponential backoff and jitter. A server-provided Retry-After header is honored; retries are abandoned when it exceeds 60 seconds. Non-transient failures such as auth errors are never retried with the same key — but they do trigger a fallback to the next configured API key when a provider lists several (see API key rotation).

Key Default Description
max_attempts 3 Total attempts per API key (1 disables retries for each key)
initial_backoff_ms 500 Delay before the first retry
max_backoff_ms 8000 Backoff ceiling
backoff_multiplier 2.0 Exponential growth factor
jitter true Randomize each delay to avoid thundering herds

Setting only one of initial_backoff_ms / max_backoff_ms moves the other's default to keep initial <= max; setting both to conflicting values is a validation error. The explicit value always wins, so initial_backoff_ms = 10000 on its own raises max_backoff_ms from its 8000 default to 10000 rather than being clamped down to it.

A per-provider retry table on a registration overrides the global policy.

HTTP client — [http]

Key Default Description
max_connections 20 Connection pool cap
max_keepalive_connections 10 Idle keep-alive connections
connect_timeout_ms 5000 TCP/TLS connect timeout
cache_enabled true Conditional-request response cache
cache_max_entries 256 In-memory cache size
raw_archive_enabled true Archive raw HTTP payloads next to the SQLite database

Setting only one of max_connections / max_keepalive_connections moves the other's default to keep keepalive <= connections; setting both to conflicting values is a validation error. The explicit value always wins, so max_keepalive_connections = 50 on its own raises max_connections from its 20 default to 50 — set both keys when you need a specific pool ceiling.

The cache is standards-aware: responses with Cache-Control: max-age or Expires are served from memory while fresh, and stale responses carrying ETag/Last-Modified validators are revalidated with conditional requests and reused on 304 Not Modified. Responses that declare Vary are only reused for requests sending the same values for the named headers, and Vary: * responses are never cached. MET Norway's terms of service require this behavior and the NWS strongly encourages it. Requests carrying Authorization or Cookie headers bypass the shared cache.

Raw payload archive

When persisting to SQLite, every network response is additionally archived as gzipped JSONL — one line per response, carrying the timestamp, method, URL, status, and body — into a raw/ directory next to the database. Each invocation writes one file, linked from forecast_runs.raw_archive_path (see Database Design). The archive preserves response bodies for future parser investigation and replay. It only covers requests recorded after archiving is enabled, so it cannot reconstruct older normalized-only runs.

URLs are stored verbatim, including API keys in query strings, so keep archives out of version control — this repository ignores raw/. Files accumulate until deleted manually. Disable with raw_archive_enabled = false here, or --no-raw-archive for a single run.

Location-timezone cache

When the CLI persists forecasts to SQLite, it keeps coordinate-to-IANA-zone resolutions in a separate companion database (forecasts.timezones.sqlite). Keys retain six decimal places. Each entry records its source, resolver version, and resolution timestamp; entries older than 30 days or from an older resolver version are refreshed. Lookup misses use the aggregation client's configured connection pool, HTTP cache, metrics hooks, and raw-response recorder. Cache failures produce warnings and do not prevent providers that can determine their own timezone from running.

Provider registrations — [[providers]]

Key Default Description
plugin_id Provider slug (see Providers)
config Provider-specific config dict
enabled true Toggle without deleting the block
rate_limit_rps None Per-provider requests-per-second cap
timeout_ms None Per-provider timeout override
max_requests_per_day None Daily quota cap (see below)
retry None Per-provider retry policy override

Daily quotas

Most free tiers are capped per day, not per second. Set max_requests_per_day on a registration and the client refuses to fetch once the day's budget (UTC) is spent, returning a quota_exceeded error for that provider instead of burning the quota:

[[providers]]
plugin_id = "openweather"
config = { api_key = "${OPENWEATHER_API_KEY}" }
max_requests_per_day = 900

Each fetch attempt (including retries) counts one request. The CLI persists counts in the SQLite database (provider_quota_usage table) so limits survive across runs; library users can pass any omni_weather_forecast_apis.quota.QuotaTracker implementation with atomic try_consume support to the client (InMemoryQuotaTracker is the default, SqliteQuotaTracker is bundled).

Environment variable placeholders

Any string value inside a provider config block can reference an environment variable instead of embedding a secret:

# whole-string reference
config = { api_key = "${OPENWEATHER_API_KEY}" }

# explicit marker table (equivalent)
config = { api_key = { env = "OPENWEATHER_API_KEY" } }

Resolution happens when the client initializes and recurses through nested tables and arrays. A placeholder naming an unset variable turns into a per-provider initialization error; other providers are unaffected. Partial interpolation ("prefix-${VAR}") is intentionally not supported — only whole-string placeholders are resolved.

omni-weather init instead collects credential values with masked prompts and stores them directly in TOML. Its exact preview includes those values by design. Confirm only in a private terminal and protect the generated file; the wizard writes it atomically with mode 0600 and creates new config/data directories privately on POSIX. omni-weather doctor reports missing environment references by name without printing their resolved values.

API key rotation

Providers that authenticate with a single api_key field accept either one key or a list of keys. With a list, the client falls back to the next key whenever a fetch conclusively fails — keys sometimes fail for reasons that are hard to diagnose, and a spare key is often the fastest fix:

[[providers]]
plugin_id = "openweather"
config = { api_key = ["${OPENWEATHER_KEY_1}", "${OPENWEATHER_KEY_2}"] }

This applies to OpenWeather, WeatherAPI, Tomorrow.io, Visual Crossing, Weatherbit, Meteosource, Pirate Weather, Stormglass, Google Weather, Met Office, and Open-Meteo. XWeather and WeatherKit use multi-part credentials and stay single-credential.

How rotation behaves:

  • Any failure rotates. Every error kind moves on to the next key — auth failures and quota errors immediately, transient failures (network, timeout, rate limit) after the key's full retry budget is exhausted. Worst case a request makes keys × max_attempts fetch attempts, each subject to the per-request timeout_ms.
  • Each key gets the full retry policy. Backoff, jitter, and Retry-After handling apply per key exactly as with a single key.
  • Promotion is sticky. Once a fallback key succeeds, later requests start from it; the walk wraps around the list, so an earlier key that recovers is picked up again after the promoted key fails. Rotation state lives on the client instance and is not persisted across processes.
  • When every key fails, the provider returns the last key's error with (all N API keys failed) appended to the message.
  • Daily quotas stay provider-scoped. Every attempt on every key counts against the provider's max_requests_per_day; a quota-gate rejection does not rotate, because the next key would be rejected identically.

Each rotation emits a key_rotated metric event and an INFO log naming the 1-based key positions — never key material. See observability for details. omni-weather doctor validates every key variant statically and reports the key count.