Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/forum_mtl.py : Le Forum / Forum Properties (forumproperties.com) —5# promoteur avec immeubles locatifs au Québec : Berson Apartments (Plateau,6# Montréal), Lustra Ph. 1 (Terrebonne), 100 Windsor (Westmount).7# Techno : widget Planpoint — l'index résidentiel passe par l'endpoint8# POST app.planpoint.io/api/enterprises/find {namespace, hostName} qui9# renvoie {projects:[...]} avec floors[].units[] inline. On réutilise le10# helper partagé _planpoint.unit_listing pour le mapping unité -> Listing.11# Portefeuille pancanadien : on ne garde que les projets au Québec12# (adresse « , QC » ou lat/lng dans la province).13# Granularité : UNITÉ (« Available » ; prix publié sur Lustra seulement).14# -----------------------------------------------------------------------------15from __future__ import annotations1617import time1819from .base import BaseConnector20from ._planpoint import PLANPOINT_API, unit_listing21from ..schema import Listing2223NAMESPACE = "forum-residential-enterprise"24HOST_NAME = "forum-residential"25FALLBACK_URL = "https://www.forumproperties.com/fr/residentiel/"2627# adresses manquantes chez Planpoint (clé = namespace du projet)28_ADDRESS_FIX = {29 "100-1-windsor": "100 avenue Windsor, Westmount",30}313233def _in_quebec(project: dict) -> bool:34 addr = (project.get("address") or "")35 if ", QC" in addr or ", Qc" in addr or "Québec" in addr:36 return True37 lat, lon = project.get("lat"), project.get("lon")38 if isinstance(lat, (int, float)) and isinstance(lon, (int, float)):39 return 44.5 <= lat <= 63.0 and -80.0 <= lon <= -56.040 return False414243class ForumMtlConnector(BaseConnector):44 """Forum Properties — unités Planpoint des immeubles québécois."""4546 source_id = "forum_mtl"47 request_delay = 1.048 use_detail_cache = False # tout est inline dans enterprises/find4950 def _enterprise_projects(self) -> list[dict]:51 """POST /api/enterprises/find -> projets (floors/units inline)."""52 wait = self.request_delay - (time.time() - self._last_request)53 if wait > 0:54 time.sleep(wait)55 resp = self.session.post(f"{PLANPOINT_API}/enterprises/find",56 json={"namespace": NAMESPACE,57 "hostName": HOST_NAME},58 timeout=self.timeout)59 self._last_request = time.time()60 resp.raise_for_status()61 return resp.json().get("projects") or []6263 def fetch(self) -> list[Listing]:64 out: list[Listing] = []65 seen: set[str] = set()66 for project in self._enterprise_projects():67 if not _in_quebec(project):68 continue # AB/BC : hors périmètre Lou-Ka69 ns = project.get("namespace") or ""70 if not (project.get("address") or "").strip() and ns in _ADDRESS_FIX:71 project["address"] = _ADDRESS_FIX[ns]72 urls = project.get("internalURLs") or {}73 page_url = (urls.get("fr") or urls.get("en")74 or project.get("websiteURL")75 or project.get("internalURL") or FALLBACK_URL).strip()76 for floor in project.get("floors") or []:77 for unit in floor.get("units") or []:78 lst = unit_listing(self.source_id, project, floor, unit,79 page_url=page_url,80 default_city="Montréal")81 if lst and lst.external_id not in seen:82 seen.add(lst.external_id)83 out.append(lst)84 return out85