Skip to content

Data availability by source

For every source this SDK can stamp on a row: what you call, how far back it goes (or how wide its rolling window is), how long after the event the value appears, and exactly what happens when you ask outside that window. Every number is either a constant in the SDK or a measured upstream fact, printed as separate statements — where the SDK’s own gate is stricter than the upstream archive, the SDK gate is the operative bound.

Three regimes appear as labels throughout the tables below.

Fixed archive floor — a date constant in the SDK. A request before it fails or returns nothing before any network call. nwp:hrrr is gated at 2014-07-30T00:00:00Z by NWP_HISTORICAL_DEPTH["hrrr"], and check_historical_depth() raises HistoricalDepthError pre-network.

Per-station archive — no universal start date. The record begins whenever that station, series or station-year begins, so these rows say station-dependent and print measured examples instead of one number. IEM ASOS publishes a per-station archive_begin: CQT 1900-12-31, OAK 1914-05-01, DSM 1928-01-01, MDW 1928-10-04, ADU 1994-12-25 — all under the SDK’s own MIN_YEAR floor of 1940-01-01. GHCNh station USW00094728 resolves from 1965, is absent for all of 1985 through 1994, and resumes in 1995. A single start date for one of these sources would be wrong for most stations, so this page never prints one.

Rolling window / capture ledger — the left edge moves with now, or the route serves only the current value and history exists only in your own captures (persist=True). awc.live reaches back 168 hours. nws.alerts.live reaches back 7 days. nwps.stage_and_flow.live with series="forecast" serves exactly one issuance — the current one — so an issuance you did not capture cannot be fetched again, from anywhere.

Every table below uses the same five columns, in this order:

  • Source tag — the value stamped on the row (df.attrs["source"], and the per-row source column where the schema carries one).
  • Call — the public verb plus the argument that selects this source.
  • Coverage — the window, labelled with one of the three regimes above.
  • Publication lag — how long after the event the value becomes readable.
  • Out of window — the exact exception class, with its error_code where one exists. Where nothing raises, the cell says what comes back instead: an empty typed frame, or — for training_table() — one row per calendar day regardless of coverage, with no exception and no empty table.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | awc.live | weather.observations(station, from_date, to_date, source="awc"); also the AWC leg of the default merged path | Rolling window. 168 hours back from now — the SDK’s own request bound: the exact-window leg requests hours=168 and the warm-cache leg applies _AWC_LOOKBACK_HOURS = 168. There is no awc.archive, so a window ending before now - 168h returns zero AWC rows. | knowledge_time = event_time_utc + 5 min | No exception. Rows outside the window are simply absent; a window entirely outside it yields zero AWC rows. | | awc.live | weather.live.latest(station, source="awc"), weather.live.stream(station, source="awc") | Rolling window. The poll requests hours=1 and rejects any returned observation older than MAX_AGE_S["awc"] = 5400.0 s (90 minutes). | Same +5 min per-source lag; poll cadence floored at 30 s | NoLiveDataError (subclass of LiveStreamError, itself a NoDataError), carrying station= and source="awc.live" — raised by the one-shot latest() both when zero rows come back and when the newest row is older than 5400 s. stream() never raises it: an empty or stale poll is a skipped tick, and the generator keeps polling. | | iem.archive | weather.observations(station, from_date, to_date, source="iem"); also the backbone leg of the default merged path | Per-station archive. IEM states a temporal domain of 1900 to present for its METAR/ASOS dataset and publishes a per-station archive_begin; the SDK adds no registry start date. The parser’s MIN_YEAR gate drops any timestamp outside calendar years 1940 through 2100 without raising, so 1940-01-01 is the earliest date the SDK can return. The upper end is the present — IEM is queried for the whole requested window with no recency gate. A pinned source="iem" call issues one request for the whole window; on the default merged path the IEM leg is split into per-calendar-year sub-requests, however short the window. | knowledge_time = event_time_utc + 15 min | No exception. A window before the station’s archive_begin returns HTTP 200 with a header-only CSV body, so you get a 0-row frame. | | iem.live | weather.live.latest(station, source="iem"), weather.live.stream(station, source="iem") | Rolling window. The poll fetches yesterday and today inclusive in UTC (report_type=3 METAR and report_type=4 SPECI) and rejects any returned observation older than MAX_AGE_S["iem"] = 7200.0 s (120 minutes). | Same +15 min per-source lag; poll cadence floored at 60 s | NoLiveDataError with source="iem.live" from the one-shot latest() — zero rows returned, or newest row older than 7200 s. stream() skips the tick instead of raising. | | ghcnh.archive | weather.observations(station, from_date, to_date, source="ghcnh"); also the lowest-priority leg of the default merged path | Per-station archive, per station-year. NCEI publishes one PSV per station-year (GHCNh_<id>_<YEAR>.psv) and a missing station-year 404s; the gaps are interior as well as leading. NCEI’s own metadata gives the archive-wide period of record as 1750-01-01 to present, so no single start year is meaningful; this SDK floors every row at 1940-01-01 through the same MIN_YEAR gate. | event_time_utc + 6 h — a floor, not the real lag. NCEI publication trails by days to weeks: measured 2026-08-06, the current-year file’s mtime was 21:01 UTC and its newest record was 2026-08-02T16:13:00. | No exception. NCEI’s 404 for a station-year with no data is caught and the year skipped, so the call returns a 0-row typed frame. | | merged.live_v1 | weather.observations(station, from_date, to_date)source=None, the default local path | Per-station archive. The union of the three legs above, not their intersection. IEM ASOS is the only leg queried for the whole window, so in practice the merged window equals that station’s IEM ASOS coverage, floored at 1940-01-01. | Per row, by that row’s own source: awc +5 min, iem +15 min, ghcnh +6 h. Materialized as the knowledge_time column. | A 0-row schema.observation.merged.v1 frame with the full typed column set and df.attrs["source"] == "merged.live_v1" — never a columnless frame, never a DataAvailabilityError. One entity-axis exception: a station resolved with allow_unknown whose result is empty raises NoDataError (cause="entity_unknown_and_window_empty") instead of returning the 0-row frame. | | cli.archive | weather.daily_summaries(station, from_date, to_date) | Per-station archive, per station-year. One HTTP request per calendar year against IEM’s cli.py mirror (the endpoint has no partial-year mode); a year the station has no CLI product for 404s and is skipped. There is no code-side floor in the CLI parser — the limits are upstream: the station must have a WFO-issued NWS CLI product, and IEM must hold that station-year. | Report-type dependent, read off the product timestamp: a same-day issue is preliminary; the overnight report the next day, issued in hours 04 through 10 UTC inclusive (04:00 to 10:59), is final. | No exception. A station-year with no product contributes no rows. | | cli.archive (label — these verbs stamp schema.core.training_table.v1 and label_source_id="weather.label.cli" respectively, not attrs["source"]) | weather.training_table(station, from_date, to_date), weather.label.daily_summary(station, from_date, to_date) | Per-station archive, the same station-year CLI availability as daily_summaries() — but the output shape hides it, because the table is minted from a calendar spine. | The same CLI report-type lag. training_table() also emits knowable_at_utc = max(decision_time_utc, label_available_time_utc). | One row per calendar day regardless of coverage; no exception and no empty table. Read daily_summary_report_type and the daily_summary_* values to tell a covered day from an uncovered one. | | cli.live | none — not reachable on any code path | Zero. No code path in the SDK emits cli.live. weather.daily_summaries() unconditionally stamps cli.archive, and the climate cache treats a *.live source as a no-op write. | n/a | n/a — the tag cannot be produced or requested. |

Four things this table does not fit in a cell:

  • The merged window is a union, and the merge is prioritized. On a collision for the same (station, event_time_utc, observation_type) key, priority is AWC (3) over IEM (2) over GHCNh (1), with a strict > comparison and first-seen wins at equal priority. AWC adds nothing older than now - 168h; GHCNh adds nothing for a non-US station, nothing for the station’s current LST month or later, and nothing for the 65 catalog stations with no ghcnh_id.
  • The dotted names on the pinned rows are registry vocabulary, not the stamped value. A pinned weather.observations(..., source="iem") call stamps df.attrs["source"] = "iem", and the per-row source column always carries the bare parser tag (awc, iem, ghcnh) — deliberately never rewritten to the dotted form. awc.live, iem.archive and ghcnh.archive are how the schema registry names those legs. The weather.live.* rows, merged.live_v1 and daily_summaries()’s cli.archive are stamped exactly as printed.
  • GHCNh covers 29 of the 94 catalog stations. Those 29 carry a ghcnh_id of USW00* form; the other 65 carry ghcnh_id="". NCEI rewrites the current-year PSV daily, but the observations inside it trail that rewrite by several days, which is why the unpinned default path excludes the station’s current LST month. An explicit source="ghcnh" pin bypasses that gate.
  • An empty IEM frame is ambiguous. A window before a station’s archive_begin returns HTTP 200 with a header-only CSV body, not a 404, so an empty frame does not distinguish “the station had no data yet” from “no rows matched”. Upstream AWC, for its part, serves a rolling 15-day database and accepts hours up to about 745 — the 168-hour bound is the SDK’s choice, not an upstream ceiling. Use source="iem" for history.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | nwp:hrrr | weather.nwp.forecasts(station, "hrrr", cycle=..., forecast_hour=...), or weather.forecasts(..., source="nwp", models=["hrrr"]) | Fixed archive floor 2014-07-30T00:00:00Z (NWP_HISTORICAL_DEPTH["hrrr"]). Hourly cycles, all 24 cycle hours. Wired end to end. | 90-minute upload backoff on the default cycle | HistoricalDepthError (error_code NWP_HISTORICAL_DEPTH, source nwp.hrrr) with attributes model, requested_cycle and archive_depth=datetime(2014, 7, 30, tzinfo=UTC), raised pre-network by check_historical_depth(). | | nwp:hrrrak | weather.nwp.forecasts(station, "hrrrak", cycle=...) | Fixed archive floor 2018-01-01T00:00:00Z. 3-hourly cycles at 00, 03, 06, 09, 12, 15, 18 and 21Z. Wired end to end. | 90-minute upload backoff on the default cycle | HistoricalDepthError (NWP_HISTORICAL_DEPTH, source nwp.hrrrak) with archive_depth=2018-01-01T00:00:00+00:00, pre-network. | | nwp:gfs, nwp:gdas | weather.nwp.forecasts(station, "gfs", cycle=...) or ..., "gdas", ... | Fixed archive floor 2021-01-01T00:00:00Z for both. 6-hourly cycles at 00, 06, 12 and 18Z. Both wired end to end. | 90-minute upload backoff on the default cycle | HistoricalDepthError with archive_depth=2021-01-01T00:00:00+00:00 and source="nwp.gfs" or "nwp.gdas", pre-network. | | nwp:gefs | weather.nwp.forecasts(station, "gefs", cycle=..., member=...) | Fixed archive floor 2017-01-01T00:00:00Z. 6-hourly cycles. Wired end to end; 33 member values (c00, p01 through p30, avg, spr; default c00). | 90-minute upload backoff on the default cycle | HistoricalDepthError with archive_depth=2017-01-01T00:00:00+00:00, pre-network. A member outside the 33 raises ValueError. | | nwp:nbm, nwp:rap | weather.nwp.forecasts(station, "nbm", cycle=...) or ..., "rap", ... | Fixed archive floor 2020-01-01T00:00:00Z for both. Hourly cycles, all 24 hours. Both wired end to end. | 90-minute upload backoff on the default cycle | HistoricalDepthError with archive_depth=2020-01-01T00:00:00+00:00, pre-network. | | nwp:rrfs, nwp:rtma, nwp:urma | weather.nwp.forecasts(station, "rrfs", cycle=...), ..., "rtma", ..., ..., "urma", ... | Fixed archive floor 2024-01-01T00:00:00Z for all three. Hourly cycles. All wired end to end. RTMA and URMA are analysis products: forecast_hour defaults to 0 and only 0 is valid. | 90-minute upload backoff on the default cycle | HistoricalDepthError with archive_depth=2024-01-01T00:00:00+00:00, pre-network. | | nwp:cfs | weather.nwp.forecasts(station, "cfs", cycle=..., member=...) | Fixed archive floor 2011-01-01T00:00:00Z — the earliest floor in the wired set. 6-hourly cycles. Wired end to end; 4 members 01 through 04, default 01. | 90-minute upload backoff on the default cycle | HistoricalDepthError with archive_depth=2011-01-01T00:00:00+00:00, pre-network. A member outside the 4 raises ValueError. | | nwp:ecmwf_ifs_hres, nwp:ecmwf_ifs_ens | weather.nwp.forecasts(station, "ecmwf_ifs_hres", ...) or ..., "ecmwf_ifs_ens", ... | Not fetchable. Every call raises NWPModelNotAvailableError; the registry records a NWP_HISTORICAL_DEPTH value of 2022-01-01T00:00:00Z that nothing reads today. | n/a | NWPModelNotAvailableError (NWP_MODEL_NOT_AVAILABLE, source nwp.ecmwf_ifs_hres, available_in="v0.2") for every call regardless of cycle — the _RESERVED_MODELS gate runs before check_historical_depth(). | | nwp:ecmwf_aifs_single, nwp:ecmwf_aifs_ens | weather.nwp.forecasts(station, "ecmwf_aifs_single", ...) or ..., "ecmwf_aifs_ens", ... | Not fetchable. The registry records 2024-02-25T06:00:00Z — note the 06Z hour, not midnight — that nothing reads today. | n/a | NWPModelNotAvailableError (NWP_MODEL_NOT_AVAILABLE, available_in="v0.2") on every call, from the _RESERVED_MODELS gate before check_historical_depth(). | | nwp:nam | weather.nwp.forecasts(station, "nam", ...) | Not fetchable, and retiring. The registry records 2024-01-01T00:00:00Z that nothing reads today. NWS scn26-47 retires NAM on 2026-08-31 (LEGACY_MODELS_RETIRE). | n/a | Two stages: a DeprecatedModelWarning (subclass of DeprecationWarning, message beginning nam retires on 31 Aug 2026 per NWS scn26-47) is emitted first, then NWPModelNotAvailableError (NWP_MODEL_NOT_AVAILABLE) is raised. | | nwp:hrdps, nwp:rdps, nwp:gdps, nwp:geps, nwp:reps | weather.nwp.forecasts(station, "hrdps", ...) and the four siblings | Rolling window, and unreachable. NWP_HISTORICAL_DEPTH is None for all five MSC Canada models; MSC Datamart keeps a 24-hour rolling window. forecast_nwp() short-circuits and raises for every call, live cycle included. | n/a | HistoricalDepthError with archive_depth=None (NWP_HISTORICAL_DEPTH, source e.g. nwp.hrdps). | | nwp:hafs, nwp:href, nwp:hiresw | weather.nwp.forecasts(station, "hafs", ...), ..., "href", ..., ..., "hiresw", ... | Rolling window, and unreachable. NWP_HISTORICAL_DEPTH is None (NOMADS-only, no AWS Big Data Program mirror); the registry’s live tolerance for None-depth models is LIVE_CYCLE_WINDOW = 7 days. All three are reserved and raise. HREF and HiResW also retire 2026-08-31. | n/a | NWPModelNotAvailableError (NWP_MODEL_NOT_AVAILABLE, available_in="v0.2") for every call via the _RESERVED_MODELS gate — not HistoricalDepthError, which is the MSC-only path. |

