Skip to content

mostlyright.weather.hazards

mostlyright.weather.hazards — standalone NWS/NHC hazard adapter.

Hazards are CAP alerts (api.weather.gov/alerts), NWS/NHC text bulletins (TCP/TCM/TCD/TCV/PSH), NHC active tropical cyclones (CurrentStorms.json), the NHC HURDAT2 best-track reanalysis, the NWPS river-gauge inventory and detail routes, NWPS stage-and-flow series, NWPS stage-to-flow rating curves (all three on api.water.noaa.gov/nwps/v1), USGS continuous water observations (api.waterdata.usgs.gov), the interagency wildfire incident services (services3.arcgis.com), the California incident list (incidents.fire.ca.gov), and NASA satellite active-fire detections (firms.modaps.eosdis.nasa.gov). Ten of those eleven are US federal government works (17 U.S.C. §105, public domain); the California incident list is a State of California record, in the public domain under that state’s own conditions of use rather than under §105. All eleven are read over plain HTTPS directly from the publishing agency, and only one route in the whole adapter takes a credential — the satellite bounding-box route, which uses a free key of the caller’s own in MOSTLYRIGHT_FIRMS_MAP_KEY. The source addresses resolve through the keyed manifest catalog (MOSTLYRIGHT_API_KEY).

The satellite detections carry the one obligation the others do not: NASA asks for an acknowledgment, and every hotspot row carries it in an attribution column — a column rather than frame metadata, because a frame’s attribute bag is dropped by the first concat or merge.

This module is deliberately standalone. A hazard row is its own product, never an observation source: a hurricane advisory’s intensity_kt is not a temperature, and a CAP alert has no place in the source-blind max/min aggregation that produces the settlement label. Hazard rows therefore carry their own schema.hazard.*.v1 ids rather than schema.observation.v1.

Four files are the parity firewall; no hazard source is registered in any of them:

  • packages/core/src/mostlyright/_internal/merge/observations.py (SOURCE_PRIORITY)
  • packages/core/src/mostlyright/_internal/merge/climate.py
  • packages/weather/src/mostlyright/_live/_sources.py (the live-dispatch table weather.live resolves to)
  • the observation_type enum of schema.observation.v1 (packages/core/src/mostlyright/core/schemas/observation.py)

No wildfire source is registered in any of those four either — not wfigs.current.live, wfigs.season.live, wfigs.history, calfire.incidents.live, firms.bulk.live, or firms.area.live. The reasoning is CWOP’s, applied to a different instrument: a satellite hotspot is a DETECTION of heat, not a temperature reading, and a reported acreage is not a temperature either. Both reach a model as their own frame that a caller joins, never through the settlement aggregation.

Nothing under weather/hazards/ imports the merge path, the live-dispatch table, or the parity cache module. Nothing under it imports a geometry package either: this adapter is attributes-only, and a repository test scans the wildfire modules for both an import and a route name that would pull one in.

Public verbs:

  • alerts() — CAP alerts at the (issuance x UGC zone) grain. source=None reads the live NWS window, source="iem" reads the Iowa State VTEC archive, and every row says which leg answered.
  • text_products() — NWS/NHC bulletins (TCP/TCM/TCD/TCV/PSH) with raw_afos kept verbatim. An out-of-retention window auto-routes to the Iowa State AFOS archive unless source="nws" pins the live route.
  • post_storm_reports()text_products(product_code="PSH", …) plus the PSH header parse. Two amendments of one report are two rows.
  • issuing_offices() — the offices that issue a product code, read from /products/types/{code}/locations, returned as IssuingOffice records.
  • tropical_cyclones() — NHC active storms; a quiet season returns an empty frame, not an error.
  • best_track() — HURDAT2 fixes for one storm, from one reanalysis vintage. An absent storm raises rather than returning nothing.
  • best_track_revisions() — every published HURDAT2 revision for a basin, as file records rather than rows.
  • gauges() — NWPS river gauges. bbox= enumerates the inventory inside a bounding box; gauge_id= returns the one detail row carrying the identifier crosswalk and the flood-category thresholds. Exactly one of the two is required.
  • stage_and_flow() — a gauge’s observed and forecast stage/flow points, discriminated by the series column. A gauge with no live forecast returns an empty frame; an unknown gauge raises.
  • rating_curve() — a gauge’s official stage-to-flow curve, whole and ascending by stage_ft.
  • water_observations() — USGS continuous water values over a window, carrying approval_status and the upstream revision time.
  • wildfires() — wildfire incident records (schema.hazard.wildfire.v1) at the (incident x capture) grain. source=None reads the national interagency feed through scope= and tags rows wfigs.current.live / wfigs.season.live / wfigs.history; source="calfire" reads the California state list and tags them calfire.incidents.live. A past window against scope="current" raises rather than returning the fires that happen to still be listed.
  • hotspots() — satellite active-fire detections (schema.hazard.hotspot.v1) at the (overpass x ground pixel) grain. The default route is KEYLESS and tags rows firms.bulk.live; source="firms.area" reads an arbitrary bounding box with a key of your own and tags them firms.area.live.
  • capture_history() — replay the local vintage store, selecting a capture with vintages= and as_of=.
  • persist_rows() — write rows to that store directly.

Errors: NoHazardDataError when a named identity has no rows, HazardRetentionError when a window predates the 7-day live retention (pass source="iem" for the archive route), and HazardCacheCorruptError when a persisted partition is unreadable. All three come from mostlyright.core.exceptions and are re-exported here, never redefined. None of the verbs returns []/None on failure.

An enumeration verb whose provider answered with a well-formed empty collection returns a valid empty frame; an identity verb handed a name that does not exist raises. That split is what keeps a quiet hurricane season distinguishable from a typo’d storm id.

Importing this module registers schema.hazard.alert.v1, schema.hazard.text_product.v1, schema.hazard.tropical_cyclone.v1, schema.hazard.best_track.v1, schema.hazard.gauge.v1, schema.hazard.stage_and_flow.v1, schema.hazard.rating_curve.v1, and schema.hazard.water_observation.v1 with the validator (lazy — a base install that never touches hazards pays nothing).

It also registers schema.hazard.wildfire.v1 and schema.hazard.hotspot.v1, for ten row contracts in all.

class mostlyright.weather.hazards.HazardAlertSchema

Section titled “class mostlyright.weather.hazards.HazardAlertSchema”

Bases: Schema

One row per (CAP issuance x UGC zone).

That grain is what both legs can express without heuristics. The live CAP feed returns one feature per issuance carrying geocode.UGC[]; the Iowa State watchwarn archive returns one CSV row per UGC zone. Fanning the CAP feature out across its zones is a mechanical transform; collapsing the archive’s zone rows back into one issuance is not.

Supersession is the majority case, not the exception: over a 200-alert live sample the message types split Alert 85 / Update 106 / Cancel 9, with 115 carrying a non-empty references[]. So alert_id is the primary key (unique per issuance) and references points at the superseded issuance — never a delete. A “latest wins per zone” dedup destroys the vintage record.

class mostlyright.weather.hazards.HazardBestTrackSchema

Section titled “class mostlyright.weather.hazards.HazardBestTrackSchema”

Bases: Schema

One row per HURDAT2 best-track fix.

