SPB Git forge

spb/auto-ka

Public
61commits 1branches 0releases
14.4 MBsize
maindefault branch
13 days agolast push
Python 61.6% TypeScript 20.9% CSS 11.4% JavaScript 5.1% HTML 1.1%
6.5 KB · 178 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/classeauto.py : connecteur Classe Auto (classeauto.ca),5#   marchand d'occasion de l'île de Montréal (boul. Industriel,6#   Montréal-Nord/Anjou) — ~140 véhicules.7#8#   Plateforme : WordPress (thème Astra) + moteur d'inventaire DriveGood9#   (cdn.drivegood.com). Le site expose un export JSON STATIQUE de tout10#   l'inventaire :11#     /wp-content/themes/astra/car_single_page_data/cars_formatted.json12#   — une seule requête pour la liste complète, avec TOUS les champs :13#   prix, km, VIN, stock, transmission, carburant, motricité, carrosserie,14#   couleurs, portes, passagers, moteur, options, photos (cdn.drivegood.com),15#   description. Aucune page détail nécessaire.16#17#   Filtres de sécurité : vehicle_type_id == 1 (automobile), post_status18#   publish/published, condition != NEW, deleted_at vide.19#20#   Anti-bot (2026-09) : SiteGround sgcaptcha challenge (HTTP 202 + meta21#   refresh /.well-known/sgcaptcha/) sur les requêtes datacenter — fetch via22#   get_resilient() qui escalade vers le proxy résidentiel Oxylabs (-cc-CA).23# -----------------------------------------------------------------------------24from __future__ import annotations2526import re2728from ..schema import Vehicle29from .base import BaseConnector3031_JSON_PATH = "/wp-content/themes/astra/car_single_page_data/cars_formatted.json"3233# garde-fou non-automobile (le marchand ne vend que des autos)34_EXCLUDE_RE = re.compile(35    r"motoneige|vtt|moto\b|motomarine|bateau|roulotte|remorque|spyder", re.I)363738def _s(value) -> str:39    if value is None:40        return ""41    return re.sub(r"\s+", " ", str(value)).strip()424344def _num(value) -> float | None:45    try:46        f = float(value)47    except (TypeError, ValueError):48        return None49    return f if f > 0 else None505152def _int(value) -> int | None:53    try:54        return int(str(value))55    except (TypeError, ValueError):56        return None575859class ClasseAuto(BaseConnector):60    """Classe Auto (Montréal) — inventaire complet en un JSON statique."""6162    source_id = "classeauto"63    base_url = "https://www.classeauto.ca"64    dealer_name = "Classe Auto"65    city = "Montréal"66    request_delay = 1.06768    def fetch(self) -> list[Vehicle]:69        # sgcaptcha SiteGround actif depuis 2026-09 : le direct sert une page70        # challenge HTTP 202 → get_resilient escalade (Oxylabs -cc-CA passe).71        data = self.get_resilient(self.base_url + _JSON_PATH).json()72        vehicles: list[Vehicle] = []73        for d in data if isinstance(data, list) else []:74            try:75                veh = self._to_vehicle(d)76            except Exception:77                continue                     # enregistrement malformé isolé78            if veh is not None:79                vehicles.append(veh)80        return vehicles8182    def _to_vehicle(self, d: dict) -> Vehicle | None:83        if not isinstance(d, dict):84            return None85        if d.get("vehicle_type_id") not in (1, "1", None):86            return None                      # 1 = automobile87        if _s(d.get("post_status")) not in ("publish", "published", ""):88            return None89        if d.get("deleted_at"):90            return None91        if _s(d.get("condition")).upper() == "NEW":92            return None                      # usagé seulement9394        ext_id = _s(d.get("vid")) or _s(d.get("ID"))95        if not ext_id:96            return None9798        make = _s(d.get("maker")).title()99        model = _s(d.get("model"))100        trim = _s(d.get("car_trim")) or _s(d.get("car_sub_model"))101        year = _int(d.get("car_year"))102        body = _s(d.get("car_body"))103        if _EXCLUDE_RE.search(f"{make} {model} {body}"):104            return None105106        title = " ".join(x for x in (str(year or ""), make, model, trim) if x)107108        price = _num(d.get("car_price"))109        km = _num(d.get("car_mileage"))110        unit = _s(d.get("car_mileage_unit")).upper()111        if km and unit.startswith("MI"):112            km = round(km * 1.609344)113114        slug = _s(d.get("slug"))115        url = (f"{self.base_url}/cars/{slug}/" if slug116               else _s(d.get("guid")) or self.base_url)117118        images = [u.strip() for u in _s(d.get("photos")).split(",")119                  if u.strip().startswith("http")][:20]120121        engine = ""122        size = _s(d.get("car_engine_size"))123        cyl = _s(d.get("car_cylinders"))124        if size:125            engine = f"{size}L" + (f" {cyl} cyl." if cyl else "")126127        features = [f.strip().replace("_", " ")128                    for f in _s(d.get("car_options")).split(",") if f.strip()]129130        details: dict = {}131        if d.get("car_no_accident"):132            details["no_accident"] = True133        if d.get("car_one_owner"):134            details["unique_owner"] = True135        old_price = _num(d.get("car_old_price"))136        if old_price and price and old_price > price:137            details["old_price"] = old_price138139        carfax = _s(d.get("carfax_url")) or _s(d.get("carfax_report"))140        if not carfax:                        # parfois rangé dans admin_note141            m = re.search(r"https?://vhr\.carfax\.ca/\S+",142                          _s(d.get("admin_note")))143            if m:144                carfax = m.group(0)145146        return Vehicle(147            source=self.source_id,148            external_id=ext_id,149            url=url,150            title=title,151            make=make,152            model=model,153            trim=trim,154            year=year,155            price=price,156            price_label=f"{price:,.0f} $".replace(",", " ") if price else "",157            mileage_km=km,158            mileage_label=f"{km:,.0f} km".replace(",", " ") if km else "",159            transmission=_s(d.get("car_transmission")),160            fuel=_s(d.get("car_fuel_type")),161            drivetrain=_s(d.get("car_drivetrain")),162            body_type=body,163            exterior_color=_s(d.get("car_exterior_color")).title(),164            interior_color=_s(d.get("car_interrior_color")).title(),165            engine=engine,166            doors=_int(d.get("car_doors_count")),167            seats=_int(d.get("number_of_passengers")),168            vin=_s(d.get("car_vin")).upper(),169            stock_number=_s(d.get("stock")),170            dealer_name=self.dealer_name,171            city=self.city,172            description=_s(d.get("post_content"))[:4000],173            features=features[:60],174            details=details,175            images=images,176            carfax_url=carfax,177        )178