SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
5.1 KB · 138 lines python
Raw Blame History
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