# -----------------------------------------------------------------------------
# Lou-Ka — Agrégateur de logements à louer (province de Québec)
# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
# connectors/le_pilier.py : connecteur Le Pilier (lepilierlevis.com) — condos
# locatifs neufs au 1275, rue J.-B.-Demers à Lévis (sections 3 et 6 étages).
# WordPress + plugin maison « themelio-real-estate » : la page /projet/
# rend côté serveur un
(Unité, Étage,
# Superficie, Pièces, Prix, Disponibilité, lien /units/unite-NNN/), filtré
# par défaut sur les unités disponibles ; la page unité fournit le plan.
# Granularité : unité, avec prix.
# -----------------------------------------------------------------------------
from __future__ import annotations
import hashlib
import re
from bs4 import BeautifulSoup
from ..schema import Listing, normalize_unit_type, parse_price
from .base import BaseConnector
BASE = "https://lepilierlevis.com"
LIST_URL = f"{BASE}/projet/"
SQFT_RE = re.compile(r"([\d\s]+)\s*pi²", re.I)
FLOOR_RE = re.compile(r"(\d+)")
PLAN_RE = re.compile(r'src="(https://lepilierlevis\.com/wp-content/uploads/'
r'[^"]+\.(?:avif|png|jpe?g|webp))"', re.I)
ADDRESS = "1275, rue J.-B.-Demers, Lévis"
CONTACT = {"phone": "418-836-7666"}
class LePilierConnector(BaseConnector):
source_id = "le_pilier"
request_delay = 0.6
def fetch(self) -> list[Listing]:
listings: list[Listing] = []
try:
html = self.get(LIST_URL).text
except Exception:
return listings
soup = BeautifulSoup(html, "html.parser")
table = soup.select_one("table.units-table")
if table is None:
return listings
seen: set[str] = set()
for row in table.select("tbody tr"):
try:
cells = row.find_all("td")
if len(cells) < 7:
continue
unit_no, floor, area_txt, rooms, price_txt, avail = \
[c.get_text(" ", strip=True) for c in cells[:6]]
if not unit_no or not re.match(r"^disponible", avail, re.I):
continue # réservé / loué
ext_id = f"unite-{unit_no}"
if ext_id in seen:
continue
seen.add(ext_id)
link = cells[6].find("a")
url = link["href"] if link and link.get("href") else LIST_URL
area = None
m = SQFT_RE.search(area_txt)
if m:
area = float(m.group(1).replace(" ", "")
.replace(" ", "").replace(" ", ""))
unit_type = normalize_unit_type(rooms)
# plan de l'unité sur sa page /units/unite-NNN/ (cache détail
# invalidé quand la ligne du tableau change)
key = hashlib.sha1(
"|".join([unit_no, floor, area_txt, rooms, price_txt,
avail]).encode()).hexdigest()
d = self.detail(ext_id, key,
lambda u=url: self._fetch_unit(u))
listings.append(Listing(
source=self.source_id,
external_id=ext_id,
url=url,
title=f"Unité {unit_no} — Le Pilier Lévis",
address=ADDRESS,
city="Lévis",
unit_type=unit_type,
price=parse_price(price_txt),
price_label=price_txt,
availability=avail,
area_sqft=area,
description=f"Unité {unit_no} ({unit_type}), {floor} — "
"Le Pilier, condos locatifs neufs au cœur "
"de Lévis.",
details={"floor": floor, "contact": dict(CONTACT)},
images=d.get("images") or [],
))
except Exception:
continue
return listings
def _fetch_unit(self, url: str) -> dict:
"""Plan/photos de la page unité (/units/unite-NNN/)."""
out: dict = {}
try:
html = self.get(url).text
except Exception:
return out
imgs = [u for u in PLAN_RE.findall(html)
if "logo" not in u.lower()]
if imgs:
out["images"] = list(dict.fromkeys(imgs))[:5]
return out