Python 61.6%
TypeScript 20.9%
CSS 11.4%
JavaScript 5.1%
HTML 1.1%
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