# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/fitz.py : connecteur Le Fitz (lefitz.ca) — condos locatifs, # 3 tours au 77, route du Président-Kennedy à Lévis. WordPress + plugin # maison « imedia-building-plans » : admin-ajax.php?action=get_unit_list& # building_id= renvoie le tableau complet des unités (no, type, # étage, chambres, sdb, superficie, PRIX, disponibilité) ; les # class="unit disabled" sont louées, class="unit" disponibles. # Granularité : unité, avec prix. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://lefitz.ca" AJAX = f"{BASE}/wp-admin/admin-ajax.php" # tours (term_id -> nom) : data-term-id des boutons de /plan-unites/ BUILDINGS = [("44", "Tour 1"), ("14", "Tour 2"), ("10", "Tour 3")] ADDRESS = "77, route du Président-Kennedy, Lévis" AMENITIES = ["Chauffé et éclairé", "Électroménagers inclus", "Internet haute vitesse inclus", "Gym et salle de yoga", "Piscine intérieure", "Terrasses et jardin communautaire"] class FitzConnector(BaseConnector): source_id = "fitz" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] seen: set[str] = set() for term_id, tower in BUILDINGS: try: html = self.get(AJAX, params={ "action": "get_unit_list", "building_id": term_id}).text except Exception: continue soup = BeautifulSoup(html, "html.parser") for row in soup.select("tr.unit"): try: classes = row.get("class") or [] if "disabled" in classes: continue # unité louée unit_no = (row.get("data-unit") or "").strip() if not unit_no or unit_no in seen: continue seen.add(unit_no) def cell(name: str) -> str: el = row.select_one(f"td.unit__{name}") return el.get_text(" ", strip=True) if el else "" unit_type = normalize_unit_type(cell("type")) price_label = cell("price") area = None m = re.search(r"([\d\s]+)\s*p\.?\s*c\.?", cell("area")) if m: area = float(m.group(1).replace(" ", "") .replace(" ", "").replace(" ", "")) bedrooms = bathrooms = None if cell("rooms").isdigit(): bedrooms = float(cell("rooms")) if cell("bathrooms").replace(".", "").isdigit(): bathrooms = float(cell("bathrooms")) plan_img = row.get("data-image-desktop") or "" details: dict = {"floor": cell("floor") or row.get("data-floor", "")} if row.get("data-file"): details["floor_plan"] = row["data-file"] if row.get("data-balcony-area"): details["balcony_area"] = row["data-balcony-area"] listings.append(Listing( source=self.source_id, external_id=unit_no, url=row.get("data-permalink") or f"{BASE}/plan-unites/", title=f"Unité {unit_no} — Le Fitz ({tower})", address=ADDRESS, city="Lévis", unit_type=unit_type, bedrooms=bedrooms, bathrooms=bathrooms, price=parse_price(price_label), price_label=price_label, availability=cell("availability"), area_sqft=area, description=f"Unité {unit_no} ({unit_type}), " f"étage {details['floor']}, {tower} du " "Fitz — condos locatifs avec vue à " "Lévis. Chauffage, éclairage, électro-" "ménagers et Internet inclus.", amenities=list(AMENITIES), details=details, images=[plan_img] if plan_img.startswith("http") else [], )) except Exception: continue return listings