Best track is reanalysis, published months after the storm and revised again in later seasons. hurdat2_revision is therefore the knowledge time — the date on which these values became knowable — while valid_at_utc is the event time. Conflating the two is exactly how a model trained today “predicts” a 2022 storm using values NHC published in 2023.

Multiple revisions of the same season coexist upstream (the 2022 Atlantic file has five versions). That multiplicity is the vintage record; the (hurdat2_revision, hurdat2_filename) pair keeps each one addressable.

exception mostlyright.weather.hazards.HazardCacheCorruptError(path, reason, , request_id=None, error_code=None)

Section titled “exception mostlyright.weather.hazards.HazardCacheCorruptError(path, reason, , request_id=None, error_code=None)”

Bases: MostlyrightError

A persisted hazard cache partition exists but cannot be read.

Descends MostlyrightError DIRECTLY. A torn parquet partition is neither a ContractError (the caller did nothing wrong) nor a NoDataError (the rows exist — they are unreadable). Giving it either parent would let a caller’s except NoDataError swallow a corrupt on-disk vintage as “no data”, which is exactly the silent-corruption path the hazards vintage store exists to prevent.

The message carries the absolute path and the literal recovery command so the user does not have to guess which file to move aside.

  • Parameters:
    • path (Path)
    • reason (str)
    • request_id (str | None)
    • error_code (str)
  • Return type: None

Subclass override — the stable string enum surfaced via error_code.

class mostlyright.weather.hazards.HazardGaugeSchema

Section titled “class mostlyright.weather.hazards.HazardGaugeSchema”

Bases: Schema

One row per gauge, per capture, per route scope.

Gauge reference data has no upstream issuance stamp: the payload says what is true now and never says when it became true. The honest knowledge time is therefore captured_at — when this install read it — and the source contract declares exactly that rather than inventing a publication time.

Flood thresholds are mostly ABSENT. In a gauge sample, 47% carried no stage thresholds at all and only 10% carried all four stage AND all four flow thresholds. thresholds_defined makes that a column instead of a surprise, and every absent threshold is NULL rather than the -9999 the upstream serves.

class mostlyright.weather.hazards.HazardHotspotSchema

Section titled “class mostlyright.weather.hazards.HazardHotspotSchema”

Bases: Schema

One row per thermal detection in one satellite overpass.

The column order is the role manifest applied to this product: a hotspot’s identity is the platform and instrument family that saw it plus the ground pixel it saw, so those five columns lead; the overpass instant follows; the measurements come next; provenance closes. Leading with a timestamp would read as if time were the key, and it is not — the same pixel is re-detected on every pass.

The knowledge time and the event time are deliberately different. An overpass happens hours before the detection is published, so the overpass instant is when the fire burned, not when this install could have known about it.

class mostlyright.weather.hazards.HazardProductSpec(tier, schema_id, verbs, ts_verbs, source_tags, raw_columns, knowledge_time_column, event_time_column)

Section titled “class mostlyright.weather.hazards.HazardProductSpec(tier, schema_id, verbs, ts_verbs, source_tags, raw_columns, knowledge_time_column, event_time_column)”

Bases: object

One hazard product: its cache tier, schema, verbs, tags, and time columns.

tier is the noun everything else derives from — the schema id is schema.hazard.{tier}.v1 and the cache tier directory is {tier}. verbs is a tuple because one product can be reached by more than one verb (post-storm reports are text products filtered to a single product code); ts_verbs is its positional camelCase twin.

class mostlyright.weather.hazards.HazardRatingCurveSchema

Section titled “class mostlyright.weather.hazards.HazardRatingCurveSchema”

Bases: Schema

One row per point of a gauge’s official stage-to-flow curve.

This is the upstream’s own curve, not a fit. It already serves cubic feet per second, so there is no second flow column and no unit string beside the number — the upstream unit field survives verbatim in raw_json like every other upstream field.

exception mostlyright.weather.hazards.HazardRetentionError(message=”, , field=None, expected=None, actual=None, location=None, doc_url=None, source=None, request_id=None, error_code=None)

Section titled “exception mostlyright.weather.hazards.HazardRetentionError(message=”, , field=None, expected=None, actual=None, location=None, doc_url=None, source=None, request_id=None, error_code=None)”

Bases: ContractError

The requested window is older than the live route’s retention limit.

api.weather.gov/alerts and api.weather.gov/products both retain exactly 7 days: a start seven days back returns rows, and eight days back returns zero features. Asking the live route for an older window is a caller contract violation caught BEFORE any I/O — it is not a data-availability failure and it never degrades to an empty frame.

The fix the message carries is a source= pin, not a second verb name: source="iem" routes the same call at the Iowa State VTEC/AFOS archive, which has no 7-day cliff. Keeping the fix on a parameter rather than on a separate archive verb is what lets this teaching error survive a verb rename.

Takes the ContractError signature unchanged (field/expected/actual) so the structured teaching payload is the same one every other contract violation in the SDK emits.

  • Parameters:
    • message (str)
    • field (str | None)
    • expected (str | None)
    • actual (str | None)
    • location (str | None)
    • doc_url (str | None)
    • source (str | None)
    • request_id (str | None)
    • error_code (str)
  • Return type: None

Subclass override — the stable string enum surfaced via error_code.

class mostlyright.weather.hazards.HazardStageAndFlowSchema

Section titled “class mostlyright.weather.hazards.HazardStageAndFlowSchema”

Bases: Schema

One row per (gauge x series x valid time x issuance x capture).

The discriminator is spelled series, never product: product_code, product_id, and product_version already exist in this schema family naming an NWS text bulletin, and one word must not name two things.

Only ONE forecast issuance is ever served, and no route parameter exposes a prior one. A forecast this install does not capture is permanently unrecoverable, which is why captured_at is part of the dedup key here and why the store is append-only.

class mostlyright.weather.hazards.HazardTextProductSchema

Section titled “class mostlyright.weather.hazards.HazardTextProductSchema”

Bases: Schema

One row per NWS/NHC text bulletin (TCP, TCM, TCD, TCV, PSH).

The parsed columns are best-effort. raw_afos holds the verbatim bulletin and is non-nullable, so a parse miss degrades to parse_status="raw_only" rather than raising or fabricating a value — PSH station tables are free text and every forecast office formats them slightly differently.

The full product_id, any amendment suffix (‘-AAA’) included, is the vintage key. Deduping on (office, storm) would silently drop a correction.

class mostlyright.weather.hazards.HazardTropicalCycloneSchema

Section titled “class mostlyright.weather.hazards.HazardTropicalCycloneSchema”

Bases: Schema

One row per active tropical cyclone in an NHC CurrentStorms.json pull.

Off-season the document serves {"activeStorms": []} — an empty list, not a 404 and not a missing key. That is a correct answer about the world, so an enumeration over it returns a schema-valid zero-row frame rather than raising.

NHC types several of these fields as strings (intensity, pressure, latitude, longitude, advNum all arrive quoted). The numeric columns here hold the coerced value and raw_json holds the verbatim object, so the original spelling is never lost.

class mostlyright.weather.hazards.HazardWaterObservationSchema

Section titled “class mostlyright.weather.hazards.HazardWaterObservationSchema”

Bases: Schema

One row per (time series x instant x upstream revision).

