SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.3 KB · 146 lines python
Raw Blame History
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