Skip to content

Guides

Live streaming

mostlyright.live is the ticker surface — a live feed of fresh METARs from a single source. Use it for dashboards, alerting, or anything that polls and reacts in real time. Both SDKs ship mirrored APIs.

live.stream() and live.latest() are intentionally not the same surface as research(). One picks for the other.

research()live.stream() / live.latest()
RoleBATCH (training pairs, settlement)STREAM (real-time monitoring)
SourcesAWC + IEM + GHCNh + CLI (fused)ONE of awc | iem
Cache writesYes (parquet, year-aligned)No
QCYesNo
SemanticsPoint-in-timeAsync generator
Use caseBacktest, train a model, settleWatch live, page on threshold

If you reach for live.stream(), you almost certainly do not want multi-source fusion: a single feed lets you reason about the latency of “what AWC said at 12:01” without IEM’s ~10-min delay smearing the picture.

import asyncio
import mostlyright
async def main() -> None:
async for row in mostlyright.live.stream("KNYC"):
print(row["observed_at"], row["temp_f"])
asyncio.run(main())

Signature:

  • Python: mostlyright.live.stream(station, *, source=None, poll_seconds=None) -> AsyncIterator[dict]
  • TypeScript: stream(station: string, opts?: { source?, pollSeconds?, signal? }): AsyncGenerator<LiveObservation>

Per-row contract:

  • Same shape as the canonical Observation schema — station, observed_at (ISO-8601 UTC), observation_type (METAR/SPECI), plus weather fields.
  • source carries the live-identity tag: "awc.live" or "iem.live" — distinguishes from research()-channel tags ("awc"/"iem").
  • Duplicate emission is suppressed: the generator yields a row only when observed_at differs from the previous one. Both AWC and IEM serve “the latest METAR” repeatedly between observation cycles; deduping collapses that noise.
row = await mostlyright.live.latest("KNYC")
print(row["temp_f"])

Signature:

  • Python: mostlyright.live.latest(station, *, source=None) -> dict
  • TypeScript: latest(station: string, opts?: { source? }): Promise<LiveObservation>

latest() shares the per-tick fetch path with stream() but returns exactly once. Use it from cron-style schedulers, or when you want a single fresh observation without managing a generator’s lifecycle.

Unlike stream(), latest() raises NoLiveDataError when the upstream returns no observations. The error’s toDict() payload includes the resolved station and the source identity tag so caller logs can branch.

SourceDefault?EndpointTypical latencyUse when
"awc"yesaviationweather.gov/api/data/metar<1 minuteDefault — lowest latency
"iem"nomesonet.agron.iastate.edu/cgi-bin/request/asos.py~5–10 minutesAWC is down, or you want an independent cross-check

Each row carries its source identity on the source field:

  • AWC rows → source == "awc.live"
  • IEM rows → source == "iem.live"

The .live suffix distinguishes these from the archive-channel tags ("awc" / "iem") emitted by research() and the historical fetchers.

mostlyright.live will not let you poll an upstream faster than its polite floor:

SourceFloor
awc30s
iem60s

Calls like stream(..., poll_seconds=10) for an AWC stream raise ValueError("poll_seconds=10 below polite floor 30s for source='awc'") before the first poll. Omitting poll_seconds= uses the floor.

Why the floor:

  1. Politeness. Both endpoints are public goods. AWC has no documented rate limit; IEM is a university server.
  2. Useful signal. METARs are issued at roughly hourly cadence (SPECIs are intra-hour). Polling faster than ~30s does not surface new data on most stations — it just costs bandwidth.
# Standard `async for` consume
async for row in mostlyright.live.stream("KNYC"):
process(row)
# Break cleanly — the polite-floor sleep is `asyncio.sleep`, which
# propagates `CancelledError`, so `break` exits without zombie tasks.
async for row in mostlyright.live.stream("KNYC"):
process(row)
if row["temp_f"] > 95:
break
# Run multiple stations in parallel — one task per station, polite
# floors apply per-task (NOT pooled).
async def watch(station: str) -> None:
async for row in mostlyright.live.stream(station):
process(station, row)
await asyncio.gather(watch("KNYC"), watch("KLAX"), watch("KORD"))
// Standard `for await` consume
for await (const row of stream("KNYC")) {
process(row);
}
// Abort via AbortSignal — interrupts the polite-floor sleep promptly.
const controller = new AbortController();
setTimeout(() => controller.abort(), 60_000);
for await (const row of stream("KNYC", { signal: controller.signal })) {
process(row);
}
// Multi-station fan-out
await Promise.all(
["KNYC", "KLAX", "KORD"].map(async (station) => {
for await (const row of stream(station)) {
process(station, row);
}
}),
);

Can I write live rows to the parquet cache? No. live.stream() never writes to disk. If you want a durable back-of-tape, call research() separately.

Can I fuse AWC and IEM? Not in this surface — it’s intentionally single-source. Use research() for fused output. If you really want both channels live, run two stream() tasks in parallel and join them yourself.

What if poll_seconds is below the polite floor? ValueError is raised before the first poll with the floor and source named.

Can I run this in a browser? Yes — IEM has CORS open, AWC needs a proxy. See Browser integration for the supported pattern.