"""Fetch real French car models with their official fuel consumption. Source: ADEME "Car Labelling" open dataset (combined-cycle WLTP consumption in L/100 km for every model commercialised in France). We map each model's energy type to the matching pump fuel and dedupe to one entry per commercial model. """ from __future__ import annotations import json import urllib.parse import urllib.request ADEME_URL = "https://data.ademe.fr/data-fair/api/v1/datasets/ademe-car-labelling/lines" SELECT = "Marque,Description_Commerciale,Energie,Conso_vitesse_mixte_Min,Conso_vitesse_mixte_Max" # ADEME "Energie" -> our pump-fuel label (None = no liquid fuel, e.g. pure electric). ENERGIE_FUEL = { "ESSENCE": "E10", "GAZOLE": "Gazole", "GAZ+ELEC HNR": "Gazole", # diesel hybrid "ESS+ELEC HNR": "E10", # petrol hybrid "ELEC+ESSENC HR": "E10", # plug-in petrol hybrid "ELEC+GAZOLE HR": "Gazole", # plug-in diesel hybrid "SUPERETHANOL": "E85", "ESS+G.P.L.": "GPLc", "ELECTRIC": None, } # Suffix appended to the model name so hybrids are explicit in the picker. HYBRID_TAG = { "ESS+ELEC HNR": " — hybride", "GAZ+ELEC HNR": " — hybride", "ELEC+ESSENC HR": " — hybride rechargeable", "ELEC+GAZOLE HR": " — hybride rechargeable", } def _fetch_all_rows(timeout: int, page_size: int = 10000) -> list[dict]: """Page through every line of the dataset via data-fair's `after` cursor. The API caps a single response, so we follow the server-provided `next` link (which carries the `after=`) until the whole dataset is collected. """ url = ADEME_URL + f"?size={page_size}&select=" + urllib.parse.quote(SELECT) rows: list[dict] = [] for _ in range(200): # hard safety cap on the number of pages req = urllib.request.Request(url, headers={"User-Agent": "petrol-map/1.0"}) with urllib.request.urlopen(req, timeout=timeout) as resp: data = json.load(resp) page = data.get("results", []) rows.extend(page) total = data.get("total") nxt = data.get("next") if not page or not nxt or (isinstance(total, int) and len(rows) >= total): break url = nxt return rows def fetch_cars(timeout: int = 90) -> list[list]: """Return [[label, fuel, consumption_L_per_100km], ...], sorted by label.""" rows = _fetch_all_rows(timeout) agg: dict[tuple, list[float]] = {} # (label, fuel) -> [sum_cons, n] for r in rows: energie = (r.get("Energie") or "").strip() fuel = ENERGIE_FUEL.get(energie) if not fuel: continue marque = (r.get("Marque") or "").strip() desc = (r.get("Description_Commerciale") or "").strip() if not marque or not desc: continue vals = [v for v in (r.get("Conso_vitesse_mixte_Min"), r.get("Conso_vitesse_mixte_Max")) if isinstance(v, (int, float)) and v > 0] if not vals: continue cons = sum(vals) / len(vals) label = f"{marque} {desc}{HYBRID_TAG.get(energie, '')}" key = (label, fuel) if key in agg: agg[key][0] += cons agg[key][1] += 1 else: agg[key] = [cons, 1] cars = [[label, fuel, round(s / n, 1)] for (label, fuel), (s, n) in agg.items()] cars.sort(key=lambda c: c[0]) return cars if __name__ == "__main__": cars = fetch_cars() blob = json.dumps(cars, ensure_ascii=False, separators=(",", ":")) print(f"{len(cars)} models, embedded size ~{len(blob) / 1024:.0f} KB") for c in cars[:5]: print(" ", c)