spb/ora-ka Public
Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)
Python 80%
TypeScript 12.9%
CSS 6.8%
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# -----------------------------------------------------------------------------20from __future__ import annotations2122import re2324from ..schema import Vehicle25from .base import BaseConnector2627_JSON_PATH = "/wp-content/themes/astra/car_single_page_data/cars_formatted.json"2829# garde-fou non-automobile (le marchand ne vend que des autos)30_EXCLUDE_RE = re.compile(31 r"motoneige|vtt|moto\b|motomarine|bateau|roulotte|remorque|spyder", re.I)323334def _s(value) -> str:35 if value is None:36 return ""37 return re.sub(r"\s+", " ", str(value)).strip()383940def _num(value) -> float | None:41 try:42 f = float(value)43 except (TypeError, ValueError):44 return None45 return f if f > 0 else None464748def _int(value) -> int | None:49 try:50 return int(str(value))51 except (TypeError, ValueError):52 return None535455class ClasseAuto(BaseConnector):56 """Classe Auto (Montréal) — inventaire complet en un JSON statique."""5758 source_id = "classeauto"59 base_url = "https://www.classeauto.ca"60 dealer_name = "Classe Auto"61 city = "Montréal"62 request_delay = 1.06364 def fetch(self) -> list[Vehicle]:65 # cache-buster comme le fait le site (nouvelle valeur par minute)66 data = self.get(self.base_url + _JSON_PATH).json()67 vehicles: list[Vehicle] = []68 for d in data if isinstance(data, list) else []:69 try:70 veh = self._to_vehicle(d)71 except Exception:72 continue # enregistrement malformé isolé73 if veh is not None:74 vehicles.append(veh)75 return vehicles7677 def _to_vehicle(self, d: dict) -> Vehicle | None:78 if not isinstance(d, dict):79 return None80 if d.get("vehicle_type_id") not in (1, "1", None):81 return None # 1 = automobile82 if _s(d.get("post_status")) not in ("publish", "published", ""):83 return None84 if d.get("deleted_at"):85 return None86 if _s(d.get("condition")).upper() == "NEW":87 return None # usagé seulement8889 ext_id = _s(d.get("vid")) or _s(d.get("ID"))90 if not ext_id:91 return None9293 make = _s(d.get("maker")).title()94 model = _s(d.get("model"))95 trim = _s(d.get("car_trim")) or _s(d.get("car_sub_model"))96 year = _int(d.get("car_year"))97 body = _s(d.get("car_body"))98 if _EXCLUDE_RE.search(f"{make} {model} {body}"):99 return None100101 title = " ".join(x for x in (str(year or ""), make, model, trim) if x)102103 price = _num(d.get("car_price"))104 km = _num(d.get("car_mileage"))105 unit = _s(d.get("car_mileage_unit")).upper()106 if km and unit.startswith("MI"):107 km = round(km * 1.609344)108109 slug = _s(d.get("slug"))110 url = (f"{self.base_url}/cars/{slug}/" if slug111 else _s(d.get("guid")) or self.base_url)112113 images = [u.strip() for u in _s(d.get("photos")).split(",")114 if u.strip().startswith("http")][:20]115116 engine = ""117 size = _s(d.get("car_engine_size"))118 cyl = _s(d.get("car_cylinders"))119 if size:120 engine = f"{size}L" + (f" {cyl} cyl." if cyl else "")121122 features = [f.strip().replace("_", " ")123 for f in _s(d.get("car_options")).split(",") if f.strip()]124125 details: dict = {}126 if d.get("car_no_accident"):127 details["no_accident"] = True128 if d.get("car_one_owner"):129 details["unique_owner"] = True130 old_price = _num(d.get("car_old_price"))131 if old_price and price and old_price > price:132 details["old_price"] = old_price133134 carfax = _s(d.get("carfax_url")) or _s(d.get("carfax_report"))135 if not carfax: # parfois rangé dans admin_note136 m = re.search(r"https?://vhr\.carfax\.ca/\S+",137 _s(d.get("admin_note")))138 if m:139 carfax = m.group(0)140141 return Vehicle(142 source=self.source_id,143 external_id=ext_id,144 url=url,145 title=title,146 make=make,147 model=model,148 trim=trim,149 year=year,150 price=price,151 price_label=f"{price:,.0f} $".replace(",", " ") if price else "",152 mileage_km=km,153 mileage_label=f"{km:,.0f} km".replace(",", " ") if km else "",154 transmission=_s(d.get("car_transmission")),155 fuel=_s(d.get("car_fuel_type")),156 drivetrain=_s(d.get("car_drivetrain")),157 body_type=body,158 exterior_color=_s(d.get("car_exterior_color")).title(),159 interior_color=_s(d.get("car_interrior_color")).title(),160 engine=engine,161 doors=_int(d.get("car_doors_count")),162 seats=_int(d.get("number_of_passengers")),163 vin=_s(d.get("car_vin")).upper(),164 stock_number=_s(d.get("stock")),165 dealer_name=self.dealer_name,166 city=self.city,167 description=_s(d.get("post_content"))[:4000],168 features=features[:60],169 details=details,170 images=images,171 carfax_url=carfax,172 )173