From df3b84a07eb7c04ed1143ffec175f184f225c44d Mon Sep 17 00:00:00 2001 From: Vahid Rafiei Date: Mon, 28 Sep 2026 20:45:13 +0200 Subject: [PATCH] groundwater_at takes an API key; a keyless 401 says a key is needed The well-record endpoint (/api/gw-wells/at) is signed-in only, so the guest call answered 401 and the client reported "invalid or revoked API key" although no key was sent (the daily CI's two groundwater tests, red since 2026-09-26). - groundwater_at(lat, lon, tol_deg=0.05, api_key=None): sends api_key or SWATGENX_API_KEY, as Client does; - a 401 on a keyless request now says the endpoint needs an API key; - the two groundwater tests run with SWATGENX_API_KEY and SKIP without it; a new test pins the keyless message. The workflow line that passes the SWATGENX_API_KEY secret follows separately (it needs a workflow-scoped token). Local run without a key: 14 passed, 2 skipped. With the old client the new test fails. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0165nvw7C6jpq45EnoiEHE7S --- README.md | 2 +- src/swatgenx/client.py | 14 +++++++++++--- tests/test_public.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e976d79..6b68afb 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ sg.calibration("01451800") # {'mode': 'engineer', 'cal_daily_nse': 0.642, 'val_daily_nse': 0.748, ...} # National groundwater inventory: 28.8M lithology intervals, 7.9M wells, 46 states -sg.groundwater_at(42.73, -84.55) # nearest well + lithology log +sg.groundwater_at(42.73, -84.55) # nearest well + lithology log (needs SWATGENX_API_KEY) sg.groundwater_summary() # National PFAS monitoring inventory (huc8 = 8-digit hydrologic unit code) diff --git a/src/swatgenx/client.py b/src/swatgenx/client.py index 91ed135..688e20a 100644 --- a/src/swatgenx/client.py +++ b/src/swatgenx/client.py @@ -47,6 +47,11 @@ def _request(method: str, path: str, *, key: str | None = None, json: dict | Non except ValueError: body = {"raw": (r.text or "")[:500]} if r.status_code == 401: + if not key: + # No key was sent, so "invalid or revoked" would be false: the endpoint needs one. + raise SwatGenXError( + "This endpoint needs an API key: pass api_key=... or set SWATGENX_API_KEY. " + "Create one at https://www.swatgenx.com -> dashboard -> API keys.", 401, body) raise SwatGenXError( "Authentication failed — invalid or revoked API key. Sign in at " "https://www.swatgenx.com -> dashboard -> API keys.", 401, body) @@ -90,11 +95,14 @@ def calibration(site_no: str) -> dict | None: return None -def groundwater_at(lat: float, lon: float, tol_deg: float = 0.05) -> dict: +def groundwater_at(lat: float, lon: float, tol_deg: float = 0.05, api_key: str | None = None) -> dict: """Nearest well to a point from the national groundwater inventory (28.8M lithology intervals, 7.9M wells), with its lithology log when available. tol_deg is the search - box half-width in degrees (~0.05 = 5 km).""" - return _request("GET", "/api/gw-wells/at", + box half-width in degrees (~0.05 = 5 km). + + Needs a free account's API key: pass api_key=... or set SWATGENX_API_KEY.""" + key = (api_key or os.environ.get("SWATGENX_API_KEY") or "").strip() or None + return _request("GET", "/api/gw-wells/at", key=key, params={"lat": lat, "lon": lon, "tol": tol_deg}) diff --git a/tests/test_public.py b/tests/test_public.py index 48f85aa..746f4d3 100644 --- a/tests/test_public.py +++ b/tests/test_public.py @@ -7,10 +7,16 @@ Run: pip install pytest && pytest tests/ -v """ +import os + import pytest import swatgenx as sg +# groundwater_at needs an account's API key (the well-record endpoint is signed-in only). CI passes a dedicated test +# account's key as the SWATGENX_API_KEY secret; without it these two tests SKIP rather than fail. +needs_key = pytest.mark.skipif(not os.environ.get("SWATGENX_API_KEY"), reason="SWATGENX_API_KEY not set") + def test_catalog_unfiltered(): models = sg.catalog() @@ -44,11 +50,13 @@ def test_calibration_nonexistent_returns_none(): assert sg.calibration("00000000") is None +@needs_key def test_groundwater_at_michigan(): well = sg.groundwater_at(42.73, -84.55) assert well.get("found") and well.get("well_id") +@needs_key def test_groundwater_at_pennsylvania(): well = sg.groundwater_at(40.602, -75.471) assert well.get("found") and well.get("well_id") @@ -87,3 +95,11 @@ def test_client_requires_key(): def test_client_bad_key_auth_guidance(): with pytest.raises(sg.SwatGenXError): sg.Client(api_key="not-a-real-key").whoami() + + +def test_groundwater_at_without_a_key_says_a_key_is_needed(monkeypatch): + """A keyless call must say the endpoint needs a key, never 'invalid or revoked' (no key was sent).""" + monkeypatch.delenv("SWATGENX_API_KEY", raising=False) + with pytest.raises(sg.SwatGenXError) as e: + sg.groundwater_at(42.73, -84.55) + assert e.value.status == 401 and "needs an API key" in str(e.value)