Commit graph

4 commits

Author SHA1 Message Date
Emi Griffith
581bdaca04 Rename the middle rain tiers and merge Mod–Heavy into Heavy (#214)
The rain-intensity scale drops its two hyphenated compound labels for single
words and loses a tier, going from eight to seven:

  Trace / Light / Brisk / Typical / Heavy / Very Heavy / Extreme

- Light–Mod -> Brisk, Moderate -> Typical (renames only; classes unchanged).
- Mod–Heavy is merged up into Heavy, whose floor drops from the 75th to the 60th
  rain-day percentile, so Heavy now spans 60–90.
- The lightest tier (already the merged Very Light) is renamed Trace.

grading.py RAIN_BANDS and the frontend SCALE_RAIN mirror stay in lockstep, and
the detail-view ladder derives from the table so it follows automatically. The
now-unreferenced wet-6 colour token is kept so any day still cached under that
class renders until the derived store recomputes; the chart's percentile fan also
keeps it for a smooth gradient.

Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8
2026-07-20 04:38:30 +00:00
Emi Griffith
4520b9e4ba Merge the Trace rain tier into Very Light (#212)
The rain-intensity scale had a Trace tier (below the 1st percentile of a place's
rain days) sitting under Very Light — a sliver category that mostly showed 1% and
crowded the distribution strip. Fold it into Very Light, which now bottoms out the
scale at 0, so the lightest measurable rain reads as Very Light.

- grading.py: RAIN_BANDS drops the Trace band; Very Light's floor goes 1 -> 0. The
  detail-view ladder derives from the table, so it follows automatically.
- shared.js: SCALE_RAIN drops the Trace row; Very Light's range becomes "<10".

Eight rain tiers now instead of nine; the strip shows one fewer column. The wet-1
colour token is kept (unreferenced by new gradings) so any day still cached with
the old class renders until the derived store recomputes.

Claude-Session: https://claude.ai/code/session_013dRZmX9D3JEntfMKWMTWZ8
2026-07-20 04:25:13 +00:00
Emi Griffith
c3bacdce0c Migrate backend dataframe layer from pandas to polars (#90)
* Migrate backend dataframe layer from pandas to polars

Replace pandas with polars across the backend, dropping both pandas and its
pyarrow parquet engine from the dependency set. numpy stays (the grading
percentile math is unchanged).

- climate.py: parquet IO, source→frame mappings, cache read/topup on polars.
  New _normalize_read casts the cached `date` column to pl.Date (older files
  were written by pandas as datetime64[ns]); frames now unify missing values as
  null so the grading boundary drops them consistently across sources.
- grading.py: keep the numpy percentile core; swap the frame→numpy bridge to
  .to_numpy()/.drop_nulls(), day-of-year/year to polars dt expressions, and the
  per-row loop to iter_rows(named=True).
- views.py: filter/anti-join/concat replace boolean-mask, isin and pd.concat;
  scalar dates are stdlib datetime.date; a local _months_before helper replaces
  DateOffset(months=) for the calendar-range default.
- app.py, migrate.py: request-date parsing uses datetime.date, removing pandas
  from the endpoint and migrate layers entirely.
- The date column is pl.Date end to end, eliminating the pandas normalize() calls
  and comparing cleanly against stdlib dates.

Payloads are unchanged: calendar, day, grade and forecast responses are
byte-for-byte identical to the pandas implementation on the same cached record.

Tests ported to polars fixtures, with added coverage for the combined feels-like
fallback, calendar month-offset (month-end/leap), and the concat/dedup
"fresher source wins" rule.

* Port notify.py to polars after merging dev's account system

Merge origin/dev (accounts + notification subscriptions) and carry the pandas→
polars migration into the newly added notify.py, which the merge brought in still
using pandas — with pandas removed from requirements this broke its import.

- notify.py: _candidate_rows filters/sorts the recent bundle with polars
  expressions and returns iter_rows dicts; date scalars are datetime.date;
  history/recent emptiness via is_empty().
- test_notify.py: synthetic history/rows built with polars + datetime.
2026-07-15 19:07:38 +00:00
4ac5323375 Add backend test suite; gate direct pushes; serialize LAN deploys (#41)
- backend/tests: 74 hermetic tests (no network, no repo data//logs/ writes)
  covering grid snapping/round-trips, grading percentiles/bands/windows/
  dry streaks, the places index (norm, one-edit matchers, search,
  corrections), the derived store (token validity, cache=False, degraded
  mode), and route-level API tests over a faked climate layer — routing,
  validation, ETag/304 revalidation, store replay, the /cell bundle, and
  the v1/v2 aliases. The API tests would have caught the /place
  AttributeError regression.
- requirements-dev.txt + make test (venv prefers uv-pinned 3.12, matching
  deploy-dev.sh — pyarrow wheels stop at 3.12 and some pyenv builds lack
  sqlite).
- CI: extract the build job into a reusable build.yml, add the test run
  and an API health probe (page-only curl can't catch route wiring
  faults); deploy-dev.yml now runs the same build gate before deploying
  direct pushes, which previously deployed with no CI at all.
- Deploys serialize under one dev-lan-deploy concurrency group across
  both workflows (previously per-PR groups could interleave two deploys
  to the same checkout), and are never cancelled mid-restart.
- deploy-dev.sh health check also probes /api/v2/place — best-effort
  externals mean a failure there is a genuine server bug.
2026-07-11 19:37:49 +00:00