SPB Git

spb/lou-ka Public

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

HTML 99.7%
5.9 KB · 140 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/floria.py : connecteur Gestion Floria (gestionfloria.ca)5#   Condos locatifs neufs à Sherbrooke, Magog (Mont-Orford) et Coaticook.6#   Site WordPress (Avada) : chaque page /immeuble/<slug>/ embarque le module7#   de plans interactifs Livya (app.livya.com, client « gestion-floria », un8#   projet/entité par immeuble). Même patron que somex_saintnicolas/louis14 :9#   la page Next.js du module est rendue serveur et son flux RSC contient10#   l'inventaire JSON complet (numéro, statut, loyer, pièces, superficie,11#   adresse, ville, GPS, photos, plans). Découverte des immeubles via la page12#   /secteurs/. Certains immeubles partagent le même projet Livya (jumelés13#   52/58 Flanc-Sud, 753/765 Rachel-Hébert…) : dédoublonnage par unitId.14#   Seules les unités AVAILABLE deviennent des annonces ; certains projets ne15#   publient pas le loyer (rentalPrice = 0) -> prix absent, rien d'inventé.16# -----------------------------------------------------------------------------17from __future__ import annotations1819import re2021from ..schema import Listing22from .base import BaseConnector23from .somex_saintnicolas import _livya_units, _unit_type2425BASE = "https://gestionfloria.ca"26SECTORS_URL = f"{BASE}/secteurs/"27LIVYA = "https://app.livya.com"2829_IMMEUBLE_RE = re.compile(r"https://gestionfloria\.ca/immeuble/[a-z0-9-]+/")30_MODULE_RE = re.compile(31    r'<div class="livya-module-container-plans"[^>]*'32    r'data-project="([^"]+)"[^>]*data-entity="([^"]+)"')33_CLIENT_RE = re.compile(r'data-client="([^"]+)"')343536class FloriaConnector(BaseConnector):37    source_id = "floria"38    request_delay = 0.739    max_buildings = 20      # garde-fou (12 immeubles publiés)4041    def fetch(self) -> list[Listing]:42        # 1) Découverte : pages immeuble listées sur /secteurs/43        sec = self.get(SECTORS_URL).text44        pages = sorted(set(_IMMEUBLE_RE.findall(sec)))4546        listings: dict[str, Listing] = {}47        for page_url in pages[: self.max_buildings]:48            try:49                html = self.get(page_url).text50            except Exception:51                continue52            m = _MODULE_RE.search(html)53            cm = _CLIENT_RE.search(html)54            if not (m and cm):55                continue          # immeuble sans module Livya (ex. 2835 Mézy)56            project, entity, client = m.group(1), m.group(2), cm.group(1)57            try:58                livya_html = self.get(59                    f"{LIVYA}/fr/{client}/projects/{project}/plans/{entity}"60                    f"?noLayout=1").text61            except Exception:62                continue63            for u in _livya_units(livya_html):64                # dédoublonnage : les immeubles jumelés partagent le projet65                uid = str(u.get("unitId") or "")66                if not uid or uid in listings:67                    continue68                if u.get("availability") != "AVAILABLE" or not u.get("rental", True):69                    continue70                listings[uid] = self._listing(u, page_url)71        return list(listings.values())7273    # -- une annonce par unité disponible ------------------------------------------74    def _listing(self, u: dict, page_url: str) -> Listing:75        num = str(u.get("number") or "").strip()76        price = u.get("rentalPrice")77        price = float(price) if isinstance(price, (int, float)) and price > 0 else None78        area = u.get("unitSize")79        area = float(area) if isinstance(area, (int, float)) and area > 0 else None8081        street = (u.get("address") or "").strip()82        city = (u.get("city") or "").strip()83        postal = (u.get("postalCode") or "").strip()84        address = ", ".join(x for x in (street, postal) if x)8586        # description : étage, modèle, pièces, balcon, plan (champs structurés87        # du flux — le textmine central en tire les dérivés)88        desc: list[str] = []89        floor = str(u.get("floorNumber") or "").strip()90        if floor:91            desc.append(f"Étage {floor}")92        if u.get("typeName"):93            desc.append(f"Modèle {u['typeName']}")94        if u.get("roomsBed"):95            desc.append(f"{u['roomsBed']} chambre(s)")96        if u.get("roomsBath"):97            desc.append(f"{u['roomsBath']} salle(s) de bain")98        if u.get("balconySize"):99            desc.append(f"Balcon de {u['balconySize']} pi²")100        if u.get("floorPlanUrl"):101            desc.append(f"Plan : {u['floorPlanUrl']}")102103        images = [img.get("fullUrl") for img in (u.get("typeImages") or [])104                  if isinstance(img, dict) and img.get("fullUrl")]105        if u.get("floorPlanImageUrl"):106            images.append(u["floorPlanImageUrl"])107108        future = u.get("futureAvailability")109        availability = str(future) if future else "Disponible"110111        details: dict = {}112        if floor.isdigit():113            details["floor"] = int(floor)114115        lat, lng = u.get("latitude"), u.get("longitude")116        unit_type = _unit_type(u.get("rooms"))117        title = f"{street} — unité {num}" if street else f"Unité {num}"118        if unit_type:119            title += f" ({unit_type})"120121        return Listing(122            source=self.source_id,123            external_id=str(u["unitId"]),124            url=page_url,            # la page immeuble héberge le sélecteur125            title=title,126            address=address,127            sector="",               # secteur non publié unité par unité128            city=city,129            unit_type=unit_type,130            price=price,131            price_label=f"{price:.0f} $ /mois" if price else "",132            availability=availability,133            area_sqft=area,134            description=" | ".join(desc),135            details=details,136            images=images[:12],137            lat=float(lat) if lat else None,138            lng=float(lng) if lng else None,139        )140