Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions docs/done/PLAN_montana_gauges.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ Switch it to read `docs/one-offs/mt.list` directly and pull metadata from
drain_area_sq_mi, altitude_ft FROM usgs_site WHERE site_no = ?` on
the gauges.db cache. Error out if any site_no is missing (curated
list should match the cache; missing rows mean the cache is stale —
re-run `scripts/fetch_usgs_sites.py`).
re-run `levels audit-gauges` to refresh, or the
`kayak.gauge_audit.usgs_sites` fetcher directly).
- Emit SQL in the order they appear in mt.list (stable diff if the
list is reordered).
- Drop the `_REVIEW_HINTS` heuristic and "REVIEW:" comments — the
Expand Down Expand Up @@ -182,7 +183,8 @@ sqlite3 Gauge-metadata-cache/gauges.db \
('06090500','06025500','06025250','12340000','12354500',
'06073500','12359800','06036650','06038800','06066500',
'06077200','06077500','06085800');"
# Expected: 13. If less, refresh: python3 scripts/fetch_usgs_sites.py
# Expected: 13. If less, refresh: levels audit-gauges (or
# python3 -m kayak.gauge_audit.usgs_sites)

# Regenerate the migration from mt.list
python3 docs/one-offs/generate_mt_migration.py
Expand Down Expand Up @@ -232,7 +234,8 @@ excludes (`src/kayak/db/sources.py:105`).

### Phase 1 retirement

The Phase 1 discovery work (extending `fetch_usgs_sites.py` to cover MT)
The Phase 1 discovery work (extending the USGS site fetcher,
now `kayak.gauge_audit.usgs_sites`, to cover MT)
stays merged. The cache it builds remains useful: the Phase 2 generator
reads it for metadata, and a future "extend MT coverage" PR can pull
from the same cache without re-running the USGS site service. The
Expand Down Expand Up @@ -383,7 +386,7 @@ goes 150 → 163. Still in the 2-batch range for `fetch-usgs-ogc`
effectively unchanged.

Audit noise on first run is now 13 newly-data-providing gauges instead
of 62 (still trips `scripts/audit_gauges.py`'s "started providing data
of 62 (still trips `levels audit-gauges`'s "started providing data
in the last week" detector once per run). Mention in the PR description.

## Files touched (final list, revised)
Expand Down Expand Up @@ -423,7 +426,7 @@ in the last week" detector once per run). Mention in the PR description.
| `data/discover/montana_candidates.csv` | `rm` — superseded by `docs/one-offs/mt.list`. Gitignored. |

Untouched by this revision (kept from earlier commits):
`scripts/fetch_usgs_sites.py` (Montana coverage stays merged),
the USGS site fetcher (`kayak.gauge_audit.usgs_sites`, Montana coverage stays merged),
`src/kayak/web/static/filters.js` (already handles `#st=` correctly),
`php/gauge_picker.php` (its `?state=` parser stays intact —
`picker.php` is the only one needing the new parser).
Expand Down
2 changes: 1 addition & 1 deletion docs/live-tree-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ at a time; the table below tracks what is now packaged vs. still repo-root:
| committed `static/` assets (map.js, leaflet, images, manifest, sw.js, …) | **packaged** under `src/kayak/web/static/` | `web/build/deploy.py`, `_shared.py` via the packaged dir | ✅ resolved by S4a-2 slice B1 |
| generated map-layer GeoJSON (`*.geojson`) | map-layer staging dir (`MAP_LAYERS_DIR`; legacy alias `OSMB_DIR`; default `BASE_DIR/var/osmb`) | `levels fetch-map-layers` writes; `deploy.py` copies into `OUTPUT_DIR/static` | ✅ not a blocker — env-located generated runtime data, like `output_dir` |
| regression reports (`*.{md,svg,json}`) | external `kayak_data` clone (`DATASET_DIR/regression/`) | `web/build/deploy.py` via `DATASET_DIR` (env), not `BASE_DIR` | ✅ not a blocker — env-located dataset content (S2-E2 moved the read off `BASE_DIR/docs/regression`) |
| gauge metadata cache (`gauges.db`) | cache path (`GAUGE_METADATA_CACHE`; default `BASE_DIR/Gauge-metadata-cache/gauges.db`) | `scripts/audit_gauges.py`/fetchers write; gauges build reads | ✅ not a blocker — env-located generated runtime cache, current default kept for live compatibility |
| gauge metadata cache (`gauges.db`) | cache path (`GAUGE_METADATA_CACHE`; default `BASE_DIR/Gauge-metadata-cache/gauges.db`) | `levels audit-gauges` (`kayak.gauge_audit`) / fetchers write; gauges build reads | ✅ not a blocker — env-located generated runtime cache, current default kept for live compatibility |

