]*>.*?'
r"]*>(.*?) ", h)
if m:
texte = re.sub(r"
", "\n", m.group(1)) texte = _html.unescape(_TAG_RE.sub(" ", texte)) texte = re.sub(r"[ \t]+", " ", texte) texte = re.sub(r"\n\s+", "\n", texte).strip() d["description"] = texte[:5000] # commodités : avec coche (les entêtes de
# catégorie sont des en gras sans icône fa-check)
amen: list[str] = []
for li in re.findall(r'(?s) ]*>(.*?) ', h):
if "fa-check" not in li:
continue
t = _text(li)
if t and t not in amen:
amen.append(t)
if amen:
d["amenities"] = amen
return d
# -- contrat ------------------------------------------------------------
def fetch(self) -> list[StListing]:
limit = int(os.environ.get("LOUKA_TREMBLANT_LIMIT", "0") or 0)
xml = self.get(SITEMAP).text
entries = re.findall(r"(?s)\s*([^<]+) "
r"(?:\s*([^<]*) )?", xml)
listings: list[StListing] = []
vus: set[str] = set()
for url, lastmod in entries:
parts = [p for p in url.split("/") if p]
# …/property// ou …/rental// (les /monthly-rentals/
# sont dans units-sitemap.xml : long terme, hors mandat)
if len(parts) < 4 or parts[-2] not in ("property", "rental"):
continue
slug = parts[-1]
if slug in vus:
continue
vus.add(slug)
det = self.detail(slug, lastmod or "v1",
lambda u=url: self._detail(u))
ld = det.get("ld") or {}
if not ld:
continue
place = ld.get("containsPlace") or {}
addr = ld.get("address") or {}
agg = ld.get("aggregateRating") or {}
occupancy = (place.get("occupancy") or {}).get("value")
title = _text(str(ld.get("name") or slug))
hay = f"{title} {slug}".lower()
ptype = next((canon for needle, canon in _TYPE_HINTS
if needle in hay), "Chalet")
amen = det.get("amenities") or []
pets = "oui" if any("pet friendly" in a.lower()
for a in amen) else None
imgs = ld.get("image") or []
if isinstance(imgs, str):
imgs = [imgs]
reviews = agg.get("reviewCount")
# tarif « à partir de » via l'API Streamline (hors cache détail)
price_night, price_label = None, ""
unit_id = str(ld.get("identifier") or "")
if unit_id.isdigit():
try:
price_night, price_label = self._price_from_rates(unit_id)
except Exception as exc: # tarif manquant ≠ annonce perdue
print(f"[tremblant_living] tarifs {unit_id} : {exc}",
file=sys.stderr)
listings.append(StListing(
source=self.source_id,
external_id=str(ld.get("identifier") or slug),
url=url,
title=title,
property_type=ptype,
address=_text(str(addr.get("streetAddress") or "")),
city=_text(str(addr.get("addressLocality") or "Mont-Tremblant")),
region="Laurentides",
price_night=price_night,
price_label=price_label,
capacity=_f(occupancy),
bedrooms=_f(place.get("numberOfBedrooms")),
bathrooms=_f(place.get("numberOfBathroomsTotal")),
pets=pets,
rating=_f(agg.get("ratingValue")),
reviews=int(reviews) if reviews else None,
description=det.get("description")
or _text(str(ld.get("description") or "")),
amenities=amen,
details={"postal_code": addr.get("postalCode") or ""},
images=[u for u in imgs if isinstance(u, str)
and u.startswith("https://")][:20],
lat=_f(ld.get("latitude")),
lng=_f(ld.get("longitude")),
))
if limit and len(listings) >= limit:
break
return listings
", "\n", m.group(1)) texte = _html.unescape(_TAG_RE.sub(" ", texte)) texte = re.sub(r"[ \t]+", " ", texte) texte = re.sub(r"\n\s+", "\n", texte).strip() d["description"] = texte[:5000] # commodités :