prix-carburant / fuel_data.py
SamDNX's picture
Add favourites, price-freshness + 24/7 filters, services in popups, sortable list
e6a157d
Raw
History Blame Contribute Delete
4.11 kB
"""Fetch and normalise French fuel-price data from the open-data portal.
Data source: https://public.opendatasoft.com/explore/dataset/prix_des_carburants_j_7
The dataset holds the prices reported by every petrol station in France over the
last 7 days, refreshed continuously upstream.
"""
from __future__ import annotations
import json
import os
from datetime import datetime, timezone
import requests
DATASET = "prix_des_carburants_j_7"
EXPORT_URL = (
"https://public.opendatasoft.com/api/explore/v2.1/"
f"catalog/datasets/{DATASET}/exports/json"
)
# Human label -> price field in the dataset (ordered: most common fuel first).
FUELS: dict[str, str] = {
"Gazole": "price_gazole",
"SP95": "price_sp95",
"SP98": "price_sp98",
"E10": "price_e10",
"E85": "price_e85",
"GPLc": "price_gplc",
}
# Plausible price band (€/L). Real road fuels in France sit well inside this;
# anything outside is a data-entry error and is dropped.
PRICE_MIN, PRICE_MAX = 0.4, 4.0
def fetch_raw(timeout: int = 180) -> list[dict]:
"""Download every record from the dataset's bulk export endpoint."""
resp = requests.get(EXPORT_URL, params={"timezone": "Europe/Paris"}, timeout=timeout)
resp.raise_for_status()
return resp.json()
def normalise(raw: list[dict]) -> list[dict]:
"""Keep geolocated stations that report at least one price; tidy the fields."""
stations = []
for r in raw:
geo = r.get("geo_point") or {}
lat, lon = geo.get("lat"), geo.get("lon")
if lat is None or lon is None:
continue
# Some stations report nonsense prices (e.g. 0.002 €/L); keep only
# plausible values so they don't skew the cheapest/cost figures.
prices = {
label: (v if isinstance(v, (int, float)) and PRICE_MIN <= v <= PRICE_MAX else None)
for label, field in FUELS.items()
for v in (r.get(field),)
}
if not any(v is not None for v in prices.values()):
continue
stations.append(
{
"id": r.get("id"),
"name": (r.get("name") or "").strip() or "Station",
"brand": (r.get("brand") or "").strip(),
"address": (r.get("address") or "").strip(),
"city": (r.get("city") or "").strip(),
"cp": (r.get("cp") or "").strip(),
"lat": lat,
"lon": lon,
"prices": prices,
"update": r.get("update"),
"highway": r.get("pop") == "A", # A = autoroute, R = route
"open24": is_24h(r.get("automate_24_24")),
"services": clean_services(r.get("services")),
}
)
return stations
def is_24h(value) -> bool:
"""Whether the station has 24/7 automated payment (field is bool or Oui/Non)."""
if value is True:
return True
return isinstance(value, str) and value.strip().lower() in ("oui", "yes", "true", "1")
def clean_services(value) -> list[str]:
"""Normalise the `services` field (list, or {'service': [...]}) to a string list."""
if isinstance(value, dict):
value = value.get("service")
if not isinstance(value, list):
return []
return [str(s).strip() for s in value if str(s).strip()]
def fetch_stations(cache_path: str | None = None) -> list[dict]:
"""Fetch + normalise the live dataset, optionally caching the result to disk."""
stations = normalise(fetch_raw())
if cache_path:
os.makedirs(os.path.dirname(cache_path) or ".", exist_ok=True)
payload = {
"fetched_at": datetime.now(timezone.utc).isoformat(),
"count": len(stations),
"stations": stations,
}
with open(cache_path, "w", encoding="utf-8") as fh:
json.dump(payload, fh, ensure_ascii=False)
return stations
if __name__ == "__main__":
data = fetch_stations()
print(f"Fetched {len(data)} geolocated stations.")
sample = data[0]
print("Example:", json.dumps(sample, ensure_ascii=False, indent=2))