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%
5.0 KB · 124 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/symbio.py : connecteur Symbio (symbiohabitat.ca) — immeuble locatif5#   de 219 unités (Développement FTG / Claridge) au 1475, rue Yves-Blais,6#   Terrebonne (Lachenaie). La page /appartements/ embarque une application7#   React « plans » (iframe wp-content/themes/hello-elementor/iframe) qui lit8#   son inventaire dans une feuille Google Sheets PUBLIÉE EN CSV : colonnes9#   Unit, Area (pi²), Type (n -> n½), Chambers, Bathrooms, Balcony (pi²),10#   Price, Status (Available/Rented), PDF (plan). On découvre l'URL du CSV en11#   suivant iframe -> main.<hash>.js (repli : URL relevée 2026-08-25).12#   Granularité UNITÉ : prix, pi², chambres/sdb, plan PDF par unité.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import csv17import io18import re1920from ..schema import Listing21from .base import BaseConnector2223SITE = "https://symbiohabitat.ca"24PAGE_URL = f"{SITE}/appartements/"25APP_BASE = f"{SITE}/wp-content/themes/hello-elementor"26IFRAME_URL = f"{APP_BASE}/iframe?lang=fr"2728# URL du CSV publié (repli si la découverte via le JS de l'app casse)29FALLBACK_CSV = ("https://docs.google.com/spreadsheets/d/e/2PACX-1vSv4yfAI-E4opE"30                "tejmwNY74SEVrHTbxt34FpyUYiN_xKPHv_Swf2iqi-OzqTa3oMALF39YDLttP"31                "-WTI/pub?output=csv&gid=0")3233ADDRESS = "1475, rue Yves-Blais, Terrebonne"34BUILDING_AMENITIES = ["Gym", "Chalet urbain avec terrasse",35                      "Bornes de recharge électrique", "Ascenseurs"]3637MAIN_JS_RE = re.compile(r'src="\./(static/js/main\.[0-9a-f]+\.js)"')38SHEET_RE = re.compile(39    r'https://docs\.google\.com/spreadsheets/d/e/[\w-]+/pub\?output=csv[^"\']*')404142class SymbioConnector(BaseConnector):43    source_id = "symbio"44    request_delay = 0.84546    # -- découverte de l'URL du CSV dans le bundle JS de l'app « plans » -------47    def _csv_url(self) -> str:48        try:49            html = self.get(IFRAME_URL).text50            m = MAIN_JS_RE.search(html)51            if m:52                js = self.get(f"{APP_BASE}/iframe/{m.group(1)}").text53                m2 = SHEET_RE.search(js)54                if m2:55                    return m2.group(0).split("&date=")[0].rstrip("&")56        except Exception:57            pass58        return FALLBACK_CSV5960    def fetch(self) -> list[Listing]:61        listings: list[Listing] = []62        try:63            raw = self.get(self._csv_url()).text64        except Exception:65            return listings66        for row in csv.DictReader(io.StringIO(raw)):67            status = (row.get("Status") or "").strip().lower()68            unit = (row.get("Unit") or "").strip()69            if status != "available" or not unit:70                continue7172            def num(key: str) -> float | None:73                try:74                    v = float((row.get(key) or "").strip())75                    return v if v > 0 else None76                except ValueError:77                    return None7879            rooms = (row.get("Type") or "").strip()      # « 2 » … « 5 » -> n½80            unit_type = (f"{rooms}½" if rooms.isdigit() else "")81            price, area = num("Price"), num("Area")82            beds, baths = num("Chambers"), num("Bathrooms")83            balcony = num("Balcony")8485            desc = [f"Unité {unit} au Symbio, Terrebonne (Lachenaie)."]86            bits = []87            if area:88                bits.append(f"{area:g} pi²")89            if beds:90                bits.append(f"{beds:g} chambre(s)")91            if baths:92                bits.append(f"{baths:g} salle(s) de bain")93            if balcony:94                bits.append(f"balcon de {balcony:g} pi²")95            if bits:96                desc.append(", ".join(bits).capitalize() + ".")9798            images: list[str] = []99            pdf = (row.get("PDF") or "").strip()100            if pdf:101                images.append(f"{APP_BASE}/iframe/pdf/{pdf}.pdf")102103            listings.append(Listing(104                source=self.source_id,105                external_id=unit,                 # numéro d'unité : stable106                url=PAGE_URL,                     # pas de page publique/unité107                title=f"Symbio — unité {unit}",108                address=ADDRESS,109                sector="Lachenaie",110                city="Terrebonne",111                unit_type=unit_type,112                bedrooms=beds,113                bathrooms=baths,114                price=price,115                price_label=f"{price:.0f} $ /mois" if price else "",116                availability="Disponible",117                area_sqft=area,118                description=" ".join(desc),119                amenities=list(BUILDING_AMENITIES),120                details={"plan_pdf": images[0]} if images else {},121                images=[],                        # plans en PDF, pas de photos/unité122            ))123        return listings124