# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/quartier_horizon.py : connecteur Quartier Horizon # (quartierhorizon.com — Lévis, secteur Saint-Jean-Chrysostome : 1802, rue # des Fines-Herbes). WordPress/Divi rendu serveur : la page /plans/ liste # ~114 cartes
dont TOUTES les données sont des # attributs data-* (unité, bâtiment, étage, type 3½-5½, chambres, # superficie, balcon, prix « à partir de », disponibilité, plan PDF, image # du plan, état). États : disponible / attente / indisponible / a-venir — # on n'ingère que « disponible » (comme aula exclut « attente »). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector BASE = "https://quartierhorizon.com" PLANS_URL = f"{BASE}/plans/" ADDRESS = "1802, rue des Fines-Herbes, Lévis, G6Z 2L1" SECTOR = "Saint-Jean-Chrysostome" CITY = "Lévis" PHONE_RE = re.compile(r"(\d{3})[\s.\-](\d{3})[\s.\-](\d{4})") class QuartierHorizonConnector(BaseConnector): source_id = "quartier_horizon" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] html = self.get(PLANS_URL).text soup = BeautifulSoup(html, "html.parser") # contact du projet (affiché dans le pied de page / bandeau) details_common: dict = {} m = PHONE_RE.search(soup.get_text(" ", strip=True)) if m: details_common["contact"] = { "phone": f"{m.group(1)}-{m.group(2)}-{m.group(3)}"} for card in soup.select("article.ps-card"): try: if (card.get("data-etat") or "").strip() != "disponible": continue unit = (card.get("data-unite") or "").strip() bkey = (card.get("data-bkey") or "").strip() if not unit: continue ext_id = f"{bkey}-{unit}" if bkey else unit unit_type = normalize_unit_type(card.get("data-type") or "") price = None montant = (card.get("data-montant") or "").replace(" ", "") if montant.isdigit() and 100 <= int(montant) <= 20000: price = float(montant) libre = (card.get("data-libre") or "").strip() phase = (card.get("data-phase") or "").strip() etage = (card.get("data-etagenom") or "").strip() bldg = (card.get("data-bldg") or "").strip() bedrooms = None ch = (card.get("data-chambres") or "").strip() if ch.isdigit(): bedrooms = float(ch) area = None sup = (card.get("data-superficie") or "").replace(" ", "") if sup.isdigit(): area = float(sup) amenities: list[str] = [] if etage: amenities.append(f"Étage : {etage}") balcon = (card.get("data-balcon") or "").strip() if balcon.isdigit() and int(balcon) > 0: amenities.append(f"Balcon de {balcon} pi²") details = dict(details_common) pdf = (card.get("data-pdf") or "").strip() if pdf: details["plan_pdf"] = pdf if phase: details["phase"] = phase images: list[str] = [] img = (card.get("data-image") or "").strip() if img.startswith("http"): images.append(img) loc_bits = [b for b in (f"Bâtiment {bkey.upper() or bldg}", etage) if b] listings.append(Listing( source=self.source_id, external_id=ext_id, url=PLANS_URL, title=f"{unit_type} — unité {unit}, Quartier Horizon", address=ADDRESS, sector=SECTOR, city=CITY, unit_type=unit_type, bedrooms=bedrooms, price=price, price_label=(f"À partir de {montant} $ /mois" if price else ""), availability=(f"Libre : {libre}" if libre else "Disponible"), area_sqft=area, description=" · ".join(loc_bits), amenities=amenities, details=details, images=images, )) except Exception: continue return listings