From 743f231d87a42ce0084e9e529c771865601990ea Mon Sep 17 00:00:00 2001 From: eastagiletracker <310448263+eastagiletracker@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:19:36 +0700 Subject: [PATCH] fix: return empty series instead of 500 when compute_node_series has no observations POST /data/compute_node_series raised a KeyError (HTTP 500) for an empty request body ([]) or a series with no points ([[]]): the resulting empty dataframe has no node-variable columns to select. Return empty median/q0.25/q0.75 series in that case instead. Also add the api/swotvis/tests pytest suite that the `make test` target already invokes but which did not exist, covering the normal computation and this empty-input regression. --- api/requirements-dev.txt | 1 + api/swotvis/app/routers/data/router.py | 10 ++- api/swotvis/tests/test_compute_node_series.py | 80 +++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 api/swotvis/tests/test_compute_node_series.py diff --git a/api/requirements-dev.txt b/api/requirements-dev.txt index 849f2820..44ddd647 100644 --- a/api/requirements-dev.txt +++ b/api/requirements-dev.txt @@ -2,3 +2,4 @@ isort==5.13.2 black==24.4.2 debugpy==1.8.2 epdb +pytest==8.2.2 diff --git a/api/swotvis/app/routers/data/router.py b/api/swotvis/app/routers/data/router.py index 01b43731..d4e25a7e 100644 --- a/api/swotvis/app/routers/data/router.py +++ b/api/swotvis/app/routers/data/router.py @@ -36,9 +36,17 @@ async def compute_node_series(data: List[List[SwotNodeDataModel]]): # from HydroCron. series = SwotNodeDataSeriesModel(all_series=data) + df = series.as_dataframe() + + # With no observations to summarize, the dataframe has no node-variable + # columns and the selection below raises a KeyError -> HTTP 500. Return + # empty series in that case instead. + if df.empty: + return {"median": [], "q0.25": [], "q0.75": []} + # group all data by p_dist_out and remove all columns except # those corresponding to node variables - grouped = series.as_dataframe()[NodeVariables.list()].groupby("p_dist_out") + grouped = df[NodeVariables.list()].groupby("p_dist_out") # Compute node-level statistics. Replace np.nan with None because np.nan # is not JSON serializable. Convert all pandas dataframes to dictionaries diff --git a/api/swotvis/tests/test_compute_node_series.py b/api/swotvis/tests/test_compute_node_series.py new file mode 100644 index 00000000..62afbf59 --- /dev/null +++ b/api/swotvis/tests/test_compute_node_series.py @@ -0,0 +1,80 @@ +"""Tests for the /data/compute_node_series statistics endpoint. + +Run with the repo's `make test` target (docker compose exec api pytest tests), +or directly from the api/swotvis directory with `PYTHONPATH=. pytest tests`. +""" + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from app.routers.data import router as data_router + + +def _client(): + app = FastAPI() + app.include_router(data_router, prefix="/data") + return TestClient(app, raise_server_exceptions=False) + + +def _observation(p_dist_out, wse, width, area_total, time_str): + return { + "time_str": time_str, + "node_q": 1, + "p_dist_out": p_dist_out, + "wse": wse, + "width": width, + "area_total": area_total, + "wse_units": "m", + "width_units": "m", + "area_total_units": "m^2", + "p_dist_out_units": "m", + "datetime": time_str, + } + + +def test_compute_node_series_summarizes_observations_by_distance(): + client = _client() + # Two observations at p_dist_out=100 (wse 10 and 20) and one at 200. + data = [ + [ + _observation(100.0, 10.0, 50.0, 500.0, "2024-01-01T00:00:00Z"), + _observation(100.0, 20.0, 60.0, 600.0, "2024-01-08T00:00:00Z"), + _observation(200.0, 30.0, 70.0, 700.0, "2024-01-01T00:00:00Z"), + ] + ] + + response = client.post("/data/compute_node_series", json=data) + + assert response.status_code == 200 + body = response.json() + assert set(body.keys()) == {"median", "q0.25", "q0.75"} + + median = {row["p_dist_out"]: row for row in body["median"]} + assert median[100.0]["wse"] == 15.0 + assert median[100.0]["width"] == 55.0 + assert median[200.0]["wse"] == 30.0 + + q25 = {row["p_dist_out"]: row for row in body["q0.25"]} + q75 = {row["p_dist_out"]: row for row in body["q0.75"]} + assert q25[100.0]["wse"] == 12.5 + assert q75[100.0]["wse"] == 17.5 + + +def test_compute_node_series_empty_body_returns_empty_series(): + # Regression: an empty request body used to raise a KeyError -> HTTP 500. + client = _client() + + response = client.post("/data/compute_node_series", json=[]) + + assert response.status_code == 200 + assert response.json() == {"median": [], "q0.25": [], "q0.75": []} + + +def test_compute_node_series_series_without_observations_returns_empty_series(): + # Regression: a series carrying no observations used to raise a 500 too. + client = _client() + + response = client.post("/data/compute_node_series", json=[[]]) + + assert response.status_code == 200 + assert response.json() == {"median": [], "q0.25": [], "q0.75": []}