# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/symbio.py : connecteur Symbio (symbiohabitat.ca) — immeuble locatif # de 219 unités (Développement FTG / Claridge) au 1475, rue Yves-Blais, # Terrebonne (Lachenaie). La page /appartements/ embarque une application # React « plans » (iframe wp-content/themes/hello-elementor/iframe) qui lit # son inventaire dans une feuille Google Sheets PUBLIÉE EN CSV : colonnes # Unit, Area (pi²), Type (n -> n½), Chambers, Bathrooms, Balcony (pi²), # Price, Status (Available/Rented), PDF (plan). On découvre l'URL du CSV en # suivant iframe -> main..js (repli : URL relevée 2026-08-25). # Granularité UNITÉ : prix, pi², chambres/sdb, plan PDF par unité. # ----------------------------------------------------------------------------- from __future__ import annotations import csv import io import re from ..schema import Listing from .base import BaseConnector SITE = "https://symbiohabitat.ca" PAGE_URL = f"{SITE}/appartements/" APP_BASE = f"{SITE}/wp-content/themes/hello-elementor" IFRAME_URL = f"{APP_BASE}/iframe?lang=fr" # URL du CSV publié (repli si la découverte via le JS de l'app casse) FALLBACK_CSV = ("https://docs.google.com/spreadsheets/d/e/2PACX-1vSv4yfAI-E4opE" "tejmwNY74SEVrHTbxt34FpyUYiN_xKPHv_Swf2iqi-OzqTa3oMALF39YDLttP" "-WTI/pub?output=csv&gid=0") ADDRESS = "1475, rue Yves-Blais, Terrebonne" BUILDING_AMENITIES = ["Gym", "Chalet urbain avec terrasse", "Bornes de recharge électrique", "Ascenseurs"] MAIN_JS_RE = re.compile(r'src="\./(static/js/main\.[0-9a-f]+\.js)"') SHEET_RE = re.compile( r'https://docs\.google\.com/spreadsheets/d/e/[\w-]+/pub\?output=csv[^"\']*') class SymbioConnector(BaseConnector): source_id = "symbio" request_delay = 0.8 # -- découverte de l'URL du CSV dans le bundle JS de l'app « plans » ------- def _csv_url(self) -> str: try: html = self.get(IFRAME_URL).text m = MAIN_JS_RE.search(html) if m: js = self.get(f"{APP_BASE}/iframe/{m.group(1)}").text m2 = SHEET_RE.search(js) if m2: return m2.group(0).split("&date=")[0].rstrip("&") except Exception: pass return FALLBACK_CSV def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: raw = self.get(self._csv_url()).text except Exception: return listings for row in csv.DictReader(io.StringIO(raw)): status = (row.get("Status") or "").strip().lower() unit = (row.get("Unit") or "").strip() if status != "available" or not unit: continue def num(key: str) -> float | None: try: v = float((row.get(key) or "").strip()) return v if v > 0 else None except ValueError: return None rooms = (row.get("Type") or "").strip() # « 2 » … « 5 » -> n½ unit_type = (f"{rooms}½" if rooms.isdigit() else "") price, area = num("Price"), num("Area") beds, baths = num("Chambers"), num("Bathrooms") balcony = num("Balcony") desc = [f"Unité {unit} au Symbio, Terrebonne (Lachenaie)."] bits = [] if area: bits.append(f"{area:g} pi²") if beds: bits.append(f"{beds:g} chambre(s)") if baths: bits.append(f"{baths:g} salle(s) de bain") if balcony: bits.append(f"balcon de {balcony:g} pi²") if bits: desc.append(", ".join(bits).capitalize() + ".") images: list[str] = [] pdf = (row.get("PDF") or "").strip() if pdf: images.append(f"{APP_BASE}/iframe/pdf/{pdf}.pdf") listings.append(Listing( source=self.source_id, external_id=unit, # numéro d'unité : stable url=PAGE_URL, # pas de page publique/unité title=f"Symbio — unité {unit}", address=ADDRESS, sector="Lachenaie", city="Terrebonne", unit_type=unit_type, bedrooms=beds, bathrooms=baths, price=price, price_label=f"{price:.0f} $ /mois" if price else "", availability="Disponible", area_sqft=area, description=" ".join(desc), amenities=list(BUILDING_AMENITIES), details={"plan_pdf": images[0]} if images else {}, images=[], # plans en PDF, pas de photos/unité )) return listings