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/samcon.py : connecteur Samcon (samcon.ca) — condos LOCATIFS5# seulement (section « Projects for rent » ; les projets en vente sont6# exclus). Le site WordPress liste les immeubles locatifs sur7# /projects-for-rent/ ; chaque fiche projet embarque le widget Planpoint8# (app.planpoint.io/samcon/<hostName>). On rejoue l'API officielle du widget9# POST https://app.planpoint.io/api/projects/find {namespace, hostName}10# (HTTP 201) qui retourne le projet complet : adresse, GPS, étages et unités11# {name, bedrooms « 1 bedroom », price, squareFeet, availability, images…}.12# Seules les unités « Available » sont retenues (« Sold » = louée).13# Une annonce par unité disponible ; external_id = <hostName>-<no d'unité>.14# -----------------------------------------------------------------------------15from __future__ import annotations1617import re1819from bs4 import BeautifulSoup2021from ..schema import Listing, normalize_unit_type22from .base import BaseConnector2324BASE = "https://www.samcon.ca"25RENT_URL = f"{BASE}/projects-for-rent/"26PLANPOINT_API = "https://app.planpoint.io/api/projects/find"2728HOST_RE = re.compile(r"app\.planpoint\.io/samcon/([\w-]+)")293031class SamconConnector(BaseConnector):32 source_id = "samcon"33 request_delay = 0.83435 def fetch(self) -> list[Listing]:36 html = self.get(RENT_URL).text37 soup = BeautifulSoup(html, "html.parser")38 main = soup.find("main") or soup39 # fiches projets locatifs (cartes de la page « Projects for rent »)40 projects = {} # url fiche -> nom affiché41 for a in main.find_all("a", href=True):42 href = a["href"]43 text = a.get_text(" ", strip=True)44 if "samcon.ca" not in href or not text \45 or text.lower() == "view more details":46 continue47 projects.setdefault(href, text)4849 listings: list[Listing] = []50 for page_url, page_name in projects.items():51 try:52 page = self.get(page_url).text53 except Exception:54 continue55 m = HOST_RE.search(page)56 if not m: # pas de widget Planpoint : ignorer57 continue58 try:59 listings.extend(self._project(m.group(1), page_url, page_name))60 except Exception:61 continue62 return listings6364 def _project(self, host: str, page_url: str,65 page_name: str) -> list[Listing]:66 resp = self.post(PLANPOINT_API, json={"namespace": "samcon",67 "hostName": host})68 proj = resp.json()69 if isinstance(proj, list):70 proj = proj[0] if proj else {}71 name = (proj.get("name") or page_name).strip()72 address = (proj.get("address") or "").replace(", Canada", "").strip()73 try:74 lat = float(proj["lat"]) if proj.get("lat") else None75 lng = float(proj["lon"]) if proj.get("lon") else None76 except (TypeError, ValueError):77 lat = lng = None78 proj_images = [u for u in (proj.get("images") or [])79 if isinstance(u, str) and u.startswith("http")]8081 listings: list[Listing] = []82 for floor in proj.get("floors") or []:83 floor_name = (floor.get("name") or "").strip()84 for u in floor.get("units") or []:85 if (u.get("availability") or "").strip() != "Available":86 continue # « Sold » = unité louée87 unum = str(u.get("name") or u.get("_id") or "").strip()88 if not unum:89 continue90 bed_raw = (u.get("bedrooms") or "").strip()91 price = u.get("price")92 try:93 price = float(price) if price else None94 except (TypeError, ValueError):95 price = None96 area = None97 try:98 area = float(u["squareFeet"]) if u.get("squareFeet") \99 else None100 except (TypeError, ValueError):101 pass102 images = [i for i in (u.get("images") or [])103 if isinstance(i, str) and i.startswith("http")] \104 or proj_images105 amenities = [str(x).strip() for x in106 (u.get("inclusionsArr") or []) if x]107 if floor_name:108 amenities.append(f"Étage : {floor_name}")109 furnished = u.get("furnished") \110 if isinstance(u.get("furnished"), bool) else None111 avail = "Disponible"112 if (u.get("deliveryDate") or "").strip():113 avail = f"Disponible {u['deliveryDate'].strip()}"114 listings.append(Listing(115 source=self.source_id,116 external_id=f"{host}-{unum}",117 url=page_url,118 title=f"{name} — {bed_raw or 'unité'} · unité {unum}",119 address=address,120 sector="",121 city="Montréal",122 unit_type=normalize_unit_type(bed_raw),123 price=price,124 price_label=f"{price:.0f} $" if price else "",125 availability=avail,126 area_sqft=area,127 furnished=furnished,128 description=(u.get("description") or "").strip()129 if isinstance(u.get("description"), str) else "",130 amenities=amenities,131 images=images[:12],132 lat=lat,133 lng=lng,134 ))135 return listings136