This is the only flood product whose vintage key comes from UPSTREAM. Every row carries its own last_modified_at_utc, so re-fetching an unrevised row is idempotent — structurally the opposite of the capture-or-lose forecast product beside it.

class mostlyright.weather.hazards.HazardWildfireSchema

Section titled “class mostlyright.weather.hazards.HazardWildfireSchema”

Bases: Schema

One row per (incident x capture), from either incident feed.

The two feeds populate disjoint subsets of the columns, which is why almost everything here is nullable. The interagency feed carries the fire-report identifier, the discovery and final acreages, the coordination centre, the dispatch centre, the personnel count, the cost to date, and the complex columns; the state feed carries the active/final flags, the administrative unit, the agency list, the incident page, and the coordinates. Coordinates are state-feed-only because the interagency route is queried with geometry off — that is this tier’s hard constraint, not an oversight.

captured_at is the knowledge time. The upstream modified_at_utc stamp is not: 9,098 of the 14,587 out-fires in a year-to-date sample were modified more than a day after being declared out with the acreage frozen, so it records paperwork rather than when a number became knowable.

class mostlyright.weather.hazards.IssuingOffice(code, name, product_code)

Section titled “class mostlyright.weather.hazards.IssuingOffice(code, name, product_code)”

Bases: object

One office that issues a given text product, with the city it covers.

A record rather than a dict[str, str]: a caller reads office.name instead of guessing which side of a mapping holds the city.

  • Parameters:

exception mostlyright.weather.hazards.NoHazardDataError(identifier, reason, , source=‘hazards.cache’, request_id=None, error_code=None)

Section titled “exception mostlyright.weather.hazards.NoHazardDataError(identifier, reason, , source=‘hazards.cache’, request_id=None, error_code=None)”

Bases: NoDataError

A named hazard identity produced no rows.

Raised by the mostlyright.weather.hazards identity verbs — the ones that are handed a specific storm id, product id, or zone — instead of returning []/None. Carries the identifier that was asked for, the human-readable reason, and the source tag of the route that answered.

Parented on NoDataError, NOT on NoLiveDataError: a valid finite query that returns zero rows is a zero-row answer, not a live-capture failure. The shape (positional identifier + reason, keyword-only source, the _payload override) mirrors NoCWOPDataError; only the parentage differs, deliberately.

Enumeration verbs do NOT raise this. hazards.tropical_cyclones() in a quiet season returns a schema-valid EMPTY frame, because CurrentStorms.json serving {"activeStorms": []} is a correct answer about the world. A missing NAMED identity is not.

  • Parameters:
    • identifier (str)
    • reason (str)
    • source (str | None)
    • request_id (str | None)
    • error_code (str)
  • Return type: None

Subclass override — the stable string enum surfaced via error_code.

