# -----------------------------------------------------------------------------
# Lou-Ka — Agrégateur de logements à louer (province de Québec)
# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
# connectors/primo.py : connecteur Primo Immobilier (primoimmobilier.ca —
# condos locatifs de luxe en Montérégie : Lumicité Condominiums à
# Saint-Mathieu-de-Beloeil et Quartier Trinité à Saint-Basile-le-Grand).
# WordPress rendu serveur mais SANS inventaire d'unités : chaque page
# projet affiche un seul prix plancher « À partir de 2075$/mois » (les
# typologies 3½/4½/5½ n'apparaissent que dans le formulaire, sans prix).
# Granularité = PROJET (une annonce par projet, prix « à partir de »).
# external_id = slug de la page projet — stable.
# -----------------------------------------------------------------------------
from __future__ import annotations
import html as _html
import re
from ..schema import Listing
from .base import BaseConnector
BASE = "https://primoimmobilier.ca"
# (slug page, nom du projet, ville)
PROJECTS: tuple[tuple[str, str, str], ...] = (
("saint-mathieu-de-beloeil", "Lumicité Condominiums",
"Saint-Mathieu-de-Beloeil"),
("saint-basile-le-grand", "Quartier Trinité", "Saint-Basile-le-Grand"),
)
H1_RE = re.compile(r"
]*>([\s\S]*?)
", re.I)
FROM_RE = re.compile(r"À\s+partir\s+de\s+([\d\s ]{3,9})\$\s*/\s*mois", re.I)
SCRIPT_RE = re.compile(r"<(script|style)[\s\S]*?\1>", re.I)
TAG_RE = re.compile(r"<[^>]+>")
IMG_RE = re.compile(
r'https://primoimmobilier\.ca/wp-content/[^"\s)]+\.(?:jpe?g|png|webp)',
re.I)
class PrimoConnector(BaseConnector):
source_id = "primo"
request_delay = 0.8
def fetch(self) -> list[Listing]:
listings: list[Listing] = []
for slug, name, city in PROJECTS:
url = f"{BASE}/{slug}/"
try:
html = self.get(url).text
except Exception:
continue
flat = re.sub(r"[\s ]+", " ",
TAG_RE.sub(" ", _html.unescape(
SCRIPT_RE.sub(" ", html))))
pm = FROM_RE.search(flat)
price = None
if pm:
try:
price = float(re.sub(r"[\s ]", "", pm.group(1)))
except ValueError:
price = None
if price is None or not (500 <= price <= 8000):
continue # pas de prix publié = pas d'annonce
hm = H1_RE.search(html)
title = (re.sub(r"\s+", " ", _html.unescape(
TAG_RE.sub(" ", hm.group(1)))).strip()
if hm else f"Condos locatifs {name}")
images = [u for u in dict.fromkeys(IMG_RE.findall(html))
if not re.search(r"logo|icon|favicon|banners", u, re.I)
][:10]
listings.append(Listing(
source=self.source_id,
external_id=slug,
url=url,
title=title,
address="", # adresse civique non publiée
city=city,
price=price,
price_label=f"à partir de {int(price)} $/mois",
description=f"{name} à {city} — condos locatifs de luxe de "
"Primo Immobilier (typologies 3½ à 5½). "
"Ascenseur, stationnement et rangement "
"intérieurs, plafonds de 9 pieds, comptoirs de "
"quartz, accessible aux personnes à mobilité "
"réduite.",
images=images,
))
return listings