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.1 KB · 112 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/lsr.py : connecteur LSR GesDev (lsrgesdev.ca — anciennement5#   lsrgesdev.com, redirige) — développeur-gestionnaire de Saint-Lambert.6#   Le site corporatif ne publie aucun prix ; l'inventaire locatif vit sur7#   lenovia.ca (WordPress) avec DEUX backends différents :8#   - NOVIA centre-ville – Longueuil (300, place Charles-Le Moyne) : widget9#     Livya (app.livya.com, client « lsr-gesdev », projet « novia ») — flux10#     RSC Next.js parsé par la base commune LivyaConnector (_livya.py) ;11#   - NOVIA de la Gare – Candiac (125, rue des Marronniers) : widget Planpoint12#     (app.planpoint.io/125-marronniers/125-marronniers) — API JSON publique13#     via le helper _planpoint.py.14#   Granularité = UNITÉ pour les deux projets (prix, pi², chambres, étage).15#   Roomî (espaceroomi.ca, résidence étudiante Trois-Rivières) exclu.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import html as _html20import re2122from ..schema import Listing23from ._livya import LivyaConnector24from ._planpoint import planpoint_projects, unit_listing2526CANDIAC_PAGE = "https://lenovia.ca/novia-de-la-gare-candiac/plans/"27CANDIAC_NAMESPACE = "125-marronniers"28CANDIAC_HOST = "125-marronniers"2930# prix planchers par typologie publiés sur lenovia.ca (Livya n'expose aucun31# prix pour Longueuil) : « 3 ½ (de 541 pi2 à 769 pi2) à partir de 1 950$ /mois »32LONGUEUIL_APTS = "https://lenovia.ca/novia-centre-ville-longueuil/appartements/"33TYPE_TOKEN_RE = re.compile(r"Studio|\d\s*½|Maison de ville", re.I)34FROM_PRICE_RE = re.compile(35    r"à\s+partir\s+de\s+([\d\s  ]{3,9})\$\s*/?\s*mois", re.I)36SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?</\1>", re.I)37TAG_RE = re.compile(r"<[^>]+>")383940class LSRConnector(LivyaConnector):41    source_id = "lsr"42    request_delay = 0.84344    # -- NOVIA centre-ville (Longueuil) via Livya --------------------------------45    client = "lsr-gesdev"46    brand = "NOVIA centre-ville"47    site_url = "https://www.lsrgesdev.ca"48    default_city = "Longueuil"49    default_projects = ["novia"]5051    def fetch(self) -> list[Listing]:52        listings = super().fetch()           # Longueuil (Livya)53        floors = self._longueuil_floor_prices()54        for lst in listings:55            lst.city = re.sub(r"^Ville de\s+", "", lst.city or "")56            # Livya ne publie aucun loyer pour Longueuil : on rattache le prix57            # plancher « à partir de » publié par lenovia.ca pour la typologie58            if lst.price is None and not lst.price_label:59                base = floors.get(lst.unit_type)60                if base:61                    lst.price_label = f"à partir de {int(base)} $ /mois"62        listings.extend(self._candiac())     # Candiac (Planpoint)63        return listings6465    def _longueuil_floor_prices(self) -> dict[str, float]:66        """{'3½': 1950.0, …} depuis la page publique « Appartements »."""67        try:68            html = self.get(LONGUEUIL_APTS).text69        except Exception:70            return {}71        flat = re.sub(r"[\s  ]+", " ",72                      TAG_RE.sub(" | ", _html.unescape(SCRIPT_RE.sub(" ", html))))73        # segment « typologie -> texte jusqu'à la typologie suivante » pour ne74        # jamais rattacher le prix d'un 3½ au Studio « Complet » qui le précède75        tokens = list(TYPE_TOKEN_RE.finditer(flat))76        prices: dict[str, float] = {}77        for i, tok in enumerate(tokens):78            end = tokens[i + 1].start() if i + 1 < len(tokens) else len(flat)79            seg = flat[tok.end():min(end, tok.end() + 200)]80            pm = FROM_PRICE_RE.search(seg)81            if not pm:82                continue83            label = re.sub(r"\s+", "", tok.group(0))84            key = "Studio" if label.lower() == "studio" else label85            if key.lower() == "maisondeville":86                continue                     # typologie non « n½ » : ignorée87            try:88                val = float(re.sub(r"[\s  ]", "", pm.group(1)))89            except ValueError:90                continue91            if 500 <= val <= 8000:92                prices.setdefault(key, val)93        return prices9495    # -- NOVIA de la Gare (Candiac) via Planpoint --------------------------------96    def _candiac(self) -> list[Listing]:97        try:98            projects = planpoint_projects(self, "projects",99                                          CANDIAC_NAMESPACE, CANDIAC_HOST)100        except Exception:101            return []102        listings: list[Listing] = []103        for project in projects:104            for floor in project.get("floors") or []:105                for unit in floor.get("units") or []:106                    lst = unit_listing(self.source_id, project, floor, unit,107                                       page_url=CANDIAC_PAGE,108                                       default_city="Candiac")109                    if lst is not None:110                        listings.append(lst)111        return listings112