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# recalls.py : rappels de sécurité — Base de données des rappels de véhicules5# de Transports Canada (données ouvertes, gratuit)6#7# Source : dump mensuel complet CSV du jeu de données « Vehicle Recalls8# Database » (ouvert.canada.ca, jeu 1ec92326-47ef-4110-b7ca-959fab03f96d) :9# https://opendatatc.tc.canada.ca/vrdb_full_monthly.csv (~200 Mo)10# 1 ligne = 1 rappel × marque × modèle × année-modèle. L'API REST officielle11# (tc.api.canada.ca) exige une clé délivrée manuellement — le CSV ouvert est12# la voie sans clé, rafraîchi mensuellement par TC.13#14# Table `recalls` (make/model/year normalisés en MAJUSCULES sans accents) +15# croisement par (make, model, year) avec les annonces : le modèle TC est16# souvent plus court que le nôtre (« CIVIC » vs « Civic Sport Touring ») —17# on matche si l'un préfixe l'autre au premier mot près, année exacte.18#19# Peuplement one-shot / mensuel : python3 -m autoka.recalls [--limit-years N]20# -----------------------------------------------------------------------------21from __future__ import annotations2223import csv24import io25import sqlite326import sys27import time28import unicodedata2930import requests3132CSV_URL = "https://opendatatc.tc.canada.ca/vrdb_full_monthly.csv"3334_SCHEMA = """35CREATE TABLE IF NOT EXISTS recalls (36 recall_number TEXT NOT NULL,37 make TEXT NOT NULL, -- MAJUSCULES sans accents (norme TC)38 model TEXT NOT NULL, -- MAJUSCULES sans accents (norme TC)39 year INTEGER, -- année-modèle visée40 date TEXT, -- date du rappel (YYYY-MM-DD)41 component TEXT, -- système visé (FR, repli EN)42 description TEXT, -- description (FR, repli EN)43 category TEXT, -- Car / Truck / Motorcycle... (norme TC)44 units_affected INTEGER,45 PRIMARY KEY (recall_number, make, model, year)46);47CREATE INDEX IF NOT EXISTS idx_recalls_mmy ON recalls(make, model, year);48"""4950# alias marques Auto-Ka -> marque TC quand l'orthographe diffère51_MAKE_ALIASES = {52 "MERCEDES-BENZ": ("MERCEDES-BENZ", "MERCEDES"),53 "LAND ROVER": ("LAND ROVER", "LANDROVER"),54 "VOLKSWAGEN": ("VOLKSWAGEN", "VW"),55 "CHEVROLET": ("CHEVROLET", "CHEV"),56 "MINI": ("MINI", "BMW MINI"),57}585960def _norm(text: str | None) -> str:61 """MAJUSCULES sans accents, espaces réduits — clé de croisement."""62 if not text:63 return ""64 t = unicodedata.normalize("NFKD", text)65 t = "".join(c for c in t if not unicodedata.combining(c))66 return " ".join(t.upper().split())676869def ensure_schema(con: sqlite3.Connection) -> None:70 con.executescript(_SCHEMA)71 con.commit()727374# ---------------------------------------------------------------------------75# Peuplement depuis le dump CSV mensuel de Transports Canada76# ---------------------------------------------------------------------------7778def _fr_or_en(fr: str, en: str) -> str:79 fr = (fr or "").strip()80 if fr and fr.lower() not in ("translation not available", "non saisie"):81 return fr82 return (en or "").strip()838485def populate(con: sqlite3.Connection, csv_path: str | None = None,86 min_year: int = 1980) -> dict:87 """(Re)charge la table depuis le dump complet (téléchargé si besoin).8889 `min_year` écarte les années-modèles antérieures au parc plausible90 d'Auto-Ka (schema.py borne les annonces à 1980+).91 """92 ensure_schema(con)93 t0 = time.time()94 if csv_path:95 fh = open(csv_path, encoding="utf-8", errors="replace", newline="")96 else:97 print(f"[auto-ka] rappels TC : téléchargement {CSV_URL} ...")98 resp = requests.get(CSV_URL, timeout=600)99 resp.raise_for_status()100 fh = io.StringIO(resp.content.decode("utf-8", errors="replace"))101102 con.execute("DELETE FROM recalls")103 n = kept = 0104 with fh:105 for row in csv.DictReader(fh):106 n += 1107 try:108 year = int(float(row.get("YEAR") or 0)) or None109 except ValueError:110 year = None111 if year is not None and year < min_year:112 continue113 make = _norm(row.get("MAKE_NAME_NM"))114 model = _norm(row.get("MODEL_NAME_NM"))115 if not make or not row.get("RECALL_NUMBER_NUM"):116 continue117 try:118 units = int(float((row.get("UNIT_AFFECTED_NBR") or "0")119 .replace(",", "")))120 except ValueError:121 units = 0122 con.execute(123 """INSERT OR REPLACE INTO recalls (recall_number, make, model,124 year, date, component, description, category, units_affected)125 VALUES (?,?,?,?,?,?,?,?,?)""",126 (row["RECALL_NUMBER_NUM"], make, model, year,127 (row.get("RECALL_DATE_DTE") or "")[:10],128 _fr_or_en(row.get("SYSTEM_TYPE_FTXT"), row.get("SYSTEM_TYPE_ETXT")),129 _fr_or_en(row.get("COMMENT_FTXT"), row.get("COMMENT_ETXT")),130 row.get("CATEGORY_ETXT") or "",131 units))132 kept += 1133 con.commit()134 total = con.execute("SELECT COUNT(*) c FROM recalls").fetchone()["c"]135 out = {"csv_rows": n, "kept": kept, "recalls_rows": total,136 "seconds": round(time.time() - t0, 1)}137 print(f"[auto-ka] rappels TC : {out}")138 return out139140141# ---------------------------------------------------------------------------142# Croisement avec une annonce (make/model/year normalisés)143# ---------------------------------------------------------------------------144145def for_vehicle(con: sqlite3.Connection, make: str, model: str,146 year: int | None) -> list[dict]:147 """Rappels TC visant ce (marque, modèle, année) — année exacte requise.148149 Le modèle TC est générique (« CIVIC », « F-150 ») alors que le nôtre porte150 parfois la version ; on matche par préfixe dans les deux sens, ancré sur151 le premier mot pour éviter les faux positifs.152 """153 if not make or not model or year is None:154 return []155 ensure_schema(con)156 n_make, n_model = _norm(make), _norm(model)157 makes = _MAKE_ALIASES.get(n_make, (n_make,))158 first_word = n_model.split()[0]159160 q = f"""SELECT recall_number, make, model, year, date, component,161 description, category, units_affected162 FROM recalls163 WHERE make IN ({','.join('?' * len(makes))}) AND year=?164 AND (model=? OR model LIKE ? OR ? LIKE model || ' %'165 OR (length(model) >= 3 AND ? LIKE model || '%'))166 ORDER BY date DESC"""167 rows = con.execute(q, (*makes, year, n_model, f"{first_word} %",168 n_model, first_word)).fetchall()169 seen: set[str] = set()170 out = []171 for r in rows:172 if r["recall_number"] in seen:173 continue174 seen.add(r["recall_number"])175 out.append(dict(r))176 return out177178179if __name__ == "__main__":180 from . import db181 con = db.connect()182 path = sys.argv[1] if len(sys.argv) > 1 else None183 populate(con, csv_path=path)184 con.close()185