Upstream reality, kept separate from the SDK gates above:

  • HRRR. s3://noaa-hrrr-bdp-pds/ begins 18 hours later than the gate: hrrr.20140730/ carries only the 18Z through 23Z cycles, so the earliest fetchable cycle is 2014-07-30T18:00:00Z and 2014-07-31 is the first day with all 24 cycle hours. The mirror is not gap-free — 2016-03-19 is absent entirely, and the wrfsfcf product the SDK reads is missing on scattered individual cycles (verified absent at 18Z on 2015-03-01 and 2016-08-24). A cycle inside the gate but absent upstream fails at fetch, not at the gate.
  • HRRR-Alaska. Upstream starts 2018-07-11T18:00:00Z — 18Z and 21Z only that day, with 2018-07-12 the first complete day.
  • Retirements. nam, href and hiresw all retire 2026-08-31 per NWS scn26-47.
  • MSC Canada and the NOMADS-only trio. The five MSC models carry NWP_HISTORICAL_DEPTH of None because Datamart keeps 24 hours; hafs, href and hiresw carry None because they have no AWS mirror at all.
  • The default cycle. With cycle= omitted, _default_cycle_for() picks the newest cycle satisfying cycle + forecast_hour*1h <= now - 90min. That 90-minute backoff is the run-plus-upload allowance and is the same for every wired model.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | iem.archive | weather.forecasts(station, from_date, to_date, source="mos", models="gfs") | Per-station archive. IEM’s GFS key is the MAV bulletin (GFS short-range, +6h to +72h at 3h steps). IEM’s archive-status table gives an archive start of 16 Dec 2003; the earliest HTTP 200 observed is 2003-12-16T12:00Z (00Z and 06Z that day 404). | The 6-hour _MOS_COMPLETION_LAG decides cacheability: a runtime younger than 6 h is fetched live on every call and never cached. | No exception. An uncovered runtime 404s and yields an empty frame. | | iem.archive | weather.forecasts(..., source="mos", models="lav") | Per-station archive. IEM’s LAV key is the LAMP bulletin. Archive start 12 Jul 2020; earliest HTTP 200 observed 2020-07-12T00:00Z. | Same 6-hour _MOS_COMPLETION_LAG. | No exception; empty frame. | | iem.archive | weather.forecasts(..., source="mos", models="nbe") | Per-station archive. IEM’s NBE key is the NBM extended bulletin. Archive start 23 Jul 2020 on IEM’s table, but the earliest HTTP 200 observed on any station tested is 2020-08-28T01:00Z. | Same 6-hour _MOS_COMPLETION_LAG. | No exception; empty frame. | | iem.archive | weather.forecasts(..., source="mos", models="met") | Zero. met is not an IEM model. The endpoint enforces ^(AVN\|GFS\|ETA\|NAM\|NBS\|NBE\|ECM\|LAV\|MEX)$ and returns HTTP 422 for it, on every station and every runtime tested. | n/a | httpx.HTTPStatusError, surfaced by raise_for_status(). | | iem.archive | weather.forecasts(..., source="mos", models="ecm") | Zero. ecm passes the endpoint’s validator, but IEM’s archive holds no ECM rows for any station or runtime tested, including the no-runtime latest query. | n/a | No exception. The request 404s and yields an empty frame. |

Notes on this source:

  • The SDK imposes no date floor at all. from_date is checked only for ISO parsing and window inversion; there is no archive-start constant in _iem_mos.py. Coverage is exactly what IEM’s MOS database holds for that (station, model, runtime) triple, and it is station-dependent. No station tested — KORD, KNYC, KDSM, KLAX, KSEA, KDEN, KATL, KMIA — returns NBE rows before 2020-08-28T01:00Z. Probe the station you actually want rather than planning a backfill against one date.
  • IEM’s GFS key is MAV, not MEX. IEM exposes GFS Extended separately as MEX (+24h to +192h at 12h steps); the SDK does not expose it.
  • Model names are uppercased before the request. IEM’s validator is uppercase-only, so model="nbe" sent verbatim returns HTTP 422; the fetcher applies .upper().
  • NBE cycle hours moved. They were 01, 07, 13 and 19Z before 2026-05-05 and are 00, 06, 12 and 18Z from 2026-05-05. A window spanning that date must query both sets.
  • A 404 on a cacheable runtime is cached permanently. For any runtime older than the 6-hour _MOS_COMPLETION_LAG, a 404 (or a 200 that parses to zero rows) writes a .404 sentinel under $HOME/.mostlyright/cache/v1/forecasts/iem_mos/{station}/{model}/, and a sentinel hit skips the request entirely. Runtimes younger than 6 hours are never cached, so their 404s are re-requested on every call. Requesting a pre-archive window — or ecm — marks those older runtimes empty for good. Delete the sentinel files if IEM backfills later, or the re-run keeps returning empty.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | open_meteo.previous_runs | weather.forecasts(station, from_date, to_date, source="open_meteo", models="gfs_global") | The only Open-Meteo mode reachable from weather.forecasts(). Per-model AVAILABILITY_FLOOR dates are listed in the next section; no code path reads them. The Previous Runs API is queried with the _previous_day1 suffix only, so every value comes from the previous day’s run. | Per-model PUBLISH_LAG, applied only on mode="live". | No exception and no floor check. A pre-floor window goes upstream unchanged and comes back empty. | | open_meteo.single_run | mostlyright.weather._fetchers._open_meteo.fetch_open_meteo(..., mode="training", issued_at="YYYY-MM-DDTHH:MM") — private, not reachable from weather.forecasts() | Starts later than previous_runs and is not encoded in any constant. This is the only Open-Meteo mode with byte-exact cycle provenance: issued_at equals the requested run, with no derivation. | None — issued_at equals the requested run exactly. | No exception and no floor check. | | open_meteo.seamless | fetch_open_meteo(..., mode="seamless", allow_leakage=True) — private, and refused by default | The Historical Forecast API, and unusable for point-in-time work: the model cycle behind each value is unrecoverable from the response, so the SDK records no availability date for it. | n/a | Not a window error — the endpoint is refused outright. OpenMeteoSeamlessLeakageError (subclass of LeakageError), carrying model and endpoint_url, raised by _dispatch_endpoint() before any request unless allow_leakage=True. | | open_meteo.live | fetch_open_meteo(..., mode="live") — private, not reachable from weather.forecasts() | Rolling window. No archive at all: forward-looking only, and the cycle rolls forward continuously. There is no start date; the window is whatever the current run covers. Never cached — _is_live_source blocks both read and write. | Per-model PUBLISH_LAG: 6 h for global models, 4 h for mid-scale, 2 h for high-resolution. See the next section for the value per model. | No exception for a past-dated window: the endpoint returns what it has, and a 404 is swallowed into an empty canonical DataFrame. |

AVAILABILITY_FLOOR is registry metadata that no code path reads. An out-of-floor request therefore returns an empty frame rather than raising, on every Open-Meteo mode.

All 36 models below carry the source tag open_meteo.previous_runs, the same call shape (weather.forecasts(station, from_date, to_date, source="open_meteo", models="<model>")) and the same out-of-window behavior (no floor check, empty frame). These tables therefore give only what varies per model: the declared AVAILABILITY_FLOOR and the PUBLISH_LAG that mode="live" applies.

NCEP / GFS family

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | gfs_seamless | 2024-01-01 | 6 h | | gfs_global | 2024-01-01 | 6 h | | gfs_graphcast025 | 2024-01-01 | 6 h | | aigfs025 | 2026-01-07 | 6 h | | hgefs025 | 2026-01-07 | 6 h | | ncep_hrrr_conus | 2024-01-01 | 2 h | | ncep_nbm_conus | 2024-10-08 | 2 h | | ncep_nam_conus | 2024-01-01 | 2 h |

ECMWF family

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | ecmwf_ifs025 | 2024-01-01 | 6 h | | ecmwf_ifs_hres | 2024-01-01 | 6 h | | ecmwf_aifs025_single | 2024-03-14 | 6 h |

DWD ICON family

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | dwd_icon_seamless | 2024-01-01 | 4 h | | dwd_icon_global | 2024-01-01 | 4 h | | dwd_icon_eu | 2024-01-01 | 4 h | | dwd_icon_d2 | 2024-01-01 | 2 h | | dwd_icon_d2_15min | 2024-01-01 | 2 h |

