# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/simplex.py : connecteur Simplex Immobilier (simpleximmobilier.com) # — agence/gestionnaire de ~600 logements à Trois-Rivières, Shawinigan et en # Mauricie (~25-30 annonces publiées à la fois). Site Astro dont le catalogue # vit dans un Supabase public : la RPC `get_public_rental_listings_page` # (clé publiable `sb_publishable_…` embarquée dans le bundle JS du site) # retourne TOUT en 1-2 requêtes JSON : titre, ville/secteur, type d'unité, # chambres/sdb, loyer, statut (available/coming_soon/reserved/rented), # date de disponibilité, description, inclusions, animaux, stationnement, # photos (bucket public `rental-listing-media`). # ⚠️ Aucune adresse civique publiée (même la RPC détail n'en expose pas) : # ville/secteur seulement, donc pas de géocodage fin possible. # ----------------------------------------------------------------------------- from __future__ import annotations import json import os from ..schema import Listing from .base import BaseConnector SB_URL = "https://tyrzkvwbiaadajwopdrj.supabase.co" RPC_URL = f"{SB_URL}/rest/v1/rpc/get_public_rental_listings_page" STORAGE_URL = f"{SB_URL}/storage/v1/object/public" SITE = "https://simpleximmobilier.com" # clé PUBLIABLE (anon) du site — surchargeable si Simplex la fait tourner API_KEY = os.environ.get( "LOUKA_SIMPLEX_KEY", "sb_publishable_HT7wke-8-o_TUxMUeLjWbw_wsVK1FC4") PAGE = 100 _KEEP = {"available", "coming_soon"} _STATUS_FR = {"available": "Disponible", "coming_soon": "Bientôt disponible"} _PETS = {"oui": "oui", "non": "non"} class SimplexConnector(BaseConnector): source_id = "simplex" request_delay = 0.8 def _rpc(self, offset: int) -> list[dict]: resp = self.post(RPC_URL, data=json.dumps({ "p_search": None, "p_status": None, "p_location": None, "p_unit_type": None, "p_max_price": None, "p_pets": None, "p_parking": None, "p_sort": "recommended", "p_offset": offset, "p_limit": PAGE, }), headers={"apikey": API_KEY, "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}) data = resp.json() return data if isinstance(data, list) else [] def fetch(self) -> list[Listing]: rows: list[dict] = [] offset = 0 while True: batch = self._rpc(offset) rows.extend(batch) total = batch[0].get("total_count", 0) if batch else 0 offset += PAGE if not batch or offset >= total: break listings: dict[str, Listing] = {} for row in rows: if row.get("availability_status") not in _KEEP: continue lst = self._listing(row) if lst and lst.uid not in listings: listings[lst.uid] = lst return list(listings.values()) def _images(self, row: dict) -> list[str]: media = sorted(row.get("media") or [], key=lambda m: (not m.get("isPrimary"), m.get("sortOrder") or 0)) out = [] for m in media: if m.get("bucket") and m.get("path"): out.append(f"{STORAGE_URL}/{m['bucket']}/{m['path']}") for p in [row.get("primary_image_path"), *(row.get("gallery_paths") or [])]: if p and p.startswith("http") and p not in out: out.append(p) return out[:20] def _listing(self, row: dict) -> Listing | None: price = row.get("rent_amount") status = row.get("availability_status") or "" avail_txt = _STATUS_FR.get(status, "") avail_date = (row.get("available_from") or "")[:10] if status == "coming_soon" and avail_date: avail_txt = f"{avail_txt} — à compter du {avail_date}" pets_raw = (row.get("pets_policy") or "").strip().lower() amen = [a for a in (row.get("inclusions") or []) + (row.get("features") or []) + (row.get("labels") or []) if a] details = {} if row.get("parking"): details["stationnement"] = str(row["parking"]).strip() if row.get("public_reference"): details["référence"] = row["public_reference"] if row.get("rent_note"): details["note loyer"] = row["rent_note"] return Listing( source=self.source_id, external_id=str(row.get("id") or row.get("slug")), url=f"{SITE}/logements-a-louer/{row.get('slug', '')}/", title=(row.get("title") or "").strip(), sector=(row.get("sector") or "").strip(), city=(row.get("city") or "").strip(), unit_type=(row.get("unit_type") or "").strip(), bedrooms=(float(row["bedrooms"]) if row.get("bedrooms") is not None else None), bathrooms=(float(row["bathrooms"]) if row.get("bathrooms") is not None else None), price=float(price) if price else None, price_label=(f"{price:,.0f} $/mois".replace(",", " ") if price else ""), availability=avail_txt, availability_date=("now" if status == "available" else (avail_date or None)), pets=_PETS.get(pets_raw.rstrip(" .!")), description=(row.get("description") or "")[:2000], amenities=amen[:30], details=details, images=self._images(row), )