# -----------------------------------------------------------------------------
# Lou-Ka — Agrégateur de logements à louer (province de Québec)
# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
# connectors/jardins_anna.py : connecteur Les Jardins d'Anna (lesjardinsdanna.com)
# Projet domiciliaire de Nicolet (Centre-du-Québec), à 15 minutes de
# Trois-Rivières — logements 4½ et 5½ de style condo. WordPress rendu
# serveur : la page /location/ présente les typologies en sections
# (« Logements 4 ½ », « … avec verrière », « … avec verrière et garage »,
# « Logements 5 ½ ») suivies de « À partir de 1 250$ par mois, nos …
# incluent : » + liste d'inclusions. Granularité = typologie (le site ne
# publie pas d'inventaire par unité). external_id = typologie en slug.
# -----------------------------------------------------------------------------
from __future__ import annotations
import re
from bs4 import BeautifulSoup
from ..schema import Listing, normalize_unit_type, strip_accents
from .base import BaseConnector
BASE = "https://lesjardinsdanna.com"
PAGE_URL = f"{BASE}/location/"
PRICE_RE = re.compile(r"À\s+partir\s+de\s+([\d\s ]{3,7})\$\s*par\s*mois", re.I)
UNIT_RE = re.compile(r"(\d)\s*(?:½|1/2)")
IMG_RE = re.compile(
r"https?://lesjardinsdanna\.com/wp-content/uploads/[^\"'\s)]+"
r"\.(?:jpg|jpeg|png|webp)", re.I)
def _slug(text: str) -> str:
s = strip_accents(text.lower())
return re.sub(r"[^a-z0-9]+", "-", s).strip("-")
def _num(txt: str) -> float | None:
n = re.sub(r"[\s ]", "", txt or "")
try:
return float(n)
except ValueError:
return None
class JardinsAnnaConnector(BaseConnector):
source_id = "jardins_anna"
request_delay = 0.8
def fetch(self) -> list[Listing]:
html = self.get(PAGE_URL).text
soup = BeautifulSoup(html, "html.parser")
images = [u for u in dict.fromkeys(IMG_RE.findall(html))
if not re.search(r"logo|icon|-\d+x\d+\.", u, re.I)][:15]
listings: list[Listing] = []
for h3 in soup.find_all("h3"):
label = re.sub(r"\s+", " ", h3.get_text(" ", strip=True))
if not re.match(r"Logements?\s", label, re.I):
continue
# texte de la section : frères suivants jusqu'au prochain h3
price = None
price_label = ""
amenities: list[str] = []
node = h3
while True:
node = node.find_next_sibling()
if node is None or node.name == "h3":
break
text = re.sub(r"\s+", " ", node.get_text(" ", strip=True))
if not price:
pm = PRICE_RE.search(text)
if pm:
price = _num(pm.group(1))
price_label = pm.group(0)
for li in node.find_all("li"):
item = re.sub(r"\s+", " ", li.get_text(" ", strip=True))
if item and item not in amenities and len(item) < 80:
amenities.append(item)
if price is None:
continue
unit_type = ""
um = UNIT_RE.search(label)
if um:
unit_type = normalize_unit_type(f"{um.group(1)} ½")
listings.append(Listing(
source=self.source_id,
external_id=_slug(label),
url=PAGE_URL,
title=f"{label} — Les Jardins d'Anna",
address="Les Jardins d'Anna, Nicolet",
sector="",
city="Nicolet",
unit_type=unit_type,
price=price,
price_label=price_label,
description=f"{label} de style condo aux Jardins d'Anna, "
"Nicolet (à 15 minutes de Trois-Rivières).",
amenities=amenities,
images=list(images),
))
return listings