# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/kare.py : connecteur Kare Gestion Immobilière (gestionkare.com) # Gestionnaire de Sherbrooke (Estrie) — ~54 unités : Sherbrooke (Les # Nations, Fleurimont, Rock Forest, Lennoxville), Waterloo, Windsor, # Richmond, Magog + quelques hors Estrie (Drummondville, Châteauguay). # SPA Angular avec SSR : la page /units/ embarque l'état hydraté dans # ', re.I) UNIT_RE = re.compile(r"(\d)\s*(?:1/2|½|et\s+demie?)", re.I) BEDS_RE = re.compile(r"(\d)\s*[Cc]hambres?") def _slug(text: str) -> str: # même esprit que le front Angular : minuscules, tirets s = strip_accents((text or "").lower()) return re.sub(r"[^a-z0-9]+", "-", s).strip("-") class KareConnector(BaseConnector): source_id = "kare" request_delay = 0.8 def fetch(self) -> list[Listing]: units = self._units_from_ng_state() if not units: units = self.get(API_URL).json() listings: list[Listing] = [] for u in units or []: listing = self._to_listing(u) if listing is not None: listings.append(listing) return listings def _units_from_ng_state(self) -> list[dict]: try: html = self.get(LIST_URL).text m = NG_STATE_RE.search(html) if not m: return [] state = json.loads(m.group(1)) except Exception: return [] for entry in state.values(): if isinstance(entry, dict) \ and str(entry.get("u", "")).endswith("/api/units") \ and isinstance(entry.get("b"), list): return entry["b"] return [] def _to_listing(self, u: dict) -> Listing | None: uid = u.get("id") or "" price = u.get("price") title = re.sub(r"\s+", " ", u.get("title") or u.get("name") or "") if not uid or not title: return None city = (u.get("city") or "").strip() utype = (u.get("type") or "").strip() # typologie : « 5 1/2 à louer » dans le titre, sinon champ type unit_type = "" um = UNIT_RE.search(title) if um: unit_type = normalize_unit_type(f"{um.group(1)} ½") elif re.search(r"studio", utype, re.I): unit_type = "Studio" elif re.search(r"chambre\s+uniquement", utype, re.I): unit_type = "Chambre" bedrooms = None bm = BEDS_RE.search(utype) if bm: bedrooms = float(bm.group(1)) if re.search(r"demie?", utype, re.I): bedrooms += 0.5 address = re.sub(r",\s*Canada\s*$", "", u.get("address") or "") amenities = [k for k, v in (u.get("amenities") or {}).items() if v] pets = None if "Animal autorisé" in amenities: pets = "oui" amenities.remove("Animal autorisé") avail = (u.get("availability") or "").strip() details: dict = {} if u.get("promotion"): details["promotion"] = u["promotion"] if u.get("sublocality"): details["sublocality"] = u["sublocality"] images = [im for im in (u.get("images") or []) if im][:12] url = f"{BASE}/units/{uid}--{_slug(city)}-{_slug(utype)}" return Listing( source=self.source_id, external_id=uid, url=url, title=title, address=address, sector=u.get("sublocality") or "", city=city, unit_type=unit_type, bedrooms=bedrooms, bathrooms=float(u["bathrooms"]) if u.get("bathrooms") else None, price=float(price) if price else None, availability=f"Libre le {avail}" if avail else "", availability_date=avail or None, area_sqft=float(u["surface"]) if u.get("surface") else None, pets=pets, description=(u.get("description") or "").strip(), amenities=amenities, details=details, images=images, lat=u.get("latitude"), lng=u.get("longitude"), )