# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/vi_laval.py : connecteur Vï Condos Locatifs (vicondoslocatifs.com, # Danam Lacourse) — condos locatifs tout inclus de la Rive-Nord : Terrebonne # (225, carré Denise-Pelletier, Urbanova) et Mascouche (1505, place de # l'Esplanade). NB : slug historique « vi_laval » de la campagne Laval/ # Rive-Nord, mais les projets sont à TERREBONNE et MASCOUCHE (Lanaudière). # Les pages projet embarquent le widget Planpoint (app.planpoint.io) : on # rejoue l'API JSON publique /api/projects/find (voir _planpoint.py). # Particularités Vï chez Planpoint : disponibilité en FRANÇAIS # (« Disponible ») à Terrebonne, chambres en « 1cc/2cc/3cc » ou # « n chambre(s) (+ DEN) », champ type = numéro de modèle. # Granularité UNITÉ : prix (Terrebonne), pi², étage, plans par unité. # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Listing from ._planpoint import planpoint_projects, unit_listing from .base import BaseConnector # (namespace, hostName, url de la page publique, adresse, ville) PROJECTS = [ ("vi-condos-locatifs", "vi-condos-locatifs", "https://www.vicondoslocatifs.com/vi-terrebonne/", "225 carré Denise-Pelletier", "Terrebonne"), ("danamlacourse", "Place-de-l-Esplanade", "https://www.vicondoslocatifs.com/vi-mascouche/", "1505 place de l'Esplanade", "Mascouche"), ] _CC_RE = re.compile(r"^\s*(\d+)\s*cc\s*(\+\s*DEN)?\s*$", re.I) class ViLavalConnector(BaseConnector): source_id = "vi_laval" request_delay = 0.8 def fetch(self) -> list[Listing]: listings: list[Listing] = [] for namespace, host, page_url, address, city in PROJECTS: try: projects = planpoint_projects(self, "projects", namespace, host) except Exception: continue # un projet cassé ne bloque pas for project in projects: # adresse Planpoint incomplète (« Terrebonne »/None) : # on impose l'adresse civique publiée sur le site public project["address"] = f"{address}, {city}" for floor in project.get("floors") or []: for unit in floor.get("units") or []: self._normalize(unit) lst = unit_listing(self.source_id, project, floor, unit, page_url=page_url, default_city=city) if lst is not None: lst.city = city listings.append(lst) return listings @staticmethod def _normalize(unit: dict) -> None: """Adapte les champs Vï au patron générique de _planpoint.py.""" # « Disponible » (fr) -> « Available » ; « Sold » = loué, exclu if (unit.get("availability") or "").strip().lower() == "disponible": unit["availability"] = "Available" # chambres « 1cc/2cc/3cc » -> « n chambres » (normalize_unit_type # sait ensuite dériver 3½/4½/5½) beds = str(unit.get("bedrooms") or "").strip() m = _CC_RE.match(beds) if m: n = int(m.group(1)) unit["bedrooms"] = (f"{n} chambre" + ("s" if n > 1 else "") + (" + DEN" if m.group(2) else "")) # le champ type est un numéro de modèle (« 38 ») : déplacé pour que # unit_listing dérive le type d'unité des chambres, pas du modèle model = str(unit.get("type") or "").strip() if model and not re.search(r"[½/]", model): unit["type"] = "" unit.setdefault("_model", model)