Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/le_730.py : connecteur Le 730 Notre-Dame (730.ca — 186 condos5# locatifs luxueux, marina et club privé au 730, rue Notre-Dame à6# Repentigny, au bord du Saint-Laurent). WordPress/Elementor une-page7# rendu SERVEUR : l'accueil publie les prix « à partir de » par typologie8# (3½/4½), l'occupation (« Occupation immédiate » pour les phases I-II)9# et un plan PDF par variante d'unité (« 3 1/2 – Unité 1 »).10# Granularité TYPOLOGIE (external_id = « le730-N.5 », stable) ; aucune11# liste d'unités individuelles publiée. Les phases III-IV (livraison12# 2027) ne créent pas d'annonce.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from bs4 import BeautifulSoup1920from ..schema import Listing, normalize_unit_type21from .base import BaseConnector2223PAGE_URL = "https://730.ca/"24ADDRESS = "730, rue Notre-Dame"25CITY = "Repentigny"2627# « 3 ½ à partir de 1750$ »28PRICE_RE = re.compile(r"(\d)\s*½\s*à partir de\s*([\d\s ]+)\s*\$", re.I)29# liens de plans PDF « 3 1/2 – Unité 5 »30PLAN_RE = re.compile(31 r'<a href="(https://730\.ca/wp-content/uploads/[^"]+\.pdf)"[^>]*>'32 r'\s*(\d)\s*<sup>1/2</sup>\s*–\s*Unité\s*(\d+)', re.I)33OCCUP_RE = re.compile(r"Occupation immédiate[^!.]*[!.]?")34IMG_RE = re.compile(r"https://730\.ca/wp-content/uploads/"35 r"[^\"\s\\]+?\.(?:jpe?g|png|webp)", re.I)36IMG_SIZE_RE = re.compile(r"-\d+x\d+(?=\.(?:jpe?g|png|webp))", re.I)3738BUILDING_AMENITIES = ["Toit terrasse", "Jardin communautaire", "BBQ",39 "Piscine", "Gym", "Bornes pour voitures électriques",40 "Club nautique et marina", "Ascenseurs"]414243class Le730Connector(BaseConnector):44 source_id = "le_730"45 request_delay = 0.84647 def fetch(self) -> list[Listing]:48 try:49 html = self.get(PAGE_URL).text50 except Exception:51 return []52 soup = BeautifulSoup(html, "html.parser")53 text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))5455 # prix « à partir de » par typologie56 prices: dict[str, float] = {}57 for m in PRICE_RE.finditer(text):58 try:59 prices.setdefault(f"{m.group(1)}½", float(60 re.sub(r"[\s ]", "", m.group(2))))61 except ValueError:62 pass6364 # plans PDF par variante -> typologies réellement offertes65 plans: dict[str, list[str]] = {}66 for m in PLAN_RE.finditer(html):67 lst = plans.setdefault(f"{m.group(2)}½", [])68 if m.group(1) not in lst: # sections desktop/mobile dupliquées69 lst.append(m.group(1))70 if not plans and not prices:71 return []7273 m = OCCUP_RE.search(text)74 occupation = m.group(0).strip() if m else ""7576 images = []77 for u in dict.fromkeys(IMG_RE.findall(html)):78 u = IMG_SIZE_RE.sub("", u)79 if not re.search(r"logo|icon|favicon|Img_Plans", u, re.I) \80 and u not in images:81 images.append(u)8283 desc_base = ("Le 730 Notre-Dame : plus de 180 condos locatifs, une "84 "marina et un club privé sur les berges du Saint-Laurent "85 "à Repentigny. Projet écoresponsable avec vue sur le "86 "fleuve.")87 listings: list[Listing] = []88 for typo in sorted(set(plans) | set(prices)):89 unit_type = normalize_unit_type(typo)90 price = prices.get(typo)91 nb_plans = len(plans.get(typo, []))92 details: dict = {}93 if plans.get(typo):94 details["floor_plans"] = plans[typo][:12]95 listings.append(Listing(96 source=self.source_id,97 external_id=f"le730-{typo.replace('½', '.5')}",98 url=PAGE_URL,99 title=f"Condo {typo} — Le 730 Notre-Dame",100 address=ADDRESS,101 city=CITY,102 unit_type=unit_type,103 price=price,104 price_label=f"à partir de {price:.0f} $ /mois" if price else "",105 availability=occupation,106 description=(desc_base107 + (f" {nb_plans} plans d'unité {typo} offerts."108 if nb_plans else ""))[:1200],109 amenities=BUILDING_AMENITIES,110 details=details,111 images=images[:20],112 ))113 return listings114