# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/gestion_isr.py : connecteur Gestion ISR (gestion-isr.com) # Le site vitrine (SPA Vite/React) renvoie vers le portail public # location.gestion-isr.com, propulsé par Supabase. On lit l'URL et la clé # anonyme publiques embarquées dans le HTML du portail, puis l'API REST # /rest/v1/listings?statut=eq.Actif livre tout le parc actif en JSON # structuré (loyer, type, ville, GPS, photos, unités par immeuble). # 2 requêtes par sync ; lien profond ?fiche= par annonce. # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Listing, normalize_unit_type, strip_accents from .base import BaseConnector PORTAL = "https://location.gestion-isr.com/" _URL_RE = re.compile(r"SUPABASE_URL\s*=\s*'([^']+)'") _KEY_RE = re.compile(r"SUPABASE_ANON\s*=\s*'([^']+)'") def _pets_value(raw: str) -> str | None: """Champ « animaux » structuré -> oui/non/conditions (jamais deviné).""" k = strip_accents((raw or "").strip().lower()) if not k: return None if k in ("aucun", "non") or k.startswith("non") or "refus" in k: return "non" if "animaux acceptes" in k or k in ("oui",): return "oui" # « Chat », « Chat, petit chien », « Petits compagnons »… = sous conditions if re.search(r"chat|chien|compagnon|condition|accepte", k): return "conditions" return None class GestionIsrConnector(BaseConnector): source_id = "gestion_isr" request_delay = 0.6 def fetch(self) -> list[Listing]: html = self.get(PORTAL).text m_url, m_key = _URL_RE.search(html), _KEY_RE.search(html) if not (m_url and m_key): raise RuntimeError("clé/URL Supabase introuvables dans le portail") base, key = m_url.group(1).rstrip("/"), m_key.group(1) rows = self.get( f"{base}/rest/v1/listings?select=*&statut=eq.Actif", headers={"apikey": key, "Authorization": f"Bearer {key}"}, ).json() listings: list[Listing] = [] for row in rows: try: lst = self._parse_row(row) except Exception: continue if lst: listings.append(lst) return listings def _parse_row(self, row: dict) -> Listing | None: ext_id = str(row.get("pk") or "") title = (row.get("titre") or "").strip() if not ext_id or not title: return None # exclusions : locaux commerciaux, stationnements, rangements if re.search(r"commercial|bureau|stationnement|rangement|entrep[ôo]t", title, re.I): return None id_app = (row.get("id_app") or "").strip() url = f"{PORTAL}?fiche={id_app}" if id_app else f"{PORTAL}#{ext_id}" # loyer structuré (le plus bas des unités dispo pour un immeuble) loyer = row.get("loyer") price = float(loyer) if isinstance(loyer, (int, float)) else None # GPS : champ « adresse_geo » = "45.754…, -72.501…" lat = lng = None geo = (row.get("adresse_geo") or "").split(",") if len(geo) == 2: try: lat, lng = float(geo[0]), float(geo[1]) except ValueError: lat = lng = None # description source + inventaire des unités disponibles (immeubles) description = (row.get("description") or "").strip() units = row.get("units") or [] dispo = [u for u in units if (u or {}).get("statut") == "Disponible"] if dispo: groups: dict[tuple, int] = {} for u in dispo: groups[(u.get("type") or "", u.get("prix"))] = \ groups.get((u.get("type") or "", u.get("prix")), 0) + 1 lines = [f"• {n} × {t} à {p} $" if n > 1 else f"• {t} à {p} $" for (t, p), n in sorted(groups.items()) if t and p] if lines: description += "\n\nUnités disponibles :\n" + "\n".join(lines) images = [u for u in (row.get("photos") or []) if str(u).startswith("http")] if not images: # repli : albums par modèle d'unité (photo_variants), puis main_photo for var in row.get("photo_variants") or []: for u in ([var.get("main")] + list(var.get("photos") or [])): if str(u or "").startswith("http") and u not in images: images.append(u) if not images and str(row.get("main_photo") or "").startswith("http"): images = [row["main_photo"]] return Listing( source=self.source_id, external_id=ext_id, url=url, title=title, address=(row.get("id") or "").strip(), # « id » = adresse civique complète sector=(row.get("secteur") or "").strip(), city=(row.get("ville") or "").strip(), unit_type=normalize_unit_type(row.get("grandeur") or ""), price=price, availability=(row.get("date_dispo_affichage") or row.get("date_dispo") or "").strip(), pets=_pets_value(row.get("animaux") or ""), description=description[:2000], images=images[:30], lat=lat, lng=lng, )