Skip to content

Guides

Weather observations

Three observation sources feed the same canonical schema. The SDK fetches in parallel, tags every row with its source, and writes a parquet/JSON cache; on overlap it applies a deterministic client-side dedup tiebreak. Every row carries its source identity — you can branch on it.

TiebreakSourceEndpointWindowNotes
3AWCaviationweather.gov/api/data/metarLast 168hLive, lowest-latency. CORS-allowed
2IEMmesonet.agron.iastate.edu/cgi-bin/request/asos.pyFull historyNear-real-time mirror, multi-year archive
1GHCNhncei.noaa.gov/oa/global-historical-climatology-network/...Full historyQuality-flagged archive

When fetched feeds overlap on a (station, observed_at) tuple, a client-side dedup tiebreak in your process keeps one row: AWC > IEM > GHCNh.

See Data sources for the rationale (it’s about raw-as-reported, not absolute correctness).

The high-level call is obs():

from mostlyright.weather import obs
rows = obs("KNYC", "2025-01-06", "2025-01-12")
print(rows[0])
# {
# "station": "KNYC",
# "observed_at": "2025-01-06T00:51:00Z",
# "source": "awc.live",
# "temp_c": 0.5,
# "temp_f": 33,
# "raw_metar": "KNYC 060051Z 35008KT 10SM CLR M01/M05 A3020",
# ...
# }

Default routing — auto-router picks exact_window for ≤7-day windows, warm_cache for larger. See Ingest strategies for the routing logic.

const rows = await obs("KNYC", "2025-01-06", "2025-01-12", { source: "iem" });
// Every row.source === "iem.archive"

Pinning to one source forces exact_window (single-source can’t honor the multi-source merge that warm_cache enables).

The canonical observation row (schema.observation.v1):

{
"station": "KNYC",
"observed_at": "2025-01-06T00:51:00Z",
"source": "awc.live",
"observation_type": "METAR",
"raw_metar": "KNYC 060051Z 35008KT 10SM CLR M01/M05 A3020",
"temp_c": 0.5,
"temp_f": 33,
"dewpoint_c": -2.0,
"dewpoint_f": 28,
"wind_speed_kt": 8,
"wind_direction_deg": 350,
"wind_gust_kt": null,
"pressure_inhg": 30.20,
"precip_mm_1h": 0,
"sky_cover_1": "CLR",
"sky_base_1_m": null
}

See Observation schema for the full field list, units, and dtype conventions.

Every row carries its source on the source field:

  • AWC live rows: "awc.live"
  • IEM archive rows: "iem.archive"
  • GHCNh rows: "ghcnh"
  • Plus .live variants on live.stream() (see Live streaming)

You can branch on this without parsing strings:

for row in rows:
if row["source"].endswith(".live"):
# Live feed; might still be revised
...
else:
# Archive; final
...

raw_metar is the exact text the station broadcast (for AWC + IEM). GHCNh rows have raw_metar: null (the archive carries decoded values only). MetPy can re-parse the raw METAR if you need fields the SDK doesn’t surface:

from metpy.io import parse_metar_to_dataframe
import pandas as pd
raw_rows = obs("KNYC", "2025-01-06", "2025-01-12")
metars = [r["raw_metar"] for r in raw_rows if r["raw_metar"]]
metpy_df = pd.concat([parse_metar_to_dataframe(m) for m in metars])

For per-day max/min temperature rollups (which is what Kalshi NHIGH/NLOW settles on):

from mostlyright.international import daily_extremes
days = daily_extremes("KNYC", "2025-01-06", "2025-01-12")
print(days[0])
# {
# "station": "KNYC",
# "date": "2025-01-06",
# "high_f": 37,
# "low_f": 23,
# "high_at": "2025-01-06T17:51:00Z",
# "low_at": "2025-01-06T06:51:00Z",
# }

The TS port uses the station’s IANA tz from the registry to bucket observations by station-local date. Integer-°F precision matches Kalshi’s settlement rounding for US stations.

Observations cache as per-month parquet (Python) or JSON (TypeScript) under ~/.mostlyright/cache/observations/{station}/{year}/{month}.{parquet|json}. The current LST month is skipped (incomplete data is never cached).

See the Cache guide for the full layout, skip rules, and schema-version invalidation.

If you need to call the source-specific fetcher directly:

from mostlyright.weather.catalog import get_adapter
awc = get_adapter("awc.live") # AwcAdapter
iem = get_adapter("iem.archive") # IEMAdapter
ghcnh = get_adapter("ghcnh.archive") # GhcnhAdapter
cli = get_adapter("cli.archive") # CliAdapter (climate, not observations)
rows = awc.fetch_observations("KNYC", "2025-01-06", "2025-01-12")

Unknown sources raise DataAvailabilityError(reason="model_unavailable").