`src/kayak/web/static/style.css` was already package-relative. With slices A + B1
+ B2 done, the engine's Python-side defaults, schema migrations, committed web
Expand Down
3 changes: 2 additions & 1 deletion docs/one-offs/generate_mt_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ def emit_sql() -> str:
if row is None:
raise SystemExit(
f"USGS {site_no} not in {CACHE_DB_PATH}. "
f"Refresh the cache: python3 scripts/fetch_usgs_sites.py"
"Refresh the cache: levels audit-gauges "
"(or python3 -m kayak.gauge_audit.usgs_sites)"
)
station_nm = row["station_nm"]
lat = row["latitude"]
Expand Down
10 changes: 10 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ module = "kayak.tracing.trace"
disallow_untyped_defs = false
check_untyped_defs = false

[[tool.mypy.overrides]]
# gauge_audit is a wholesale move of the former scripts/audit_gauges.py closure
# (audit logic + two site-cache fetchers) that pre-dates the strict-typing
# regime; typing it gradually rather than front-loading the annotations in the
# move commit (same precedent as kayak.tracing.trace above). The thin CLI
# wrapper kayak.cli.audit_gauges stays under the strict gate.
module = ["kayak.gauge_audit.audit", "kayak.gauge_audit.usgs_sites", "kayak.gauge_audit.nwps_sites"]
disallow_untyped_defs = false
check_untyped_defs = false

[[tool.mypy.overrides]]
# Third-party geospatial/plotting libs ship no stubs.
module = ["osgeo.*", "matplotlib.*", "contextily.*", "geopandas.*", "shapely.*", "pyogrio.*"]
Expand Down
11 changes: 0 additions & 11 deletions scripts/_gauge_metadata_cache.py

This file was deleted.

5 changes: 2 additions & 3 deletions scripts/seed_gauge_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@
# Reuse the build pipeline's station-name parsers so we stay in lockstep with
# current behavior.
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
from _gauge_metadata_cache import DEFAULT_GAUGE_METADATA_CACHE

from kayak.config import GAUGE_METADATA_CACHE
from kayak.db.safety import (
ProductionWriteRefused,
maintenance_target_db,
Expand All @@ -48,7 +47,7 @@
_parse_station_uppercase,
)

DEFAULT_CACHE = str(DEFAULT_GAUGE_METADATA_CACHE)
DEFAULT_CACHE = str(GAUGE_METADATA_CACHE)