Météo-France family

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | meteofrance_seamless | 2024-01-01 | 4 h | | meteofrance_arpege_world025 | 2024-01-01 | 6 h | | meteofrance_arpege_europe | 2024-01-01 | 4 h | | meteofrance_arome_france0025 | 2024-01-01 | 2 h | | meteofrance_arome_france_hd | 2024-01-01 | 2 h | | meteofrance_arome_france_hd_15min | 2024-01-01 | 2 h |

JMA family

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | jma_seamless | 2018-01-01 | 6 h | | jma_gsm | 2018-01-01 | 6 h | | jma_msm | 2018-01-01 | 2 h |

KMA family

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | kma_seamless | 2024-01-01 | 6 h | | kma_gdps | 2024-01-01 | 6 h | | kma_ldps | 2024-01-01 | 2 h |

CMA

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | cma_grapes_global | 2024-01-01 | 6 h |

BoM

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | bom_access_global | 2024-01-01 | 6 h |

UKMO and MET Norway

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | ukmo_global_deterministic_10km | 2024-01-01 | 6 h | | ukmo_uk_deterministic_2km | 2024-01-01 | 2 h | | metno_nordic_pp | 2024-01-01 | 2 h |

GEM Canada

| models= | AVAILABILITY_FLOOR | PUBLISH_LAG | |---|---|---| | cmc_gem_gdps | 2024-01-01 | 6 h | | cmc_gem_rdps | 2024-01-01 | 4 h | | cmc_gem_hrdps | 2024-01-01 | 2 h |

Four floors are worth knowing by name: the three JMA models declare 2018-01-01, six years earlier than the 2024-01-01 bulk; ncep_nbm_conus (2024-10-08) and ecmwf_aifs025_single (2024-03-14) declare in-year floors that differ from their family siblings; and aigfs025 and hgefs025 tie for the shortest record at 2026-01-07. Where the forecast-sources page states a different gfs_global start, the registry value above is the one the code carries.

weather.forecasts() itself imposes no date floor and no date ceiling. There is no “earliest supported date” constant anywhere in the forecast dispatch code. The verb validates ISO parsing, timezone-awareness and window inversion, then delegates availability entirely to the engine you select with source=.

It raises ContractError, before any I/O, for a missing from_date or to_date, a malformed ISO date, a naive or non-UTC datetime, an inverted window, an unknown source, or the rejected spelling source="nwp_grib". Everything else — floors, retirements, empty frames — comes from the NWP registry, IEM MOS or Open-Meteo, as described above.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | noaa_goes:goes16 | weather.satellite(station, "goes16", product="ABI-L2-ACMC", from_time=..., to_time=...)delivery="live", needs the [satellite] extra | Fixed archive floor 2017-05-24 onward (GOES-East). Nothing before this date is fetchable on the local path. | Not recorded per platform; see the goes19 row for the measured CONUS cadence. | Empty result, no exception: list_product_entries returns [] with no I/O. | | noaa_goes:goes17 | weather.satellite(station, "goes17", product="ABI-L2-ACMC", ...) | Fixed archive floor 2018-08-27 onward (ABI first light on the GOES-West predecessor). Real data ends 2023-01-10. | Not recorded per platform. | Empty result, no exception. | | noaa_goes:goes18 | weather.satellite(station, "goes18", product="ABI-L2-ACMC", ...) | Fixed archive floor 2022-07-28 onward (operational GOES-West / PACUS). Deliberately conservative: commissioning-era objects exist back to 2022-05-11, are gappy, and the SDK refuses them. | Not recorded per platform. | Empty result, no exception. | | noaa_goes:goes19 | weather.satellite(station, "goes19", product="ABI-L2-ACMC", ...) | Fixed archive floor 2024-11-15 onward (SDK floor). Current operational GOES-East since 2025-04-07. No end clamp — this is the live platform. | ABI L2 CONUS objects appear in s3://noaa-goes19/ roughly 3 minutes after scan start, on the 5-minute CONUS cadence. | Empty result, no exception. | | noaa_goes:goes16, noaa_goes:goes17 (per-product floors) | weather.satellite(..., product="ABI-L2-TPWC") and the six siblings ABI-L2-ACHAC, ABI-L2-DSRC, ABI-L2-LVMPC, ABI-L2-LVTPC, ABI-L2-CTPC, ABI-L2-DSIC | Per-product rollout floors exist but are not applied on the live path: TPWC, ACHAC, DSRC, LVMPC and LVTPC from 2019-12-01; CTPC and DSIC from 2019-01-01. Effective live availability is still the platform floor. | As per platform. | No exception and no short-circuit either — the product floor shows up only as naturally-empty listings. | | jma_himawari:himawari8 | weather.satellite(station, "himawari8", product="AHI-L2-FLDK-Clouds", ...) | Fixed archive floor 2019-01-01 onward (AHI L2 clouds on the anonymous noaa-himawari8 NODD bucket). | Not recorded. | Empty result, no exception. | | jma_himawari:himawari9 | weather.satellite(station, "himawari9", product="AHI-L2-FLDK-Clouds", ...) | Fixed archive floor 2023-01-01 onward — the current operational AHI at the 140.7E slot, and the auto-route default for longitude at or above 90. | Not recorded. | Empty result, no exception. | | noaa_viirs:viirs-npp | weather.satellite(station, "viirs-npp", product="VIIRS-JRR-CloudMask", ...) | Fixed archive floor, per product, effective floor = max(platform, product): AOD 2022-01-01 (the platform floor), CloudHeight 2023-01-01, CloudMask 2023-06-20, LST and SnowCover 2024-11-01. | Not recorded. | Empty result, no exception. | | noaa_viirs:viirs-n20 | weather.satellite(station, "viirs-n20", product="VIIRS-JRR-CloudMask", ...) — the auto-route fill default | Fixed archive floor, per product, max(platform, product): AOD 2022-01-01, CloudHeight 2023-01-01, CloudMask 2023-02-19, LST and SnowCover 2024-11-01. | Not recorded. | Empty result, no exception. | | noaa_viirs:viirs-n21 | weather.satellite(station, "viirs-n21", product="VIIRS-JRR-CloudMask", ...) | Fixed archive floor, per product, max(platform, product): AOD 2023-01-01 (the platform floor), CloudHeight 2023-01-01, CloudMask 2023-11-13, LST and SnowCover 2024-11-01. | Not recorded. | Empty result, no exception. | | eumetsat_meteosat:meteosat-0deg | weather.satellite(station, "meteosat-0deg", product="MSG-CLM", ...) — needs credentials: a complete EUMETSAT_CONSUMER_KEY + EUMETSAT_CONSUMER_SECRET pair wins, else eumdac’s stored credentials are used | Fixed archive floor 2004-01-29 onward for EO:EUM:DAT:MSG:CLM (SEVIRI Cloud Mask, 0-degree service). The first day is partial: the earliest product held is the 13:00Z slot. Cadence is a nominal 96 slots per day, at a 15-minute repeat. | A new slot appears in the Data Store roughly 14 minutes after its nominal slot time (measured 13.5 to 13.7 min, median 13.6, across 60 consecutive slots). | Two behaviors, and they are different. With no credentials from either source: MeteosatCredentialsRequiredError (subclass of both SatelliteError and SourceUnavailableError, error_code METEOSAT_CREDENTIALS_REQUIRED), which never falls back to another instrument. With credentials, a day before 2004-01-29: the per-day live path applies no floor, issues a real Data Store search, gets nothing, and returns an empty DataFrame. The 2004-01-29 clamp fires only on the month/backfill path. | | eumetsat_meteosat:meteosat-iodc | weather.satellite(station, "meteosat-iodc", product="MSG-CLM", ...) — same credential resolution | No floor is recorded and none is applied for EO:EUM:DAT:MSG:CLM-IODC (Indian Ocean Data Coverage, 45.5E). The window is whatever the Data Store serves. | As for the 0-degree service. | No exception and no clamp at all. With no credentials, MeteosatCredentialsRequiredError. | | satellite.hosted (label; rows keep the family tag) | weather.satellite(station, satellite, product=..., from_time=..., to_time=..., delivery="hosted") with MOSTLYRIGHT_WEATHER_HOSTED_URL and MOSTLYRIGHT_API_KEY | Capture ledger. No fixed start date exists in code. Coverage is exactly the set of (satellite, product, station, YYYY, MM) parquet partitions the operator-run backfill has written to R2 — query GET /capabilities for the live per-station month list. | The current UTC month is never present, because cache.write_satellite_cache is a no-op for it and the backfill writes through that function. Older months appear only after an operator backfill run. | Three outcomes. A missing partition: the server skips it and returns [], so the client builds an empty DataFrame, with no exception. A window wider than 120 months: HTTP 422, turned into SourceUnavailableError(source="satellite.hosted", http_status=422, retryable=False). Any other R2 read failure: a deliberate HTTP 502 rather than a false empty, surfacing as SourceUnavailableError(http_status=502, retryable=True). Unset environment variables raise HostedConfigError (HOSTED_CONFIG_MISSING) before any network call. | | none — the call raises before a row exists (satellite.extra-missing) | weather.satellite(...) — any local delivery="live" call without the [satellite] extra | Zero. pip install "mostlyrightmd-weather[satellite]" is a precondition for every local satellite row. | n/a | SourceUnavailableError, message: “GOES satellite extraction requires the [satellite] optional extra.” | | satellite.typescript (label; rows keep the family tag) | satelliteHosted({ station, hostedUrl, apiKey, ... }) from @mostlyrightmd/weather/hosted — a subpath export, kept off the root barrel | Hosted only. TypeScript has no local extraction path at all, so TS coverage equals the hosted partition list above — never the anonymous NODD archive floors. | Same as the hosted row: the current UTC month is never present. | HostedConfigError when hostedUrl (MOSTLYRIGHT_WEATHER_HOSTED_URL) or apiKey (MOSTLYRIGHT_API_KEY) is missing; HostedResponseError on a non-200, including 422 for a window wider than 120 months and 502 on an upstream read failure. |

