# -----------------------------------------------------------------------------
# Lou-Ka — Agrégateur de logements à louer (province de Québec)
# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
# connectors/signature_bois_franc.py : Signature Bois-Franc — condos locatifs
# (location.signatureboisfranc.com) au 2020, rue Lucien-Thimens, quartier
# Bois-Franc, Saint-Laurent (Montréal).
# Techno : WordPress + FacetWP, mais la liste /unites-disponibles/ est
# RENDUE CÔTÉ SERVEUR (toutes les cartes .unit-row dans le HTML initial,
# pas de pagination) — simple parse regex des champs par carte.
# Granularité : UNITÉ (numéro, pièces, étage, pi², « à partir de » $,
# statut). On écarte « Unité louée » et « Réservée ».
# -----------------------------------------------------------------------------
from __future__ import annotations
import re
from .base import BaseConnector
from ..schema import Listing
LIST_URL = "https://location.signatureboisfranc.com/unites-disponibles/"
ADDRESS = "2020, rue Lucien-Thimens"
CITY = "Montréal"
SECTOR = "Saint-Laurent"
_ROW_SPLIT_RE = re.compile(r'
')
_FIELD_RE = {
"number": re.compile(r'unit-number__text">\s*#?([^<]+?)\s*<'),
"rooms": re.compile(r'unit-room__text">\s*([^<]+?)\s*<'),
"floor": re.compile(r'unit-floor__text">\s*([^<]+?)\s*<'),
"area": re.compile(r'unit-area__text">\s*([^<]+?)\s*<'),
"price": re.compile(r'unit-price__text">\s*([^<]+?)\s*<'),
"status": re.compile(r'unit-availability__text">\s*([^<]*?)\s*<'),
}
_PLAN_RE = re.compile(r'unit-plan[^>]*>.*?href="([^"]+\.pdf)"', re.S)
_AREA_NUM_RE = re.compile(r'([\d\s ,.]+)\s*pi')
_PRICE_NUM_RE = re.compile(r'([\d\s ]{3,})\s*\$')
# statuts qui excluent l'unité de l'inventaire
_GONE_RE = re.compile(r'lou[ée]e?|r[ée]serv[ée]e?', re.I)
class SignatureBoisFrancConnector(BaseConnector):
"""Signature Bois-Franc (Saint-Laurent, Montréal) — unités disponibles."""
source_id = "signature_bois_franc"
request_delay = 0.8
use_detail_cache = False # une seule page, tout est dans la liste
def fetch(self) -> list[Listing]:
page = re.sub(r"\s+", " ", self.get(LIST_URL).text)
out: list[Listing] = []
seen: set[str] = set()
for block in _ROW_SPLIT_RE.split(page)[1:]:
f = {k: (rx.search(block).group(1).strip()
if rx.search(block) else "")
for k, rx in _FIELD_RE.items()}
num = f["number"].lstrip("#").strip()
if not num:
continue
status = f["status"]
if _GONE_RE.search(status):
continue # louée / réservée
ext_id = f"unite-{num}" # un seul immeuble : nº stable
if ext_id in seen:
continue
seen.add(ext_id)
area = None
m = _AREA_NUM_RE.search(f["area"])
if m:
try:
area = float(re.sub(r"[\s ,]", "", m.group(1)))
except ValueError:
pass
price = None
m = _PRICE_NUM_RE.search(f["price"].replace("$", " $"))
if m:
try:
price = float(re.sub(r"[\s ]", "", m.group(1)))
except ValueError:
pass
details: dict = {}
if f["floor"]:
details["floor"] = f["floor"]
plan = _PLAN_RE.search(block)
if plan:
details["floor_plan"] = plan.group(1)
out.append(Listing(
source=self.source_id,
external_id=ext_id,
url=LIST_URL, # pas de page par unité
title=f"Signature Bois-Franc — Unité {num}",
address=ADDRESS,
sector=SECTOR,
city=CITY,
unit_type=f["rooms"],
price=price,
price_label=(f"À partir de {price:.0f} $ /mois"
if price else ""),
availability=status or "Disponible",
area_sqft=area,
details=details,
))
return out