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/kare.py : connecteur Kare Gestion Immobilière (gestionkare.com)5# Gestionnaire de Sherbrooke (Estrie) — ~54 unités : Sherbrooke (Les6# Nations, Fleurimont, Rock Forest, Lennoxville), Waterloo, Windsor,7# Richmond, Magog + quelques hors Estrie (Drummondville, Châteauguay).8# SPA Angular avec SSR : la page /units/ embarque l'état hydraté dans9# <script id="ng-state"> — on y lit la réponse JSON complète de10# /api/units (id, title, address, city, sublocality, price, availability11# ISO, amenities booléens, images CDN, lat/lng, promotion). Repli : GET12# direct sur l'API (kare-test-dhpzr.ondigitalocean.app — hôte lu dans le13# ng-state quand possible). URL détail = /units/<id>--<ville>-<type>14# (même slugification que le front). external_id = id (UID Kare) — stable.15# -----------------------------------------------------------------------------16from __future__ import annotations1718import json19import re2021from ..schema import Listing, normalize_unit_type, strip_accents22from .base import BaseConnector2324BASE = "https://www.gestionkare.com"25LIST_URL = f"{BASE}/units/"26API_URL = "https://kare-test-dhpzr.ondigitalocean.app/api/units"2728NG_STATE_RE = re.compile(29 r'<script[^>]*id="ng-state"[^>]*>([\s\S]*?)</script>', re.I)30UNIT_RE = re.compile(r"(\d)\s*(?:1/2|½|et\s+demie?)", re.I)31BEDS_RE = re.compile(r"(\d)\s*[Cc]hambres?")323334def _slug(text: str) -> str:35 # même esprit que le front Angular : minuscules, tirets36 s = strip_accents((text or "").lower())37 return re.sub(r"[^a-z0-9]+", "-", s).strip("-")383940class KareConnector(BaseConnector):41 source_id = "kare"42 request_delay = 0.84344 def fetch(self) -> list[Listing]:45 units = self._units_from_ng_state()46 if not units:47 units = self.get(API_URL).json()48 listings: list[Listing] = []49 for u in units or []:50 listing = self._to_listing(u)51 if listing is not None:52 listings.append(listing)53 return listings5455 def _units_from_ng_state(self) -> list[dict]:56 try:57 html = self.get(LIST_URL).text58 m = NG_STATE_RE.search(html)59 if not m:60 return []61 state = json.loads(m.group(1))62 except Exception:63 return []64 for entry in state.values():65 if isinstance(entry, dict) \66 and str(entry.get("u", "")).endswith("/api/units") \67 and isinstance(entry.get("b"), list):68 return entry["b"]69 return []7071 def _to_listing(self, u: dict) -> Listing | None:72 uid = u.get("id") or ""73 price = u.get("price")74 title = re.sub(r"\s+", " ", u.get("title") or u.get("name") or "")75 if not uid or not title:76 return None7778 city = (u.get("city") or "").strip()79 utype = (u.get("type") or "").strip()8081 # typologie : « 5 1/2 à louer » dans le titre, sinon champ type82 unit_type = ""83 um = UNIT_RE.search(title)84 if um:85 unit_type = normalize_unit_type(f"{um.group(1)} ½")86 elif re.search(r"studio", utype, re.I):87 unit_type = "Studio"88 elif re.search(r"chambre\s+uniquement", utype, re.I):89 unit_type = "Chambre"9091 bedrooms = None92 bm = BEDS_RE.search(utype)93 if bm:94 bedrooms = float(bm.group(1))95 if re.search(r"demie?", utype, re.I):96 bedrooms += 0.59798 address = re.sub(r",\s*Canada\s*$", "", u.get("address") or "")99 amenities = [k for k, v in (u.get("amenities") or {}).items() if v]100 pets = None101 if "Animal autorisé" in amenities:102 pets = "oui"103 amenities.remove("Animal autorisé")104105 avail = (u.get("availability") or "").strip()106 details: dict = {}107 if u.get("promotion"):108 details["promotion"] = u["promotion"]109 if u.get("sublocality"):110 details["sublocality"] = u["sublocality"]111112 images = [im for im in (u.get("images") or []) if im][:12]113 url = f"{BASE}/units/{uid}--{_slug(city)}-{_slug(utype)}"114115 return Listing(116 source=self.source_id,117 external_id=uid,118 url=url,119 title=title,120 address=address,121 sector=u.get("sublocality") or "",122 city=city,123 unit_type=unit_type,124 bedrooms=bedrooms,125 bathrooms=float(u["bathrooms"]) if u.get("bathrooms") else None,126 price=float(price) if price else None,127 availability=f"Libre le {avail}" if avail else "",128 availability_date=avail or None,129 area_sqft=float(u["surface"]) if u.get("surface") else None,130 pets=pets,131 description=(u.get("description") or "").strip(),132 amenities=amenities,133 details=details,134 images=images,135 lat=u.get("latitude"),136 lng=u.get("longitude"),137 )138