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.8 KB · 143 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/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