mostlyright.weather.hazards.alerts(, source=None, zone=None, area=None, point=None, event=None, from_time=None, to_time=None, active=False, phenomena=None, significance=None, wfo=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.alerts(, source=None, zone=None, area=None, point=None, event=None, from_time=None, to_time=None, active=False, phenomena=None, significance=None, wfo=None, persist=False, client=None)”

Read NWS CAP alerts at the (issuance x UGC zone) grain.

One verb, two providers. source=None reads the live NWS CAP window; source="iem" reads the Iowa State VTEC archive, which has no retention limit. Both return schema.hazard.alert.v1 with the same columns.

  • Parameters:
    • source (Literal['nws', 'iem'] | None) – None serves the live NWS window, "nws" pins that same leg, "iem" pins the Iowa State VTEC archive. Any other value raises before a request is issued.
    • zone (str | None) – One UGC zone or county code, e.g. "FLC086". On the archive leg this filters the parsed rows on ugc_zone.
    • area (str | None) – One two-letter state or marine area, e.g. "FL".
    • point (tuple[float, float] | None) – (latitude, longitude) in degrees, e.g. (25.7617, -80.1918). Live leg only.
    • event (str | None) – NWS event name, e.g. "Hurricane Warning". Live leg only — the archive serves VTEC codes, so pin phenomena / significance there instead.
    • from_time (datetime | None) – Window start. Alerts are instant-grained, so the window is from_time / to_time; there is no from_date.
    • to_time (datetime | None) – Window end.
    • active (bool) – Read /alerts/active — the alerts in force right now. Cannot be combined with a window or with source="iem".
    • phenomena (str | None) – VTEC phenomena code, e.g. "HU". On the archive leg this narrows the query itself; on the live leg the CAP route has no such parameter, so it filters the parsed rows on the phenomena column the VTEC decomposition already produced.
    • significance (str | None) – VTEC significance code, e.g. "W". Same two legs as phenomena.
    • wfo (str | None) – Forecast office, e.g. "MLB". Same two legs as phenomena.
    • persist (bool) – Write the returned rows to the local vintage store before returning them. Two calls append two vintages; nothing is overwritten.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Returns: alert_id, ugc_zone, event, message_type, severity, certainty, urgency, sent_at_utc, effective_at_utc, onset_at_utc, expires_at_utc, ends_at_utc, vtec_class, vtec_action, vtec_office, wfo, phenomena, significance, event_tracking_number, vtec_year, vtec_string, references, product_id, sender_name, headline, area_description, raw_cap, raw_csv, captured_at, source. A window with no alerts in it returns zero rows, not an error.
  • Return type: A schema.hazard.alert.v1 DataFrame. Columns, in order
  • Raises:
    • ContractErrorsource is not a recognised provider; more than one of zone / area / point was passed; point is not a degree pair inside [-90, 90] / [-180, 180]; active=True was combined with a window or with source="iem"; the archive leg was asked for point or event, or was called without a window.
    • HazardRetentionErrorfrom_time predates the 7-day live retention and source is None or "nws". The message names source="iem" as the route that answers it.
    • ValueErrorfrom_time is after to_time.
    • httpx.HTTPStatusError – A provider answered non-2xx.

Source: : Accepted source= values are None, "nws", and "iem". None routes to the live NWS CAP window. The per-row source column and df.attrs["source"] always name the leg that actually answered — "nws.alerts.live" or "iem.vtec.archive" — so a caller can always tell live from archive. An unrecognised value raises ContractError before any network call; it never falls back to the other provider.

alerts.weather.gov — the host the hurricane-category contract rules cite for their per-zone CAP feed — returns NXDOMAIN on two independent public resolvers, measured August 2026. zone= on this verb is the live equivalent of that per-zone CAP URL: it reads the same alerts for the same UGC zone from api.weather.gov/alerts.

Read the alerts in force for one Florida zone:

from mostlyright.weather import hazards
df = hazards.alerts(zone="FLC086", active=True)
df[["alert_id", "event", "vtec_action", "sent_at_utc", "source"]]

Read the same zone two years back, through the archive:

from datetime import UTC, datetime
df = hazards.alerts(
source="iem",
zone="FLC086",
from_time=datetime(2024, 10, 1, tzinfo=UTC),
to_time=datetime(2024, 10, 15, tzinfo=UTC),
phenomena="HU",
significance="W",
)
df.attrs["source"] # 'iem.vtec.archive'

mostlyright.weather.hazards.best_track(storm_id, , revision=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.best_track(storm_id, , revision=None, persist=False, client=None)”

Return the HURDAT2 best-track fixes for one storm, from one reanalysis.

Lists the published revisions for the storm’s basin, selects one, downloads that file, and parses only that storm’s records. Every returned row carries hurdat2_revision and hurdat2_filename, so the frame always states which reanalysis answered.

  • Parameters:
    • storm_id (str) – A HURDAT2 storm id, e.g. "AL142024". Case-insensitive; upper-cased before use. A malformed id raises before any request.
    • revision (date | None) – The reanalysis vintage to read, as its published date. Omit it to read the latest published revision.
    • persist (bool) – Write the returned rows to the local vintage store. Re-running this call for the same revision does not duplicate rows: the revision is itself the vintage, so a second download is idempotent.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.best_track.v1 DataFrame with attrs["source"] == "nhc.hurdat2", one row per synoptic fix. landfall_flag == "L" marks the fix at which the centre crossed the coastline — the settlement-relevant flag for “hurricane hits X” contracts.
  • Raises:
    • ContractErrorstorm_id is not a HURDAT2 storm id.
    • NoHazardDataError – the storm is absent from the selected revision, the requested revision was never published, or NHC publishes no revision for the storm’s basin. The message names the storm and the revision searched.
    • httpx.HTTPStatusError – NHC answered non-2xx.

HURDAT2 is reanalysed annually, and several revisions of the same season coexist upstream. A call without an explicit revision returns the latest one, which is a value that did not exist while the storm was happening. A best-track row whose hurdat2_revision post-dates a research as_of is the warning sign of leakage: the model is reading numbers NHC had not published yet.

Read the latest reanalysis of one storm:

from mostlyright.weather import hazards
df = hazards.best_track("AL142024")
df.loc[df["landfall_flag"] == "L", ["valid_at_utc", "max_wind_kt"]]

Pin the reanalysis that was current at an earlier decision time:

from datetime import date
df = hazards.best_track("AL142024", revision=date(2025, 2, 27))

mostlyright.weather.hazards.best_track_revisions(, basin=‘atl’, client=None)

Section titled “mostlyright.weather.hazards.best_track_revisions(, basin=‘atl’, client=None)”

List every published HURDAT2 revision for basin, oldest first.

This is the catalog verb, and it deliberately returns BestTrackRevision objects rather than a DataFrame. Every other verb here returns a frame because it returns observations — rows about the world. This one describes files: a basin, a filename, a publication date, and a URL. Wrapping four file attributes in a schema-validated frame would imply a row grain and a knowledge time that a directory listing does not have.

Nothing is deduplicated. The 1851-2022 Atlantic archive has five coexisting revisions, and that multiplicity is the vintage record: the values NHC published for a 2022 storm in 2022 and in 2025 differ.

  • Parameters:
    • basin (str) – "atl" (Atlantic) or "nepac" (north-east Pacific, which covers both the EP and CP storm-id prefixes).
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: tuple[BestTrackRevision, ...]
  • Returns: A tuple sorted ascending by revision_date — the parsed date, never the filename string.
  • Raises:
    • ContractErrorbasin is not a published HURDAT2 basin.
    • ValueError – a listing entry named like a best-track file could not be parsed into a revision date.
    • httpx.HTTPStatusError – NHC answered non-2xx.

See which reanalysis vintages exist before pinning one:

from mostlyright.weather import hazards
for revision in hazards.best_track_revisions():
print(revision.revision_date, revision.filename)

mostlyright.weather.hazards.capture_history(product, from_time, to_time, , vintages=‘all’, as_of=None, identifier=None)

Section titled “mostlyright.weather.hazards.capture_history(product, from_time, to_time, , vintages=‘all’, as_of=None, identifier=None)”

Return the persisted hazard captures for product in [from_time, to_time].

Reads the local vintage store, selects a vintage, and returns a validated frame for the product’s schema tagged source="hazards.cache". The window is inclusive on both ends; a bare date covers the whole calendar day in UTC. Most products window on captured_at; best_track, stage_and_flow, and water_observation window on valid_at_utc, and hotspot windows on acquired_at_utc — because one download of each carries a long span of instants, and for detections that instant is the overpass rather than the download.

  • Parameters:
    • product (Literal['alert', 'text_product', 'tropical_cyclone', 'best_track', 'gauge', 'stage_and_flow', 'rating_curve', 'water_observation', 'wildfire', 'hotspot']) – one of "alert", "text_product", "tropical_cyclone", "best_track", "gauge", "stage_and_flow", "rating_curve", "water_observation", "wildfire", "hotspot".

    • from_time (date | datetime) – window start (inclusive). date or datetime.

    • to_time (date | datetime) – window end (inclusive). date or datetime.

    • vintages (Literal['all', 'latest']) –

      "all" returns every capture in the window (the append-only record); "latest" returns the newest capture per identity — (alert_id, ugc_zone) for alerts, (product_id) for text products, (storm_id, last_update_at_utc) for tropical cyclones, (storm_id, valid_at_utc) for best track, (gauge_id, record_scope) for gauges, (gauge_id, series, valid_at_utc, forecast_issued_at_utc) for stage and flow, (gauge_id, stage_ft) for a rating curve, (gauge_id, parameter_code, time_series_id, valid_at_utc) for water observations, (incident_id) for wildfire incidents, and (acquired_at_utc, latitude, longitude, satellite) for detections — a detection carries no upstream id, so its identity is the overpass instant plus the ground pixel plus the platform.

      A REVISION of one thing is not a second thing. hotspot.product_version is part of the stored key, so the near-real-time reading and the calibrated standard-quality reading that replaces it months later both survive on disk — but it is NOT part of the identity above, so vintages="latest" returns ONE row for that overpass, the newest capture. Counting detections over a window that straddles the substitution would otherwise count every re-processed one twice.

      Named to match economy.series(vintages=…), which selects a vintage mode the same way; the value sets differ because the semantics do.

    • as_of (datetime | None) – restrict to rows whose vintage is at or before this instant, applied before the vintages reduction. This is the point-in-time replay: pass the decision time and the result is what was knowable then. An as_of in the future is accepted and filters nothing.

    • identifier (str | None) –

      filter on the product’s primary identity column — alert_id for alerts, product_id for text products, storm_id for both tropical products, gauge_id for all four flood products, and incident_id for wildfire. For water_observation that is the id the CALLER captured with, not the upstream time_series_id: the filter selected the series id before gauge_id led that product’s identity, so passing the site number returned a silently empty frame.

      The value is CANONICALIZED with the same rule the product’s verb applies before it is compared, so every spelling that reaches the same rows through the verb reaches them here: "abbg1", " ABBG1 ", and "ABBG1" are one query on the three NWPS products, "02215260", " 02215260 ", and "USGS-02215260" are one query on water_observation, and a braced or lower-cased incident id is one query on wildfire. A value that cannot be an identifier of this product raises ContractError rather than returning an empty frame.

      NOT ACCEPTED on "hotspot", which raises. A detection carries no upstream identifier — its identity is the whole (acquired_at_utc, latitude, longitude, satellite) tuple — so no string could ever match, and every value passed returned a silently empty frame. Read the window and filter the returned frame instead.

  • Return type: DataFrame
  • Returns: A schema-valid DataFrame with attrs["source"] == "hazards.cache".
  • Raises:

A window with nothing persisted returns a valid empty frame; it does not raise. This reads a local store, so an empty result is a fact about when this install started capturing — the left edge of the local record — not a fetch failure. The network verbs behave the other way and raise when a window has fallen out of upstream retention.

mostlyright.weather.hazards.gauges(, bbox=None, gauge_id=None, srid=‘EPSG_4326’, max_tile_degrees=10.0, persist=False, client=None)

Section titled “mostlyright.weather.hazards.gauges(, bbox=None, gauge_id=None, srid=‘EPSG_4326’, max_tile_degrees=10.0, persist=False, client=None)”

Return NWPS river gauges — a bounding box of them, or one by identifier.

One verb, two scopes, one identifier word. bbox= enumerates the gauge INVENTORY inside a box, tiling the request because the route serves no pagination and its unfiltered answer was measured at 13 MB and a gateway timeout. gauge_id= returns the ONE-row DETAIL record, which is the only place the identifier crosswalk, the timezone, and the flood-category thresholds are served. record_scope says which shape a row is.

  • Parameters:
    • bbox (tuple[float, float, float, float] | None) – (xmin, ymin, xmax, ymax) — west, south, east, north — in the units of srid. Split into tiles no larger than max_tile_degrees on either axis. An inverted, zero-width, or off-the-globe box raises before any request.
    • gauge_id (str | None) – An NWS Location ID ("ABBG1") or a bare USGS site number ("02215260"). The route accepts both on the same path and was measured answering identically, which is why there is one argument rather than two. Stripped and upper-cased once, and that canonical spelling is both what is requested and what the row’s gauge_id column carries.
    • srid (str) – Spatial reference of the bbox corners. Always sent: the route answers a missing or unrecognised projection with HTTP 200 and an EMPTY gauge list, so there is no code path here that omits it.
    • max_tile_degrees (float) – Largest tile a single request will cover.
    • persist (bool) – Write the returned rows to the local vintage store before returning them. Gauge reference data carries no upstream publication stamp, so captured_at IS the vintage and every refresh appends beside the last one rather than replacing it.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.gauge.v1 DataFrame with attrs["source"] == "nwps.gauge.live". On an inventory row usgs_site_id, reach_id, timezone, and the eight threshold columns are NULL because the LIST route omits them, not because the gauge lacks them — read record_scope to tell the two apart.
  • Raises:
    • ContractError – both selectors were passed, or neither; the box is inverted, zero-width, or off the globe; srid is not a documented projection; or the route answered with something that is not JSON.
    • NoHazardDataErrorgauge_id addresses no gauge. An identity failure is never converted into an empty frame.
    • httpx.HTTPStatusError – A route answered non-2xx.

Flood thresholds are mostly ABSENT upstream. In a 60-gauge sample only 10% carried all four stage AND all four flow thresholds, and 47% carried no stage thresholds at all. thresholds_defined makes that a column, and every undefined threshold is NULL rather than the -9999 the upstream serves. Building a complete threshold table would cost 12,820 detail requests at roughly 47 kB each against a host with no rate-limit headers and a measured HTTP 504, so no crawl verb ships: thresholds are fetched one gauge at a time, on demand.

Enumerate a basin:

from mostlyright.weather import hazards
df = hazards.gauges(bbox=(-84.0, 33.0, -83.0, 34.0))
df[["gauge_id", "name", "observed_flood_category"]]

Read one gauge’s thresholds and crosswalk:

detail = hazards.gauges(gauge_id="ABBG1")
detail[["usgs_site_id", "action_stage_ft", "major_stage_ft"]]

An empty tile is an answer; an unknown gauge is an error:

len(hazards.gauges(bbox=(-140.0, 5.0, -139.0, 6.0))) # 0
hazards.gauges(gauge_id="ZZZZ9") # NoHazardDataError

mostlyright.weather.hazards.hotspots(, source=None, sensor=‘viirs_snpp’, region=‘usa’, from_time=None, to_time=None, bbox=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.hotspots(, source=None, sensor=‘viirs_snpp’, region=‘usa’, from_time=None, to_time=None, bbox=None, persist=False, client=None)”

Return satellite active-fire detections — where an instrument saw heat.

hotspots() with no arguments downloads the published file for the contiguous United States and Hawaii and returns its detections. It needs no FIRMS registration and reads no MOSTLYRIGHT_FIRMS_MAP_KEY; like every other source in this SDK its address is resolved through the manifest catalog, which reads MOSTLYRIGHT_API_KEY.

  • Parameters:
    • source (Literal['firms.bulk', 'firms.area'] | None) – None and "firms.bulk" take the keyless published files; "firms.area" takes the bounding-box route, which needs a key of your own in MOSTLYRIGHT_FIRMS_MAP_KEY (free, one form). Any other value raises before a request is issued.
    • sensor (str) – The platform — "viirs_snpp" (the default), "viirs_noaa20", "viirs_noaa21", or "modis". The two instrument families report confidence in incomparable units, which is why a row carries either confidence_category (a word) or confidence_percent (a number) and sensor_family says which.
    • region (str) – Which published file to read — "usa" (the default), "alaska", "canada", or "global". Keyless route only: the bounding-box route is addressed by coordinates, and passing both raises rather than ignoring one.
    • from_time (datetime | None) – Start of the window, timezone-aware. Given together with to_time or not at all.
    • to_time (datetime | None) – End of the window, timezone-aware.
    • bbox (tuple[float, float, float, float] | None) – (west, south, east, north) in degrees. REQUIRED on source="firms.area" and refused on the keyless route, which serves fixed regional files.
    • persist (bool) – Write the returned rows to the local vintage store before returning them. It writes parquet vintages of the parsed rows; the downloaded CSV is a temporary intermediate and is never kept.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.hotspot.v1 DataFrame carrying attrs["source"] ("firms.bulk.live" or "firms.area.live"), attrs["retrieved_at"], attrs["attribution"], and the same acknowledgment in the attribution COLUMN of every row. On a keyless row region and window say which published file the detection came from, spelled as the upstream file-name tokens ("USA_contiguous_and_Hawaii", "24h") rather than as the short region= name; both are NULL on a bounding-box row. A pass that detected nothing returns a valid EMPTY frame — that is the healthy answer for a quiet region.
  • Raises:
    • ContractErrorsource, sensor, region, bbox, or a window bound is unusable; bbox= was passed to the keyless route or omitted from the bounding-box one; region= was passed to the bounding-box route; the window is wider than the route can serve; or no key is configured for the keyed route. Every one of these is refused before or instead of a request, and none of them falls back to the other route.
    • SourceUnavailableError – a route answered non-2xx, or served a body that is not an active-fire file.
    • RateLimitError – the key’s transaction budget was spent, or the route answered HTTP 429 on every attempt.
    • ApiKeyRequiredError – no API key is configured for catalog resolution. This is the SDK’s own MOSTLYRIGHT_API_KEY, not the FIRMS one, and it is raised on BOTH routes — the keyless tier is keyless of the FIRMS registration.

THE PUBLISHED FILES ARE FIXED TRAILING WINDOWS of 24 hours, 48 hours, and 7 days. This verb picks the smallest one that covers from_time and then filters the parsed rows to the window you asked for, so a narrower request costs the same download. A window reaching further back than the widest file raises and names the bounding-box route, which takes a past date directly (at most 5 days per call).

THE KEY IS A URL PATH SEGMENT, not a header. Calling the bounding-box route from a browser exposes the key to anyone who can read the request, so both routes are Node-only in the TypeScript twin and no proxy is offered. The documented budget is 5000 transactions / 10-minute interval, and a multi-day request may count as several.

THE SAME OVERPASS CAN YIELD DIFFERENT ROWS. Near-real-time detections are replaced by the calibrated standard-quality product after roughly three months, so product_version is part of a stored row’s WRITE key and both readings survive in the vintage store. It is not part of the read-time identity: capture_history("hotspot", …, vintages="latest") returns one row per detection, the newest capture, because a re-processing revises that overpass rather than adding a second one.

What a satellite saw over the United States in the last day:

from mostlyright.weather import hazards
df = hazards.hotspots()
df[["acquired_at_utc", "latitude", "longitude", "brightness_k", "source"]]

A narrower window, still keyless:

from datetime import UTC, datetime, timedelta
now = datetime.now(UTC)
df = hazards.hotspots(from_time=now - timedelta(hours=6), to_time=now)

An arbitrary box, with a key of your own:

df = hazards.hotspots(
source="firms.area",
bbox=(-125.0, 32.0, -114.0, 42.0),
from_time=now - timedelta(days=2),
to_time=now,
)

mostlyright.weather.hazards.issuing_offices(, product_code=‘PSH’, client=None)

Section titled “mostlyright.weather.hazards.issuing_offices(, product_code=‘PSH’, client=None)”

Return the offices that issue product_code, read from the endpoint.

A function, not a module constant. The office map is served by /products/types/{code}/locations, and a hardcoded copy would silently drop an office that started issuing after this module was written — for a settlement product, that reads as “no report was filed”.

  • Parameters:
    • product_code (str) – One of TCP, TCM, TCD, TCV, PSH.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: list[IssuingOffice]
  • Returns: One IssuingOffice per office, sorted by code. Read office.code for the id to pass as issuing_office= and office.name for the city it covers.
  • Raises:

Find which office covers the city you are settling against:

from mostlyright.weather import hazards
for office in hazards.issuing_offices():
print(office.code, office.name)

mostlyright.weather.hazards.persist_rows(product, rows)

Section titled “mostlyright.weather.hazards.persist_rows(product, rows)”

Merge rows into their monthly partitions. Append-only and idempotent.

Groups rows by the (year, month) of the product’s partition time column, then, per partition, takes one FileLock, re-reads the existing table under it, merges, dedups on the vintage key, and writes atomically. A re-capture never overwrites an earlier one: two rows differing only in captured_at are two vintages and both stay on disk.

best_track is the exception in both respects — it partitions on valid_at_utc and its key excludes captured_at, so downloading the same HURDAT2 revision twice leaves one row.

There is no current-month write-skip. The current month is exactly what a live capture needs to retain, because the live CAP feed will not serve it again.

Returns the total row count of every partition touched, after the merge. Returns 0 for an empty rows.

  • Raises:
    • TypeErrorproduct is not a str.
    • ValueErrorproduct is unregistered or not path-safe, or a row carries a source the product’s schema does not accept.
  • Return type: int
  • Parameters:
    • product (HazardProduct)
    • rows (Sequence *[*Mapping [str , object ] ])

mostlyright.weather.hazards.post_storm_reports(, source=None, issuing_office=None, from_time=None, to_time=None, product_id=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.post_storm_reports(, source=None, issuing_office=None, from_time=None, to_time=None, product_id=None, persist=False, client=None)”

Read NWS post-storm reports (PSH) with their header block parsed.

Identical to text_products(product_code="PSH", ...) plus the PSH header parse, which reads the WMO product identifier, the AWIPS identifier, the storm name, and any amendment marker off the bulletin heading.

The full product_id, amendment suffix included, is the key, so two amendments of one report appear as two rows. A PSH is the literal settlement text for the city “hurricane hits X” contracts, and a correction that silently replaced its original would erase the record of what was settled against.

No per-station table is extracted. Every office formats its peak-wind, storm-surge, and rainfall tables differently, and a column-position parser written against one office reports a number from the wrong column on the next. raw_afos holds the whole report, so that extraction can be added later as a re-parse of rows already on disk.

Read the reports one office filed for a storm:

from datetime import UTC, datetime
from mostlyright.weather import hazards
df = hazards.post_storm_reports(
source="iem",
issuing_office="KLIX",
from_time=datetime(2026, 7, 30, tzinfo=UTC),
to_time=datetime(2026, 7, 31, tzinfo=UTC),
)
df[["product_id", "amendment_suffix", "storm_name"]]

mostlyright.weather.hazards.rating_curve(gauge_id, , persist=False, client=None)

Section titled “mostlyright.weather.hazards.rating_curve(gauge_id, , persist=False, client=None)”

Return a gauge’s official stage-to-flow curve, whole and ascending.

  • Parameters:
    • gauge_id (str) – An NWS Location ID or a bare USGS site number. Echoed onto every row.
    • persist (bool) – Write the returned rows to the local vintage store. A curve carries no upstream publication stamp, so captured_at is its vintage and every refresh appends beside the last one.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.rating_curve.v1 DataFrame with attrs["source"] == "nwps.rating_curve.live", sorted ASCENDING by stage_ft. This route serves cubic feet per second, which is what makes flow_cfs here directly comparable to a gauge’s *_flow_cfs flood threshold.
  • Raises:
    • ContractErrorgauge_id is not addressable, or the payload reports flow in a unit the shared table does not convert.
    • NoHazardDataErrorgauge_id addresses no gauge.
    • httpx.HTTPStatusError – The route answered non-2xx.

There is deliberately no limit, sort, or only_tenths argument. The measured curve is 303 rows and 16,169 bytes and is always returned whole, so three interacting options over a small ordered table buy a caller nothing that .head(10) does not already give them.

Convert a forecast stage to a flow:

from mostlyright.weather import hazards
curve = hazards.rating_curve("ABBG1")
curve.head(10)[["stage_ft", "flow_cfs"]]

mostlyright.weather.hazards.stage_and_flow(gauge_id, , series=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.stage_and_flow(gauge_id, , series=None, persist=False, client=None)”

Return a gauge’s observed and/or forecast stage-and-flow points.

series=None reads the combined document and returns BOTH products in one frame, discriminated by the series column. series="observed" or series="forecast" reads only that block.

  • Parameters:
    • gauge_id (str) – An NWS Location ID or a bare USGS site number. Echoed onto every row.
    • series (Literal['observed', 'forecast'] | None) – "observed", "forecast", or None for both. The argument is called series and not product because product_code already names an NWS text bulletin in this schema family, and one word must not name two things.
    • persist (bool) – Write the returned rows to the local vintage store. Two DIFFERENT issuances both survive; re-capturing ONE issuance at the same instant stores one copy.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.stage_and_flow.v1 DataFrame with attrs["source"] == "nwps.stage_and_flow.live", observed rows first. flow_kcfs is the verbatim upstream number and flow_cfs the converted one — this route serves kilo-cubic-feet per second while the gauge threshold and rating routes serve cubic feet per second, so only flow_cfs is comparable to a *_flow_cfs threshold.
  • Raises:
    • ContractErrorseries is not one of the two the route serves, gauge_id is not addressable, a block reports stage in a unit other than feet, or a block carries points but no issuance time.
    • NoHazardDataErrorgauge_id addresses no gauge.
    • httpx.HTTPStatusError – The route answered non-2xx.

CAPTURE-OR-LOSE. Exactly one forecast issuance is ever served and no route parameter asks for a prior one, so an issuance this install did not capture is permanently unrecoverable — which is what persist= exists for. A gauge with no current forecast answers HTTP 200 with a zero-valued block and yields ZERO rows; fewer than one gauge in four carries a live forecast at any moment, so that is the ordinary answer.

Forecast rows carry TWO stamps. forecast_issued_at_utc is the River Forecast Center issuance and is the vintage; generated_at_utc is the per-point production stamp, measured five minutes and thirteen seconds later. Observed rows populate latest_observation_at_utc instead, because on that block the same upstream field is merely the newest point’s timestamp rather than an issuance.

Read both series at once:

from mostlyright.weather import hazards
df = hazards.stage_and_flow("ABBG1")
df.loc[df["series"] == "forecast", ["valid_at_utc", "stage_ft"]]

Capture each issuance as it is published:

hazards.stage_and_flow("ABBG1", series="forecast", persist=True)

mostlyright.weather.hazards.text_products(, source=None, product_code=‘TCP’, issuing_office=None, from_time=None, to_time=None, product_id=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.text_products(, source=None, product_code=‘TCP’, issuing_office=None, from_time=None, to_time=None, product_id=None, persist=False, client=None)”

Read NWS/NHC text bulletins as schema.hazard.text_product.v1 rows.

  • Parameters:
    • source (Literal['nws', 'iem'] | None) – None reads the live NWS window and auto-routes an out-of-retention window to the Iowa State AFOS archive. "nws" pins the live route and raises for such a window. "iem" pins the archive, which accepts any age.
    • product_code (str) – One of TCP, TCM, TCD, TCV, PSH.
    • issuing_office (str | None) – One office, e.g. "KNHC". On the archive leg this is the four-letter communications-header office.
    • from_time (datetime | None) – Window start. Bulletins are instant-grained, so the window is from_time / to_time; there is no on_date. To read one archive day, pass that day’s UTC bounds.
    • to_time (datetime | None) – Window end.
    • product_id (str | None) – Read one bulletin by id. An AFOS-form id (202410090000-KNHC-WTNT34-TCPAT4) addresses the archive; any other opaque id addresses the live route. Cannot be combined with a window.
    • persist (bool) – Write the returned rows to the local vintage store before returning them. Two calls append two vintages.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Returns: product_id, product_code, issuing_office, wmo_id, awips_id, issued_at_utc, storm_id, storm_name, advisory_number, amendment_suffix, raw_afos, parse_status, captured_at, source. A window with no bulletins in it returns zero rows, not an error.
  • Return type: A schema.hazard.text_product.v1 DataFrame. Columns, in order
  • Raises:
    • ContractErrorsource or product_code is not recognised; product_id was combined with a window; the archive leg was called without both window ends; or the archive window spans more than MAX_ARCHIVE_WINDOW_DAYS days.
    • HazardRetentionErrorsource="nws" with a window older than the 7-day live retention. The message names source="iem".
    • NoHazardDataErrorproduct_id names a bulletin that does not exist.
    • ValueErrorfrom_time is after to_time.

Source: : Accepted source= values are None, "nws", and "iem". The per-row source column and df.attrs["source"] always name the leg that actually answered — "nws.text_products.live" or "iem.afos.archive" — so an auto-route is visible in the result. An unrecognised value raises before any network call.

Every row keeps raw_afos byte for byte, and parse_status is "parsed" or "raw_only". A bulletin the free-text parse cannot read still produces a row with its full text, so it can be re-parsed later without a re-fetch.

Read the last two days of public advisories from the hurricane center:

from datetime import UTC, datetime, timedelta
from mostlyright.weather import hazards
now = datetime.now(UTC)
df = hazards.text_products(
product_code="TCP",
issuing_office="KNHC",
from_time=now - timedelta(days=2),
to_time=now,
)
df[["product_id", "issued_at_utc", "advisory_number", "source"]]

Read one archived bulletin by id:

df = hazards.text_products(product_id="202410090000-KNHC-WTNT34-TCPAT4")
df["raw_afos"].iloc[0]

mostlyright.weather.hazards.tropical_cyclones(, source=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.tropical_cyclones(, source=None, persist=False, client=None)”

Return one row per tropical cyclone NHC currently lists as active.

Reads CurrentStorms.json and returns schema.hazard.tropical_cyclone.v1. Off-season that document serves {"activeStorms": []} — an empty list, HTTP 200 — and this returns a schema-valid zero-row frame for it. A quiet season is a fact, not a failure, so nothing raises.

  • Parameters:
    • source (Literal['nhc'] | None) – None and "nhc" both read NHC’s active-storm index; they are the same route today. The parameter exists so adding a second provider never requires a new keyword argument. Any other value raises before a request is issued.
    • persist (bool) – Write the returned rows to the local vintage store before returning them. Two calls append two vintages — the storm state changes every advisory cycle, and captured_at differs.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.tropical_cyclone.v1 DataFrame with attrs["source"] == "nhc.tropical_cyclones.live". intensity_kt, pressure_mb, latitude, longitude, movement_direction_degrees, and movement_speed_kt are coerced numerically; advisory_number stays the zero-padded string NHC serves; raw_json holds the verbatim per-storm object, so every spelling upstream used survives the coercion.
  • Raises:
    • ContractErrorsource is not a recognised provider.
    • httpx.HTTPStatusError – NHC answered non-2xx.

basin is derived from the storm-id prefix (al / ep / cp). An id that does not match that shape yields basin=None rather than raising — an upstream id-format change must cost one column, not the whole verb. A storm object carrying no usable id at all is skipped with a warning on this module’s logger, for the same reason: storm_id is non-nullable, so keeping it would fail the frame and take every other storm in the pull down with it.

Read what is active right now:

from mostlyright.weather import hazards
df = hazards.tropical_cyclones()
df[["storm_id", "storm_name", "classification", "intensity_kt"]]

Off-season the same call returns zero rows and does not raise:

len(hazards.tropical_cyclones()) # 0

mostlyright.weather.hazards.water_observations(gauge_id, from_time, to_time, , parameter_code=‘00065’, approval_status=None, persist=False, client=None)

Section titled “mostlyright.weather.hazards.water_observations(gauge_id, from_time, to_time, , parameter_code=‘00065’, approval_status=None, persist=False, client=None)”

Return USGS continuous water values for one site over a window.

Resolves the site ONCE — one monitoring-locations request per call, never one per page — to validate it and to read the standard-time abbreviation and daylight-saving flag every returned row is stamped with, then walks the continuous collection over the window.

  • Parameters:
    • gauge_id (str) – A USGS site number in either spelling — the bare "02215260" the water-prediction service carries, or the "USGS-02215260" this API addresses. Both are accepted and both canonicalise to the BARE number, which is what every row’s gauge_id carries, so a water frame and a gauge frame join on one column and one reading is never stored under two identities. Anything that is not 8 to 15 digits raises before any request.
    • from_time (datetime) – Window start. Must be timezone-aware.
    • to_time (datetime) – Window end. Must be timezone-aware and at or after from_time.
    • parameter_code (str) – "00065" (gage height, ft) or "00060" (discharge, ft³/s).
    • approval_status (Literal['Provisional', 'Approved'] | None) – Keep only rows in this state. This is an ordinary FILTER over a column and is not a vintage selector — “provisional” is a property of the datum, not of when this install learned it. For point-in-time replay pass capture_history(..., as_of=), which selects on last_modified_at_utc.
    • persist (bool) – Write the returned rows to the local vintage store.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.water_observation.v1 DataFrame with attrs["source"] == "usgs.waterdata.live". A VALIDATED site that recorded nothing in the window returns a valid EMPTY frame.
  • Raises:
    • ContractErrorapproval_status or parameter_code is not a recognised value, gauge_id is not a USGS site number in one of the two accepted spellings, a bound is naive, from_time is after to_time, or a served pagination cursor leaves the resolved address.
    • NoHazardDataErrorgauge_id addresses no monitoring location. An unknown site is an identity failure, unlike a quiet window.
    • httpx.HTTPStatusError – The route answered non-2xx.

USGS marks recent values Provisional and flips them to Approved roughly 60 to 90 days later. That flip can change the VALUE, not only the label, and it republishes the same instant under a newer last_modified_at_utc — so a re-fetch of an unrevised row is idempotent while a revision APPENDS beside the earlier reading. This is the only flood product whose vintage comes from upstream.

Read a day of stage readings:

from datetime import UTC, datetime
from mostlyright.weather import hazards
df = hazards.water_observations(
"02215260",
datetime(2026, 8, 1, tzinfo=UTC),
datetime(2026, 8, 2, tzinfo=UTC),
)
df[["valid_at_utc", "value", "approval_status"]]

mostlyright.weather.hazards.wildfires(, source=None, scope=‘current’, state=None, incident_type=‘WF’, include_complex_children=False, min_acres=None, from_date=None, to_date=None, year=None, active_only=False, persist=False, client=None)

Section titled “mostlyright.weather.hazards.wildfires(, source=None, scope=‘current’, state=None, incident_type=‘WF’, include_complex_children=False, min_acres=None, from_date=None, to_date=None, year=None, active_only=False, persist=False, client=None)”

Return wildfire incident records — acreage, containment, and dates.

One verb, two providers, one row contract. source=None reads the national interagency feed and source="calfire" reads the California state incident list; both return schema.hazard.wildfire.v1 and every row says which leg answered.

  • Parameters:
    • source (Literal['wfigs', 'calfire'] | None) – None and "wfigs" serve the national interagency feed through scope=; "calfire" serves the California state feed through year= / active_only=. Any other value raises before a request is issued and never falls back to the other provider.
    • scope (Literal['current', 'season', 'history']) – Which interagency window answers. "current" is the fires burning now — and the one service that DROPS records, see the note below. "season" is every discovery of the current year, with no such rule. "history" is every year the interagency system has recorded. Interagency leg only.
    • state (str | None) – A two-letter state code, e.g. "CA". Matched against a closed pattern before it becomes part of the query expression. Interagency leg only — the California route serves that state and nothing else.
    • incident_type (str | None) – The incident category, interpreted PER SOURCE. On the interagency leg it is one of "WF" (wildfire, the default), "RX" (prescribed burn), or "CX" (incident complex). On the California leg it is one of "Wildfire", "Fire", "Flood", or "Hazmat". The default "WF" is the one word both legs answer — a signature carries one default, and on the California leg it selects that feed’s "Wildfire" rows, which is what the returned incident_type column shows. Every other word of the wrong vocabulary raises and names the leg that accepts it. None keeps every category.
    • include_complex_children (bool) – Keep the individual fires that roll up into an incident complex. False by default, because a complex parent and its children both report acreage and counting both double-counts the same ground. Interagency leg only.
    • min_acres (float | None) – Keep only fires at or above this reported size, in acres. Zero or more; a negative floor excludes nothing and is refused rather than sent as a filter that does not filter. Interagency leg only.
    • from_date (date | None) – Earliest fire-discovery day, inclusive. Converted to an absolute epoch-millisecond boundary in Python. Interagency leg only.
    • to_date (date | None) – Latest fire-discovery day, inclusive. Same conversion. Interagency leg only.
    • year (int | None) – A California season from 2013 to the current year. The fetcher validates it: the route answers a year it cannot read with HTTP 200 and the CURRENT season. California leg only.
    • active_only (bool) – Ask the California route for the incidents burning right now rather than the whole season. California leg only, and not combinable with year=: a past season is a record of fires that are out, and the route answers that combination with zero records.
    • persist (bool) – Write the returned rows to the local vintage store before returning them. Two calls append two vintages; nothing is overwritten, because captured_at is part of the key.
    • client (Client | None) – An httpx.Client to reuse. A client passed in is never closed here.
  • Return type: DataFrame
  • Returns: A schema.hazard.wildfire.v1 DataFrame carrying attrs["source"] and a non-null source column — one of "wfigs.current.live", "wfigs.season.live", "wfigs.history", or "calfire.incidents.live". A window with no matching incidents returns a valid EMPTY frame: a quiet fire season is a fact.
  • Raises:
    • ContractErrorsource, scope, incident_type, state, min_acres, or a date bound is unusable; an argument belonging to the other provider was passed (the message names the source that accepts it); or a scope="history" walk would cover more than WFIGS_HISTORY_PREFLIGHT_LIMIT records — measured on every history call, bounded or not, because a date bound is not a size. All of these are refused before any records request.
    • HazardRetentionErrorfrom_date or to_date predates the shortest fall-off horizon of the live incident service and scope is "current". The message names the bound it read, carries the publisher’s own rules, and names scope="season" and scope="history".
    • SourceUnavailableError – a service answered HTTP 200 with an error object instead of records, or the California route answered non-2xx.
    • RateLimitError – a route answered HTTP 429 on every attempt.
    • ApiKeyRequiredError – no API key is configured for catalog resolution.

THE LIVE LEG DROPS RECORDS. scope="current" lists only fires that have not been declared contained, controlled, or out, and it removes a record after three, eight, or fourteen days without an update depending on size class. A revision this install did not capture cannot be fetched from that service again, which is what persist=True exists for.

THE SEASON AND HISTORY LEGS ARE OPERATIONAL, NOT CERTIFIED. The publisher states that the data “represents a snapshot in time of what was known for each incident during the operational period of the fire” and is “not QA/QC’d after a fire is declared out”. The authoritative certified record is a separate service, InFORM_FireOccurrence_Public, which carries the certified acreage and is cataloged in docs/hazards-adapter.md rather than shipped here: it is post-hoc and stable, so it is not capture-or-lose.

THIS VERB OWNS THE QUERY EXPRESSION. There is no where= and no out_fields= argument. The expression is assembled from validated, enumerated components, and the requested attribute list is the set verified present on all three interagency services.

What is burning right now:

from mostlyright.weather import hazards
df = hazards.wildfires()
df[["incident_name", "acres", "percent_contained", "state", "source"]]

One state’s season, prescribed burns excluded:

df = hazards.wildfires(scope="season", state="CA", min_acres=100.0)

The California state acreage of record:

df = hazards.wildfires(source="calfire", year=2025)
df[["incident_name", "acres", "is_final", "source"]]