# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/le_730.py : connecteur Le 730 Notre-Dame (730.ca — 186 condos # locatifs luxueux, marina et club privé au 730, rue Notre-Dame à # Repentigny, au bord du Saint-Laurent). WordPress/Elementor une-page # rendu SERVEUR : l'accueil publie les prix « à partir de » par typologie # (3½/4½), l'occupation (« Occupation immédiate » pour les phases I-II) # et un plan PDF par variante d'unité (« 3 1/2 – Unité 1 »). # Granularité TYPOLOGIE (external_id = « le730-N.5 », stable) ; aucune # liste d'unités individuelles publiée. Les phases III-IV (livraison # 2027) ne créent pas d'annonce. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type from .base import BaseConnector PAGE_URL = "https://730.ca/" ADDRESS = "730, rue Notre-Dame" CITY = "Repentigny" # « 3 ½ à partir de 1750$ » PRICE_RE = re.compile(r"(\d)\s*½\s*à partir de\s*([\d\s ]+)\s*\$", re.I) # liens de plans PDF « 3 1/2 – Unité 5 » PLAN_RE = re.compile( r']*>' r'\s*(\d)\s*1/2\s*–\s*Unité\s*(\d+)', re.I) OCCUP_RE = re.compile(r"Occupation immédiate[^!.]*[!.]?") IMG_RE = re.compile(r"https://730\.ca/wp-content/uploads/" r"[^\"\s\\]+?\.(?:jpe?g|png|webp)", re.I) IMG_SIZE_RE = re.compile(r"-\d+x\d+(?=\.(?:jpe?g|png|webp))", re.I) BUILDING_AMENITIES = ["Toit terrasse", "Jardin communautaire", "BBQ", "Piscine", "Gym", "Bornes pour voitures électriques", "Club nautique et marina", "Ascenseurs"] class Le730Connector(BaseConnector): source_id = "le_730" request_delay = 0.8 def fetch(self) -> list[Listing]: try: html = self.get(PAGE_URL).text except Exception: return [] soup = BeautifulSoup(html, "html.parser") text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True)) # prix « à partir de » par typologie prices: dict[str, float] = {} for m in PRICE_RE.finditer(text): try: prices.setdefault(f"{m.group(1)}½", float( re.sub(r"[\s ]", "", m.group(2)))) except ValueError: pass # plans PDF par variante -> typologies réellement offertes plans: dict[str, list[str]] = {} for m in PLAN_RE.finditer(html): lst = plans.setdefault(f"{m.group(2)}½", []) if m.group(1) not in lst: # sections desktop/mobile dupliquées lst.append(m.group(1)) if not plans and not prices: return [] m = OCCUP_RE.search(text) occupation = m.group(0).strip() if m else "" images = [] for u in dict.fromkeys(IMG_RE.findall(html)): u = IMG_SIZE_RE.sub("", u) if not re.search(r"logo|icon|favicon|Img_Plans", u, re.I) \ and u not in images: images.append(u) desc_base = ("Le 730 Notre-Dame : plus de 180 condos locatifs, une " "marina et un club privé sur les berges du Saint-Laurent " "à Repentigny. Projet écoresponsable avec vue sur le " "fleuve.") listings: list[Listing] = [] for typo in sorted(set(plans) | set(prices)): unit_type = normalize_unit_type(typo) price = prices.get(typo) nb_plans = len(plans.get(typo, [])) details: dict = {} if plans.get(typo): details["floor_plans"] = plans[typo][:12] listings.append(Listing( source=self.source_id, external_id=f"le730-{typo.replace('½', '.5')}", url=PAGE_URL, title=f"Condo {typo} — Le 730 Notre-Dame", address=ADDRESS, city=CITY, unit_type=unit_type, price=price, price_label=f"à partir de {price:.0f} $ /mois" if price else "", availability=occupation, description=(desc_base + (f" {nb_plans} plans d'unité {typo} offerts." if nb_plans else ""))[:1200], amenities=BUILDING_AMENITIES, details=details, images=images[:20], )) return listings