# -----------------------------------------------------------------------------
# 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