Fix optional-discharge crashes and dedupe measurement mapping

- MeasurementResponse.discharge is now Optional[float]; measurements with a
  null discharge no longer raise a Pydantic ValidationError (HTTP 500) on the
  /measurements/latest and /measurements/station endpoints.
- InfluxDB save_measurements guards float(discharge) against None instead of
  crashing with TypeError.
- Extract the duplicated measurement->response mapping into a single
  _to_measurement_response helper used by both measurement endpoints.
This commit is contained in:
2026-07-22 12:34:46 +07:00
parent f4c63cabef
commit 6e78225d00
2 changed files with 25 additions and 35 deletions
+4 -2
View File
@@ -111,9 +111,11 @@ class InfluxDBAdapter(DatabaseAdapter):
"time": measurement["timestamp"].isoformat(),
"fields": {
"water_level": float(measurement["water_level"]),
"discharge": float(measurement["discharge"]),
"discharge": float(measurement["discharge"])
if measurement.get("discharge") is not None
else None,
"discharge_percent": float(measurement["discharge_percent"])
if measurement["discharge_percent"]
if measurement.get("discharge_percent")
else None,
},
}
+21 -33
View File
@@ -59,7 +59,7 @@ class MeasurementResponse(BaseModel):
station_name_en: str
station_name_th: str
water_level: float
discharge: float
discharge: Optional[float] = None
discharge_percent: Optional[float] = None
status: str = "active"
@@ -478,6 +478,24 @@ async def get_station(station_id: int):
)
def _to_measurement_response(measurement: Dict[str, Any]) -> MeasurementResponse:
"""Map a raw measurement dict from a DB adapter to the API response model.
``discharge`` is optional in the data (some stations report only level), so
it is read with ``.get`` rather than assumed present.
"""
return MeasurementResponse(
timestamp=measurement["timestamp"],
station_code=measurement["station_code"],
station_name_en=measurement["station_name_en"],
station_name_th=measurement["station_name_th"],
water_level=measurement["water_level"],
discharge=measurement.get("discharge"),
discharge_percent=measurement.get("discharge_percent"),
status=measurement.get("status", "active"),
)
@app.get("/measurements/latest", response_model=List[MeasurementResponse])
async def get_latest_measurements(limit: int = 100):
"""Get latest measurements from all stations"""
@@ -490,22 +508,7 @@ async def get_latest_measurements(limit: int = 100):
try:
measurements = scraper.get_latest_data(limit=limit)
response = []
for measurement in measurements:
response.append(
MeasurementResponse(
timestamp=measurement["timestamp"],
station_code=measurement["station_code"],
station_name_en=measurement["station_name_en"],
station_name_th=measurement["station_name_th"],
water_level=measurement["water_level"],
discharge=measurement["discharge"],
discharge_percent=measurement.get("discharge_percent"),
status=measurement.get("status", "active"),
)
)
return response
return [_to_measurement_response(m) for m in measurements]
except Exception as e:
logger.error(f"Error fetching latest measurements: {e}")
@@ -533,22 +536,7 @@ async def get_station_measurements(station_code: str, hours: int = 24, limit: in
# Limit results
measurements = measurements[:limit]
response = []
for measurement in measurements:
response.append(
MeasurementResponse(
timestamp=measurement["timestamp"],
station_code=measurement["station_code"],
station_name_en=measurement["station_name_en"],
station_name_th=measurement["station_name_th"],
water_level=measurement["water_level"],
discharge=measurement["discharge"],
discharge_percent=measurement.get("discharge_percent"),
status=measurement.get("status", "active"),
)
)
return response
return [_to_measurement_response(m) for m in measurements]
except Exception as e:
logger.error(f"Error fetching station measurements: {e}")