Facts that decide whether a backfill is worth running:

  • Below a local GOES, Himawari or VIIRS floor you get an empty result, not an error. On those three anonymous-bucket platforms list_product_entries returns [] with no I/O and no exception. Meteosat behaves differently, as its rows above state: the per-day live path applies no floor (a real Data Store search runs and comes back empty), and the 2004-01-29 clamp fires only on the month/backfill path.
  • goes19’s SDK floor is deliberately later than upstream. NODD starts 2024-10-15 for most ABI-L2 products and 2024-10-10 for CMIP and MCMIP, but objects before 2025-04-01 are post-launch checkout imagery from the 89.5°W position, not the GOES-East fixed grid. The SDK floor of 2024-11-15 is conservative for that reason.
  • VIIRS first objects are later than the floors for four of five Suomi-NPP products. AOD 2022-10-29, CloudMask 2023-06-20 (the floor is exact), CloudHeight 2023-11-13, LST 2024-12-18, SnowCover 2024-12-18. Nothing before 2022-10-29 is reachable on any Suomi-NPP product this source reads — NOAA onboarded JPSS to NODD in 2022, so the bucket holds no earlier JRR objects.
  • VIIRS retention changes. Per the AWS Open Data registry, from 2026-09-14 JPSS data on AWS maintains an average total storage of 1 PB with a 90-day rolling retention applied to a subset of products. The published no-retention exemption list contains none of the five JRR products this source reads, and NOAA notes the list may change.
  • Three rows above are labels on this page, not values the SDK stamps. satellite.hosted names the hosted delivery path — hosted rows keep the family identity (noaa_goes, jma_himawari, noaa_viirs, eumetsat_meteosat) in df.attrs["source"], and the string satellite.hosted appears only as the source= field on the errors that path raises. satellite.extra-missing names the missing-extra condition, which raises before any row exists. satellite.typescript names the TypeScript entry point, whose rows likewise carry the family identity passed through verbatim from the server, with delivery: "hosted" set on every row.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | nws.alerts.live | hazards.alerts(zone=..., from_time=..., to_time=...)source=None or source="nws" | Rolling window, 7 days ending now. Not an archive: there is no start date, only a moving left edge at now - 7 days. | Real time: CAP is read at issuance. No archive-lag constant exists in code. | HazardRetentionError (error_code HAZARD_RETENTION_EXCEEDED), raised before any HTTP request, with field="from_time", expected="within the last 7 days" and actual the ISO instant. | | nws.alerts.live with active=True | hazards.alerts(zone=..., active=True) | No window at all. Reads /alerts/active, the alerts in force at this instant. Nothing historical is reachable on this route. | Real time. | There is no out-of-window case. ContractError instead when active=True is combined with from_time or to_time. | | iem.vtec.archive | hazards.alerts(source="iem", from_time=..., to_time=...) | Archive, no retention cliff, with a per-phenomenon floor rather than one date: tornado and severe thunderstorm warnings back to 1986, flash flood warnings to about 2002, most other types 2005 or 2008. IEM’s download form opens at 1986; VTEC became operational 12 Nov 2005 and IEM back-computed VTEC ids for the earlier events. | An SDK-side 24 h settle floor: IEM_VTEC_ARCHIVE_SETTLE = timedelta(hours=24). A window whose to_time is within 24 h of now is re-fetched on every call and never written to cache. | Nothing raises for an out-of-range window. | | nws.text_products.live | hazards.text_products(from_time=..., to_time=...), hazards.post_storm_reports(...)source=None or source="nws" | Rolling window, 7 days ending now, the same cliff as the alerts route. | Real time: bulletins appear on api.weather.gov/products at issuance. | Depends on the pin. source=None auto-routes an older window to iem.afos.archive and raises nothing — df.attrs["source"] and the per-row source column change to iem.afos.archive, so the reroute is visible. source="nws" raises HazardRetentionError before any request, with field="from_time" and expected="within the last 7 days". | | iem.afos.archive | hazards.text_products(source="iem", from_time=..., to_time=...) — also reached by auto-route from source=None | Archive back to 1983-01-01, the minimum date on IEM’s AFOS browse form. No earliest-date constant exists in the SDK. KNHC tropical PILs start mid-May 1985: 1983 and 1984 hold only routine KNHC observation products (ROBMIA, REPRPD), with no tropical advisory anywhere in the 1984 season. | A one-second courtesy delay between requests to the university host — a politeness rule, not a publication lag. | A date before coverage returns HTTP 200 with an empty list, not an error. ContractError for a window over 366 days. | | iem.afos.archive (PSH) | hazards.post_storm_reports(source="iem", issuing_office=..., from_time=..., to_time=...) | Archive, identical to the row above — post_storm_reports() is text_products(product_code="PSH") plus the PSH header parse. | PSH is filed after the storm, per office, with no fixed schedule in code. | ContractError for a window over 366 days; HazardRetentionError only when source="nws" is pinned on a window older than 7 days; an empty frame when no PSH was filed in the window. | | nhc.tropical_cyclones.live | hazards.tropical_cyclones() | No window and no history. Reads NHC’s CurrentStorms.json, the storms active at this instant. There is no archive of that document anywhere in the SDK, so a past advisory cycle is reachable only if this install captured it with persist=True. | 5 minutes: the served response carries cache-control: max-age=300, and NHC_CURRENT_STORMS_TTL = timedelta(minutes=5) matches it. | There is no window argument, so nothing can be out of window. | | nhc.hurdat2 | hazards.best_track("AL142024", revision=None), hazards.best_track_revisions(basin="atl") | Archive, per basin. HURDAT2 is the post-season reanalysed best track, one file per reanalysis pass. atl reaches its earliest season at 1851; nepac (Northeast and North Central Pacific, EP and CP storm ids in one file) at 1949. The 1851 floor is Atlantic-only. Revisions of the same span coexist — five published files carry the 1851-2022 Atlantic span — and nothing is deduplicated. | Annual: a season’s certified track is published roughly a year after the season. Latest as of 2026-02-27: hurdat2-1851-2025-02272026.txt and hurdat2-nepac-1949-2025-02272026.txt. | NoHazardDataError, never an empty frame, in three shapes: the storm_id is absent from the selected revision (the message names the storm, the revision filename and its publication date, and suggests an earlier revision=); a revision= NHC never published (the message lists every published revision date and filename for that basin); or no revision published at all for the basin. An unknown basin raises ContractError naming HURDAT2_BASINS. | | nwps.gauge.live | hazards.gauges(bbox=...), hazards.gauges(gauge_id=...) | Capture ledger. No window and no history: this is the current gauge inventory as NWPS serves it now. Historical gauge reference data is available only from your own captures — persist=True, read back with capture_history("gauge", ...) — because the route carries no upstream publication stamp. | None stated in code; the inventory is served as-is. | No window argument exists, so no retention error. The empty-versus-error split is by scope: gauges(bbox=...) over an empty tile returns a valid empty frame — an empty tile is an answer — while gauges(gauge_id=...) addressing no gauge raises NoHazardDataError. RateLimitError when the host throttles. | | nwps.stage_and_flow.live (observed) | hazards.stage_and_flow("ABBG1", series="observed") | Rolling window of roughly 30 days, set entirely by the provider — the verb takes no date, start, end or as-of argument, so you get whatever NWPS currently serves and cannot ask for more. Points arrive at 15-minute spacing. For older observations use water_observations(). | Near real time; latest_observation_at_utc carries the newest point’s timestamp. Observed rows populate that column, never forecast_issued_at_utc. | Unreachable by construction: there is no window parameter to put out of range, so no retention error can be raised. | | nwps.stage_and_flow.live (forecast) | hazards.stage_and_flow("ABBG1", series="forecast", persist=True) | Capture ledger — capture or lose. Exactly one forecast issuance is ever served: the current one. There is no archive and no route parameter that asks for a prior issuance, so an issuance this install does not capture cannot be fetched again, from anywhere. Available history equals your own capture ledger. | The RFC’s issuance time (forecast_issued_at_utc) is the vintage; the per-point production stamp (generated_at_utc) was measured 5 minutes 13 seconds later on one capture. | No window exists, so nothing raises for one. | | nwps.rating_curve.live | hazards.rating_curve("ABBG1") | Capture ledger. No window: this is the gauge’s current official stage-to-flow curve, whole and ascending. No historical curve is served; a superseded curve exists only in your own captures, and persist=True appends a new vintage on every refresh because the curve carries no upstream publication stamp. | None stated in code. | No window argument, so no retention error. NoHazardDataError on HTTP 404 — gauge_id addresses no gauge. A gauge that exists but publishes an empty curve returns a valid empty frame; nothing raises on an empty curve. | | usgs.waterdata.live | hazards.water_observations("02215260", from_time, to_time, parameter_code="00065") | Per-station archive on api.waterdata.usgs.gov/ogcapi/v0. The SDK imposes no earliest date and validates no floor; a site’s record begins whenever that gauge began reporting. This is the historical leg for water, because NWPS keeps only about 30 days. | USGS marks recent values Provisional and flips them to Approved at roughly 60 to 90 days. The flip can change the VALUE, not only the label. | No retention error exists on this route. A validated site that recorded nothing in the window returns a valid empty frame; NoHazardDataError only when gauge_id addresses no monitoring location — an unknown site is an identity failure, unlike a quiet window. | | wfigs.current.live | hazards.wildfires()scope="current", the default; source=None or "wfigs" | Rolling window with the publisher’s own size-dependent fall-off: a record leaves after 3 days without an update (under 10 acres), 8 days (10 to 100 acres), or 14 days (over 100 acres). Only incidents not yet contained, controlled or out appear at all, so a fire vanishes the moment it is contained. Fires that started before December of the previous calendar year are excluded outright. 570 records on 1 August 2026. | modified_at_utc measures paperwork, not the fire: on a year-to-date sample, 9,098 of 14,587 out-fires were modified more than a day after the fire was declared out. | HazardRetentionError, raised before any request, when either from_date or to_date is more than WFIGS_SHORTEST_FALL_OFF_DAYS = 3 calendar days back. | | wfigs.season.live | hazards.wildfires(scope="season", from_date=..., to_date=...) | Archive, the current calendar year to date, with no fall-off — every discovery of the current year stays listed. 31,136 records on 1 August 2026. This is the scope the retention error names first for a past window inside this year. | The same paperwork-lag caveat as the current scope: season and history rows are a snapshot of what was known for each incident during the operational period. | No retention error on this scope — _guard_fall_off returns immediately for any scope other than "current". | | wfigs.history | hazards.wildfires(scope="history", from_date=..., to_date=...) | Archive, every year the interagency reporting system has recorded. The SDK’s own retention message says “2014 onward”; that is a message string, not a validated constant — no code checks from_date against it. No fall-off. 417,728 records on 1 August 2026. | Operational snapshot data, not QA/QC’d after a fire is declared out. No publication-lag constant in code. | No retention error. ContractError for a malformed window. | | calfire.incidents.live | hazards.wildfires(source="calfire", year=2025), or active_only=True | Fixed archive floor: seasons 2013 through the current year. CALFIRE_EARLIEST_YEAR = 2013 is validated before any request — the one hazards route whose start date is a real code constant rather than prose. This is the California acreage of record, and the number prediction markets have settled against. | Records are rewritten in place, with a long tail: of the season’s records carrying both an extinguished and a modified time, 81 were modified more than 30 days after. | ContractError, raised before any request — never HazardRetentionError — with expected="an integer from 2013 to <current year>". | | firms.bulk.live | hazards.hotspots()source=None, the keyless default; sensor=, region=, from_time=, to_time= | Rolling window: three fixed trailing files ending at the present, covering 24 hours, 48 hours and 7 days. The widest available look-back is 7 days. The verb picks the smallest published file that covers the age of your from_time, then filters the parsed rows to the window you asked for, so a narrower request costs the same download. | Near-real-time products are available within roughly three hours of an overpass. The calibrated standard-quality product replaces them roughly three months later. | ContractError — not HazardRetentionError — raised before any download when from_time is older than the widest file: field="from_time", expected="within 7 day(s) of now". | | firms.area.live | hazards.hotspots(source="firms.area", bbox=(west, south, east, north), from_time=..., to_time=...) — needs MOSTLYRIGHT_FIRMS_MAP_KEY | Rolling window, with an opt-in escape hatch for an arbitrary bounding box and a past date. At most FIRMS_MAX_DAY_RANGE = 5 calendar days per call. The SDK imposes no earliest date, so how far back this reaches is a property of the *_NRT products it addresses, not an SDK floor. | Same as the bulk tier: near-real-time within roughly three hours of overpass, replaced by the calibrated standard-quality reading at roughly three months. | ContractError, never HazardRetentionError. RateLimitError when the map key’s quota is exhausted. | | hazards.cache | hazards.capture_history(product, from_time, to_time, *, vintages="all" or "latest", as_of=None, identifier=None) | Capture ledger. Availability equals your own captures — there is no provider window here. It starts the day this install first called a verb with persist=True (or persist_rows()). Covers all ten products: alert, text_product, tropical_cyclone, best_track, gauge, stage_and_flow, rating_curve, water_observation, wildfire, hotspot. Stored at ~/.mostlyright/cache/v1/hazards/<product>/<YYYY>/<MM>.parquet (honors MOSTLYRIGHT_CACHE_DIR), append-only. | None — this is local disk. as_of= restricts the candidate set to rows already knowable at an instant, applied before the vintages reduction. | A window with nothing captured returns a valid empty frame and does not raise. That is the ledger’s left edge: this install was not watching yet, which is a different fact from a provider having dropped the data. |

