SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
3.8 KB · 85 lines python
Raw Blame History
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