SPB Git forge

spb/auto-ka

Public
61commits 1branches 0releases
14.4 MBsize
maindefault branch
12 days agolast push
Python 61.6% TypeScript 20.9% CSS 11.4% JavaScript 5.1% HTML 1.1%
5.5 KB · 141 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# vindecode.py : fiche technique constructeur via le VIN — API vPIC de la NHTSA5#                (gratuite, sans clé), décodage paresseux + cache SQLite.6# -----------------------------------------------------------------------------7from __future__ import annotations89import json10import sqlite311import time1213import requests1415VPIC_URL = "https://vpic.nhtsa.dot.gov/api/vehicles/DecodeVINValuesBatch/"16TIMEOUT = 121718# Champs vPIC retenus → libellé français affiché sur la fiche.19# L'ordre du dict = l'ordre d'affichage.20_FIELDS: dict[str, str] = {21    "Trim": "Version (usine)",22    "Series": "Série",23    "BodyClass": "Carrosserie (usine)",24    "DriveType": "Motricité (usine)",25    "EngineCylinders": "Cylindres",26    "DisplacementL": "Cylindrée (L)",27    "EngineHP": "Puissance (ch)",28    "EngineConfiguration": "Configuration moteur",29    "FuelTypePrimary": "Carburant (usine)",30    "FuelTypeSecondary": "Carburant secondaire",31    "ElectrificationLevel": "Électrification",32    "BatteryKWh": "Batterie (kWh)",33    "TransmissionStyle": "Transmission (usine)",34    "TransmissionSpeeds": "Rapports",35    "Turbo": "Turbo",36    "Doors": "Portes (usine)",37    "Seats": "Places (usine)",38    "GVWR": "PNBV",39    "PlantCountry": "Pays d'assemblage",40    "PlantCity": "Ville d'assemblage",41    "ABS": "Freins ABS",42    "AirBagLocFront": "Coussins avant",43    "AirBagLocSide": "Coussins latéraux",44    "AirBagLocCurtain": "Coussins rideaux",45    "TPMS": "Surveillance pneus (TPMS)",46    "AdaptiveCruiseControl": "Régulateur adaptatif",47    "ForwardCollisionWarning": "Alerte de collision avant",48    "LaneDepartureWarning": "Alerte de sortie de voie",49    "BlindSpotMon": "Surveillance angles morts",50    "RearVisibilitySystem": "Caméra de recul",51}5253# Traductions des valeurs vPIC les plus fréquentes (anglais → français).54_VALUES_FR: dict[str, str] = {55    "Gasoline": "Essence", "Diesel": "Diesel", "Electric": "Électrique",56    "Sedan/Saloon": "Berline", "Hatchback/Liftback/Notchback": "Hayon",57    "Sport Utility Vehicle (SUV)/Multi-Purpose Vehicle (MPV)": "VUS",58    "Sport Utility Vehicle [SUV]/Multipurpose Vehicle [MPV]": "VUS",59    "Crossover Utility Vehicle (CUV)": "VUS multisegment",60    "Pickup": "Camionnette", "Minivan": "Minifourgonnette",61    "Coupe": "Coupé", "Convertible/Cabriolet": "Cabriolet", "Wagon": "Familiale",62    "4WD/4-Wheel Drive/4x4": "4x4", "AWD/All-Wheel Drive": "Intégrale (AWD)",63    "FWD/Front-Wheel Drive": "Traction (FWD)",64    "RWD/Rear-Wheel Drive": "Propulsion (RWD)",65    "Automatic": "Automatique", "Manual/Standard": "Manuelle",66    "Continuously Variable Transmission (CVT)": "CVT",67    "Automated Manual Transmission (AMT)": "Manuelle automatisée",68    "Dual-Clutch Transmission (DCT)": "Double embrayage (DCT)",69    "Strong HEV (Hybrid Electric Vehicle)": "Hybride",70    "Mild HEV (Hybrid Electric Vehicle)": "Hybride légère",71    "PHEV (Plug-in Hybrid Electric Vehicle)": "Hybride rechargeable",72    "BEV (Battery Electric Vehicle)": "100 % électrique",73    "Standard": "De série", "Optional": "En option", "Yes": "Oui", "No": "Non",74    "Direct": "Directe (TPMS direct)", "Indirect": "Indirecte",75    "1st Row (Driver and Passenger)": "1re rangée (conducteur et passager)",76    "1st and 2nd Rows": "1re et 2e rangées",77    "1st, 2nd and 3rd Rows": "1re, 2e et 3e rangées",78    "All Rows": "Toutes les rangées",79    "UNITED STATES (USA)": "États-Unis", "CANADA": "Canada", "MEXICO": "Mexique",80    "JAPAN": "Japon", "GERMANY": "Allemagne", "SOUTH KOREA": "Corée du Sud",81    "V-Shaped": "En V", "In-Line": "En ligne", "Horizontally opposed (boxer)": "Boxer",82}8384_SCHEMA = """85CREATE TABLE IF NOT EXISTS vin_decode (86    vin        TEXT PRIMARY KEY,87    data       TEXT,      -- JSON {libellé fr: valeur} (vide = décodage sans résultat)88    fetched_at REAL89);90"""919293def _ensure(con: sqlite3.Connection) -> None:94    con.executescript(_SCHEMA)959697def _fr(value: str) -> str:98    return _VALUES_FR.get(value, value)99100101def _extract(raw: dict) -> dict[str, str]:102    out: dict[str, str] = {}103    for key, label in _FIELDS.items():104        val = (raw.get(key) or "").strip()105        if not val or val.lower() in ("not applicable", "n/a", "0"):106            continue107        out[label] = _fr(val)108    return out109110111def decode(con: sqlite3.Connection, vin: str) -> dict[str, str] | None:112    """Fiche constructeur d'un VIN (cache DB, appel réseau au premier accès).113114    Retourne un dict {libellé fr: valeur} ou None si indisponible.115    """116    vin = (vin or "").strip().upper()117    if len(vin) != 17:118        return None119    _ensure(con)120    row = con.execute("SELECT data FROM vin_decode WHERE vin=?", (vin,)).fetchone()121    if row is not None:122        try:123            data = json.loads(row["data"] or "{}")124        except ValueError:125            data = {}126        return data or None127    try:128        resp = requests.post(VPIC_URL, data={"format": "json", "data": vin},129                             timeout=TIMEOUT)130        resp.raise_for_status()131        results = resp.json().get("Results") or []132        data = _extract(results[0]) if results else {}133    except Exception:134        # échec réseau : ne pas mettre en cache, on retentera au prochain accès135        return None136    con.execute(137        "INSERT OR REPLACE INTO vin_decode (vin, data, fetched_at) VALUES (?,?,?)",138        (vin, json.dumps(data, ensure_ascii=False), time.time()))139    con.commit()140    return data or None141