# ----------------------------------------------------------------------------- # Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/classeauto.py : connecteur Classe Auto (classeauto.ca), # marchand d'occasion de l'île de Montréal (boul. Industriel, # Montréal-Nord/Anjou) — ~140 véhicules. # # Plateforme : WordPress (thème Astra) + moteur d'inventaire DriveGood # (cdn.drivegood.com). Le site expose un export JSON STATIQUE de tout # l'inventaire : # /wp-content/themes/astra/car_single_page_data/cars_formatted.json # — une seule requête pour la liste complète, avec TOUS les champs : # prix, km, VIN, stock, transmission, carburant, motricité, carrosserie, # couleurs, portes, passagers, moteur, options, photos (cdn.drivegood.com), # description. Aucune page détail nécessaire. # # Filtres de sécurité : vehicle_type_id == 1 (automobile), post_status # publish/published, condition != NEW, deleted_at vide. # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Vehicle from .base import BaseConnector _JSON_PATH = "/wp-content/themes/astra/car_single_page_data/cars_formatted.json" # garde-fou non-automobile (le marchand ne vend que des autos) _EXCLUDE_RE = re.compile( r"motoneige|vtt|moto\b|motomarine|bateau|roulotte|remorque|spyder", re.I) def _s(value) -> str: if value is None: return "" return re.sub(r"\s+", " ", str(value)).strip() def _num(value) -> float | None: try: f = float(value) except (TypeError, ValueError): return None return f if f > 0 else None def _int(value) -> int | None: try: return int(str(value)) except (TypeError, ValueError): return None class ClasseAuto(BaseConnector): """Classe Auto (Montréal) — inventaire complet en un JSON statique.""" source_id = "classeauto" base_url = "https://www.classeauto.ca" dealer_name = "Classe Auto" city = "Montréal" request_delay = 1.0 def fetch(self) -> list[Vehicle]: # cache-buster comme le fait le site (nouvelle valeur par minute) data = self.get(self.base_url + _JSON_PATH).json() vehicles: list[Vehicle] = [] for d in data if isinstance(data, list) else []: try: veh = self._to_vehicle(d) except Exception: continue # enregistrement malformé isolé if veh is not None: vehicles.append(veh) return vehicles def _to_vehicle(self, d: dict) -> Vehicle | None: if not isinstance(d, dict): return None if d.get("vehicle_type_id") not in (1, "1", None): return None # 1 = automobile if _s(d.get("post_status")) not in ("publish", "published", ""): return None if d.get("deleted_at"): return None if _s(d.get("condition")).upper() == "NEW": return None # usagé seulement ext_id = _s(d.get("vid")) or _s(d.get("ID")) if not ext_id: return None make = _s(d.get("maker")).title() model = _s(d.get("model")) trim = _s(d.get("car_trim")) or _s(d.get("car_sub_model")) year = _int(d.get("car_year")) body = _s(d.get("car_body")) if _EXCLUDE_RE.search(f"{make} {model} {body}"): return None title = " ".join(x for x in (str(year or ""), make, model, trim) if x) price = _num(d.get("car_price")) km = _num(d.get("car_mileage")) unit = _s(d.get("car_mileage_unit")).upper() if km and unit.startswith("MI"): km = round(km * 1.609344) slug = _s(d.get("slug")) url = (f"{self.base_url}/cars/{slug}/" if slug else _s(d.get("guid")) or self.base_url) images = [u.strip() for u in _s(d.get("photos")).split(",") if u.strip().startswith("http")][:20] engine = "" size = _s(d.get("car_engine_size")) cyl = _s(d.get("car_cylinders")) if size: engine = f"{size}L" + (f" {cyl} cyl." if cyl else "") features = [f.strip().replace("_", " ") for f in _s(d.get("car_options")).split(",") if f.strip()] details: dict = {} if d.get("car_no_accident"): details["no_accident"] = True if d.get("car_one_owner"): details["unique_owner"] = True old_price = _num(d.get("car_old_price")) if old_price and price and old_price > price: details["old_price"] = old_price carfax = _s(d.get("carfax_url")) or _s(d.get("carfax_report")) if not carfax: # parfois rangé dans admin_note m = re.search(r"https?://vhr\.carfax\.ca/\S+", _s(d.get("admin_note"))) if m: carfax = m.group(0) return Vehicle( source=self.source_id, external_id=ext_id, url=url, title=title, make=make, model=model, trim=trim, year=year, price=price, price_label=f"{price:,.0f} $".replace(",", " ") if price else "", mileage_km=km, mileage_label=f"{km:,.0f} km".replace(",", " ") if km else "", transmission=_s(d.get("car_transmission")), fuel=_s(d.get("car_fuel_type")), drivetrain=_s(d.get("car_drivetrain")), body_type=body, exterior_color=_s(d.get("car_exterior_color")).title(), interior_color=_s(d.get("car_interrior_color")).title(), engine=engine, doors=_int(d.get("car_doors_count")), seats=_int(d.get("number_of_passengers")), vin=_s(d.get("car_vin")).upper(), stock_number=_s(d.get("stock")), dealer_name=self.dealer_name, city=self.city, description=_s(d.get("post_content"))[:4000], features=features[:60], details=details, images=images, carfax_url=carfax, )