Capture-or-lose is the rule across hazards, not the exception: of the ten products, only best_track and water_observation take their vintage from an upstream publication stamp and can be re-fetched later. Every other product serves a current snapshot no route parameter can ask for again, so a value this install did not capture with persist=True cannot be fetched again from anywhere. The reason differs by route: series="forecast" on stage_and_flow() does carry an upstream issuance stamp (forecast_issued_at_utc) but serves exactly one issuance — the current one — while the rating curve and the gauge inventory carry no upstream publication stamp at all, so the capture instant is the vintage. Read your captures back with hazards.capture_history(...).

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | none — a pre-fetch guard (economy.floor) | economy.series(indicator, from_date, to_date), economy.snapshot(indicator, as_of=...) — the guard runs on every economy read, before any cache read or network call | Fixed archive floor, per indicator rather than per source, and the binding limit on the default local path. US ids, from FEDS-2026-010 Table 1 (first Kalshi contract): cpi 2021-06-01, cpi_core 2021-06-01, cpi_yoy 2022-11-01, cpi_core_yoy 2022-11-01, u3 2021-07-01, nfp 2023-03-01, gdp 2021-04-01, ppi 2022-11-01, ppi_yoy 2022-11-01, jobless_claims 2022-01-01, fed_funds 2021-12-01, fed_decision 2023-05-01. Reference-area ids, derived from agency series inception: uk.cpi_yoy 1989-01-01, uk.gdp_monthly 1997-02-01, uk.retail_sales 1996-02-01, uk.unemployment_rate 1971-02-01, ca.cpi 1914-01-01, ca.cpi_yoy 1915-01-01, ca.unemployment_rate 1976-01-01, ca.gdp_monthly 1997-01-01, ca.policy_rate 1960-07-01, ea.cpi_yoy 1996-12-01, ea.policy_rate 1999-01-01, ea.deposit_rate 1999-01-01, ea.gdp_yoy 1996-01-01, au.cpi 1948-07-01, au.cpi_yoy 2025-04-01, au.unemployment_rate 1978-02-01, au.policy_rate 1976-04-01. | n/a — a guard, not a fetch. | DataAvailabilityError(reason="out_of_window"). | | bls.v1, bls.v2 | economy.series("cpi", from, to) and the seven siblings cpi_core, cpi_yoy, cpi_core_yoy, nfp, u3, ppi, ppi_yoy, with no FRED_API_KEY, or source="bls" | Per-series archive: the earliest value is what the BLS timeseries API returns for that series id. On the default path the reachable start is the floor row above, not the BLS archive’s own left edge. The SDK sends startyear = from_year - 1 and endyear = to_year — one extra year back so a print released in the window’s opening month is not dropped — then window-filters on a period-derived vintage_time. Every BLS row is latest-revised: settlement_grade=False. The raw fetcher stamps both vintage_time and knowledge_time as the fetch instant; on the economy.series() path vintage_time is then restamped to the period-derived release instant — the value the window filter uses — while knowledge_time stays the fetch instant. There is no as-first-released endpoint on this API. | About 1 month after the reference month, at 08:30 ET on the BLS CPI, Employment Situation and PPI schedule (12:30Z under EDT, 13:30Z under EST). | Below the floor: DataAvailabilityError(reason="out_of_window"). When truncation removes every row inside the window: IndicatorNotYetReleasedError from the empty-window guard, not SourceUnavailableError. | | alfred | economy.series(indicator, from, to, vintages="settlement") or vintages="all", with FRED_API_KEY set, or source="fred" | Per-series archive, bounded upstream rather than by the SDK. The SDK requests the widest real-time window on every call (realtime_start=1776-07-04, realtime_end=9999-12-31) and keeps every vintage, so the earliest realtime_start you get back is the series’ own first archived vintage: CPIAUCSL (cpi) 1972-07-21, CPILFESL (cpi_core) 1996-12-12, PAYEMS (nfp) 1955-05-06, UNRATE (u3) 1960-03-15, PPIFIS (ppi) 2014-02-19, A191RL1Q225SBEA (gdp) 2014-09-26. In practice none of them binds — every one predates the floor. This is the only source of settlement_grade=True for the BLS family and for GDP. | Day-granular only: every ALFRED row carries vintage_precision="day", because realtime_start is a calendar date. An 08:30 ET first print and a 09:45 ET same-morning revision therefore share one vintage_time. | Keyless: DataAvailabilityError(reason="source_404") raised before any manifest read or socket, with a hint naming fred.stlouisfed.org/docs/api/api_key.html. Below the floor: DataAvailabilityError(reason="out_of_window"). | | bea | economy.series("gdp", from, to) with BEA_API_KEY and no FRED key, or source="bea" | Per-series archive. Unavailable keyless: fetch_gdp raises before the network without BEA_API_KEY. With a key, BEA publishes NIPA table T10101 quarterly from 1947Q2 forward, so upstream reach is not what limits the 2021-04-01 GDP floor; series() narrows the request to (from_year - 1, to_year). Rows are latest-revised (settlement_grade=False, release_type="revised"), and series() exposes no release_type, so its BEA rows are always revised. GDP’s settlement first print comes from ALFRED A191RL1Q225SBEA, not from here. | Advance estimate about 1 month after quarter end (2026Q2 at 2026-07-30 12:30Z in the curated table), second estimate +1 month, third +2 months. | Keyless: DataAvailabilityError(reason="source_404"), hint “BEA GDP requires a BEA_API_KEY; set it (free at apps.bea.gov/API/signup)… GDP has no keyless first-print source.” Below 2021-04-01: DataAvailabilityError(reason="out_of_window"). | | dol.icsa | economy.series("jobless_claims", from, to) — requires FRED_API_KEY | Per-series archive, with no keyless route at all. The DOL machine path (oui.doleta.gov/unemploy/csv/*.csv) answers 403 HTML to every scripted request, so the bytes come from the FRED ICSA series and the fetcher raises before the network when no key is present. With a key, the ALFRED real-time pull returns the full ICSA vintage history, which series() window-filters on vintage_time; the earliest reachable week on the default path is the 2022-01-01 floor. The first print per week-ending Saturday is settlement_grade=True, release_type="advance". | Thursday 08:30 ET, covering the week ending the prior Saturday — 5 days. Thanksgiving week moves the release up to Wednesday. | Keyless: DataAvailabilityError(reason="source_404"), hint “jobless_claims has no keyless source (the DOL claims CSV is bot-walled and the FRED ICSA fallback needs a key); set FRED_API_KEY”. | | fed | economy.series("fed_funds", from, to), economy.series("fed_decision", from, to) — keyless | Per-series archive, bounded by the page rather than by a constant: fetch_decisions parses the entire federalreserve.gov/monetarypolicy/openmarket.htm rate-change record on every call, with no query parameters and no date bounds, and series() window-filters it by vintage_time (the meeting/effective date). The reachable window on the default path starts at the floor — fed_funds 2021-12-01, fed_decision 2023-05-01. Rows carry the target-range midpoint as value, the categorical in series_id (fed_funds:hike, :hold, :cut), release_type="final" and settlement_grade=True. This is the only US indicator that is settlement-grade with zero keys. | The FOMC statement is 14:00 ET on the meeting day (18:00Z under EDT, 19:00Z under EST in the curated table), but openmarket.htm records the change at its effective date. | Below the floor: DataAvailabilityError(reason="out_of_window"). | | ons.timeseries (current document) | economy.series("uk.cpi_yoy", from, to, vintages="all") and the three siblings uk.gdp_monthly, uk.retail_sales, uk.unemployment_rate — keyless | Per-series archive. Floors are code-pinned to the ONS series inception and re-measured by a live canary: uk.cpi_yoy 1989-01 (D7G7/MM23), uk.gdp_monthly 1997-02 (ECYX/MGDP), uk.retail_sales 1996-02 (J5EC/DRSI), uk.unemployment_rate 1971-02 (MGSX/LMS). But the from/to window is a VINTAGE window, not a period window: one GET returns the whole current document and every row in it shares one vintage_timeversions[-1].updateDate, the release that produced this revision — so only a window containing the latest ONS release returns anything. A purely historical window returns nothing from this route. Every current-document row is settlement_grade=False, so vintages="settlement" is empty by construction until a vintage backfill has run. | 07:00 UK local for the modern era, 09:30 for an earlier one, and irregular for a scattered handful across one series’ 128 versions — there is no constant UTC hour. | Below the floor: DataAvailabilityError(reason="out_of_window") naming the ONS series. | | ons.timeseries (previous/v{n} vintage archive) | mostlyright.economy._fetchers.ons.backfill_ons_vintages(indicator) — an operator action — then economy.series(indicator, from, to, vintages="settlement") or vintages="all" | Capture ledger. UK point-in-time vintages are reconstructable only after an explicit operator backfill, and only back to the archive’s usable left edge, previous/v2. How far that reaches is per series = len(versions[]) in the current document: measured 128, 96, 125 and 128 versions across the four series, roughly 2015-12 for D7G7, MGSX and J5EC, and 2018-08 for ECYX. previous/v1 has no predecessor entry, so its publication is recoverable only at date precision; it grades nothing and the vintage store refuses it. A cold crawl of all four series is 481 requests (the backfill runs per indicator, so four index reads, plus 477 bodies) and, at ONS’s documented 200-per-minute ceiling, a floor of about 2.5 minutes — which is why series() never does it inside a read. | One archived document per past release. previous/v{N}’s publication instant is versions[N-1].updateDate, the successor’s stamp — not versions[N]. | A requested vintage index past len(versions): ValueError, vintage index {i} is past the archive depth {d} the versions[] array describes. | | statcan.wds | economy.series("ca.cpi", from, to, vintages="all") and the three siblings ca.cpi_yoy, ca.unemployment_rate, ca.gdp_monthly — keyless | Per-series archive. Floors are the agency’s own cubeStartDate, read from getCubeMetadata: ca.cpi 1914-01 (table 18-10-0004, vector v41690973), ca.unemployment_rate 1976-01 (14-10-0287, v2062815), ca.gdp_monthly 1997-01 (36-10-0434, v65201210); ca.cpi_yoy derives from ca.cpi, so its floor is 1915-01, because a 12-month rate has no first period until twelve months of index exist. The fetch asks getDataFromVectorsAndLatestNPeriods with latestN=10000, which the service clamps to the series length, so the full table comes back in one POST. The window is a VINTAGE window on the WDS releaseTime — when the value was last written, not when the period was first published — so ca.gdp_monthly re-stamps its whole history on every release and a one-month window can answer with the entire series, while a window predating the last write returns nothing. Every row is settlement_grade=False, release_type="revised", release_datetime=None permanently. | Date-only. Statistics Canada’s user guide describes an 08:30 America/Toronto convention and this SDK never stamps it — a convention is not a published instant. | Below the floor: DataAvailabilityError(reason="out_of_window") naming the table and vector. | | ecb.sdmx | economy.series("ea.cpi_yoy", from, to, vintages="all") and the three siblings ea.policy_rate, ea.deposit_rate, ea.gdp_yoy — keyless | Per-series archive. Floors are series inception, each measured with a full-history pull: ea.cpi_yoy 1996-12 (356 monthly observations, HICP/M.U2.N.000000.4D0.ANR), ea.policy_rate 1999-01 (FM/D.U2.EUR.4F.KR.MRR_FR.LEV), ea.deposit_rate 1999-01 (FM/D.U2.EUR.4F.KR.DFR.LEV, 10,079 observations covering every calendar day), ea.gdp_yoy 1996Q1 (121 quarterly observations, MNA with REF_AREA=I9, not U2). One GET returns the whole series. Two caveats: the window is a VINTAGE window, so every row of one response shares the response’s last-modified instant and a window ending before the series’ last update returns nothing; and ea.policy_rate has a real 3,032-day hole between June 2000 and October 2008 — the variable-rate tender years, when a fixed tender rate did not exist — where a window inside the break returns nothing and that is the series being accurate. Every ea.* row is settlement_grade=False. | Per series, encoded as staleness_budget_days: ea.cpi_yoy 45 days, since the flash estimate for month M is published on the last working day of month M. | Below the floor: DataAvailabilityError(reason="out_of_window") naming the flow and key. For ea.policy_rate the floor-series string itself names the June-2000-to-October-2008 hole. | | abs.sdmx | economy.series("au.cpi", from, to, vintages="all") and the two siblings au.cpi_yoy, au.unemployment_rate — keyless | Per-series archive, measured per KEY, and the per-key part is load-bearing: au.cpi 1948 Q3 (312 quarterly observations, CPI 2.0.0 key 1.10001.10.50.Q) and au.cpi_yoy 2025-04 (fifteen monthly observations, the SAME flow at the SAME version, key 3.10001.10.50.M) are seventy-seven years apart. au.unemployment_rate 1978-02 (581 monthly observations, LF 1.0.0 key M13.3.1599.20.AUS.M). The bureau’s long monthly consumer-price history lives in a separate monthly-indicator dataflow that stopped publishing and is not served. The window is a vintage window and the service publishes no per-observation vintage, so every row of one response shares the FETCH instant — a window ending before today returns nothing. Every au.* row is settlement_grade=False; there is no Australian Kalshi consumer-price or unemployment contract, so these are research covariates. | Measured 37 days from the newest period’s coverage end on the capture day, for both CPI keys; the labour-force lag is shorter. | Below the floor: DataAvailabilityError(reason="out_of_window") naming the flow, its VERSION and the key. | | bis.cbpol | economy.series("ca.policy_rate", from, to, vintages="all"), economy.series("au.policy_rate", ...) — keyless | Per-series archive. Floors read out of one M./all full-history request (25,023 rows across all 49 reference areas, 12.7 MB): ca.policy_rate 1960-07 (792 monthly observations, WS_CBPOL key M.CA, Bank of Canada) and au.policy_rate 1976-04 (603 monthly observations, key M.AU, Reserve Bank of Australia). Frequency is part of the identity — the same REF_AREA is also served daily, and the two do not share a first observation. The compilation publishes no per-observation publication stamp, no vintage and no last-modified header on any measured response, so vintage_time and knowledge_time are both the FETCH instant: a window ending before today returns nothing. Every row is settlement_grade=False, release_datetime=None, and no settlement rule names either id. | 8 to 18 days, measured at daily frequency on one capture day: 8 days for the US and the euro area, 9 for Canada and the UK, 10 to 15 for Australia, 18 for China. | Below the floor: DataAvailabilityError(reason="out_of_window") naming the flow and key. | | economy.hosted | economy.series(..., delivery="hosted"), economy.snapshot(..., delivery="hosted") | Zero coverage at any date. Hosted economy delivery does not exist. MOSTLYRIGHT_ECONOMY_HOSTED_URL and MOSTLYRIGHT_API_KEY are reserved names only; the three economy modules that mention them validate and REJECT, so no hosted call ever leaves the default path. release_calendar() takes no delivery= keyword at all. | n/a | SourceUnavailableError(source="economy.hosted"): “economy delivery=‘hosted’ is not currently available.” | | bea.calendar.ics, bea.calendar.json | economy.release_calendar("gdp") — keyless | Capture ledger, forward-only. The iCalendar subscription feed carries the releases BEA currently lists, and there is no upstream backward reach; historical schedule rows exist only for what this machine has already captured. schedule_knowledge_time is always the SDK capture instant, never BEA’s DTSTAMP (a bulk-load stamp: five distinct values across 92 events in the measured capture, carried as the advisory agency_stamp_time column instead). The iCal leg is primary because it is the only one carrying period and release_type; the JSON endpoint is parsed for cross-check and contributes no row. time_precision="exact_time". | n/a — a schedule, not an observation. Instants are exact_time. | A requested scope holding no row raises ReleaseScheduleUnavailableError naming the routes that do work — never an empty frame, because an empty schedule read back as data says every future release was cancelled. | | fed.curated | economy.release_calendar("fed_funds"), economy.releases("fed_decision") and the sibling ids — keyless | Archive, hand-maintained, and it goes stale. The FOMC family currently runs 2026-06-17 through 2027-12-08; the BLS families 2026-05 through 2026-11; jobless claims 2026-06-20 through 2026-12-26; GDP only 2026Q1, 2026Q2 and 2026Q3, because BEA publishes no 2027 dates yet — the shortest run of forward dates in the table. Rows are append-only, so backward coverage is exactly what has accumulated, and a test alarms when any family runs out of forward dates. exact_time: 08:30 ET is 12:30Z under EDT and 13:30Z under EST; 14:00 ET FOMC is 18:00Z and 19:00Z. | n/a — this IS the lag table. It is also the settlement anchor: _resolve_settlement_release_dt reads these instants first (tier 1 is releases()), and _restamp_settlement_row stamps knowledge_time from what it resolves. | releases(indicator) raises TypeError for a non-str and ValueError for an indicator with no schedule, listing the known ones — never [] and never None. | | fred.releases | economy.release_calendar(agency="FRED") — requires FRED_API_KEY | Rolling window, BYO-key only, and the only machine-readable route to release DATES for the nine BLS-family indicators (www.bls.gov answers 403 to programmatic fetch of its schedule pages and api.bls.gov has no calendar endpoint). The leg sends no realtime_start or realtime_end, plus include_release_dates_with_no_data=true so forward-dated releases carrying no data yet are returned, then walks by offset to FRED’s reported count, stopping at MAX_RELEASE_DATE_PAGES=20. indicator= does NOT route here: exactly one FRED release id (53, “Gross Domestic Product”) is verified, and every other row is emitted with FRED’s own release_name and indicator=None, so release_calendar("cpi") raises even with a key set. Rows are time_precision="date_only", anchored at end of day UTC. | Date-only. FRED’s own caveat: release dates are published by data sources and do not necessarily represent when data will be available on FRED or ALFRED. | Keyless: DataAvailabilityError(reason="source_404") raised before any manifest resolution or socket, so the published default path issues no request to api.stlouisfed.org at all. | | ons.releases | economy.release_calendar("uk.cpi_yoy") and the three sibling ids, or release_calendar(agency="ONS") — keyless | Capture ledger, forward-only: the leg requests release-type=type-upcoming from api.beta.ons.gov.uk/v1/search/releases and pages by offset while ONS’s own count says there is more. Backward coverage is only what this machine captured. Uniquely among the legs, the states are attested rather than inferred — each record carries cancelled, postponed or finalised and a date_changes[] array — so every row is state_evidence="agency_declared" and cancellation_detectable=True from the FIRST capture, and _calendar.detect_cancellations never diffs ONS. A postponement is a reschedule (a new future release_date), never a tombstone. time_precision="exact_time". | exact_time — ONS publishes the scheduled publication instant. | An empty scope: ReleaseScheduleUnavailableError. | | statcan.releases | economy.release_calendar("ca.cpi") and the three sibling ids, or release_calendar(agency="StatCan") — keyless | Archive, and the only economy calendar leg with real backward reach from upstream: one static JSON file, measured at 2,938 rows spanning 2012 to 2027, of which 130 were future-dated when the leg was written. Published and scheduled releases alike, keyless, no opt-in. time_precision="date_only" — the SDK never stamps the 08:30 America/Toronto convention and resolves rows to end of day UTC instead. The file’s only state signal is whether url is empty, so disappearance between two captures is the only cancellation evidence it will ever produce; the SDK reads it and emits schedule_state='cancelled' with state_evidence='sdk_inferred_absence'. period is present only for the two grammars the parser matches (measured: “July 2026” on 104 of 130 future rows, “2026” on one); the 21 “second quarter 2026” rows match neither grammar, so they and the four prose rows — 25 in all — carry period=None and period_available=False. | Date-only: 2,933 of 2,938 rows sit at 00:00 within their day, so the file’s time component is an ordinal artefact and is anchored to hour 00. | An empty scope: ReleaseScheduleUnavailableError. | | economy.calendar.cache | economy.release_calendar(indicator=None, agency=None, as_of=datetime, from_date=..., to_date=...) | Capture ledger. The point-in-time SCHEDULE reconstructs back only to this machine’s first capture — there is no hosted or shared ledger. schedule_knowledge_time is always the SDK capture instant, never the agency’s stamp, and nothing is ever overwritten (a reschedule is a NEW row at a new knowledge time), so as_of= drops rows captured after that instant and, per release, the newest surviving capture wins. A backfill therefore cannot claim knowledge earlier than the moment this SDK first read the calendar. Frames read back from the local parquet store are stamped source='economy.calendar.cache'; per-leg provenance stays recoverable from agency plus time_precision. There is no delivery= keyword and no hosted call on this verb. | n/a | as_of= before the first capture leaves no surviving rows, and the scope then raises ReleaseScheduleUnavailableError naming the routes that do work — never an empty frame. |

economy.floor is this page’s label for the pre-fetch guard, not a value the SDK stamps on a row. The US floors state when Kalshi contracts first existed, NOT how far the agency archive reaches; the reference-area floors ARE the agency series inception.

Two more things the BLS rows do not fit in a cell. Per-request caps: v1 unregistered allows 10 years per query, 25 series per query and 25 queries per day; v2 registered allows 20 years, 50 series and 500 queries per day. Both are rate-limited at 50 requests per 10 seconds. A BLS_API_KEY raises those limits and switches v1 to v2; it never unlocks a first print. A silent truncation: nothing in this repo caps or pages the year span, and BLS does not reject an over-long request — it answers HTTP 200 with status="REQUEST_SUCCEEDED" and message: ["Year range has been reduced to the system-allowed limit of 10 years."], keeps startyear, and clips endyear to startyear + 9, so the NEWEST years are the ones dropped. Both SDKs read message only on the non-REQUEST_SUCCEEDED branch, so the warning is discarded and the short frame is returned as if complete. Because the request starts one year before your window, the keyless v1 path exceeds the 10-year cap once to_date.year - from_date.year >= 9.

Upstream caveats on the ALFRED row: ALFRED truncates its own early CPIAUCSL vintages — those dated 1972-07-21 through 1994-02-16 carry only the final 1-1/2 years of observations each — and those early vintages are not the modern index (before 1981-02-25 the series was CPI-W, and units were Index 1967=100 until 1988-02-25). UNRATE vintages before 1967-02-08 use the 14-years-and-older definition. FRED also caps one fred/series/observations response at 100,000 rows, and the SDK sends no limit or offset and does not page. BEA throttles every API account at 100 requests per minute, 100 MB per minute and 30 errors per minute; exceeding any of the three returns HTTP 429 with a Retry-After header.

There is no us.policy_rate: the US target rate ships as fed_funds from the Federal Reserve Board, and ea.policy_rate deliberately stays with the ECB, which publishes the same number through today while BIS carries it eight days later.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | economy.cache | economy.series(indicator, from_date, to_date) — default source=None, delivery="live"; also reached through economy.snapshot() and economy.training_table() | Capture ledger — there is no start date to print. Rows go back exactly as far as this machine has captured, per indicator id. A fresh install has nothing: the store is empty until a series() call fetches rows and persists them, after which the same window reads back from disk. Once written, nothing ages out — no TTL, no eviction, no current-period write-skip, and no code path in the package deletes a vintage partition (clear_coverage() deletes only the .coverage.json sidecar). Bounded above by what you have queried and below by the indicator’s first-contract floor, which is checked before the store is read. | None — the store is written inside the same series() call that fetched the rows, and the window is re-read immediately. | A miss is not an error: read_economy_cache() returns None, read_economy_window() returns [], and series() falls through to a live agency fetch. | | economy.cache — the fred_gated tags (alfred, dol.icsa) | economy.series("cpi", ..., vintages="settlement") with FRED_API_KEY set, which yields alfred rows; economy.series("jobless_claims", ...), which yields dol.icsa rows | Zero local rows by default. Rows whose source maps to "fred_gated" in ECONOMY_PERSISTENCE_DECISIONSalfred and dol.icsa, the latter DOL-labeled but FRED-transported — are dropped at write time unless MOSTLYRIGHT_PERSIST_FRED=1. These series are fetch-through: re-fetched on every call, with no accumulated vintage history on disk at all. Set MOSTLYRIGHT_PERSIST_FRED=1 and they persist like any other row, at which point coverage becomes capture-dependent as for every other tag. | None — fetch-through, so every call goes to the network. | Not an error. | | economy.cache — public-domain latest-revised skip | economy.series("cpi", from_date, to_date) and the siblings cpi_core, nfp, u3, ppi, gdp — keyless BLS/BEA routing, settlement_grade=False rows | Zero local rows, for a second and independent reason. A row whose persistence decision is NOT fred_gated but whose settlement_grade is falsy — every keyless BLS/BEA latest-revised value — is skipped at write time unless the batch was flagged point_in_time_vintage=True. A latest-revised value already embeds later revisions, so it is never frozen into a point-in-time store. On a keyless install the vintage store accumulates nothing for the BLS/BEA family. What is written instead is public-domain settlement-grade first prints — in practice source="fed" FOMC decisions — and archived as-published documents from the operator backfill in the next row. | None — fetch-through. | Not an error. | | economy.cache — ONS vintage-archive backfill (uk.* ids) | mostlyright.economy._fetchers.ons.backfill_ons_vintages("uk.cpi_yoy") — an operator action — then economy.series("uk.cpi_yoy", ..., vintages="all") | Capture ledger, and the only route that builds real historical coverage in the local store. An operator-run crawl of the ONS previous/v{n} archive persists each archived document with point_in_time_vintage=True, so its revision rows persist regardless of grade. Local coverage equals how many archive versions ONS still serves for that CDID, times how many you have crawled; the crawl is resumable and checkpointed per version in <cache>/v1/economy/<area>/<indicator>/.versions.json. The store’s usable left edge is previous/v2. series() NEVER starts this crawl, because a cold backfill is hundreds of throttled requests. Read floors are separate and much older than the archive: uk.cpi_yoy 1989-01-01, uk.gdp_monthly 1997-02-01, uk.retail_sales 1996-02-01, uk.unemployment_rate 1971-02-01. | None for the store itself; the crawl is throttled to the ONS ceiling of about 200 requests per minute, on a process-wide budget. | Without a backfill you get honest labels, not a silent partial archive: vintages="all" returns current-document rows tagged with the live ons.timeseries source and settlement_grade=False. | | economy.merged_v1 | economy.series(indicator, from_date, to_date), then read df.attrs["source"] | No window of its own — this is a frame identity, not a data source. It is deliberately absent from ECONOMY_ROW_SOURCES, so no row can ever carry it, and it therefore has no entry in ECONOMY_PERSISTENCE_DECISIONS. It appears in df.attrs["source"] exactly when one read returns rows with more than one distinct per-row tag — in practice stored economy.cache rows merged with rows the same call just fetched live, such as a backfilled uk.* archive plus the newest not-yet-archived periods from the current document. Availability of such a frame is the union of its rows’; every row keeps its own truthful tag. When the returned rows share one tag, the frame is stamped with that tag instead. | n/a | Never raised for the tag itself. | | economy.cache (TypeScript SDK) | series(indicator, fromDate, toDate) from @mostlyrightmd/economy | Not available in TypeScript — there is no local vintage store at all. The TS SDK persists nothing (no parquet, no file lock, no on-disk partitions), so a TS caller never receives a row tagged economy.cache; every row carries its live agency tag and every read is a fresh fetch. The TS tree declares ECONOMY_CACHE_SOURCE in an internal module only so its source-identity vocabulary matches the Python SDK; it is not re-exported from the package entry point, so a consumer cannot import it. | None — every TS read is live. | There is no store to miss, so there is no cache-miss path. |

FRED-derived rows are not persisted unless you set MOSTLYRIGHT_PERSIST_FRED=1, which is your own acceptance of the FRED terms of use. The licence text behind that behavior, and the attribution the SDK emits for the reference-area sources, are in the economy documentation under “Data sources, licensing & required notices”. This page does not restate them.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | kalshi | markets.kalshi.candles(url_or_id, interval=, from_time=, to_time=), markets.kalshi.trades(url_or_id, from_time=, to_time=), markets.kalshi.market(url_or_id) | Rolling window, per market, and the SDK asserts no length. Kalshi splits market data into a live tier (api.elections.kalshi.com/trade-api/v2) and a /historical tier on the same host, partitioned by exchange-wide cutoff timestamps — for markets and candlesticks the governing field is market_settled_ts. Kalshi gives a 3-month TARGET for the live window, which is a target and not a guarantee, so the SDK never reads GET /historical/cutoff and publishes no window length. It attempts the live tier, falls back to /historical on HTTP 404 and nothing else, and stamps the answering tier on every row. | None stated in code — every verb is a direct REST read of the venue’s current state. | Not an error. A 403, 429 or 5xx propagates instead of being re-asked as “maybe it is old”; a 404 on BOTH tiers raises rather than returning an empty result. | | kalshi.historical | the same three verbs after the automatic 404 fallback; markets.kalshi.markets(series="KXHIGHNY", historical=True) | Per-series archive, not universal. Kalshi documents no start date and no retention policy for /trade-api/v2/historical/*. Measured 30 July 2026 — a dated measurement, not a contract: /historical/markets?series_ticker=KXHIGHNY paged back to 2025-11-11 in 6 pages of 200, and /historical/markets/{ticker}/candlesticks served hourly candles for a market that far back. The hard ceiling on any claim is the exchange itself: Kalshi opened public beta on 26 July 2021. | None stated in code; the tier serves settled markets, so rows appear after a market settles. | A window the tier cannot serve returns an ordinary empty frame carrying the full column set. | | kalshi | markets.kalshi.orderbook(url_or_id, depth=50) | No history at all — a zero-length window. The book is a point-in-time read; there is no historical book tier on this venue and the SDK persists nothing, so the only instant available is the one stamped in captured_at on every row of the frame. | Real-time snapshot. | There is no window argument to be outside of. | | kalshi | markets.kalshi.series(category=, all=), markets.kalshi.events(series=, status=, all=), markets.kalshi.markets(series=, event=, status=, category=, all=) | Live catalog only — the current listing universe, with no time axis. Only markets(series=..., historical=True) reaches settled-market listings past the live tier. Measured 30 July 2026: /series returned 12,330 rows in one 16.3 MB response, and Kalshi’s open universe was 75,603 non-multivariate markets. Multivariate event combinations (over 400,000 rows) are always excluded, with no keyword to let them back in. | Real time. | No window argument. | | kalshi | markets.economy_trades.candles(ticker, interval=, from_time=, to_time=) | Fixed archive floor plus venue reach, and the two must not be merged. The SDK-imposed first-contract floor is per indicator and is checked before any fetch — the same dates as the economy floor row above. Actual venue reach is the Kalshi tier reach in the first two rows here, which is shallower than the floor: the floor states when contracts first existed, explicitly NOT that the public API serves them. Two tiers are read in order — the live /series/{s}/markets/{m}/candlesticks first, falling back to /historical/markets/{ticker}/candlesticks on HTTP 404 only. interval is one of 1m, 1h, 1d. | None beyond the candle interval — these are venue prints, not a published statistic. end_time_utc is the period END, so the newest bucket is readable one interval after it opens. | Three behaviors. Below the indicator’s floor: DataAvailabilityError(reason='out_of_window'), raised pre-fetch, with FEDS wording for US tickers and agency-series wording for reference areas. In-window but the venue served nothing: an ordinary empty DataFrame with the full 9-column set and the schema.markets.candles.v1 stamp — never a fabricated candle. A window wider than 5,000 buckets at the interval: a raw httpx.HTTPStatusError (HTTP 400, “max candlesticks: 5000”), because this verb does not chunk. Per-call ceilings are about 3 d 11 h at 1m, about 208 days at 1h and about 13.7 years at 1d. A ticker whose series root is not in SETTLEMENT_ROUTING skips the floor check entirely; a 404 on both tiers propagates the historical tier’s 404. | | polymarket.gamma | markets.polymarket.events(status=, event=, all=), markets.polymarket.markets(event=, status=, all=), markets.polymarket.market(url_or_slug) | Full catalog, no SDK date floor and no venue-published start date. Gamma serves current and resolved events and markets; the oldest row reachable is an event with startDate 2020-10-02, consistent with Polymarket’s June 2020 public launch. The four status words (open, closed, settled, unopened) are DERIVED client-side from the venue’s closed, active and acceptingOrders booleans, because Gamma returns no status field on either object. | Real time. | No window argument on the listing verbs. | | polymarket.clob | markets.polymarket.candles(url_or_slug, side=, interval=, from_time=, to_time=) | Per-market archive, per outcome token: the price series starts when that CLOB token started trading. No SDK floor, no venue-documented start, and no chunking — startTs, endTs and fidelity (minutes) are forwarded verbatim in ONE request, unlike the Kalshi verb which splits at 4,500 buckets. | Real time. | An empty schema-stamped frame with the full 23-column set: the endpoint answers 200 with {"history": []}. | | polymarket.clob | markets.polymarket.orderbook(url_or_slug, side=) | No history at all — a zero-length window. A book belongs to one outcome token and is a point-in-time read; captured_at is one instant, identical across the whole frame (the venue’s own snapshot stamp when it serves one, the client clock when it does not). | Real-time snapshot. | No window argument. | | polymarket.data | markets.polymarket.trades(url_or_slug, from_time=, to_time=, side=None) | Per-market archive, whole tape: the Data-API serves every fill for a conditionId back to that market’s first print and accepts NO time bounds at all, so the SDK fetches the entire tape and applies the half-open [from_time, to_time) window client-side. Reachable coverage equals the market’s own lifetime, and a wide window costs the same fetch as a narrow one. | Real time. | A quiet window returns an empty schema-stamped frame with the full column set — never None, never []. | | cli.archive (via the Kalshi settlement composition verb) | markets.kalshi.training_table(entity, from_date, to_date, outcome=, market=, features=), markets.kalshi.settlement_days(...), markets.kalshi.label.settlement(...) | NOT intersected with any Kalshi window — this verb makes ZERO venue calls. The ticker is parsed offline only to resolve its settlement station through the vendored catalog whitelist, then the whole composition is forwarded to the core label="cli" path. Effective availability is entirely the weather side: the NWS CLI settlement product plus the AWC/IEM/GHCNh observation merge. There is no venue-launch floor and no market-history floor here. | Inherits the NWS CLI product release lag — the label column’s knowledge_time is product_release_time. No markets-side lag. | No markets-side out-of-window error exists. | | daily_extremes (via the Polymarket settlement composition verb) | markets.polymarket.training_table(entity, from_date, to_date, outcome=, event=, features=), markets.polymarket.settle(event_id, event=) | Same structure as Kalshi: training_table() makes NO live Gamma round-trip on any path, because you supply the event payload via event=, so the backward window is entirely the weather label path (label="daily_extremes" — WU and NOAA-WRH daily extremes, not the NWS CLI product). The FORWARD edge is the one markets-side limit. | 6 h for wunderground, 4 h for noaa_wrh, 24 h for anything else, measured from station-LOCAL end of day, before a settlement can be read. | Forward edge: TooEarlyToSettleError, carrying wait_hours and resolution_source_type, with a message naming elapsed hours against the required finalization window. | | vendored catalog, and live cross-venue search | mostlyright.discover(query) (offline), mostlyright.discover(query, kind="market", live=True) | Two different availabilities. Offline, the default, searches only the catalog vendored inside the SDK build: that build’s snapshot, deterministic, no network, no time axis. live=True searches OPEN markets only, bounded per venue — Kalshi 2 pages × 1,000 rows = 2,000 rows; Polymarket 5 keyset pages × 100 rows = 500 rows. Settled, closed and unopened markets are not searchable at all. | Offline: as stale as the installed SDK build. Live: real time. | No window argument. |

Pagination caveat on Gamma: the legacy offset-paginated /events and /markets reject any offset above 2,000 with HTTP 422 and the message offset too large, use /events/keyset for deeper pagination, which is why the listing verbs call the /events/keyset and /markets/keyset cursor endpoints instead. Those clamp limit to 100 rows per page and return 422 for offset, but keep issuing next_cursor well past the 2,000-row cap.

Two settlement targets have no source adapter and therefore no availability window. Taipei settles against Taiwan’s CWA (station RCSS, venues={polymarket}) and Hong Kong against the Hong Kong Observatory (HKO, which is not a station code — it has no airport ICAO and is absent from the station registry). Requesting a Polymarket settlement for either raises DeferredMarketError (error_code DEFERRED_MARKET). RCSS still accepts observation requests as an ordinary international station; what is missing is the settlement label, not the feature rows. HKO is rejected earlier, by station validation, as an unknown station.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | cwop.live | weather.cwop.snapshot(station, duration_seconds=60), weather.cwop.latest(station, timeout_seconds=120), weather.cwop.stream(station) | No history — real time only. These three calls return ONLY packets that arrive on the APRS-IS TCP socket (cwop.aprs.net:14580) while the call is open: snapshot() drains the reader queue for duration_seconds, latest() blocks for the first packet up to timeout_seconds, stream() yields until cancelled. None takes a date or time argument and there is no REST backfill, so the window always ends at now and reaches nothing backward. | Station report cadence is 5 to 15 minutes. Per packet, knowledge_time is the socket-receive instant (APRSPacket.receive_time). | NoCWOPDataError (subclass of NoLiveDataError, itself a LiveStreamError and a NoDataError; default_error_code="NO_CWOP_DATA", source="cwop.live"). | | cwop.live | weather.cwop.nearby(icao, radius_km=25.0, listen_seconds=60), weather.cwop.scan(lat, lon, radius_km, listen_seconds=60, min_reports=1) | Rolling window, 24 hours, and this is station DISCOVERY rather than observations. scan() serves cached stations from $HOME/.mostlyright/cwop/stations.json immediately when at least min_reports of them are individually fresh — under 24 h since their own last_observation_at — and inside radius_km; otherwise it opens an APRS-IS listen of up to listen_seconds and merges the live results back. There is no historical station list: a station that stopped reporting more than 24 h ago is not fresh and forces a live re-scan. | Up to 24 h for a cache hit, since a returned station’s last_observation_at may be almost a day old; otherwise bounded by listen_seconds. | NoCWOPDataError. | | cwop.cache | weather.cwop.history(station, from_date, to_date, qc_status=None) | Capture ledger, per install and per station — there is no SDK-wide capture-start date and no such constant exists in the codebase. history() reads only the monthly parquet partitions this install previously wrote via snapshot(persist=True) or persist_observations(), at $HOME/.mostlyright/cache/cwop/{STATION}/{YYYY}/{MM}.parquet (honors MOSTLYRIGHT_CACHE_DIR). The left edge is the earliest observed_at you personally captured for that station. Range is inclusive on both ends; a bare date covers the whole UTC calendar day. | None beyond your own write. persist_observations() writes synchronously under a FileLock, and rows are readable by history() immediately, including for the current month. | NoCWOPDataError, raised whenever the filtered row set is empty — the SAME error for “before capture start”, “after capture stop”, “gap in the middle”, “wrong station”, and “filtered to empty by qc_status”. | | cwop.cache | markets.kalshi.training_table(entity, from_date, to_date, features=["cwop"]), markets.polymarket.training_table(..., features=["cwop"]) | Capture ledger — the same per-install, per-station left edge as history(), expressed as columns. The join reads ONLY the persisted parquet cache (read-only, zero APRS-IS sockets) and only QC-clean rows (qc_status == "clean", combined score above 0.7). Columns: cwop_temp_high_qc, cwop_temp_low_qc, cwop_clean_station_count, cwop_clean_obs_count. | None — a local parquet read. The join is post-aggregation and runs strictly after the observation merge. | Two behaviors that must not be conflated. PARTIAL coverage: days before your capture start, and any other uncovered day, come back as NaN in the cwop_* columns — rows are never dropped, never backfilled, and the label columns stay byte-identical with the feature on or off. ZERO coverage: when no clean rows exist anywhere in the window, or no persisted PWS sits within radius_km of the ICAO, the cwop_* columns are ABSENT entirely rather than present-and-all-NaN. Two separate fail-loud errors also exist: NoCWOPDataError(station, "unknown station code (not in the SDK registry)") for an unresolvable ICAO, and SourceUnavailableError(source="cwop.cache", retryable=False) naming the [cwop] extra when pandas is missing. |

The CWOP monthly parquet cache has NO current-month skip, deliberately: unlike the re-fetchable AWC/IEM/GHCNh observation cache, a month this cache misses cannot be re-fetched from the APRS stream. history() reads only what this install persisted.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | earnings.hosted | mostlyright.finance.transcripts.adapter.EarningsAdapter().fetch("earnings.hosted", ticker, event_date) | Hosted only, and the SDK declares no coverage whatsoever. There is no default host, no capture-start constant and no earliest-date registry anywhere in packages/finance. The call GETs <MOSTLYRIGHT_FINANCE_HOSTED_URL>/facts?ticker=…&event_date=…, so availability is exactly whatever the operator’s deployment captured; the published SDK cannot state a since-date. A keyless caller on the default path gets nothing, because this is not reachable without setting the environment variable. | Post-call, publisher-determined. The overlay knowledge_time is derived strictly from each row’s transcript_available_at — when the transcript was published — rather than assumed. | Asymmetric, and this is the sharpest gotcha here. Environment variable unset: EarningsError naming MOSTLYRIGHT_FINANCE_HOSTED_URL, raised before any I/O. Feed reachable but returning an empty JSON array for that ticker and date — the call is outside captured coverage — Python returns an EMPTY DataFrame with the overlay columns present and raises NOTHING, while the TypeScript mirror throws NoDataError with cause no_transcript_for_ticker_date: same wire response, different contract. HTTP non-2xx: EarningsError(source="earnings.hosted") quoting the status code; an unreachable host gives EarningsError “…is unreachable”. A payload that is not a JSON array of objects: SchemaValidationError(schema_id="schema.finance.fact.v1"). A row lacking a parseable transcript_available_at / as_of_time / knowledge_time: ValueError naming the row indices, refused rather than emitted as NaT. | | earnings.live | EarningsAdapter.from_rows(rows, source="earnings.live") | Bring your own, keyless and offline — availability is entirely whatever rows you hand in. from_rows() is a pure projector: it stamps the overlay (source="earnings_call", delivery="live", retrieved_at, knowledge_time, event_time) onto rows you already extracted, makes no network call and reads no environment variable. On the default path with no hosted URL, the only finance data you can obtain is data you supplied yourself. | n/a — you set knowledge_time through each row’s transcript_available_at. | EarningsAdapter().fetch(source="earnings.live", ...) is NOT a working data path: it lazy-imports faster-whisper only to check the [transcripts] extra is present, then raises NotImplementedError directing you to from_rows(). | | earnings_call (Q4 capture replay) | mostlyright.finance.transcripts.capture.Q4CaptureAdapter().capture(event), plus CaptureAdapter.live() | Issuer-retention-bounded, and the SDK pins no duration. The Q4 adapter is the REPLAY path: it cold-fetches the static post-call MP4 (static.events.q4inc.com/edited-recordings/{eventId}/{uuid}.mp4) over the public guest path with a User-Agent only, because the guest form gates the page rather than the asset. Q4 publishes no retention policy; the window is set by each issuer and stated in that issuer’s own earnings advisory, and it varies widely on the same platform — Owens Corning states one year, Welltower 90 days, ePlus a fixed calendar date about a week after the call. Assume no fixed window. The asset does not exist before the call ends, and captured audio is transient: extracted, transcribed, then deleted (AudioArtifact.cleanup(), is_transient=True), with the intermediate MP4 unlinked on every path. | Available only after the call ends; the SDK pins no numeric lag. | CaptureNotAvailableError (subclass of EarningsError, default_error_code="EARNINGS_CAPTURE_NOT_AVAILABLE", source="earnings_call"), including for a 2xx or 206 carrying a ZERO-byte body — an expired or edited replay raises instead of flowing empty audio to STT. The live-during-call path raises LiveCaptureUnsupported for any provider without a fetchable live HLS edge (dial-in bridge, SABR/YouTube, replay-only such as Lumi); Q4 itself has one, over Amazon IVS. | | earnings.hosted.stream | EarningsAdapter().stream(ticker, call_id, base_url=..., token=..., headers=...) (Python, async); finance.transcripts.stream(...) (TypeScript) | Live-during-call only, with a 128-event resume ring. The SSE consumer follows a call as it airs; the producer’s SegmentBus keeps a per-call rolling ring buffer of the last 128 FINAL events (_DEFAULT_RING_BUFFER_SIZE) for short-gap reconnects, so a brand-new subscriber gets whatever is still in that ring backfilled, then live items. PARTIAL utterances do cross the bus to live subscribers (the backpressure rule drops the oldest partial first), but only FINAL segments enter the resume ring, and a PARTIAL utterance can never reach the fact ledger or settle a market. The SDK ships no default base_url and no default host for this endpoint; the TypeScript finance.transcripts.stream(...) additionally requires MOSTLYRIGHT_API_KEY and throws HostedConfigError without it. | Near real time within the call. Each row’s knowledge_time is the STT-finalization publish wallclock, always at or after spoken_at, the aired instant. | No exception — a typed marker instead. | | schema.finance.transcript.v1, schema.finance.fact.v1 (local ledger) | TranscriptLedger().read(ticker, call_id), FactLedger().read(ticker, call_id), .read_ticker(ticker), .list_call_ids(ticker), .list_tickers() | Capture ledger, per install and per (ticker, call_id) — availability is exactly the calls this install captured and appended, nothing more. Append-only parquet at <cache-root>/earnings/transcripts/<TICKER>/<CALL_ID>.parquet and <cache-root>/earnings/facts/<TICKER>/<CALL_ID>.parquet (honors MOSTLYRIGHT_CACHE_DIR), one partition per call. There is no date-range read verb: address a single call by id, or enumerate what exists with list_tickers() and list_call_ids(). No audio column exists in the on-disk schema at all — the parquet schema is derived strictly from the canonical columns, and a caller-supplied audio-shaped key (audio, waveform, pcm, media extensions) raises ValueError before anything is written — refused loud, never silently dropped. | None — a local write, readable immediately. | read() returns an EMPTY LIST on a miss: a call you never captured is silently [], not an exception. |

EARNINGS_CALENDAR_SEED is a static four-row test fixture with past dates and no runtime consumer. It is not a calendar feed and advertises no coverage window; do not read it as one.

| Source tag | Call | Coverage | Publication lag | Out of window | |---|---|---|---|---| | obs.hosted | Python: emitted only by the private mostlyright.weather._observations._dispatch_strategy(...). Unreachable from weather.observations() on the default local path, where auto never resolves to it. | No coverage at any date. Hosted weather observations do not exist: there is no hosted observations endpoint and no archive behind this tag. The hosted ingest tactic is a stub that raises unconditionally, for every station and every window, before any I/O. | n/a | DataAvailabilityError(reason="model_unavailable", source="obs.hosted", hint="hosted observations are unavailable; use the live public-data source"), raised with no network call. | | obs-hosted-stub | TypeScript: observations("KNYC", from, to, { strategy: "hosted" }) from @mostlyrightmd/weather | No coverage at any date, same as the Python stub — but in TypeScript this IS a publicly reachable option value: ObsOptions.strategy is a documented public option, so any TS caller can pass strategy: "hosted". | n/a | DataAvailabilityError({ reason: "model_unavailable", source: "obs-hosted-stub", hint: ... }), thrown for every station and every window before any fetch is issued. The hint names strategy: "exact_window" and strategy: "warm_cache" as the working options. |