SPB Git

spb/auto-ka Public

Python 81.9% TypeScript 12.4% CSS 5.5%
5.6 KB · 158 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# 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