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# 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