SPB Git

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%
6.2 KB · 173 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# -----------------------------------------------------------------------------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