_DIRECTIONS = ("North", "South", "East", "West", "Middle")
_DIRECTION_LETTERS = {
Expand Down
78 changes: 78 additions & 0 deletions src/kayak/cli/audit_gauges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""``levels audit-gauges`` — audit gauge metadata against the kayak DB.

Thin CLI wrapper: registers the subcommand and its options, then hands the
parsed namespace to :func:`kayak.gauge_audit.audit.run_audit`, which refreshes
the USGS/NWPS site caches and reports new candidates, stopped/started feeds, and
stale gauges (optionally emailing a digest). The audit logic lives in
:mod:`kayak.gauge_audit.audit`; this module stays fully typed.
"""

from __future__ import annotations

import argparse
import os
from pathlib import Path

from kayak.config import GAUGE_METADATA_CACHE

# Default kayak DB for the audit. The live unit reads the prod DB; ``--kayak-db``
# overrides for a scratch copy. (Kept here, not in the audit module, because it's
# only a CLI default.)
KAYAK_DB = Path.home() / "DB" / "kayak.db"


def addArgs(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
"""Register the 'audit-gauges' subcommand."""
parser = subparsers.add_parser(
"audit-gauges",
help="Audit gauge metadata: refresh caches, find candidates, detect data changes",
)
parser.set_defaults(func=audit_gauges)
parser.add_argument(
"--no-refresh",
action="store_true",
help="Skip refreshing the metadata caches",
)
parser.add_argument(
"--days",
type=int,
default=7,
help="Window in days for data status checks (default: 7)",
)
parser.add_argument(
"--cache-db",
type=str,
default=str(GAUGE_METADATA_CACHE),
help=f"Path to gauge metadata cache (default: {GAUGE_METADATA_CACHE})",
)
parser.add_argument(
"--kayak-db",
type=str,
default=str(KAYAK_DB),
help=f"Path to kayak database (default: {KAYAK_DB})",
)
parser.add_argument(
"--email",
type=str,
default=os.environ.get("AUDIT_EMAIL"),
help="Email digest to this address (or set AUDIT_EMAIL). Always sends if set.",
)
parser.add_argument(
"--candidate-miles",
type=float,
default=3.0,
help="Max distance (mi) from a reach midpoint for a candidate gauge (default: 3)",
)
parser.add_argument(
"--include-gauged",
action="store_true",
help="Also list candidates for reaches that already have a linked gauge "
"(off by default — these are rarely actionable)",
)


def audit_gauges(args: argparse.Namespace) -> None:
"""Entry point for ``levels audit-gauges``."""
from kayak.gauge_audit.audit import run_audit

run_audit(args)
2 changes: 2 additions & 0 deletions src/kayak/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from kayak.cli import (
analyze_logs,
assign_huc,
audit_gauges,
build,
calc_rating,
calculator,
Expand Down Expand Up @@ -60,6 +61,7 @@ def main() -> None:
decimate.addArgs(subparsers)
orphan_check.addArgs(subparsers)
check_reaches.addArgs(subparsers)
audit_gauges.addArgs(subparsers)
pipeline.addArgs(subparsers)
seed_maintainer.addArgs(subparsers)
delete_editor.addArgs(subparsers)
Expand Down
2 changes: 1 addition & 1 deletion src/kayak/cli/validate_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"KAYAK_BACKUP_DIR",
"KAYAK_OFFSITE_REMOTE",
"KAYAK_OFFSITE_KEEP",
# Read via os.environ by fetch_usgs_ogc.py / fetch_usgs_sites.py.
# Read via os.environ by cli/fetch_usgs_ogc.py / gauge_audit/usgs_sites.py.
# Deliberately NOT a KayakConfig field: emit-config would write it
# into the www-data-readable runtime-config.json, and PHP has no
# use for it.
Expand Down
21 changes: 17 additions & 4 deletions src/kayak/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,14 @@ class KayakConfig(BaseSettings):
mail_reply_to: EmailStr | None = None
mail_dump_dir: Path | None = None

# Operator email-digest destination. Read by scripts/audit_gauges.py
# (the weekly gauge-metadata audit timer) and emitted into the JSON
# Operator email-digest destination. Read by ``levels audit-gauges``
# (the twice-monthly gauge-metadata audit timer — 2nd + 17th) and emitted into the JSON
# so future PHP / Python consumers don't have to re-implement the env
# read. Currently consumed only by the script, but living in the
# typed model keeps it within validate-config's allowlist.
# read. An empty / whitespace-only value coerces to ``None`` (see
# ``_blank_email_to_none``): the systemd unit passes ``${AUDIT_EMAIL}``,
# which expands to an empty string when the var is unset, and that
# must mean "no digest", not a config-load failure that breaks every
# ``levels`` subcommand at import time.
audit_email: EmailStr | None = None

# Where the CSP-violation reporter writes JSON lines. Hardcoded
Expand Down Expand Up @@ -246,6 +249,16 @@ def _split_csv(cls, v: object) -> object:
return [s.strip() for s in v.split(",") if s.strip()]
return v

@field_validator("audit_email", mode="before")
@classmethod
def _blank_email_to_none(cls, v: object) -> object:
# An unset ``${AUDIT_EMAIL}`` in the systemd unit expands to "" — treat
# that (and any whitespace-only value) as "no digest configured" rather
# than an EmailStr validation error that would fail the whole CLI load.
if isinstance(v, str) and not v.strip():
return None
return v

@property
def map_layers_dir(self) -> Path:
"""Preferred name for the generated map-overlay staging directory."""
Expand Down
8 changes: 8 additions & 0 deletions src/kayak/gauge_audit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Gauge metadata audit: refresh caches, find candidates, detect data changes.

A wholesale move of the former ``scripts/audit_gauges.py`` closure into the
package so ``levels audit-gauges`` can run it from an installed (paired-release)
venv rather than a source script absent from releases. The thin CLI wrapper
lives in :mod:`kayak.cli.audit_gauges`; the audit logic and the two site-cache
fetchers live here.
"""
Loading