# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/cinquante_ans_plus.py : connecteur Logements 50 ans et plus # (logements50ans.ca — Gestion T. Francoeur). Trois immeubles locatifs pour # clientèle autonome de 50 ans et plus (non RPA) : Le Chambéry et Boisé du # Parc Équestre à Blainville, Le Bourg-Joli à Sainte-Adèle. Site vitrine # MotoCMS rendu serveur : l'accueil publie « À PARTIR DE X$ » par immeuble, # les pages projet donnent l'adresse, les commodités (puces « • ») et les # photos. Granularité IMMEUBLE (aucune unité/typologie détaillée en ligne). # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Listing from .base import BaseConnector BASE = "https://logements50ans.ca" # nom affiché sur l'accueil (repère du prix) -> (slug de page, nom, ville) BUILDINGS = [ ("CHAMBÉRY", "chambery", "Le Chambéry", "Blainville"), ("BOURG-JOLI", "bourg-joli", "Le Bourg-Joli", "Sainte-Adèle"), ("PARC ÉQUESTRE", "boise-du-parc-equestre", "Boisé du Parc Équestre", "Blainville"), ] PRICE_RE_TPL = r"%s.{0,600}?À PARTIR DE\s*([\d\s ,]+)\s*\$" ADDR_RE = re.compile( r"(\d{1,5}[-\d]*,?\s+(?:boul\.?|rue|avenue|av\.|chemin|mont[ée]e|rang)" r"[^|<>]{0,60}?),\s*(?:Ville de\s*)?(Blainville|Ste-Ad[èe]le|" r"Sainte-Ad[èe]le)", re.I) BULLET_RE = re.compile(r"•\s*([^|<>•]{3,60})") IMG_RE = re.compile( r'(?:src|href)="(/mt-content/uploads/[^"]+\.(?:jpg|jpeg|png|webp))"', re.I) def _flat(html: str) -> str: txt = re.sub(r"", "", html, flags=re.S) txt = re.sub(r"", "", txt, flags=re.S) txt = re.sub(r"<[^>]+>", "|", txt).replace(" ", " ") return re.sub(r"[ \t\n]+", " ", re.sub(r"\|+", "|", txt)) class CinquanteAnsPlusConnector(BaseConnector): source_id = "cinquante_ans_plus" request_delay = 0.6 def fetch(self) -> list[Listing]: # échec HONNÊTE : une erreur réseau sur l'accueil doit lever (ok=0 dans # sync_log) et non retourner [] — un « 0 trouvé ok » dépublie les 3 # immeubles (missed=3, vu le 2026-09-06 16:04) listings: list[Listing] = [] home = _flat(self.get(BASE + "/").text) if not any(marker in home for marker, *_ in BUILDINGS): raise RuntimeError( "logements50ans.ca : accueil sans aucun immeuble connu " "(rendu partiel ou site restructuré)") for marker, slug, name, city in BUILDINGS: m = re.search(PRICE_RE_TPL % re.escape(marker), home, re.S) price_label = "" if m: price_label = ("À partir de " + re.sub(r"[\s ]", "", m.group(1)) + " $/mois") page_url = f"{BASE}/{slug}/" address, amenities, images, description = "", [], [], "" try: page = self.get(page_url).text except Exception: page = "" if page: flat = _flat(page) am = ADDR_RE.search(flat) if am: address = am.group(1).strip(" ,") for b in BULLET_RE.findall(flat): b = b.strip(" .") if b and b not in amenities: amenities.append(b) images = [BASE + u for u in dict.fromkeys(IMG_RE.findall(page)) if not re.search(r"logo|icon|-small\.", u, re.I)][:20] # premier paragraphe descriptif substantiel de la page projet for seg in flat.split("|"): seg = seg.strip() if len(seg) > 120 and "cookie" not in seg.lower(): description = seg break if not price_label and not address: continue # immeuble retiré du site listings.append(Listing( source=self.source_id, external_id=slug, url=page_url, title=f"{name} — logements 50 ans et plus, {city}", address=address, city=city, price_label=price_label, description=description[:1500], amenities=amenities[:25], details={"clientele": "50 ans et plus"}, images=images, )) return listings