# ----------------------------------------------------------------------------- # Lou-Ka — Location court terme # connectors/panoraloges.py : Panora Loges Fluviales (panoraloges.com) — # 10 loges (mini-chalets) en bord de mer à Sainte-Anne-des-Monts (Gaspésie), # déclinées en DEUX gammes identiques à l'intérieur d'une même gamme : # loges simples (nos 8-10, 2 pers.) et loges doubles (nos 1-7, 4 pers.). # # Méthode : site Squarespace ; l'API ?format=json de la page /fr/loges est # vide (items=0) → on parse les DEUX pages de gamme statiques # /fr/logessimples et /fr/logesdoubles. Chaque page expose en : # prix (« 175 $ » sous « À partir de »), « N invités », « N chambres », # « N lits Queen », « 1 salle de bain », puis la liste des commodités # jusqu'à « Stationnements ». 2 fiches « gamme » (multi_unit) plutôt que # 10 fiches par unité artificiellement identiques — le site lui-même # laisse le libre choix de la loge à l'intérieur d'une gamme. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from ..schema import StListing from .base import StConnector SITE = "https://www.panoraloges.com" _PAGES = [ # (external_id, chemin, titre, unités) ("loges-doubles", "/fr/logesdoubles", "Loges doubles — Panora Loges " "Fluviales", "Loges 1 à 7"), ("loges-simples", "/fr/logessimples", "Loges simples — Panora Loges " "Fluviales", "Loges 8 à 10"), ] _TAG_RE = re.compile(r"<[^>]+>") # images de gabarit à exclure de la galerie _IMG_EXCLUDE = ("favicon", "logo", "icon", "texture", "panora_ecran", "Fond+de+page", "plan") def _text(fragment: str) -> str: return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))).strip() class PanoraLoges(StConnector): source_id = "panoraloges" request_delay = 1.0 def _parse(self, path: str) -> dict: h = self.get(SITE + path).text d: dict = {} heads = [t for t in (_text(x) for x in re.findall( r"(?s)]*>(.*?)", h)) if t] m = re.search(r"(\d+)\s*\$", " ".join(heads)) if m: d["price_night"] = float(m.group(1)) # commodités : entêtes entre « N invités » et « Stationnements » start = next((i for i, t in enumerate(heads) if re.fullmatch(r"\d+\s+invités?", t)), None) end = next((i for i, t in enumerate(heads) if t.lower().startswith("stationnement")), None) amen: list[str] = [] if start is not None and end is not None and start < end: amen = heads[start:end + 1] blob = " | ".join(amen) for pat, key in ((r"(\d+)\s+invités?", "capacity"), (r"(\d+)\s+chambres?", "bedrooms"), (r"(\d+)\s+lits?\s+Queen", "beds"), (r"(\d+)\s+salles?\s+de\s+bain", "bathrooms")): m = re.search(pat, blob, re.I) if m: d[key] = float(m.group(1)) d["amenities"] = [a for a in amen if not re.fullmatch(r"\d+\s+(invités?|chambres?" r"|salles?\s+de\s+bain)", a)] # description : sous-titre de gamme + infos pratiques (les

du # gabarit Squarespace contiennent beaucoup de CSS → filtrés) paras = [_text(p) for p in re.findall(r"(?s)]*>(.*?)

", h)] keep = [p for p in paras if 60 < len(p) < 600 and "{" not in p and not p.startswith(("FR:", "0 ")) and "Infolettre" not in p and "418-" not in p] if keep: d["description"] = "\n".join(keep[:6])[:3000] imgs: list[str] = [] for u in re.findall(r"https://images\.squarespace-cdn\.com/[^\"\s?]+", h): if any(x.lower() in u.lower() for x in _IMG_EXCLUDE): continue u = u.rstrip("\\,&") if not re.search(r"\.(?:jpe?g|png|webp)$", u, re.I): continue if u not in imgs: imgs.append(u) if len(imgs) >= 15: break d["images"] = imgs return d # -- contrat -------------------------------------------------------------- def fetch(self) -> list[StListing]: listings: list[StListing] = [] for ext_id, path, title, units in _PAGES: d = self._parse(path) if not d.get("price_night") and not d.get("capacity"): continue price = d.get("price_night") listings.append(StListing( source=self.source_id, external_id=ext_id, url=SITE + path, title=title, property_type="Chalet", address="610 boul. Sainte-Anne E", city="Sainte-Anne-des-Monts", region="Gaspésie", price_night=price, price_label=f"À partir de {price:g} $ / nuit" if price else "", capacity=d.get("capacity"), bedrooms=d.get("bedrooms"), beds=d.get("beds"), bathrooms=d.get("bathrooms"), description=d.get("description") or "", amenities=d.get("amenities") or [], details={"multi_unit": True, "units": units, "domain": "Panora Loges Fluviales"}, images=d.get("images") or [], )) return listings