mostlyright.weather.cache
mostlyright.weather.cache
Section titled “mostlyright.weather.cache”Local parquet cache for mostlyright weather observations and climate.
NEW module (no v0.14.1 reference). v0.14.1 is a hosted-client SDK — merging
and caching happen server-side there. The mostlyright local-first SDK ships
caching client-side so re-runs of research() against the same date range
hit a local parquet file instead of re-fetching from IEM/AWC/GHCNh.
Path layout (CACHE-01):
$HOME/.mostlyright/cache/v1/observations/<STATION>/<YYYY>/<MM>.parquet$HOME/.mostlyright/cache/v1/climate/<STATION>/<YYYY>.parquetOverride the root via the MOSTLYRIGHT_CACHE_DIR environment variable.
Safety guarantees (CACHE-07):
: - Atomic write: write to a sibling .tmp file inside a FileLock,
then os.rename to the final path. A crash mid-write never leaves a
truncated parquet at the read path.
- FileLock-guarded: two concurrent
write_cacheworkers serialize on a.locksidecar file. Verified by the multiprocess test. - LST-current-month-skip: the current calendar month in the station’s
Local Standard Time is mutable (observations still arriving). Writes
to that (year, month) are no-ops, and reads return
Noneeven if a stale file exists. Climate cache applies the same rule at year granularity. This prevents serving incomplete data on re-runs.
Parquet options (CACHE-01):
: Every write uses version="2.6" and coerce_timestamps="us" so the
byte output is stable across pyarrow versions and microsecond-resolution
timestamps survive roundtrip without nanosecond inflation.
This module is parser-agnostic — it operates on already-parsed
list[dict] rows. Importing a parser (_iem, _awc, etc.) would
couple cache failures to fetcher failures; keeping the cache pure lets
research() swap fetchers without touching the cache layer.
Functions
Section titled “Functions”cache_path(station, year, month) | Return the parquet cache path for the (station, year, month) tuple. |
|---|---|
climate_cache_path(station, year) | Return the parquet cache path for annual climate data. |
forecast_cache_path(station, source, model, …) | Return the parquet cache path for the (station, source, model, year, month) tuple per Phase 20 D-09. |
invalidate(station, year, month) | Remove the cache entry for (station, year, month). |
invalidate_climate(station, year) | Remove the climate cache entry for (station, year). |
invalidate_forecast(station, source, model, …) | Remove a forecast cache partition if it exists; return whether removed. |
invalidate_satellite(satellite, product, …) | Remove a satellite cache partition if it exists; return whether removed. |
read_cache(station, year, month) | Return cached observation rows for (station, year, month) or None. |
read_climate_cache(station, year) | Return cached climate rows for (station, year) or None. |
read_forecast_cache(station, source, model, …) | Return cached forecast rows for the partition key or None on miss. |
read_satellite_cache(satellite, product, …) | Return cached satellite rows for the partition key or None on miss. |
satellite_cache_path(satellite, product, …) | Return the parquet cache path for a satellite partition (SAT-25-04, D8). |
write_cache(station, year, month, rows, *[, …]) | Atomically write rows to the observation cache. |
write_climate_cache(station, year, rows, *) | Atomically write rows to the annual climate cache. |
write_forecast_cache(station, source, model, …) | Atomically write rows to the forecast cache partition. |
write_satellite_cache(satellite, product, …) | Atomically write rows to the satellite cache partition (D8). |
mostlyright.weather.cache.cache_path(station, year, month)
Section titled “mostlyright.weather.cache.cache_path(station, year, month)”Return the parquet cache path for the (station, year, month) tuple.
Example:
cache_path("KNYC", 2025, 1)# -> Path("$HOME/.mostlyright/cache/v1/observations/KNYC/2025/01.parquet")The month is zero-padded to two digits so a lexicographic directory listing matches chronological order.
Validates station against STATION_CODE_RE (Rob C1) and asserts the
resolved path stays under the cache root (defense-in-depth backstop -
Rob C1).
mostlyright.weather.cache.climate_cache_path(station, year)
Section titled “mostlyright.weather.cache.climate_cache_path(station, year)”Return the parquet cache path for annual climate data.
Example:
climate_cache_path("KNYC", 2025)# -> Path("$HOME/.mostlyright/cache/v1/climate/KNYC/2025.parquet")Same validation contract as cache_path() (Rob C1).
mostlyright.weather.cache.forecast_cache_path(station, source, model, year, month)
Section titled “mostlyright.weather.cache.forecast_cache_path(station, source, model, year, month)”Return the parquet cache path for the (station, source, model, year, month) tuple per Phase 20 D-09.
Layout:
~/.mostlyright/cache/v1/forecasts/{source}/{model}/{station}/{YYYY}/{MM}.parquetPartition by issued_at cycle month (immutable once published).
mostlyright.weather.cache.invalidate(station, year, month)
Section titled “mostlyright.weather.cache.invalidate(station, year, month)”Remove the cache entry for (station, year, month).
Returns True if a file was removed, False if the file did not
exist. Acquires the same FileLock as write_cache so an invalidation
racing a write either runs strictly before or strictly after — never
mid-rename.
mostlyright.weather.cache.invalidate_climate(station, year)
Section titled “mostlyright.weather.cache.invalidate_climate(station, year)”Remove the climate cache entry for (station, year).
Returns True if removed, False if absent.
mostlyright.weather.cache.invalidate_forecast(station, source, model, year, month)
Section titled “mostlyright.weather.cache.invalidate_forecast(station, source, model, year, month)”Remove a forecast cache partition if it exists; return whether removed.
mostlyright.weather.cache.invalidate_satellite(satellite, product, station, year, month, , cache_root=None)
Section titled “mostlyright.weather.cache.invalidate_satellite(satellite, product, station, year, month, , cache_root=None)”Remove a satellite cache partition if it exists; return whether removed.
cache_root (P2-1) mirrors the read/write override so a partition written
under a --out directory can be invalidated from there; None preserves
the default resolution.
- Return type:
bool - Parameters:
mostlyright.weather.cache.read_cache(station, year, month)
Section titled “mostlyright.weather.cache.read_cache(station, year, month)”Return cached observation rows for (station, year, month) or None.
Returns None when:
: - the cache file does not exist
- (year, month) is the station’s current LST month (file may be stale)
- a concurrent
invalidate()removes the file between theexists()check and the read (treated as cache-miss; the caller re-fetches transparently)
Returns list[dict] otherwise. The list is materialised eagerly from
pyarrow - callers can iterate freely without holding a file handle.
mostlyright.weather.cache.read_climate_cache(station, year)
Section titled “mostlyright.weather.cache.read_climate_cache(station, year)”Return cached climate rows for (station, year) or None.
Returns None when:
: - the cache file does not exist
yearis the station’s current LST year (file may be stale)- a concurrent
invalidate_climate()removes the file between theexists()check and the read (cache-miss semantics; caller re-fetches transparently)
mostlyright.weather.cache.read_forecast_cache(station, source, model, year, month)
Section titled “mostlyright.weather.cache.read_forecast_cache(station, source, model, year, month)”Return cached forecast rows for the partition key or None on miss.
Returns None when:
: - the partition file does not exist
sourceis live or seamless (never cached)- (year, month) is the current UTC month (cycles may still publish)
mostlyright.weather.cache.read_satellite_cache(satellite, product, station, year, month, , cache_root=None)
Section titled “mostlyright.weather.cache.read_satellite_cache(satellite, product, station, year, month, , cache_root=None)”Return cached satellite rows for the partition key or None on miss.
Returns None when the partition does not exist or (year, month) is the
current UTC month (A6 — mirrors the forecast tier; the current month may
still receive scans). cache_root (P2-1) optionally overrides the root so
a partition written under a --out directory reads back from there; when
None the default resolution is byte-for-byte unchanged.
mostlyright.weather.cache.satellite_cache_path(satellite, product, station, year, month, , cache_root=None)
Section titled “mostlyright.weather.cache.satellite_cache_path(satellite, product, station, year, month, , cache_root=None)”Return the parquet cache path for a satellite partition (SAT-25-04, D8).
Hardens EVERY user-controlled path segment (P2-e — the forecast analog
validates only station):
stationviavalidate_icao_for_path()(4-letter ICAO).satellitemust be a registered native-ring satellite (Phase 26 W1: the whole ringgoes16/17/18/19 | himawari8/9 | viirs-* | meteosat-*, per the per-source registry) and contain no path separator.productmust be a registered product FORsatellite’s OWNING SOURCE (per-source registry — e.g. an ABI product is invalid on a Himawari satellite) and contain no path separator.
Then assert_path_under() is the final path-traversal backstop. The
path has NO mirror segment (D9 — mirror is transport-only).
cache_root (P2-1) optionally overrides the resolved cache root. The
backfill CLI threads its --out directory here so the parquet partition
lands under --out rather than the home/env cache root. When None
(every existing caller) the behavior is byte-for-byte unchanged:
_cache_root() resolves the root exactly as before. The path-traversal
backstop validates against whichever root is in effect.
- Return type:
Path - Parameters:
mostlyright.weather.cache.write_cache(station, year, month, rows, , source=None)
Section titled “mostlyright.weather.cache.write_cache(station, year, month, rows, , source=None)”Atomically write rows to the observation cache.
- Return type:
None - Parameters:
No-op (does NOT raise) when: : - (year, month) is the station’s current LST month
sourceends with.live— live endpoints are never cached
The source kwarg is optional. The standard call site (research())
passes the fetcher’s endpoint identifier (e.g. "iem.asos",
"awc.live") so the cache can gate writes uniformly without each
fetcher having its own no-cache branch.
mostlyright.weather.cache.write_climate_cache(station, year, rows, , source=None)
Section titled “mostlyright.weather.cache.write_climate_cache(station, year, rows, , source=None)”Atomically write rows to the annual climate cache.
No-op (does NOT raise) when:
: - year is the station’s current LST year
sourceends with.live
mostlyright.weather.cache.write_forecast_cache(station, source, model, year, month, rows)
Section titled “mostlyright.weather.cache.write_forecast_cache(station, source, model, year, month, rows)”Atomically write rows to the forecast cache partition.
- Return type:
None - Parameters:
No-op (does NOT raise) when:
: - source is live or seamless (never cached)
- (year, month) is the current UTC month (cycles may still publish)
rowsis empty
mostlyright.weather.cache.write_satellite_cache(satellite, product, station, year, month, rows, , cache_root=None)
Section titled “mostlyright.weather.cache.write_satellite_cache(satellite, product, station, year, month, rows, , cache_root=None)”Atomically write rows to the satellite cache partition (D8).
No-op (does NOT raise) when rows is empty or (year, month) is the
current UTC month (A6). On merge into an existing partition, the entire
read→concat→“_dedup_satellite_rows“ (first-seen-wins, mirror-invariant)
→write sequence runs under a single FileLock acquisition, so two
writers targeting the same (satellite, product, station, year, month)
partition cannot lost-update each other: the second writer reads the
first writer’s already-committed rows and merges on top of them rather
than clobbering them. The inner write is the usual .tmp +
os.replace atomic swap. No staging dir, no R2.
Unlike the overwrite-only forecast tier (write_forecast_cache does not
read-modify-write), this tier is a true read-modify-write merge, so the
lock must span the read as well as the write — see _write_table_unlocked.
cache_root (P2-1) optionally overrides the cache root so the partition
lands under a caller-supplied directory (the backfill CLI threads its
--out here). When None the default _cache_root() resolution is
byte-for-byte unchanged — the forecast/observation/climate tiers and all
existing satellite-cache callers are unaffected.