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/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