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:
+31
-1
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user