# ----------------------------------------------------------------------------- # Lou-Ka — Location court terme # connectors/hebergia.py : Hébergia (hebergia.ca) # # Agence multi-régions (Cantons-de-l'Est, Laurentides, Lanaudière, Charlevoix, # Centre-du-Québec) — WordPress + moteur Guesty, ~60 chalets. # # Méthode : # 1. LISTE : la page /chalets/ embarque `window.chalets = [...]` (JSON # complet : gid Guesty, prix de base/nuit, url, titre, RÉGION, capacité, # chambres, lits, lat/lng, vignette). Une seule requête. # 2. DÉTAIL (cache self.detail, clé = champs stables de la liste) : la page # WP fr /chalets// fournit ville (« Austin, Cantons-de-l'Est »), # salles de bain (bloc inshort), description, commodités (section # « Commodités incluses »), photos (assets.guesty.com) et numéro CITQ # (dans le règlement). # External_id = gid (id de listing Guesty, stable). # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import json import re from ..schema import StListing from .base import StConnector SITE = "https://hebergia.ca" _TAG_RE = re.compile(r"<[^>]+>") def _text(fragment: str) -> str: return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))).strip() def _num(v) -> float | None: try: return float(v) if v not in (None, "") else None except (TypeError, ValueError): return None class Hebergia(StConnector): source_id = "hebergia" # -- liste ---------------------------------------------------------------- def _list_items(self) -> list[dict]: h = self.get(f"{SITE}/chalets/").text m = re.search(r"window\.chalets\s*=\s*(\[.*?\]);", h, re.S) if not m: return [] try: return json.loads(m.group(1)) except ValueError: return [] # -- page détail ------------------------------------------------------ def _detail(self, url: str) -> dict: h = self.get(url).text d: dict = {} m = re.search(r'class="sous-titre-localisation">([^<]+)<', h) if m: loc = _text(m.group(1)) d["city"] = loc.split(",")[0].strip() # bloc « inshort » : 8 voyageurs / 3 chambres / lits / 2 salles de bain m = re.search(r"(\d+)\s*salles?\s*de\s*bain", h) if m: d["bathrooms"] = float(m.group(1)) # description : paragraphes de l'article avant les accordéons m = re.search(r"(?s)
" r".*?(.*?)

.*?

', " ", m.group(1)) paras = re.findall(r"(?s)]*>(.*?)

", frag) d["description"] = " ".join(_text(p) for p in paras if _text(p))[:4000] # commodités : section « Commodités incluses » (lignes à puces) m = re.search(r'(?s)Commodités incluses

\s*' r'
]*>(.*?)
', h) if m: amens = [] for line in re.split(r"|

", m.group(1)): t = _text(line).lstrip("•· ").strip() if 2 <= len(t) <= 80 and not t.isupper(): amens.append(t) d["amenities"] = amens[:60] # photos (CDN Guesty, dédupliquées) imgs: list[str] = [] for u in re.findall(r'data-flickity-lazyload-src="' r'(https://assets\.guesty\.com/[^"]+)"', h): if u not in imgs: imgs.append(u) d["images"] = imgs[:20] m = re.search(r"CITQ\D{0,12}(\d{6})", h, re.I) if m: d["citq"] = m.group(1) m = re.search(r"animaux non admis|pas d.animaux|no pets", h, re.I) if m: d["pets"] = "non" return d # -- contrat ---------------------------------------------------------- def fetch(self) -> list[StListing]: listings: list[StListing] = [] for it in self._list_items(): gid = str(it.get("gid") or "").strip() url = it.get("url") or "" title = _text(str(it.get("title") or "")) if not gid or not url or not title or "/en/" in url: continue key = json.dumps([gid, title, it.get("price"), it.get("accommodates"), it.get("bedrooms"), it.get("beds"), it.get("shortDesc")], ensure_ascii=False) try: det = self.detail(gid, key, lambda u=url: self._detail(u)) except Exception: # une fiche détail cassée ≠ annonce perdue det = {} price = _num(it.get("price")) starting = _num(it.get("startingPrice")) if starting and starting > 0: price = starting desc = det.get("description") or "" short = _text(str(it.get("shortDesc") or "")) if short and short not in desc: desc = f"{short} {desc}".strip() listings.append(StListing( source=self.source_id, external_id=gid, url=url, title=title, property_type="Chalet", city=det.get("city") or "", region=it.get("region") or "", price_night=price, price_label=(f"à partir de {price:.0f} $ / nuit" if price else ""), capacity=_num(it.get("accommodates")), bedrooms=_num(it.get("bedrooms")), beds=_num(it.get("beds")), bathrooms=det.get("bathrooms"), pets=det.get("pets"), citq=det.get("citq") or "", description=desc[:4000], amenities=det.get("amenities") or [], details={"guesty_id": gid}, images=det.get("images") or ([it["thumbnail"]] if it.get("thumbnail") else []), lat=_num(it.get("lat")), lng=_num(it.get("lng")), )) return listings