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.9 KB · 136 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/samcon.py : connecteur Samcon (samcon.ca) — condos LOCATIFS5#   seulement (section « Projects for rent » ; les projets en vente sont6#   exclus). Le site WordPress liste les immeubles locatifs sur7#   /projects-for-rent/ ; chaque fiche projet embarque le widget Planpoint8#   (app.planpoint.io/samcon/<hostName>). On rejoue l'API officielle du widget9#   POST https://app.planpoint.io/api/projects/find {namespace, hostName}10#   (HTTP 201) qui retourne le projet complet : adresse, GPS, étages et unités11#   {name, bedrooms « 1 bedroom », price, squareFeet, availability, images…}.12#   Seules les unités « Available » sont retenues (« Sold » = louée).13#   Une annonce par unité disponible ; external_id = <hostName>-<no d'unité>.14# -----------------------------------------------------------------------------15from __future__ import annotations1617import re1819from bs4 import BeautifulSoup2021from ..schema import Listing, normalize_unit_type22from .base import BaseConnector2324BASE = "https://www.samcon.ca"25RENT_URL = f"{BASE}/projects-for-rent/"26PLANPOINT_API = "https://app.planpoint.io/api/projects/find"2728HOST_RE = re.compile(r"app\.planpoint\.io/samcon/([\w-]+)")293031class SamconConnector(BaseConnector):32    source_id = "samcon"33    request_delay = 0.83435    def fetch(self) -> list[Listing]:36        html = self.get(RENT_URL).text37        soup = BeautifulSoup(html, "html.parser")38        main = soup.find("main") or soup39        # fiches projets locatifs (cartes de la page « Projects for rent »)40        projects = {}                    # url fiche -> nom affiché41        for a in main.find_all("a", href=True):42            href = a["href"]43            text = a.get_text(" ", strip=True)44            if "samcon.ca" not in href or not text \45                    or text.lower() == "view more details":46                continue47            projects.setdefault(href, text)4849        listings: list[Listing] = []50        for page_url, page_name in projects.items():51            try:52                page = self.get(page_url).text53            except Exception:54                continue55            m = HOST_RE.search(page)56            if not m:                    # pas de widget Planpoint : ignorer57                continue58            try:59                listings.extend(self._project(m.group(1), page_url, page_name))60            except Exception:61                continue62        return listings6364    def _project(self, host: str, page_url: str,65                 page_name: str) -> list[Listing]:66        resp = self.post(PLANPOINT_API, json={"namespace": "samcon",67                                              "hostName": host})68        proj = resp.json()69        if isinstance(proj, list):70            proj = proj[0] if proj else {}71        name = (proj.get("name") or page_name).strip()72        address = (proj.get("address") or "").replace(", Canada", "").strip()73        try:74            lat = float(proj["lat"]) if proj.get("lat") else None75            lng = float(proj["lon"]) if proj.get("lon") else None76        except (TypeError, ValueError):77            lat = lng = None78        proj_images = [u for u in (proj.get("images") or [])79                       if isinstance(u, str) and u.startswith("http")]8081        listings: list[Listing] = []82        for floor in proj.get("floors") or []:83            floor_name = (floor.get("name") or "").strip()84            for u in floor.get("units") or []:85                if (u.get("availability") or "").strip() != "Available":86                    continue             # « Sold » = unité louée87                unum = str(u.get("name") or u.get("_id") or "").strip()88                if not unum:89                    continue90                bed_raw = (u.get("bedrooms") or "").strip()91                price = u.get("price")92                try:93                    price = float(price) if price else None94                except (TypeError, ValueError):95                    price = None96                area = None97                try:98                    area = float(u["squareFeet"]) if u.get("squareFeet") \99                        else None100                except (TypeError, ValueError):101                    pass102                images = [i for i in (u.get("images") or [])103                          if isinstance(i, str) and i.startswith("http")] \104                    or proj_images105                amenities = [str(x).strip() for x in106                             (u.get("inclusionsArr") or []) if x]107                if floor_name:108                    amenities.append(f"Étage : {floor_name}")109                furnished = u.get("furnished") \110                    if isinstance(u.get("furnished"), bool) else None111                avail = "Disponible"112                if (u.get("deliveryDate") or "").strip():113                    avail = f"Disponible {u['deliveryDate'].strip()}"114                listings.append(Listing(115                    source=self.source_id,116                    external_id=f"{host}-{unum}",117                    url=page_url,118                    title=f"{name} — {bed_raw or 'unité'} · unité {unum}",119                    address=address,120                    sector="",121                    city="Montréal",122                    unit_type=normalize_unit_type(bed_raw),123                    price=price,124                    price_label=f"{price:.0f} $" if price else "",125                    availability=avail,126                    area_sqft=area,127                    furnished=furnished,128                    description=(u.get("description") or "").strip()129                    if isinstance(u.get("description"), str) else "",130                    amenities=amenities,131                    images=images[:12],132                    lat=lat,133                    lng=lng,134                ))135        return listings136