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_attempts": 0,
|
||||||
"sync_failures": 0,
|
"sync_failures": 0,
|
||||||
"source_used": None,
|
"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:
|
def _fetch_csv() -> list:
|
||||||
"""Download the hourly full-UK CSV mirror."""
|
"""Download the hourly full-UK CSV mirror."""
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
with httpx.Client(timeout=120) as client:
|
with httpx.Client(timeout=120) as client:
|
||||||
resp = client.get(CSV_URL)
|
resp = client.get(CSV_URL)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
stations = _parse_csv_to_stations(resp.text)
|
stations = _parse_csv_to_stations(resp.text)
|
||||||
if not stations:
|
if not stations:
|
||||||
raise RuntimeError("CSV parse returned zero 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
|
return stations
|
||||||
|
|
||||||
|
|
||||||
@@ -383,6 +396,15 @@ def _fetch_api() -> list:
|
|||||||
stations = _normalise_api_payload(stations_info, fuel_prices)
|
stations = _normalise_api_payload(stations_info, fuel_prices)
|
||||||
if not stations:
|
if not stations:
|
||||||
raise RuntimeError("API fetch returned zero 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
|
return stations
|
||||||
|
|
||||||
|
|
||||||
@@ -504,6 +526,7 @@ def health():
|
|||||||
"source": _state["source_used"],
|
"source": _state["source_used"],
|
||||||
"stations_cached": _state["stations"] is not None,
|
"stations_cached": _state["stations"] is not None,
|
||||||
"stations_count": len(_stations_list()),
|
"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,
|
"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_attempts": _state["sync_attempts"],
|
||||||
"sync_failures": _state["sync_failures"],
|
"sync_failures": _state["sync_failures"],
|
||||||
@@ -565,7 +588,14 @@ def stations(
|
|||||||
else:
|
else:
|
||||||
rows.sort(key=lambda r: r["price"] if r["price"] is not None else float("inf"))
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user