Envelope metadata: source, stations_count, data_updated in stations + health

- /api/v1/stations now returns source (api|csv), stations_count, and
  data_updated = freshest price_last_updated across the dataset, i.e. the
  GOV.UK server's own update time (not the relay's sync time).
- /health gains data_updated too.
- Tracked during _fetch_api (max price_last_updated) and _fetch_csv (max
  price_change_effective_timestamp).
This commit is contained in:
FuelBoard Contributor
2026-08-12 16:23:47 +01:00
parent bd3d3862ec
commit 525ef38533
+31 -1
View File
@@ -95,6 +95,7 @@ _state = {
"sync_attempts": 0,
"sync_failures": 0,
"source_used": None,
"data_updated": None, # freshest govUK price_last_updated across dataset
}
# ---------------------------------------------------------------------------
@@ -299,12 +300,24 @@ def _parse_csv_to_stations(text: str) -> list:
def _fetch_csv() -> list:
"""Download the hourly full-UK CSV mirror."""
import csv
import io
with httpx.Client(timeout=120) as client:
resp = client.get(CSV_URL)
resp.raise_for_status()
stations = _parse_csv_to_stations(resp.text)
if not stations:
raise RuntimeError("CSV parse returned zero stations")
# Freshest price-change timestamp in the CSV dump (govUK-side update
# time for the mirror's source data).
latest = None
for row in csv.DictReader(io.StringIO(resp.text)):
for key, val in row.items():
if key.startswith("forecourts.price_change_effective_timestamp."):
if val and (latest is None or val > latest):
latest = val
_state["data_updated"] = latest
return stations
@@ -383,6 +396,15 @@ def _fetch_api() -> list:
stations = _normalise_api_payload(stations_info, fuel_prices)
if not stations:
raise RuntimeError("API fetch returned zero stations")
# Freshest price timestamp the GOV.UK server itself reports — this is
# the dataset's own update time, independent of when we synced.
latest = None
for rec in fuel_prices:
for entry in rec.get("fuel_prices") or []:
ts = entry.get("price_last_updated")
if ts and (latest is None or ts > latest):
latest = ts
_state["data_updated"] = latest
return stations
@@ -504,6 +526,7 @@ def health():
"source": _state["source_used"],
"stations_cached": _state["stations"] is not None,
"stations_count": len(_stations_list()),
"data_updated": _state["data_updated"],
"last_sync": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(_state["stations_fetched_at"])) if _state["stations_fetched_at"] else None,
"sync_attempts": _state["sync_attempts"],
"sync_failures": _state["sync_failures"],
@@ -565,7 +588,14 @@ def stations(
else:
rows.sort(key=lambda r: r["price"] if r["price"] is not None else float("inf"))
return {"fuel": fuel, "count": len(rows[:limit]), "stations": rows[:limit]}
return {
"fuel": fuel,
"count": len(rows[:limit]),
"stations_count": len(rows),
"source": _state["source_used"],
"data_updated": _state["data_updated"],
"stations": rows[:limit],
}
if __name__ == "__main__":