spb/auto-ka Public
Python 81.9%
TypeScript 12.4%
CSS 5.5%
1# -----------------------------------------------------------------------------2# Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/d2c_dealers.py : connecteurs des concessionnaires sur plateforme5# D2C Media (le fournisseur de sites le plus répandu chez les marchands6# d'occasion au Québec).7#8# Stratégie : le sitemap (/fr/sitemap.xml) recense chaque page véhicule9# (…-id123456.html). Chaque page détail expose un JSON-LD schema.org10# complet (@type Vehicle/Car) : VIN, kilométrage, transmission, carburant,11# carrosserie, couleurs, prix, photos. Les pages détail sont mises en cache12# (clé hebdomadaire) : seuls les nouveaux véhicules — et une revalidation13# par semaine pour les prix — génèrent de vraies requêtes.14#15# Enrichissement (clé de cache v2) — en plus du JSON-LD (qui ne liste que16# 3 photos, parfois avec des ids corrompus — bug D2C) :17# - galerie COMPLÈTE : les URLs imagescdn.d2cmedia.ca/<hash>/<dealer>/<id>/18# <n>/… présentes dans le HTML (préférence à la variante « cb » grand19# format, sinon « mb »/« s8 »), triées par index de photo ;20# - endpoint JSON par véhicule /js/json/<id>.json (celui que le JS du site21# appelle pour révéler la section « Options ») : optionsTextFR (liste22# d'équipements en français), moteur + cylindre, carprooflink (rapport23# Carfax direct quand le concessionnaire l'expose) ;24# - repli équipements : titres des topFeatures/safetyFeatures du blob25# window.__vdpJSON embarqué dans la page.26#27# Le lastmod du sitemap est bumpé quotidiennement par D2C (inutilisable28# comme clé de cache) — d'où la clé temporelle hebdomadaire.29# -----------------------------------------------------------------------------30from __future__ import annotations3132import datetime33import html as htmllib34import json35import os36import re3738from ..schema import Vehicle39from .base import BaseConnector4041# segments d'URL /occasion/<segment>/ qui ne sont PAS des automobiles42_EXCLUDE_SEGMENTS = re.compile(43 r"motoneige|vtt|bateau|ponton|motomarine|moto(?:s)?\b|roulotte|remorque|"44 r"\bvr\b|campeur|tracteur|souffleuse|scooter|spyder|side|atv|snow|"45 r"powersport|nautique|habitation", re.I)4647_VEH_URL_RE = re.compile(r"-id(\d+)\.html?$")4849_DRIVE_MAP = {50 "FrontWheelDriveConfiguration": "Traction",51 "RearWheelDriveConfiguration": "Propulsion",52 "AllWheelDriveConfiguration": "Intégrale / 4x4",53 "FourWheelDriveConfiguration": "Intégrale / 4x4",54}5556_LD_RE = re.compile(57 r'<script type=["\']application/ld\+json["\']>(.*?)</script>', re.S)5859# galerie CDN D2C : /<variante(2)+hash>/<dealer>/<idVéhicule>/<indexPhoto>/…60# (le nom de fichier peut contenir des espaces : « Silverado 1500-2023.jpg »)61_GALLERY_RE = re.compile(62 r'(https://imagescdn\.d2cmedia\.ca/'63 r'([a-z0-9]{2})[a-f0-9]+/\d+/(\d+)/(\d+)/[^"\'<>]+?\.(?:jpe?g|png|webp))',64 re.I)6566# variante d'image préférée : cb = grand format (1440px), mb = moyen, s8 = mini67_IMG_VARIANT_RANK = {"cb": 0, "mb": 1, "s8": 2}6869_VDP_JSON_RE = re.compile(r'window\.__vdpJSON\s*=\s*')7071MAX_IMAGES = 25727374def _extract_gallery(html: str, ext_id: str) -> list[str]:75 """Galerie complète depuis le HTML, dédupliquée par index de photo,76 dans l'ordre de la galerie (index croissant), meilleure variante d'abord.77 """78 best: dict[int, tuple[int, str]] = {} # index photo -> (rang, url)79 for url, variant, veh_id, idx in _GALLERY_RE.findall(html):80 if veh_id != ext_id: # vignettes d'autres véhicules81 continue82 try:83 idx = int(idx)84 except ValueError:85 continue86 rank = _IMG_VARIANT_RANK.get(variant.lower(), 3)87 if idx not in best or rank < best[idx][0]:88 best[idx] = (rank, url.replace(" ", "%20"))89 return [best[i][1] for i in sorted(best)][:MAX_IMAGES]909192def _extract_vdp_json(html: str) -> dict:93 """Blob `window.__vdpJSON = {...}` embarqué dans la page (si présent)."""94 m = _VDP_JSON_RE.search(html)95 if not m:96 return {}97 try:98 obj, _ = json.JSONDecoder().raw_decode(html, m.end())99 except ValueError:100 return {}101 return obj if isinstance(obj, dict) else {}102103104def _clean(text: str | None) -> str:105 """Désencode les entités HTML (souvent doublement encodées chez D2C)."""106 if not text:107 return ""108 out = htmllib.unescape(htmllib.unescape(str(text)))109 out = re.sub(r"<[^>]+>", " ", out)110 return re.sub(r"\s{2,}", " ", out).strip()111112113class D2CConnector(BaseConnector):114 """Base commune aux sites D2C Media. Sous-classes : base_url + ville."""115116 base_url: str = ""117 dealer_name: str = ""118 city: str = ""119 request_delay: float = 1.0 # robots.txt D2C demande de la politesse120 use_firecrawl: bool = False # sites derrière Cloudflare121122 # plafond de vraies requêtes détail par sync (AUTOKA_MAX_DETAILS le réduit123 # pour les tests) — le reste sera complété aux syncs suivants124 max_details = 600125126 def _get_text(self, url: str) -> str:127 if self.use_firecrawl:128 return self.get_rendered(url)129 return self.get(url).text130131 # -- sitemap ---------------------------------------------------------------132 def _vehicle_urls(self) -> list[str]:133 xml = self._get_text(f"{self.base_url}/fr/sitemap.xml")134 urls = []135 for loc in re.findall(r"<loc>([^<]+)</loc>", xml):136 loc = loc.strip()137 if "/occasion/" not in loc or not _VEH_URL_RE.search(loc):138 continue139 # catégorie intermédiaire ? (/occasion/motoneiges/…)140 path = loc.split("/occasion/", 1)[1]141 segments = path.split("/")[:-1]142 if any(_EXCLUDE_SEGMENTS.search(s) for s in segments):143 continue144 urls.append(loc)145 return urls146147 # -- page détail -> payload JSON-LD enrichi --------------------------------148 def _fetch_detail(self, url: str) -> dict:149 html = self._get_text(url)150 data: dict = {}151 for block in _LD_RE.findall(html):152 try:153 cand = json.loads(block.strip())154 except ValueError:155 continue156 types = cand.get("@type")157 types = types if isinstance(types, list) else [types]158 if "Vehicle" in types or "Car" in types:159 data = cand160 break161 if not data:162 return {}163164 # --- enrichissements (tous optionnels : jamais bloquants) -------------165 m = _VEH_URL_RE.search(url)166 ext_id = m.group(1) if m else ""167168 try:169 gallery = _extract_gallery(html, ext_id)170 except Exception:171 gallery = []172 if gallery:173 data["_gallery"] = gallery174175 vdp = {}176 try:177 vdp = _extract_vdp_json(html)178 except Exception:179 pass180181 # endpoint JSON par véhicule : équipements, moteur, lien Carfax182 extra: dict = {}183 if ext_id:184 try:185 extra = self.get(f"{self.base_url}/js/json/{ext_id}.json").json()186 if not isinstance(extra, dict):187 extra = {}188 except Exception:189 extra = {}190191 features = [_clean(t) for t in (extra.get("optionsTextFR") or [])192 if isinstance(t, str) and _clean(t)]193 if not features: # repli : blob __vdpJSON de la page194 for group in (vdp.get("topFeatures"), vdp.get("safetyFeatures")):195 for item in group or []:196 if isinstance(item, dict):197 t = _clean(item.get("title"))198 if t and t not in features:199 features.append(t)200 if features:201 data["_features"] = features202203 engine = _clean(str(extra.get("moteur") or vdp.get("engine") or ""))204 # D2C renvoie « N.D. » ou « . L » (véhicules électriques) : rejeter205 # toute cylindrée sans chiffre206 if engine.upper() in ("N.D.", "N/A", "ND") or not re.search(r"\d", engine):207 engine = ""208 cyl = _clean(str(extra.get("cylindre") or vdp.get("cylinder") or ""))209 if cyl and cyl.upper() not in ("N.D.", "N/A", "ND", "0"):210 if cyl.isdigit():211 # nombre nu (parfois erroné pour les VÉ) : seulement en212 # complément d'une cylindrée valide, jamais seul213 if engine:214 engine = f"{engine} {cyl} cyl."215 elif cyl.lower() not in engine.lower():216 engine = f"{engine} {cyl}".strip()217 if engine:218 data["_engine"] = engine219220 carfax = str(extra.get("carprooflink") or "").strip()221 if not carfax:222 cp = vdp.get("carproof")223 if isinstance(cp, dict):224 carfax = str(cp.get("link") or "").strip()225 if carfax.startswith("http"):226 data["_carfax_url"] = carfax227228 return data229230 # -- contrat ---------------------------------------------------------------231 def fetch(self) -> list[Vehicle]:232 urls = self._vehicle_urls()233 week = datetime.date.today().isocalendar()234 # v2 : payloads enrichis (galerie complète, équipements, moteur,235 # carfax) — le bump force la re-crawl progressive des pages en cache236 cache_key = f"v2:{week.year}w{week.week}" # revalidation hebdomadaire237 cap = int(os.environ.get("AUTOKA_MAX_DETAILS", self.max_details))238239 vehicles: list[Vehicle] = []240 real_fetches = 0241 for url in urls:242 ext_id = _VEH_URL_RE.search(url).group(1)243244 def _fetch(u=url):245 return self._fetch_detail(u)246247 # plafond atteint : ne servir que le cache (fetch réel interdit)248 if real_fetches >= cap:249 from .. import db250 if self._detail_con is None:251 self._detail_con = db.connect()252 data = db.get_cached_detail(self._detail_con, self.source_id,253 ext_id, cache_key)254 if data is None:255 continue256 else:257 before = self._last_request258 try:259 data = self.detail(ext_id, cache_key, _fetch)260 except Exception:261 # page détail disparue (vendu, sitemap pas encore purgé)262 # ou erreur ponctuelle : ne jamais bloquer la source263 if self._last_request != before:264 real_fetches += 1265 continue266 if self._last_request != before: # une vraie requête a eu lieu267 real_fetches += 1268 if not data:269 continue270 veh = self._to_vehicle(ext_id, url, data)271 if veh is not None:272 vehicles.append(veh)273 return vehicles274275 def _to_vehicle(self, ext_id: str, url: str, d: dict) -> Vehicle | None:276 offers = d.get("offers") or {}277 price = offers.get("price")278 try:279 price = float(price) if price not in (None, "", 0, "0") else None280 except (TypeError, ValueError):281 price = None282283 odo = d.get("mileageFromOdometer") or {}284 km = odo.get("value") if isinstance(odo, dict) else odo285 try:286 km = float(km) if km not in (None, "") else None287 except (TypeError, ValueError):288 km = None289290 brand = d.get("brand") or {}291 make = brand.get("name") if isinstance(brand, dict) else str(brand)292293 drive_raw = str(d.get("driveWheelConfiguration") or "")294 drivetrain = ""295 for key, canon in _DRIVE_MAP.items():296 if key in drive_raw:297 drivetrain = canon298 break299300 year = None301 if d.get("vehicleModelDate"):302 try:303 year = int(str(d["vehicleModelDate"])[:4])304 except ValueError:305 pass306307 # galerie complète extraite du HTML (payloads v2) de préférence au308 # JSON-LD (limité à 3 photos, ids parfois corrompus — bug D2C)309 images = d.get("_gallery") or d.get("image") or []310 if isinstance(images, str):311 images = [images]312313 title = _clean(d.get("name"))314 if not title:315 return None316317 return Vehicle(318 source=self.source_id,319 external_id=ext_id,320 url=url,321 title=title,322 make=_clean(make),323 model=_clean(d.get("model")),324 trim=_clean(d.get("vehicleConfiguration")),325 year=year,326 price=price,327 price_label=f"{price:,.0f} $".replace(",", " ") if price else "",328 mileage_km=km,329 mileage_label=f"{km:,.0f} km".replace(",", " ") if km else "",330 transmission=_clean(d.get("vehicleTransmission")),331 fuel=_clean(d.get("fuelType")),332 drivetrain=drivetrain,333 body_type=_clean(d.get("bodyType")),334 exterior_color=_clean(d.get("color")),335 interior_color=_clean(d.get("vehicleInteriorColor")),336 doors=d.get("numberOfDoors"),337 seats=d.get("vehicleSeatingCapacity"),338 vin=_clean(d.get("vehicleIdentificationNumber")),339 stock_number=str(d.get("sku") or ""),340 engine=_clean(d.get("_engine")),341 dealer_name=self.dealer_name,342 city=self.city,343 description=_clean(d.get("description"))[:4000],344 features=[f for f in (d.get("_features") or [])345 if isinstance(f, str) and f],346 images=[u for u in images if isinstance(u, str)][:MAX_IMAGES],347 carfax_url=str(d.get("_carfax_url") or ""),348 )349350351# ---------------------------------------------------------------------------352# Concessionnaires (1 sous-classe = 1 source)353# ---------------------------------------------------------------------------354355class GirardAutos(D2CConnector):356 source_id = "girardautos"357 base_url = "https://www.girardautos.ca"358 dealer_name = "Girard Autos"359 city = "Trois-Rivières"360361362class ForceOccasion(D2CConnector):363 source_id = "forceoccasion"364 base_url = "https://www.forceoccasion.ca"365 dealer_name = "Force Occasion"366 city = "Québec"367368369class GervaisAuto(D2CConnector):370 source_id = "gervaisauto"371 base_url = "https://www.gervaisauto.com"372 dealer_name = "Gervais Auto"373 city = "Shawinigan"374375376class EquipeFormule(D2CConnector):377 source_id = "equipeformule"378 base_url = "https://www.equipeformule.com"379 dealer_name = "Équipe Formule"380 city = "Rimouski"381382383class AutosEvasion(D2CConnector):384 source_id = "autosevasion"385 base_url = "https://www.autosevasion.com"386 dealer_name = "Autos Évasion"387 city = "Sherbrooke"388389390class MFerlandAuto(D2CConnector):391 source_id = "mferlandauto"392 base_url = "https://www.mferlandauto.com"393 dealer_name = "M. Ferland Auto"394 city = "Sherbrooke"395396397class AutosOccasionLD(D2CConnector):398 source_id = "autosoccasionld"399 base_url = "https://www.autosoccasions.ca"400 dealer_name = "Autos Occasion LD"401 city = "Lévis"402403404class LevisAutos(D2CConnector):405 source_id = "levisautos"406 base_url = "https://www.levisautos.com"407 dealer_name = "Lévis Autos"408 city = "Lévis"409410411class AutoQuebecLevis(D2CConnector):412 source_id = "autoquebeclevis"413 base_url = "https://www.autoquebecoccasionlevis.com"414 dealer_name = "AutoQuébec Occasion Lévis"415 city = "Lévis"416417418class AutoQuebecThetford(D2CConnector):419 source_id = "autoquebectm"420 base_url = "https://www.autoquebectm.com"421 dealer_name = "AutoQuébec Occasion Thetford Mines"422 city = "Thetford Mines"423424425class AbitibiAutoCredit(D2CConnector):426 source_id = "abitibiautocredit"427 base_url = "https://www.abitibiautocredit.ca"428 dealer_name = "Abitibi Auto Crédit"429 city = "Rouyn-Noranda"430431432class NicolOccasion(D2CConnector):433 source_id = "nicoloccasion"434 base_url = "https://www.nicoloccasion.com"435 dealer_name = "Nicol Occasion"436 city = "La Sarre"437438439class PoirierOccasion(D2CConnector):440 source_id = "poirieroccasion"441 base_url = "https://www.poirieroccasion.com"442 dealer_name = "Poirier Occasion"443 city = "Val-d'Or"444445446class AutosStHubert(D2CConnector):447 source_id = "autosthubert"448 base_url = "https://www.autosthubert.com"449 dealer_name = "Auto St-Hubert"450 city = "Saint-Hubert"451452453class SherbrookeAutoOccasion(D2CConnector):454 source_id = "sherbrookeautooccasion"455 base_url = "https://www.sherbrookeautooccasion.com"456 dealer_name = "Sherbrooke Auto Occasion"457 city = "Sherbrooke"458459460# ---------------------------------------------------------------------------461# Expansion 2 — concessionnaires de marque et groupes sur D2C Media462# (inventaire OCCASION seulement : le sitemap /fr/sitemap.xml ne référence463# que les pages /occasion/…-idNNN.html ; le neuf vit dans un sitemap séparé)464# ---------------------------------------------------------------------------465466class BernierCrepeau(D2CConnector):467 source_id = "berniercrepeau"468 base_url = "https://www.berniercrepeau.com"469 dealer_name = "Bernier Crépeau"470 city = "Trois-Rivières"471472473class ChomedeyToyota(D2CConnector):474 source_id = "chomedeytoyota"475 base_url = "https://www.chomedeytoyotalaval.ca"476 dealer_name = "Chomedey Toyota Laval"477 city = "Laval"478479480class DrummondvilleFord(D2CConnector):481 source_id = "drummondvilleford"482 base_url = "https://www.drummondvilleford.com"483 dealer_name = "Drummondville Ford"484 city = "Drummondville"485486487class DrummondvilleVW(D2CConnector):488 source_id = "drummondvillevw"489 base_url = "https://www.drummondvillevolkswagen.ca"490 dealer_name = "Drummondville Volkswagen"491 city = "Drummondville"492493494class DubeChrysler(D2CConnector):495 source_id = "dubechrysler"496 base_url = "https://www.dubechrysler.com"497 dealer_name = "Dubé Chrysler"498 city = "Rivière-du-Loup"499500501class DuclosLongueuil(D2CConnector):502 source_id = "ducloslongueuil"503 base_url = "https://www.ducloschryslerlongueuil.com"504 dealer_name = "Duclos Longueuil Chrysler"505 city = "Longueuil"506507508class DynastieHyundai(D2CConnector):509 source_id = "dynastiehyundai"510 base_url = "https://www.dynastiehyundai.com"511 dealer_name = "Dynastie Hyundai"512 city = "Rouyn-Noranda"513514515class FichaultKia(D2CConnector):516 source_id = "fichaultkia"517 base_url = "https://www.fichaultkia.com"518 dealer_name = "Fichault Kia"519 city = "Châteauguay"520521522class FordThetford(D2CConnector):523 source_id = "fordthetford"524 base_url = "https://www.fordthetford.com"525 dealer_name = "Ford Thetford"526 city = "Thetford Mines"527528529class GaspeToyota(D2CConnector):530 source_id = "gaspetoyota"531 base_url = "https://www.gaspe-toyota.com"532 dealer_name = "Gaspé Toyota"533 city = "Gaspé"534535536class GaspesieHyundai(D2CConnector):537 source_id = "gaspesiehyundai"538 base_url = "https://www.gaspesiehyundai.com"539 dealer_name = "Gaspésie Hyundai"540 city = "Bonaventure"541542543class GranbyToyota(D2CConnector):544 source_id = "granbytoyota"545 base_url = "https://www.granbytoyota.ca"546 dealer_name = "Granby Toyota"547 city = "Granby"548549550class GrandPortage(D2CConnector):551 source_id = "grandportage"552 base_url = "https://www.grandportageauto.com"553 dealer_name = "Groupe Grand-Portage"554 city = "Rivière-du-Loup"555556557class GroupeMotion(D2CConnector):558 source_id = "groupemotion"559 base_url = "https://www.groupemotion.com"560 dealer_name = "Groupe Motion"561 city = "Longueuil"562563564class GroupePaquin(D2CConnector):565 source_id = "groupepaquin"566 base_url = "https://www.groupepaquin.com"567 dealer_name = "Groupe Paquin"568 city = "Rouyn-Noranda"569570571class HullHyundai(D2CConnector):572 source_id = "hullhyundai"573 base_url = "https://www.hullhyundai.com"574 dealer_name = "Hull Hyundai"575 city = "Gatineau"576577578class HyundaiBeauce(D2CConnector):579 source_id = "hyundaibeauce"580 base_url = "https://www.hyundaibeauce.com"581 dealer_name = "Hyundai Beauce"582 city = "Saint-Georges"583584585class HyundaiDrummondville(D2CConnector):586 source_id = "hyundaidrummondville"587 base_url = "https://www.hyundaidrummondville.com"588 dealer_name = "Hyundai Drummondville"589 city = "Drummondville"590591592class HyundaiLanaudiere(D2CConnector):593 source_id = "hyundailanaudiere"594 base_url = "https://www.hyundailanaudiere.com"595 dealer_name = "Hyundai Lanaudière"596 city = "Joliette"597598599class HyundaiLongueuil(D2CConnector):600 source_id = "hyundailongueuil"601 base_url = "https://www.hyundailongueuil.com"602 dealer_name = "Hyundai Longueuil"603 city = "Longueuil"604605606class HyundaiStJerome(D2CConnector):607 source_id = "hyundaistjerome"608 base_url = "https://www.hyundaistjerome.com"609 dealer_name = "Hyundai St-Jérôme"610 city = "Mirabel"611612613class JeanDumas(D2CConnector):614 source_id = "jeandumas"615 base_url = "https://www.jeandumas.com"616 dealer_name = "Jean Dumas Multiconcessionnaire"617 city = "Alma"618619620class JeanDumasFordBC(D2CConnector):621 source_id = "jeandumasfordbc"622 base_url = "https://www.jeandumasfordbaiecomeau.com"623 dealer_name = "Jean Dumas Ford Baie-Comeau"624 city = "Baie-Comeau"625626627class JeanDumasHondaBC(D2CConnector):628 source_id = "jeandumashondabc"629 base_url = "https://www.jeandumashondabaiecomeau.com"630 dealer_name = "Jean Dumas Honda Baie-Comeau"631 city = "Baie-Comeau"632633634class JolietteMitsubishi(D2CConnector):635 source_id = "joliettemitsubishi"636 base_url = "https://www.joliettemitsubishi.com"637 dealer_name = "Joliette Mitsubishi"638 city = "Joliette"639640641class KiaJoliette(D2CConnector):642 source_id = "kiajoliette"643 base_url = "https://www.kiajoliette.com"644 dealer_name = "Kia Joliette"645 city = "Joliette"646647648class LAmiHonda(D2CConnector):649 source_id = "lamihonda"650 base_url = "https://www.lamihonda.com"651 dealer_name = "L'Ami Honda"652 city = "Rouyn-Noranda"653654655class LeoAutomobile(D2CConnector):656 source_id = "leoautomobile"657 base_url = "https://www.leoautomobile.com"658 dealer_name = "Léo Automobile"659 city = "Chicoutimi"660661662class LevisToyota(D2CConnector):663 source_id = "levistoyota"664 base_url = "https://www.levistoyota.ca"665 dealer_name = "Lévis Toyota"666 city = "Lévis"667668669class LongueuilHonda(D2CConnector):670 source_id = "longueuilhonda"671 base_url = "https://www.longueuilhonda.com"672 dealer_name = "Longueuil Honda"673 city = "Longueuil"674675676class OlivierChryslerBC(D2CConnector):677 source_id = "olivierchryslerbc"678 base_url = "https://www.olivierchryslerbaiecomeau.com"679 dealer_name = "Olivier Chrysler Baie-Comeau"680 city = "Baie-Comeau"681682683class StGeorgesGM(D2CConnector):684 source_id = "stgeorgesgm"685 base_url = "https://www.stgeorgesgm.ca"686 dealer_name = "St-Georges Chevrolet Buick GMC"687 city = "Saint-Georges"688689690class StJeromeMitsubishi(D2CConnector):691 source_id = "stjeromemitsubishi"692 base_url = "https://www.stjeromemitsubishi.ca"693 dealer_name = "St-Jérôme Mitsubishi"694 city = "Saint-Jérôme"695696697class SubaruVictoriaville(D2CConnector):698 source_id = "subaruvictoriaville"699 base_url = "https://www.subaruvictoriaville.ca"700 dealer_name = "Subaru Victoriaville"701 city = "Victoriaville"702703704class ToyotaBaieDesChaleurs(D2CConnector):705 source_id = "toyotabdc"706 base_url = "https://www.toyotabaiedeschaleurs.com"707 dealer_name = "Toyota Baie des Chaleurs"708 city = "Caplan"709710711class TroisRivieresHonda(D2CConnector):712 source_id = "troisriviereshonda"713 base_url = "https://www.troisriviereshonda.com"714 dealer_name = "Trois-Rivières Honda"715 city = "Trois-Rivières"716717718class TRToyota(D2CConnector):719 source_id = "trtoyota"720 base_url = "https://www.trtoyota.ca"721 dealer_name = "Trois-Rivières Toyota"722 city = "Trois-Rivières"723724725class VaudreuilVW(D2CConnector):726 source_id = "vaudreuilvw"727 base_url = "https://www.vaudreuilvolkswagen.com"728 dealer_name = "Vaudreuil Volkswagen"729 city = "Vaudreuil-Dorion"730731732class VWSherbrooke(D2CConnector):733 source_id = "vwsherbrooke"734 base_url = "https://www.vwsherbrooke.com"735 dealer_name = "VW Sherbrooke"736 city = "Sherbrooke"737738739# ---------------------------------------------------------------------------740# Expansion 3 — régions sous-couvertes (Québec métro, Charlevoix, Saguenay–741# Lac-Saint-Jean, Côte-Nord, Bas-Saint-Laurent, Gaspésie)742#743# Groupe Olivier : les quatre sites Olivier de Sept-Îles (Ford / Nissan /744# Chrysler / Occasion) publient EXACTEMENT le même inventaire d'occasion745# (mêmes ids D2C) — on ne connecte que olivieroccasionseptiles.com. Aucun746# recoupement avec Olivier Chrysler Baie-Comeau (déjà connecté) ni avec747# Olivier Hyundai Baie-Comeau.748# ---------------------------------------------------------------------------749750class DupontAutomobile(D2CConnector):751 source_id = "dupontautomobile"752 base_url = "https://www.dupontautomobile.com"753 dealer_name = "Dupont Automobile"754 city = "Alma"755756757class AlmaHonda(D2CConnector):758 source_id = "almahonda"759 base_url = "https://www.almahonda.com"760 dealer_name = "Alma Honda"761 city = "Alma"762763764class HondaNewRichmond(D2CConnector):765 source_id = "hondanewrichmond"766 base_url = "https://www.hondanewrichmond.com"767 dealer_name = "Honda New Richmond"768 city = "New Richmond"769770771class OlivierHyundaiBaieComeau(D2CConnector):772 source_id = "olivierhyundaibc"773 base_url = "https://www.olivierhyundaibaiecomeau.com"774 dealer_name = "Olivier Hyundai Baie-Comeau"775 city = "Baie-Comeau"776777778class OlivierOccasionSeptIles(D2CConnector):779 # inventaire commun aux 4 sites Olivier de Sept-Îles (voir note ci-dessus)780 source_id = "olivieroccasionseptiles"781 base_url = "https://www.olivieroccasionseptiles.com"782 dealer_name = "Olivier Occasion Sept-Îles"783 city = "Sept-Îles"784785786class CharlesbourgToyota(D2CConnector):787 source_id = "charlesbourgtoyota"788 base_url = "https://www.charlesbourgtoyota.com"789 dealer_name = "Charlesbourg Toyota"790 city = "Québec"791792793class HondaCharlesbourg(D2CConnector):794 source_id = "hondacharlesbourg"795 base_url = "https://www.hondacharlesbourg.com"796 dealer_name = "Honda Charlesbourg"797 city = "Québec"798799800class KiaBeauport(D2CConnector):801 source_id = "kiabeauport"802 base_url = "https://www.kiabeauport.com"803 dealer_name = "Kia Beauport"804 city = "Québec"805806807class HondaCharlevoix(D2CConnector):808 source_id = "hondacharlevoix"809 base_url = "https://www.hondacharlevoix.com"810 dealer_name = "Honda Charlevoix"811 city = "La Malbaie"812813814class HyundaiCharlevoix(D2CConnector):815 source_id = "hyundaicharlevoix"816 base_url = "https://www.hyundaicharlevoix.com"817 dealer_name = "Hyundai Charlevoix"818 city = "Baie-Saint-Paul"819820821class CharlevoixToyota(D2CConnector):822 source_id = "charlevoixtoyota"823 base_url = "https://www.charlevoixtoyota.com"824 dealer_name = "Charlevoix Toyota"825 city = "La Malbaie"826827828class AutoVilleneuve(D2CConnector):829 # concessionnaire Ford d'Amqui (vallée de la Matapédia) — petit inventaire830 source_id = "autovilleneuve"831 base_url = "https://www.autovilleneuve.com"832 dealer_name = "Automobile Villeneuve Amqui"833 city = "Amqui"834