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/san_leon.py : connecteur San Leon (sanleon.ca)5# Appartements locatifs neufs au Faubourg Boisbriand — 3230, avenue des6# Grandes Tourelles, Boisbriand (phases 4 et 5, ~118 unités/phase).7# Le site embarque Planpoint : l'API Next.js répond en JSON à8# POST https://app.planpoint.io/api/groups/find9# body {"namespace":"san-leon","hostName":"san-leon"} → projects[] → floors[]10# → units[] (name, bedrooms, bathrooms, squareFeet, price — None en phase 5,11# availability Available/Sold/Leased/Reserved, inclusions, images, delivery).12# On ne garde que les unités « Available ». Granularité : unité.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re17from datetime import date as _date1819from ..schema import Listing2021from .base import BaseConnector2223API_URL = "https://app.planpoint.io/api/groups/find"24API_BODY = {"namespace": "san-leon", "hostName": "san-leon"}25SITE_URL = "https://sanleon.ca/"2627ADDRESS = "3230, avenue des Grandes Tourelles, Boisbriand"28CITY = "Boisbriand"2930SLUG_RE = re.compile(r"[^a-z0-9]+")31# « 11/1/26 » ou « 2025-01-01 »32US_DATE_RE = re.compile(r"^(\d{1,2})/(\d{1,2})/(\d{2})$")333435class SanLeonConnector(BaseConnector):36 source_id = "san_leon"37 request_delay = 0.638 max_images = 103940 @staticmethod41 def _unit_type(bedrooms: str | int | None) -> str:42 try:43 n = int(str(bedrooms).strip())44 except (TypeError, ValueError):45 return ""46 return f"{n + 2}½" if 0 < n < 5 else ("Studio" if n == 0 else "")4748 def fetch(self) -> list[Listing]:49 data = self.post(API_URL, json=API_BODY).json()5051 listings: list[Listing] = []52 seen: set[str] = set()53 for proj in data.get("projects") or []:54 pname = (proj.get("name") or "San Leon").strip()55 phase = pname.rsplit(" ", 1)[-1] if "Phase" in pname else ""56 pslug = SLUG_RE.sub("-", pname.lower()).strip("-")57 addr = (proj.get("address") or "").strip() or ADDRESS5859 for floor in proj.get("floors") or []:60 fname = str(floor.get("name") or "").strip()61 for u in floor.get("units") or []:62 if (u.get("availability") or "").strip() != "Available":63 continue64 num = str(u.get("name") or "").strip()65 if not num:66 continue67 ext_id = f"{pslug}-{num}"68 if ext_id in seen:69 continue70 seen.add(ext_id)7172 price = None73 try:74 v = float(u.get("price") or 0)75 if 300 <= v <= 20000:76 price = v77 except (TypeError, ValueError):78 pass7980 area = None81 try:82 v = float(u.get("squareFeet") or 0)83 if 100 <= v <= 10000:84 area = v85 except (TypeError, ValueError):86 pass8788 beds = None89 try:90 beds = float(int(str(u.get("bedrooms")).strip()))91 except (TypeError, ValueError):92 pass93 baths = None94 try:95 baths = float(u.get("bathrooms") or 0) or None96 except (TypeError, ValueError):97 pass9899 availability = "Disponible"100 delivery = str(u.get("deliveryDate") or "").strip()101 m = US_DATE_RE.match(delivery)102 if m:103 delivery = f"20{m.group(3)}-{int(m.group(1)):02d}" \104 f"-{int(m.group(2)):02d}"105 if delivery and delivery > _date.today().isoformat():106 availability = f"Disponible ({delivery})"107108 amenities = [a.strip() for a in109 re.split(r"[,;]", u.get("inclusions") or "")110 if a.strip() and a.strip() != "*"]111112 imgs = [i for i in113 dict.fromkeys(u.get("images") or [])114 if isinstance(i, str) and i.startswith("http")115 ][: self.max_images]116117 desc_bits = [118 f"Unité {num}" + (f", étage {fname}" if fname else ""),119 pname if phase else "",120 f"Orientation {u['orientation']}"121 if u.get("orientation") else "",122 "Meublé" if u.get("furnished") else "",123 ]124 listings.append(Listing(125 source=self.source_id,126 external_id=ext_id,127 url=SITE_URL,128 title=f"{pname} — Unité {num}",129 address=addr,130 city=CITY,131 unit_type=self._unit_type(u.get("bedrooms")),132 bedrooms=beds,133 bathrooms=baths,134 price=price,135 price_label="" if price else "Prix sur demande",136 availability=availability,137 area_sqft=area,138 description=" — ".join(b for b in desc_bits if b),139 amenities=amenities,140 images=imgs,141 ))142 return listings143