# ----------------------------------------------------------------------------- # Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/gatsby_dealers.py : connecteurs des concessionnaires de marque # sur la plateforme Gatsby « autoroot.io » (inventaire EvalAuto — Groupe # Beaucage : Kia Granby/Drummondville, Mazda Sherbrooke, Mitsubishi et # Nissan Drummondville, Nissan Victoriaville). # # Même plateforme que megacentredeliquidation (voir megacentre.py) : chaque # véhicule expose /page-data/auto-usage//page-data.json -> `usedCar` # complet. MAIS contrairement au Méga Centre, le sitemap de ces sites ne # référence pas les pages véhicule individuelles — seulement les pages # catégorie. L'énumération passe donc par l'API AWS AppSync que le site # utilise lui-même côté client : le endpoint GraphQL et la clé x-api-key # sont publiés dans siteParameters du page-data de la page liste # (/page-data/auto-usage/page-data.json). La requête # getUsedCarSearchByRelevanceMultiTable, filtrée par nameDealer, renvoie # 30 items/page + nextFilter (curseur) -> tous les slugs. # # Le reste (cache hebdo des page-data détail, plafond AUTOKA_MAX_DETAILS, # usedCar -> Vehicle) est hérité de MegaCentreDeLiquidation. # AUTOKA_MAX_PAGES (env) limite la pagination AppSync pour les tests. # ----------------------------------------------------------------------------- from __future__ import annotations import json import os import time from .megacentre import MegaCentreDeLiquidation _QUERY_NAME = "getUsedCarSearchByRelevanceMultiTable" # champs minimaux : on ne veut que les slugs (le détail vient du page-data) _ITEM_FIELDS = "{ nextFilter items { id slug nameDealer } }" class GatsbyEvalAutoConnector(MegaCentreDeLiquidation): """Base des sites Gatsby autoroot/EvalAuto sans sitemap véhicule.""" source_id = "" # base non enregistrée par le registre base_url = "" dealer_name = "" city = "" request_delay = 1.0 max_details = 300 max_aws_pages = 60 # garde-fou dur (60 x 30 = 1 800 véh.) # -- POST GraphQL poli ---------------------------------------------------- def _post_json(self, url: str, payload: dict, headers: dict) -> dict: wait = self.request_delay - (time.time() - self._last_request) if wait > 0: time.sleep(wait) resp = self.session.post(url, json=payload, headers=headers, timeout=self.timeout) self._last_request = time.time() resp.raise_for_status() return resp.json() @staticmethod def _gql(dealer: str, next_item: str | None) -> str: nxt = f', usedCarNextItem:"{next_item}"' if next_item else "" return f""" {{ {_QUERY_NAME}(order: "DESC", searchValue: "", maxPrice: 1000000, minPrice: 0, maxYear: 2035, minYear: 1900, brand: [], model: [], nameDealer: {json.dumps([dealer])}, extColor: [], drivetrain: [], passengers: [], engine: [], bodytype: [], transmission: [], certified: [true,false], discount: [true,false], prestige: [true,false] {nxt}) {_ITEM_FIELDS} }} """ # -- énumération des slugs via la page liste + AppSync --------------------- def _vehicle_slugs(self) -> list[str]: data = self.get( f"{self.base_url}/page-data/auto-usage/page-data.json").json() result = data.get("result") or {} params = (result.get("data") or {}).get("siteParameters") or {} context = result.get("pageContext") or {} # les 30 premiers véhicules sont pré-rendus dans le pageContext slugs = {str(c.get("slug")) for c in context.get("allUsedCar") or [] if c.get("slug")} endpoint = str(params.get("awsEndpoint") or "") token = str(params.get("awsToken") or "") dealer = str(params.get("awsDealerNameFilter") or context.get("nameDealer") or self.dealer_name) if not endpoint or not token: # repli : pré-rendu seulement return sorted(slugs) cap = min(int(os.environ.get("AUTOKA_MAX_PAGES", self.max_aws_pages)), self.max_aws_pages) headers = {"x-api-key": token, "Content-Type": "application/json"} next_item: str | None = None for _ in range(cap): try: resp = self._post_json(endpoint, {"query": self._gql(dealer, next_item)}, headers) except Exception: break # AppSync HS -> pré-rendu payload = (resp.get("data") or {}).get(_QUERY_NAME) or {} items = payload.get("items") or [] for item in items: slug = str(item.get("slug") or "") if slug: slugs.add(slug) next_item = payload.get("nextFilter") if not items or not next_item: break return sorted(slugs) # -- usedCar -> Vehicle (ville de repli) ----------------------------------- def _to_vehicle(self, slug, d): veh = super()._to_vehicle(slug, d) if veh is not None and not veh.city: veh.city = self.city return veh # --------------------------------------------------------------------------- # Concessionnaires (1 sous-classe = 1 source) — Groupe Beaucage # --------------------------------------------------------------------------- class KiaGranby(GatsbyEvalAutoConnector): source_id = "kiagranby" base_url = "https://www.kiagranby.com" dealer_name = "Kia de Granby" city = "Granby" class KiaDrummondville(GatsbyEvalAutoConnector): source_id = "kiadrummondville" base_url = "https://www.kiadrummondville.com" dealer_name = "Kia Drummondville" city = "Drummondville" class MazdaDeSherbrooke(GatsbyEvalAutoConnector): source_id = "mazdadesherbrooke" base_url = "https://www.mazdadesherbrooke.com" dealer_name = "Mazda de Sherbrooke" city = "Sherbrooke" class MitsubishiDrummondville(GatsbyEvalAutoConnector): source_id = "mitsubishidrummondville" base_url = "https://www.mitsubishidrummondville.com" dealer_name = "Mitsubishi Drummondville" city = "Drummondville" class NissanDrummondville(GatsbyEvalAutoConnector): source_id = "nissandrummondville" base_url = "https://www.nissandrummondville.com" dealer_name = "Nissan Drummondville" city = "Drummondville" class NissanVictoriaville(GatsbyEvalAutoConnector): source_id = "nissanvictoriaville" base_url = "https://www.nissanvictoriaville.com" dealer_name = "Nissan de Victoriaville" city = "Victoriaville"