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_attemptsfetch attempts, each subject to the per-requesttimeout_ms. - Each key gets the full retry policy. Backoff, jitter, and
Retry-Afterhandling 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.