spb/auto-ka Public
Python 81.9%
TypeScript 12.4%
CSS 5.5%
1# -----------------------------------------------------------------------------2# Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/dupontford.py : connecteur Dupont & Dupont Ford5# (dupontdupontford.com), concessionnaire Ford de Gatineau.6#7# Plateforme : WordPress + thème Leadbox « lbx-egypt » (SPA Vue pour8# l'inventaire). Le SPA charge un export JSON statique complet :9# /wp-content/uploads/data/inventory.json -> {"vehicles": [...]} où chaque10# véhicule est COMPLET : condition (New/Used), année, marque, modèle,11# version, prix, km, VIN, stock, transmission, rouage, carburant,12# carrosserie, moteur, couleur, photo, description. UNE seule requête pour13# tout l'inventaire — on filtre condition == Used (voitures seulement).14#15# URL détail reconstruite comme dans le sitemap inventory.xml :16# /vue/used-<année>-<marque>-<modèle>-<id>/.17# -----------------------------------------------------------------------------18from __future__ import annotations1920import html as htmllib21import re2223from ..schema import Vehicle, strip_accents24from .base import BaseConnector2526_EXCLUDE_RE = re.compile(27 r"moto(?:cyclette|neige|marine)?\b|\bvtt\b|scooter|spyder|ryker|"28 r"can-?am|ski-?doo|sea-?doo|roulotte|remorque|\bvr\b|campeur|trailer",29 re.I)303132def _slug(text: str) -> str:33 t = strip_accents(str(text or "")).lower()34 t = re.sub(r"[^a-z0-9]+", "-", t)35 return t.strip("-")363738def _clean(text) -> str:39 if not text:40 return ""41 out = htmllib.unescape(str(text))42 out = re.sub(r"<[^>]+>", " ", out)43 return re.sub(r"\s{2,}", " ", out).strip()444546def _num(value, cast=float):47 try:48 v = cast(value)49 except (TypeError, ValueError):50 return None51 return v if v else None525354class DupontDupontFord(BaseConnector):55 """Dupont & Dupont Ford — export JSON Leadbox (inventory.json)."""5657 source_id = "dupontdupontford"58 base_url = "https://dupontdupontford.com"59 dealer_name = "Dupont & Dupont Ford"60 city = "Gatineau"61 request_delay = 1.062 use_detail_cache = False # une seule requête, pas de pages détail6364 def fetch(self) -> list[Vehicle]:65 data = self.get(66 f"{self.base_url}/wp-content/uploads/data/inventory.json").json()67 vehicles: list[Vehicle] = []68 for d in data.get("vehicles") or []:69 veh = self._to_vehicle(d)70 if veh is not None:71 vehicles.append(veh)72 return vehicles7374 def _to_vehicle(self, d: dict) -> Vehicle | None:75 if str(d.get("condition") or "").lower() != "used":76 return None # occasion seulement77 if d.get("nonvehicle") or d.get("onweb") is False:78 return None79 if d.get("salepending") or d.get("sold"):80 return None81 if str(d.get("kind") or "car").lower() != "car":82 return None8384 ext_id = str(d.get("id") or "")85 make = _clean(d.get("make"))86 model = _clean(d.get("model"))87 if not ext_id or not (make or model):88 return None89 body = _clean(d.get("bodystyle") or d.get("type"))90 if _EXCLUDE_RE.search(f"{make} {model} {body}"):91 return None9293 year = _num(d.get("year"), int)94 trim = _clean(d.get("trim"))95 title = " ".join(str(x) for x in (make, model, trim, year or "") if x)9697 url = (f"{self.base_url}/vue/used-{year or ''}-"98 f"{_slug(make)}-{_slug(model)}-{ext_id}/")99100 price = _num(d.get("specialprice")) or _num(d.get("price")) \101 or _num(d.get("saleprice"))102 km = _num(d.get("mileage"))103104 images = []105 if d.get("picture"):106 images = [str(d["picture"])]107 media = d.get("media")108 if isinstance(media, list):109 for u in media:110 if isinstance(u, str) and u not in images:111 images.append(u)112 elif isinstance(u, dict):113 link = u.get("url") or u.get("src")114 if link and link not in images:115 images.append(str(link))116117 details: dict = {}118 for key, out in (("certified", "certified"), ("demo", "demo"),119 ("asis", "as_is")):120 if d.get(key):121 details[out] = True122 if d.get("carprooflink"):123 carfax = str(d["carprooflink"])124 else:125 carfax = ""126 if d.get("rebatesamount"):127 details["rebates"] = d["rebatesamount"]128129 return Vehicle(130 source=self.source_id,131 external_id=ext_id,132 url=url,133 title=title,134 make=make,135 model=model,136 trim=trim,137 year=year,138 price=price,139 price_label=f"{price:,.0f} $".replace(",", " ") if price else "",140 mileage_km=km,141 mileage_label=f"{km:,.0f} km".replace(",", " ") if km else "",142 transmission=_clean(d.get("transmission")),143 fuel=_clean(d.get("fueltype")),144 drivetrain=_clean(d.get("drivetrain")),145 body_type=body,146 exterior_color=_clean(d.get("exteriorcolor")),147 engine=_clean(d.get("engine")),148 seats=_num(d.get("seats"), int),149 vin=_clean(d.get("vin")),150 stock_number=_clean(d.get("stocknumber")),151 dealer_name=self.dealer_name,152 city=self.city,153 description=_clean(d.get("description"))[:4000],154 details=details,155 images=images[:20],156 carfax_url=carfax,157 )158