Universal weather forecast aggregation library with a typed normalized schema.
Project description
omni-weather-forecast-apis
Async Python library that fans out forecast requests across multiple weather providers and normalizes the results into one typed Pydantic schema. It preserves provider-native cadence and time boundaries while converting units and condition codes into a common representation.
Features
- Multi-provider fan-out with async orchestration and partial-failure tolerance
- Typed normalized schema — common Pydantic models for minutely, hourly, daily, and alert data
- Plugin architecture — 13 providers with typed per-provider config validation
- Rate limiting — global concurrency and RPS limits with per-provider overrides
- CLI — loads a TOML config, queries providers, and persists normalized output to SQLite
Supported Providers
| Provider | Plugin ID | API Key | Notes |
|---|---|---|---|
| Open-Meteo | open_meteo |
Optional | Free tier; multiple forecast models |
| MET Norway | met_norway |
No | Requires user_agent identification |
| NWS / NOAA | nws |
No | US coverage only; requires user_agent |
| OpenWeather | openweather |
Yes | |
| WeatherAPI | weatherapi |
Yes | |
| Tomorrow.io | tomorrow_io |
Yes | |
| Visual Crossing | visual_crossing |
Yes | |
| Weatherbit | weatherbit |
Yes | |
| Meteosource | meteosource |
Yes | |
| Pirate Weather | pirate_weather |
Yes | Dark Sky-compatible API |
| Stormglass | stormglass |
Yes | Hourly only; multi-model |
| Weather Unlocked | weather_unlocked |
Yes | Requires app_id + app_key |
| Google Weather | google_weather |
— | Placeholder; currently unavailable |
Quick Start
# 1. Install
uv sync
# 2. Create a minimal config (Open-Meteo and MET Norway require no API keys)
cat > config.toml << 'EOF'
[[providers]]
plugin_id = "open_meteo"
config = { models = ["best_match"] }
[[providers]]
plugin_id = "met_norway"
config = { user_agent = "MyApp/1.0 ops@example.com" }
EOF
# 3. Run a forecast
uv run omni-weather \
--config ./config.toml \
--lat 40.7128 \
--lon -74.0060 \
--sqlite ./forecasts.sqlite
Installation
uv sync
How It Works
- Fan-out — A
ForecastRequestis dispatched concurrently to every enabled provider using async tasks, bounded by configurable concurrency and rate limits. - Normalize — Each provider plugin converts its native response into the common
SourceForecastschema, translating units (e.g. Fahrenheit to Celsius, mph to m/s) and mapping provider-specific condition codes to a sharedWeatherConditionenum. - Aggregate — Results are collected into a single
ForecastResponse. Providers that succeed returnProviderSuccesswith their forecasts; providers that fail returnProviderErrorwith a typed error code. The response always completes, even if some providers fail.
Configuration
The client and CLI both use a TOML configuration file that matches OmniWeatherConfig.
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
[[providers]]
plugin_id = "open_meteo"
enabled = true
config = { models = ["best_match", "ecmwf_ifs025"] }
[[providers]]
plugin_id = "met_norway"
enabled = true
config = { user_agent = "MyApp/1.0 ops@example.com", variant = "complete" }
[[providers]]
plugin_id = "openweather"
enabled = true
config = { api_key = "ow-...", units = "metric" }
rate_limit_rps = 5
timeout_ms = 8000
Library Usage
import asyncio
from omni_weather_forecast_apis import (
ForecastRequest,
Granularity,
OmniWeatherConfig,
ProviderError,
ProviderRegistration,
ProviderId,
ProviderSuccess,
create_omni_weather,
)
async def main() -> None:
config = OmniWeatherConfig(
providers=[
ProviderRegistration(
plugin_id=ProviderId.OPEN_METEO,
config={"models": ["best_match"]},
),
ProviderRegistration(
plugin_id=ProviderId.MET_NORWAY,
config={"user_agent": "MyApp/1.0 ops@example.com"},
),
],
)
async with await create_omni_weather(config) as client:
response = await client.forecast(
ForecastRequest(
latitude=34.2484,
longitude=-117.1931,
granularity=[Granularity.HOURLY, Granularity.DAILY],
),
)
print(response.summary)
# ForecastResponseSummary(total=2, succeeded=2, failed=0)
for result in response.results:
match result:
case ProviderSuccess(provider=pid, forecasts=forecasts):
for fc in forecasts:
for pt in fc.hourly:
print(f"{pid} {pt.timestamp}: {pt.temperature}°C, {pt.condition}")
case ProviderError(provider=pid, error=err):
print(f"{pid} failed: {err.code} — {err.message}")
asyncio.run(main())
Example output:
ProviderId.OPEN_METEO 2026-03-13 18:00:00+00:00: 12.3°C, WeatherCondition.PARTLY_CLOUDY
ProviderId.OPEN_METEO 2026-03-13 19:00:00+00:00: 11.8°C, WeatherCondition.OVERCAST
ProviderId.MET_NORWAY 2026-03-13 18:00:00+00:00: 12.1°C, WeatherCondition.RAIN
...
CLI Usage
uv run omni-weather \
--config ./config.toml \
--lat 34.2484 \
--lon -117.1931 \
--sqlite ./forecasts.sqlite
# Query only specific providers
uv run omni-weather \
--config ./config.toml \
--lat 34.2484 \
--lon -117.1931 \
--sqlite ./forecasts.sqlite \
--provider open_meteo \
--provider nws \
--granularity hourly
| Flag | Required | Default | Description |
|---|---|---|---|
--config PATH |
No | ~/.config/omni_weather_forecast_apis.toml |
Path to TOML configuration file |
--lat FLOAT |
No | config value | Latitude (-90 to 90); overrides config |
--lon FLOAT |
No | config value | Longitude (-180 to 180); overrides config |
--sqlite PATH |
No | config value | SQLite database output path; overrides config |
--provider ID |
No | all enabled | Restrict to specific provider(s); repeatable |
--granularity GRAN |
No | config value | minutely, hourly, or daily; repeatable |
--language LANG |
No | config value | Provider language preference |
--include-raw |
No | config value | Persist raw provider payloads |
--timeout-ms MS |
No | config value | Override the default timeout; provider-specific timeouts still take precedence |
--debug |
No | config value | Enable verbose debug output to stderr and write a .log file next to the SQLite database |
Exit codes: 0 all providers succeeded, 1 at least one provider failed, 2 invalid arguments or configuration/load error.
Partial Failures
The library is designed for partial-failure tolerance. When some providers fail (network errors, rate limits, auth issues), the response still completes with results from the providers that succeeded.
Each entry in response.results is either a ProviderSuccess or ProviderError, distinguished by the status field. The response.summary provides counts at a glance:
response.summary
# ForecastResponseSummary(total=3, succeeded=2, failed=1)
ProviderError includes a typed error.code (AUTH_FAILED, RATE_LIMITED, TIMEOUT, NETWORK, PARSE, NOT_AVAILABLE, UNKNOWN), a human-readable error.message, the error.http_status when available, and error.latency_ms for how long the request ran before failing.
The CLI reflects this in exit codes: 0 means all providers succeeded, 1 means at least one failed (but partial results are still written to SQLite).
Normalized Schema
All provider responses are normalized into a common set of Pydantic models. Units are standardized: temperatures in °C, wind speeds in m/s, pressure in hPa, precipitation in mm, visibility in km.
WeatherDataPoint (hourly)
| Field | Type | Unit |
|---|---|---|
temperature, apparent_temperature, dew_point |
float | None | °C |
humidity |
float | None | % (0-100) |
wind_speed, wind_gust |
float | None | m/s |
wind_direction |
float | None | degrees |
pressure_sea, pressure_surface |
float | None | hPa |
precipitation, rain, snow, snow_depth |
float | None | mm |
precipitation_probability |
float | None | 0-1 |
cloud_cover, cloud_cover_low, cloud_cover_mid, cloud_cover_high |
float | None | % |
visibility |
float | None | km |
uv_index |
float | None | 0-11+ |
solar_radiation_ghi, solar_radiation_dni, solar_radiation_dhi |
float | None | W/m² |
condition |
WeatherCondition | None | enum |
is_day |
bool | None |
DailyDataPoint
| Field | Type | Unit |
|---|---|---|
date |
date | |
temperature_max, temperature_min |
float | None | °C |
apparent_temperature_max, apparent_temperature_min |
float | None | °C |
wind_speed_max, wind_gust_max |
float | None | m/s |
precipitation_sum, rain_sum, snowfall_sum |
float | None | mm |
precipitation_probability_max |
float | None | 0-1 |
cloud_cover_mean |
float | None | % |
humidity_mean |
float | None | % |
uv_index_max |
float | None | 0-11+ |
sunrise, sunset, moonrise, moonset |
datetime | None | UTC |
moon_phase |
float | None | 0-1 |
daylight_duration |
float | None | seconds |
condition |
WeatherCondition | None | enum |
summary |
str | None |
MinutelyDataPoint
| Field | Type | Unit |
|---|---|---|
precipitation_intensity |
float | None | mm/h |
precipitation_probability |
float | None | 0-1 |
WeatherAlert
| Field | Type |
|---|---|
sender_name |
str |
event |
str |
start, end |
datetime (UTC) |
description |
str |
severity |
EXTREME | SEVERE | MODERATE | MINOR | UNKNOWN |
url |
str | None |
WeatherCondition enum
CLEAR, MOSTLY_CLEAR, PARTLY_CLOUDY, MOSTLY_CLOUDY, OVERCAST, FOG, DRIZZLE, LIGHT_RAIN, RAIN, HEAVY_RAIN, FREEZING_RAIN, LIGHT_SNOW, SNOW, HEAVY_SNOW, SLEET, HAIL, THUNDERSTORM, THUNDERSTORM_RAIN, THUNDERSTORM_HEAVY, DUST, SAND, SMOKE, HAZE, TORNADO, HURRICANE, UNKNOWN
Provider Configuration Reference
Each provider accepts a typed config dict. Required fields are marked with bold.
| Provider | Config Keys |
|---|---|
open_meteo |
api_key?, models (default: ["best_match"]), extra_hourly_vars?, extra_daily_vars? |
met_norway |
user_agent, altitude?, variant ("compact" | "complete", default: "complete") |
nws |
user_agent, grid_override? ({office, grid_x, grid_y}) |
openweather |
api_key, exclude?, units ("standard" | "metric" | "imperial", default: "metric") |
weatherapi |
api_key, days (1-14, default: 7), aqi (default: false), alerts (default: true) |
tomorrow_io |
api_key, fields? |
visual_crossing |
api_key, include (default: "hours,days,alerts") |
weatherbit |
api_key, hours (1-240, default: 48), units ("M" | "S" | "I", default: "M") |
meteosource |
api_key, sections (default: ["current", "hourly", "daily"]) |
pirate_weather |
api_key, extend_hourly (default: false), version ("1" | "2", default: "2") |
stormglass |
api_key, sources (default: ["sg"]), params (list of weather variables) |
weather_unlocked |
app_id, app_key, lang? |
google_weather |
api_key? (placeholder, currently unavailable) |
SQLite Output
The CLI creates a normalized database with these tables:
| Table | Contents |
|---|---|
forecast_runs |
Request metadata per invocation |
provider_results |
One row per provider outcome (success or error) |
source_forecasts |
One row per model/source forecast within a provider |
minutely_points |
Precipitation intensity at minute intervals |
hourly_points |
Normalized hourly forecast rows |
daily_points |
Normalized daily summary rows |
alerts |
Weather alerts and warnings |
provider_logs |
Per-provider lifecycle log entries (start, success, error) per run |
Development
# Lint and type-check
uv run black src
uv run ruff check src --fix
uv run pyrefly check src
uv run ty check src
# Complexity and tests
uv run lizard -Eduplicate src
uv run pytest tests/
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file omni_weather_forecast_apis-0.2.0.tar.gz.
File metadata
- Download URL: omni_weather_forecast_apis-0.2.0.tar.gz
- Upload date:
- Size: 47.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
95b2d5b410e539732d7789212b8d99d7c2076d040e3de67492b46a9308751a4e
|
|
| MD5 |
b7851e8d48ed54647bdf8bfaef1eea7d
|
|
| BLAKE2b-256 |
fa23cb90b7859004e46513ce818bead24d04974d1fd33fe01c367308d92cae23
|
File details
Details for the file omni_weather_forecast_apis-0.2.0-py3-none-any.whl.
File metadata
- Download URL: omni_weather_forecast_apis-0.2.0-py3-none-any.whl
- Upload date:
- Size: 69.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
88dbcb3cc1587818a383cbe7cfd4fb48cd5efceb92c3acee4c12bcd2a1952205
|
|
| MD5 |
a46953e6e687d2dcf85733779493540d
|
|
| BLAKE2b-256 |
0ddc4df221ab53f32b0085f14eaa92a97a0845c547ec52a75a76cee6187406a5
|