# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/novo_stjean.py : connecteur NOVO St-Jean (novostjean.com) # Condos locatifs tout-inclus au 201, rue Saint-Georges, # Saint-Jean-sur-Richelieu (projet Atimco). ⚠️ Compagnie distincte du # connecteur `novelo`. WordPress + Bricks rendu serveur : la page d'accueil # liste 4 typologies en blocs .units_item — « 3½ », « 3½ + den », « 4½ », # « 4½ + den » avec prix plancher (« À partir de 1370$ / mois »). Une # annonce par typologie (pas d'inventaire unité par unité public) ; les # inclusions (électroménagers, eau chaude, électricité, clim, câble, # Wi-Fi) viennent de la grille .inclusion_grid-item et la mention # « Disponible dès maintenant » sert de disponibilité. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://www.novostjean.com" LIST_URL = f"{BASE}/" ADDRESS = "201, rue Saint-Georges, Saint-Jean-sur-Richelieu" CITY = "Saint-Jean-sur-Richelieu" TYPE_RE = re.compile(r"^(\d)\s*\|?\s*1/2$") PRICE_RE = re.compile(r"(\d[\d\s ]{2,6})\s*\$\s*/\s*mois", re.I) IMG_RE = re.compile( r"https://www\.novostjean\.com/wp-content/uploads/[^\"\s\\']+" r"\.(?:jpe?g|png|webp)", re.I) SKIP_IMG_RE = re.compile( r"logo|icon|favicon|cropped-|plan|preview|-\d{2,4}x\d{2,4}\.", re.I) class NovoStJeanConnector(BaseConnector): source_id = "novo_stjean" 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") # inclusions tout-inclus (grille Bricks) amenities = [el.get_text(" ", strip=True) for el in soup.select(".inclusion_grid-item")] amenities = [re.sub(r"\s+", " ", a) for a in amenities if a][:12] availability = "Disponible dès maintenant" \ if re.search(r"Disponible dès maintenant", html) else "" images = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not SKIP_IMG_RE.search(u)][:10] seen: set[str] = set() for item in soup.select(".units_item"): try: h = item.select_one("h3") if h is None: continue m = TYPE_RE.match(h.get_text(" ", strip=True) .replace(" | ", " ")) if not m: continue unit_type = normalize_unit_type(f"{m.group(1)} 1/2") den = bool(re.search(r"\+\s*den", item.get_text(" "), re.I)) text = item.get_text(" ", strip=True) mp = PRICE_RE.search(text) price_label = f"À partir de {mp.group(0)}" if mp else "" price = parse_price(mp.group(0)) if mp else None ext_id = unit_type.replace("½", ".5") + ("-den" if den else "") if ext_id in seen: continue seen.add(ext_id) suffix = " + den" if den else "" listings.append(Listing( source=self.source_id, external_id=ext_id, url=LIST_URL, title=f"{unit_type}{suffix} — NOVO St-Jean", address=ADDRESS, city=CITY, unit_type=unit_type, price=price, price_label=price_label, availability=availability, amenities=list(amenities), details={"den": True} if den else {}, images=images, )) except Exception: continue return listings