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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 11 additions & 3 deletions src/swatgenx/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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})


Expand Down
16 changes: 16 additions & 0 deletions tests/test_public.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Loading