# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/floria.py : connecteur Gestion Floria (gestionfloria.ca) # Condos locatifs neufs à Sherbrooke, Magog (Mont-Orford) et Coaticook. # Site WordPress (Avada) : chaque page /immeuble// embarque le module # de plans interactifs Livya (app.livya.com, client « gestion-floria », un # projet/entité par immeuble). Même patron que somex_saintnicolas/louis14 : # la page Next.js du module est rendue serveur et son flux RSC contient # l'inventaire JSON complet (numéro, statut, loyer, pièces, superficie, # adresse, ville, GPS, photos, plans). Découverte des immeubles via la page # /secteurs/. Certains immeubles partagent le même projet Livya (jumelés # 52/58 Flanc-Sud, 753/765 Rachel-Hébert…) : dédoublonnage par unitId. # Seules les unités AVAILABLE deviennent des annonces ; certains projets ne # publient pas le loyer (rentalPrice = 0) -> prix absent, rien d'inventé. # ----------------------------------------------------------------------------- from __future__ import annotations import re from ..schema import Listing from .base import BaseConnector from .somex_saintnicolas import _livya_units, _unit_type BASE = "https://gestionfloria.ca" SECTORS_URL = f"{BASE}/secteurs/" LIVYA = "https://app.livya.com" _IMMEUBLE_RE = re.compile(r"https://gestionfloria\.ca/immeuble/[a-z0-9-]+/") _MODULE_RE = re.compile( r'
]*' r'data-project="([^"]+)"[^>]*data-entity="([^"]+)"') _CLIENT_RE = re.compile(r'data-client="([^"]+)"') class FloriaConnector(BaseConnector): source_id = "floria" request_delay = 0.7 max_buildings = 20 # garde-fou (12 immeubles publiés) def fetch(self) -> list[Listing]: # 1) Découverte : pages immeuble listées sur /secteurs/ sec = self.get(SECTORS_URL).text pages = sorted(set(_IMMEUBLE_RE.findall(sec))) listings: dict[str, Listing] = {} for page_url in pages[: self.max_buildings]: try: html = self.get(page_url).text except Exception: continue m = _MODULE_RE.search(html) cm = _CLIENT_RE.search(html) if not (m and cm): continue # immeuble sans module Livya (ex. 2835 Mézy) project, entity, client = m.group(1), m.group(2), cm.group(1) try: livya_html = self.get( f"{LIVYA}/fr/{client}/projects/{project}/plans/{entity}" f"?noLayout=1").text except Exception: continue for u in _livya_units(livya_html): # dédoublonnage : les immeubles jumelés partagent le projet uid = str(u.get("unitId") or "") if not uid or uid in listings: continue if u.get("availability") != "AVAILABLE" or not u.get("rental", True): continue listings[uid] = self._listing(u, page_url) return list(listings.values()) # -- une annonce par unité disponible ------------------------------------------ def _listing(self, u: dict, page_url: str) -> Listing: num = str(u.get("number") or "").strip() price = u.get("rentalPrice") price = float(price) if isinstance(price, (int, float)) and price > 0 else None area = u.get("unitSize") area = float(area) if isinstance(area, (int, float)) and area > 0 else None street = (u.get("address") or "").strip() city = (u.get("city") or "").strip() postal = (u.get("postalCode") or "").strip() address = ", ".join(x for x in (street, postal) if x) # description : étage, modèle, pièces, balcon, plan (champs structurés # du flux — le textmine central en tire les dérivés) desc: list[str] = [] floor = str(u.get("floorNumber") or "").strip() if floor: desc.append(f"Étage {floor}") if u.get("typeName"): desc.append(f"Modèle {u['typeName']}") if u.get("roomsBed"): desc.append(f"{u['roomsBed']} chambre(s)") if u.get("roomsBath"): desc.append(f"{u['roomsBath']} salle(s) de bain") if u.get("balconySize"): desc.append(f"Balcon de {u['balconySize']} pi²") if u.get("floorPlanUrl"): desc.append(f"Plan : {u['floorPlanUrl']}") images = [img.get("fullUrl") for img in (u.get("typeImages") or []) if isinstance(img, dict) and img.get("fullUrl")] if u.get("floorPlanImageUrl"): images.append(u["floorPlanImageUrl"]) future = u.get("futureAvailability") availability = str(future) if future else "Disponible" details: dict = {} if floor.isdigit(): details["floor"] = int(floor) lat, lng = u.get("latitude"), u.get("longitude") unit_type = _unit_type(u.get("rooms")) title = f"{street} — unité {num}" if street else f"Unité {num}" if unit_type: title += f" ({unit_type})" return Listing( source=self.source_id, external_id=str(u["unitId"]), url=page_url, # la page immeuble héberge le sélecteur title=title, address=address, sector="", # secteur non publié unité par unité city=city, unit_type=unit_type, price=price, price_label=f"{price:.0f} $ /mois" if price else "", availability=availability, area_sqft=area, description=" | ".join(desc), details=details, images=images[:12], lat=float(lat) if lat else None, lng=float(lng) if lng else None, )