# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/lsr.py : connecteur LSR GesDev (lsrgesdev.ca — anciennement # lsrgesdev.com, redirige) — développeur-gestionnaire de Saint-Lambert. # Le site corporatif ne publie aucun prix ; l'inventaire locatif vit sur # lenovia.ca (WordPress) avec DEUX backends différents : # - NOVIA centre-ville – Longueuil (300, place Charles-Le Moyne) : widget # Livya (app.livya.com, client « lsr-gesdev », projet « novia ») — flux # RSC Next.js parsé par la base commune LivyaConnector (_livya.py) ; # - NOVIA de la Gare – Candiac (125, rue des Marronniers) : widget Planpoint # (app.planpoint.io/125-marronniers/125-marronniers) — API JSON publique # via le helper _planpoint.py. # Granularité = UNITÉ pour les deux projets (prix, pi², chambres, étage). # Roomî (espaceroomi.ca, résidence étudiante Trois-Rivières) exclu. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from ..schema import Listing from ._livya import LivyaConnector from ._planpoint import planpoint_projects, unit_listing CANDIAC_PAGE = "https://lenovia.ca/novia-de-la-gare-candiac/plans/" CANDIAC_NAMESPACE = "125-marronniers" CANDIAC_HOST = "125-marronniers" # prix planchers par typologie publiés sur lenovia.ca (Livya n'expose aucun # prix pour Longueuil) : « 3 ½ (de 541 pi2 à 769 pi2) à partir de 1 950$ /mois » LONGUEUIL_APTS = "https://lenovia.ca/novia-centre-ville-longueuil/appartements/" TYPE_TOKEN_RE = re.compile(r"Studio|\d\s*½|Maison de ville", re.I) FROM_PRICE_RE = re.compile( r"à\s+partir\s+de\s+([\d\s ]{3,9})\$\s*/?\s*mois", re.I) SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?", re.I) TAG_RE = re.compile(r"<[^>]+>") class LSRConnector(LivyaConnector): source_id = "lsr" request_delay = 0.8 # -- NOVIA centre-ville (Longueuil) via Livya -------------------------------- client = "lsr-gesdev" brand = "NOVIA centre-ville" site_url = "https://www.lsrgesdev.ca" default_city = "Longueuil" default_projects = ["novia"] def fetch(self) -> list[Listing]: listings = super().fetch() # Longueuil (Livya) floors = self._longueuil_floor_prices() for lst in listings: lst.city = re.sub(r"^Ville de\s+", "", lst.city or "") # Livya ne publie aucun loyer pour Longueuil : on rattache le prix # plancher « à partir de » publié par lenovia.ca pour la typologie if lst.price is None and not lst.price_label: base = floors.get(lst.unit_type) if base: lst.price_label = f"à partir de {int(base)} $ /mois" listings.extend(self._candiac()) # Candiac (Planpoint) return listings def _longueuil_floor_prices(self) -> dict[str, float]: """{'3½': 1950.0, …} depuis la page publique « Appartements ».""" try: html = self.get(LONGUEUIL_APTS).text except Exception: return {} flat = re.sub(r"[\s ]+", " ", TAG_RE.sub(" | ", _html.unescape(SCRIPT_RE.sub(" ", html)))) # segment « typologie -> texte jusqu'à la typologie suivante » pour ne # jamais rattacher le prix d'un 3½ au Studio « Complet » qui le précède tokens = list(TYPE_TOKEN_RE.finditer(flat)) prices: dict[str, float] = {} for i, tok in enumerate(tokens): end = tokens[i + 1].start() if i + 1 < len(tokens) else len(flat) seg = flat[tok.end():min(end, tok.end() + 200)] pm = FROM_PRICE_RE.search(seg) if not pm: continue label = re.sub(r"\s+", "", tok.group(0)) key = "Studio" if label.lower() == "studio" else label if key.lower() == "maisondeville": continue # typologie non « n½ » : ignorée try: val = float(re.sub(r"[\s ]", "", pm.group(1))) except ValueError: continue if 500 <= val <= 8000: prices.setdefault(key, val) return prices # -- NOVIA de la Gare (Candiac) via Planpoint -------------------------------- def _candiac(self) -> list[Listing]: try: projects = planpoint_projects(self, "projects", CANDIAC_NAMESPACE, CANDIAC_HOST) except Exception: return [] listings: list[Listing] = [] for project in projects: for floor in project.get("floors") or []: for unit in floor.get("units") or []: lst = unit_listing(self.source_id, project, floor, unit, page_url=CANDIAC_PAGE, default_city="Candiac") if lst is not None: listings.append(lst) return listings