SPB Git

spb/auto-ka Public

Python 81.9% TypeScript 12.4% CSS 5.5%
6.8 KB · 181 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/gatsby_dealers.py : connecteurs des concessionnaires de marque5#   sur la plateforme Gatsby « autoroot.io » (inventaire EvalAuto — Groupe6#   Beaucage : Kia Granby/Drummondville, Mazda Sherbrooke, Mitsubishi et7#   Nissan Drummondville, Nissan Victoriaville).8#9#   Même plateforme que megacentredeliquidation (voir megacentre.py) : chaque10#   véhicule expose /page-data/auto-usage/<slug>/page-data.json -> `usedCar`11#   complet. MAIS contrairement au Méga Centre, le sitemap de ces sites ne12#   référence pas les pages véhicule individuelles — seulement les pages13#   catégorie. L'énumération passe donc par l'API AWS AppSync que le site14#   utilise lui-même côté client : le endpoint GraphQL et la clé x-api-key15#   sont publiés dans siteParameters du page-data de la page liste16#   (/page-data/auto-usage/page-data.json). La requête17#   getUsedCarSearchByRelevanceMultiTable, filtrée par nameDealer, renvoie18#   30 items/page + nextFilter (curseur) -> tous les slugs.19#20#   Le reste (cache hebdo des page-data détail, plafond AUTOKA_MAX_DETAILS,21#   usedCar -> Vehicle) est hérité de MegaCentreDeLiquidation.22#   AUTOKA_MAX_PAGES (env) limite la pagination AppSync pour les tests.23# -----------------------------------------------------------------------------24from __future__ import annotations2526import json27import os28import time2930from .megacentre import MegaCentreDeLiquidation3132_QUERY_NAME = "getUsedCarSearchByRelevanceMultiTable"3334# champs minimaux : on ne veut que les slugs (le détail vient du page-data)35_ITEM_FIELDS = "{ nextFilter items { id slug nameDealer } }"363738class GatsbyEvalAutoConnector(MegaCentreDeLiquidation):39    """Base des sites Gatsby autoroot/EvalAuto sans sitemap véhicule."""4041    source_id = ""                    # base non enregistrée par le registre42    base_url = ""43    dealer_name = ""44    city = ""45    request_delay = 1.046    max_details = 30047    max_aws_pages = 60                # garde-fou dur (60 x 30 = 1 800 véh.)4849    # -- POST GraphQL poli ----------------------------------------------------50    def _post_json(self, url: str, payload: dict, headers: dict) -> dict:51        wait = self.request_delay - (time.time() - self._last_request)52        if wait > 0:53            time.sleep(wait)54        resp = self.session.post(url, json=payload, headers=headers,55                                 timeout=self.timeout)56        self._last_request = time.time()57        resp.raise_for_status()58        return resp.json()5960    @staticmethod61    def _gql(dealer: str, next_item: str | None) -> str:62        nxt = f', usedCarNextItem:"{next_item}"' if next_item else ""63        return f"""64  {{65    {_QUERY_NAME}(order: "DESC",66    searchValue: "",67    maxPrice: 1000000,68    minPrice: 0,69    maxYear: 2035,70    minYear: 1900,71    brand: [],72    model: [],73    nameDealer: {json.dumps([dealer])},74    extColor: [],75    drivetrain: [],76    passengers: [],77    engine: [],78    bodytype: [],79    transmission: [],80    certified: [true,false],81    discount: [true,false],82    prestige: [true,false]83    {nxt})84    {_ITEM_FIELDS}85  }}86  """8788    # -- énumération des slugs via la page liste + AppSync ---------------------89    def _vehicle_slugs(self) -> list[str]:90        data = self.get(91            f"{self.base_url}/page-data/auto-usage/page-data.json").json()92        result = data.get("result") or {}93        params = (result.get("data") or {}).get("siteParameters") or {}94        context = result.get("pageContext") or {}9596        # les 30 premiers véhicules sont pré-rendus dans le pageContext97        slugs = {str(c.get("slug")) for c in context.get("allUsedCar") or []98                 if c.get("slug")}99100        endpoint = str(params.get("awsEndpoint") or "")101        token = str(params.get("awsToken") or "")102        dealer = str(params.get("awsDealerNameFilter")103                     or context.get("nameDealer") or self.dealer_name)104        if not endpoint or not token:            # repli : pré-rendu seulement105            return sorted(slugs)106107        cap = min(int(os.environ.get("AUTOKA_MAX_PAGES", self.max_aws_pages)),108                  self.max_aws_pages)109        headers = {"x-api-key": token, "Content-Type": "application/json"}110        next_item: str | None = None111        for _ in range(cap):112            try:113                resp = self._post_json(endpoint,114                                       {"query": self._gql(dealer, next_item)},115                                       headers)116            except Exception:117                break                             # AppSync HS -> pré-rendu118            payload = (resp.get("data") or {}).get(_QUERY_NAME) or {}119            items = payload.get("items") or []120            for item in items:121                slug = str(item.get("slug") or "")122                if slug:123                    slugs.add(slug)124            next_item = payload.get("nextFilter")125            if not items or not next_item:126                break127        return sorted(slugs)128129    # -- usedCar -> Vehicle (ville de repli) -----------------------------------130    def _to_vehicle(self, slug, d):131        veh = super()._to_vehicle(slug, d)132        if veh is not None and not veh.city:133            veh.city = self.city134        return veh135136137# ---------------------------------------------------------------------------138# Concessionnaires (1 sous-classe = 1 source) — Groupe Beaucage139# ---------------------------------------------------------------------------140141class KiaGranby(GatsbyEvalAutoConnector):142    source_id = "kiagranby"143    base_url = "https://www.kiagranby.com"144    dealer_name = "Kia de Granby"145    city = "Granby"146147148class KiaDrummondville(GatsbyEvalAutoConnector):149    source_id = "kiadrummondville"150    base_url = "https://www.kiadrummondville.com"151    dealer_name = "Kia Drummondville"152    city = "Drummondville"153154155class MazdaDeSherbrooke(GatsbyEvalAutoConnector):156    source_id = "mazdadesherbrooke"157    base_url = "https://www.mazdadesherbrooke.com"158    dealer_name = "Mazda de Sherbrooke"159    city = "Sherbrooke"160161162class MitsubishiDrummondville(GatsbyEvalAutoConnector):163    source_id = "mitsubishidrummondville"164    base_url = "https://www.mitsubishidrummondville.com"165    dealer_name = "Mitsubishi Drummondville"166    city = "Drummondville"167168169class NissanDrummondville(GatsbyEvalAutoConnector):170    source_id = "nissandrummondville"171    base_url = "https://www.nissandrummondville.com"172    dealer_name = "Nissan Drummondville"173    city = "Drummondville"174175176class NissanVictoriaville(GatsbyEvalAutoConnector):177    source_id = "nissanvictoriaville"178    base_url = "https://www.nissanvictoriaville.com"179    dealer_name = "Nissan de Victoriaville"180    city = "Victoriaville"181