# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/le_george.py : connecteur Le George (legeorge.ca) # Tour locative au 1001, rue Lucien-L'Allier, centre-ville de Montréal # (Ville-Marie), studio à 5½. Site JS lourd : le sélecteur d'unités est un # iframe Planpoint — on interroge directement son API officielle # (POST app.planpoint.io/api/projects/find, namespace « le-george ») qui # retourne les 43 étages et ~730 unités. Seules les unités # « Available » sont annoncées (les « Leased »/« Unavailable » sont # exclues) : numéro, type (bedrooms « 1.5 » -> 1½, « Studio »), prix, pi², # meublé, inclusions FR, galerie photos + plan de l'unité (PDF). # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Listing from .base import BaseConnector SITE = "https://legeorge.ca" API = "https://app.planpoint.io/api/projects/find" NAMESPACE = "le-george" ADDRESS = "1001, rue Lucien-L'Allier, Montréal, QC H3G 0G7" _HALF_RE = re.compile(r"^([1-6])[.,]5$") class LeGeorgeConnector(BaseConnector): source_id = "le_george" request_delay = 0.6 max_units = 200 # garde-fou (36 unités dispo à l'écriture) def fetch(self) -> list[Listing]: proj = self.post(API, json={ "namespace": NAMESPACE, "hostName": NAMESPACE, }).json() if not isinstance(proj, dict): return [] listings: list[Listing] = [] count = 0 for floor in proj.get("floors") or []: floor_name = str(floor.get("name") or "").strip() for u in floor.get("units") or []: try: if (u.get("availability") or "") != "Available": continue if count >= self.max_units: return listings count += 1 listings.append(self._unit_listing(u, floor_name)) except Exception: continue return listings def _unit_listing(self, u: dict, floor_name: str) -> Listing: number = str(u.get("name") or "").strip() ext = number or str(u.get("_id") or "") # bedrooms Planpoint : « 1.5 » … « 5.5 » = type QC n½, ou « Studio » braw = str(u.get("bedrooms") or "").strip() unit_type = "" bedrooms = None hm = _HALF_RE.match(braw) if hm: n = int(hm.group(1)) unit_type = f"{n}½" bedrooms = float(max(n - 2, 0)) elif braw.lower() == "studio": unit_type = "Studio" bedrooms = 0.0 try: bathrooms = float(u.get("bathrooms")) if u.get("bathrooms") \ else None except (TypeError, ValueError): bathrooms = None try: price = float(u.get("price")) if u.get("price") else None except (TypeError, ValueError): price = None if price is not None and not (100 <= price <= 20000): price = None try: sqft = float(u.get("squareFeet")) if u.get("squareFeet") else None except (TypeError, ValueError): sqft = None # inclusions FR structurées (souvent vides) amenities: list[str] = [] for inc in u.get("inclusionsArr") or []: t = (inc.get("fr") or inc.get("en") or "").strip() if t and t not in amenities: amenities.append(t) inc_txt = (u.get("inclusions") or "").strip() if inc_txt and inc_txt not in amenities: amenities.append(inc_txt) amenities = amenities[:25] # photos de l'unité puis plans (layoutGallery) images = [x for x in (u.get("images") or []) if x][:20] for x in (u.get("layoutGallery") or []): if x and x not in images: images.append(x) images = images[:25] details: dict = {} if floor_name: details["floor"] = floor_name plan = (u.get("downloadableAsset") or "").strip() if plan: details["floor_plan_pdf"] = plan furnished = u.get("furnished") if isinstance(u.get("furnished"), bool) else None title = f"Le George — Unité {number}" if number else "Le George" if unit_type: title += f" ({unit_type})" return Listing( source=self.source_id, external_id=f"u{ext}", url=f"{SITE}/choisir-mon-unite/", title=title, address=ADDRESS, sector="Ville-Marie", city="Montréal", unit_type=unit_type, bedrooms=bedrooms, bathrooms=bathrooms, price=price, price_label=f"{int(price)} $/mois" if price else "", availability="Disponible", area_sqft=sqft, furnished=furnished, description=(u.get("description") or "").strip()[:600], amenities=amenities, details=details, images=images, )