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/le_george.py : connecteur Le George (legeorge.ca)5# Tour locative au 1001, rue Lucien-L'Allier, centre-ville de Montréal6# (Ville-Marie), studio à 5½. Site JS lourd : le sélecteur d'unités est un7# iframe Planpoint — on interroge directement son API officielle8# (POST app.planpoint.io/api/projects/find, namespace « le-george ») qui9# retourne les 43 étages et ~730 unités. Seules les unités10# « Available » sont annoncées (les « Leased »/« Unavailable » sont11# exclues) : numéro, type (bedrooms « 1.5 » -> 1½, « Studio »), prix, pi²,12# meublé, inclusions FR, galerie photos + plan de l'unité (PDF).13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from ..schema import Listing19from .base import BaseConnector2021SITE = "https://legeorge.ca"22API = "https://app.planpoint.io/api/projects/find"23NAMESPACE = "le-george"24ADDRESS = "1001, rue Lucien-L'Allier, Montréal, QC H3G 0G7"2526_HALF_RE = re.compile(r"^([1-6])[.,]5$")272829class LeGeorgeConnector(BaseConnector):30 source_id = "le_george"31 request_delay = 0.632 max_units = 200 # garde-fou (36 unités dispo à l'écriture)3334 def fetch(self) -> list[Listing]:35 proj = self.post(API, json={36 "namespace": NAMESPACE,37 "hostName": NAMESPACE,38 }).json()39 if not isinstance(proj, dict):40 return []4142 listings: list[Listing] = []43 count = 044 for floor in proj.get("floors") or []:45 floor_name = str(floor.get("name") or "").strip()46 for u in floor.get("units") or []:47 try:48 if (u.get("availability") or "") != "Available":49 continue50 if count >= self.max_units:51 return listings52 count += 153 listings.append(self._unit_listing(u, floor_name))54 except Exception:55 continue56 return listings5758 def _unit_listing(self, u: dict, floor_name: str) -> Listing:59 number = str(u.get("name") or "").strip()60 ext = number or str(u.get("_id") or "")6162 # bedrooms Planpoint : « 1.5 » … « 5.5 » = type QC n½, ou « Studio »63 braw = str(u.get("bedrooms") or "").strip()64 unit_type = ""65 bedrooms = None66 hm = _HALF_RE.match(braw)67 if hm:68 n = int(hm.group(1))69 unit_type = f"{n}½"70 bedrooms = float(max(n - 2, 0))71 elif braw.lower() == "studio":72 unit_type = "Studio"73 bedrooms = 0.07475 try:76 bathrooms = float(u.get("bathrooms")) if u.get("bathrooms") \77 else None78 except (TypeError, ValueError):79 bathrooms = None8081 try:82 price = float(u.get("price")) if u.get("price") else None83 except (TypeError, ValueError):84 price = None85 if price is not None and not (100 <= price <= 20000):86 price = None8788 try:89 sqft = float(u.get("squareFeet")) if u.get("squareFeet") else None90 except (TypeError, ValueError):91 sqft = None9293 # inclusions FR structurées (souvent vides)94 amenities: list[str] = []95 for inc in u.get("inclusionsArr") or []:96 t = (inc.get("fr") or inc.get("en") or "").strip()97 if t and t not in amenities:98 amenities.append(t)99 inc_txt = (u.get("inclusions") or "").strip()100 if inc_txt and inc_txt not in amenities:101 amenities.append(inc_txt)102 amenities = amenities[:25]103104 # photos de l'unité puis plans (layoutGallery)105 images = [x for x in (u.get("images") or []) if x][:20]106 for x in (u.get("layoutGallery") or []):107 if x and x not in images:108 images.append(x)109 images = images[:25]110111 details: dict = {}112 if floor_name:113 details["floor"] = floor_name114 plan = (u.get("downloadableAsset") or "").strip()115 if plan:116 details["floor_plan_pdf"] = plan117118 furnished = u.get("furnished") if isinstance(u.get("furnished"),119 bool) else None120121 title = f"Le George — Unité {number}" if number else "Le George"122 if unit_type:123 title += f" ({unit_type})"124125 return Listing(126 source=self.source_id,127 external_id=f"u{ext}",128 url=f"{SITE}/choisir-mon-unite/",129 title=title,130 address=ADDRESS,131 sector="Ville-Marie",132 city="Montréal",133 unit_type=unit_type,134 bedrooms=bedrooms,135 bathrooms=bathrooms,136 price=price,137 price_label=f"{int(price)} $/mois" if price else "",138 availability="Disponible",139 area_sqft=sqft,140 furnished=furnished,141 description=(u.get("description") or "").strip()[:600],142 amenities=amenities,143 details=details,144 images=images,145 )146