# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/signature_bois_franc.py : Signature Bois-Franc — condos locatifs # (location.signatureboisfranc.com) au 2020, rue Lucien-Thimens, quartier # Bois-Franc, Saint-Laurent (Montréal). # Techno : WordPress + FacetWP, mais la liste /unites-disponibles/ est # RENDUE CÔTÉ SERVEUR (toutes les cartes .unit-row dans le HTML initial, # pas de pagination) — simple parse regex des champs par carte. # Granularité : UNITÉ (numéro, pièces, étage, pi², « à partir de » $, # statut). On écarte « Unité louée » et « Réservée ». # ----------------------------------------------------------------------------- from __future__ import annotations import re from .base import BaseConnector from ..schema import Listing LIST_URL = "https://location.signatureboisfranc.com/unites-disponibles/" ADDRESS = "2020, rue Lucien-Thimens" CITY = "Montréal" SECTOR = "Saint-Laurent" _ROW_SPLIT_RE = re.compile(r'
') _FIELD_RE = { "number": re.compile(r'unit-number__text">\s*#?([^<]+?)\s*<'), "rooms": re.compile(r'unit-room__text">\s*([^<]+?)\s*<'), "floor": re.compile(r'unit-floor__text">\s*([^<]+?)\s*<'), "area": re.compile(r'unit-area__text">\s*([^<]+?)\s*<'), "price": re.compile(r'unit-price__text">\s*([^<]+?)\s*<'), "status": re.compile(r'unit-availability__text">\s*([^<]*?)\s*<'), } _PLAN_RE = re.compile(r'unit-plan[^>]*>.*?href="([^"]+\.pdf)"', re.S) _AREA_NUM_RE = re.compile(r'([\d\s ,.]+)\s*pi') _PRICE_NUM_RE = re.compile(r'([\d\s ]{3,})\s*\$') # statuts qui excluent l'unité de l'inventaire _GONE_RE = re.compile(r'lou[ée]e?|r[ée]serv[ée]e?', re.I) class SignatureBoisFrancConnector(BaseConnector): """Signature Bois-Franc (Saint-Laurent, Montréal) — unités disponibles.""" source_id = "signature_bois_franc" request_delay = 0.8 use_detail_cache = False # une seule page, tout est dans la liste def fetch(self) -> list[Listing]: page = re.sub(r"\s+", " ", self.get(LIST_URL).text) out: list[Listing] = [] seen: set[str] = set() for block in _ROW_SPLIT_RE.split(page)[1:]: f = {k: (rx.search(block).group(1).strip() if rx.search(block) else "") for k, rx in _FIELD_RE.items()} num = f["number"].lstrip("#").strip() if not num: continue status = f["status"] if _GONE_RE.search(status): continue # louée / réservée ext_id = f"unite-{num}" # un seul immeuble : nº stable if ext_id in seen: continue seen.add(ext_id) area = None m = _AREA_NUM_RE.search(f["area"]) if m: try: area = float(re.sub(r"[\s ,]", "", m.group(1))) except ValueError: pass price = None m = _PRICE_NUM_RE.search(f["price"].replace("$", " $")) if m: try: price = float(re.sub(r"[\s ]", "", m.group(1))) except ValueError: pass details: dict = {} if f["floor"]: details["floor"] = f["floor"] plan = _PLAN_RE.search(block) if plan: details["floor_plan"] = plan.group(1) out.append(Listing( source=self.source_id, external_id=ext_id, url=LIST_URL, # pas de page par unité title=f"Signature Bois-Franc — Unité {num}", address=ADDRESS, sector=SECTOR, city=CITY, unit_type=f["rooms"], price=price, price_label=(f"À partir de {price:.0f} $ /mois" if price else ""), availability=status or "Disponible", area_sqft=area, details=details, )) return out