# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/samcon.py : connecteur Samcon (samcon.ca) — condos LOCATIFS # seulement (section « Projects for rent » ; les projets en vente sont # exclus). Le site WordPress liste les immeubles locatifs sur # /projects-for-rent/ ; chaque fiche projet embarque le widget Planpoint # (app.planpoint.io/samcon/). On rejoue l'API officielle du widget # POST https://app.planpoint.io/api/projects/find {namespace, hostName} # (HTTP 201) qui retourne le projet complet : adresse, GPS, étages et unités # {name, bedrooms « 1 bedroom », price, squareFeet, availability, images…}. # Seules les unités « Available » sont retenues (« Sold » = louée). # Une annonce par unité disponible ; external_id = -. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://www.samcon.ca" RENT_URL = f"{BASE}/projects-for-rent/" PLANPOINT_API = "https://app.planpoint.io/api/projects/find" HOST_RE = re.compile(r"app\.planpoint\.io/samcon/([\w-]+)") class SamconConnector(BaseConnector): source_id = "samcon" request_delay = 0.8 def fetch(self) -> list[Listing]: html = self.get(RENT_URL).text soup = BeautifulSoup(html, "html.parser") main = soup.find("main") or soup # fiches projets locatifs (cartes de la page « Projects for rent ») projects = {} # url fiche -> nom affiché for a in main.find_all("a", href=True): href = a["href"] text = a.get_text(" ", strip=True) if "samcon.ca" not in href or not text \ or text.lower() == "view more details": continue projects.setdefault(href, text) listings: list[Listing] = [] for page_url, page_name in projects.items(): try: page = self.get(page_url).text except Exception: continue m = HOST_RE.search(page) if not m: # pas de widget Planpoint : ignorer continue try: listings.extend(self._project(m.group(1), page_url, page_name)) except Exception: continue return listings def _project(self, host: str, page_url: str, page_name: str) -> list[Listing]: resp = self.post(PLANPOINT_API, json={"namespace": "samcon", "hostName": host}) proj = resp.json() if isinstance(proj, list): proj = proj[0] if proj else {} name = (proj.get("name") or page_name).strip() address = (proj.get("address") or "").replace(", Canada", "").strip() try: lat = float(proj["lat"]) if proj.get("lat") else None lng = float(proj["lon"]) if proj.get("lon") else None except (TypeError, ValueError): lat = lng = None proj_images = [u for u in (proj.get("images") or []) if isinstance(u, str) and u.startswith("http")] listings: list[Listing] = [] for floor in proj.get("floors") or []: floor_name = (floor.get("name") or "").strip() for u in floor.get("units") or []: if (u.get("availability") or "").strip() != "Available": continue # « Sold » = unité louée unum = str(u.get("name") or u.get("_id") or "").strip() if not unum: continue bed_raw = (u.get("bedrooms") or "").strip() price = u.get("price") try: price = float(price) if price else None except (TypeError, ValueError): price = None area = None try: area = float(u["squareFeet"]) if u.get("squareFeet") \ else None except (TypeError, ValueError): pass images = [i for i in (u.get("images") or []) if isinstance(i, str) and i.startswith("http")] \ or proj_images amenities = [str(x).strip() for x in (u.get("inclusionsArr") or []) if x] if floor_name: amenities.append(f"Étage : {floor_name}") furnished = u.get("furnished") \ if isinstance(u.get("furnished"), bool) else None avail = "Disponible" if (u.get("deliveryDate") or "").strip(): avail = f"Disponible {u['deliveryDate'].strip()}" listings.append(Listing( source=self.source_id, external_id=f"{host}-{unum}", url=page_url, title=f"{name} — {bed_raw or 'unité'} · unité {unum}", address=address, sector="", city="Montréal", unit_type=normalize_unit_type(bed_raw), price=price, price_label=f"{price:.0f} $" if price else "", availability=avail, area_sqft=area, furnished=furnished, description=(u.get("description") or "").strip() if isinstance(u.get("description"), str) else "", amenities=amenities, images=images[:12], lat=lat, lng=lng, )) return listings