SPB Git

spb/ora-ka Public

Ora-Ka — cinq agrégateurs Ka, une barre de recherche hybride (exact + sémantique)

Python 80% TypeScript 12.9% CSS 6.8%
4.4 KB · 102 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/ubee.py : Ubee (ubee.com) — plateforme immobilière québécoise5#   API publique anonyme : POST api.ubee.ca/api/anonymous/Search/SearchProperties6#   (pageIndex=N, 24 résultats/page, JSON riche : adresse, GPS, prix, pièces,7#   superficies m², année, galerie Cloudinary complète).8#   Deux volets À VENDRE (listingType=Seller) : résidentiel + commercial.9# -----------------------------------------------------------------------------10from __future__ import annotations1112from ..schema import PropertyListing13from .base import BaseConnector1415API = "https://api.ubee.ca/api/anonymous/Search/SearchProperties"16SITE = "https://ubee.com"17M2_TO_SQFT = 10.76391819_TYPES = {20    "Unifamiliale": "Maison", "Condo": "Condo", "Terrain": "Terrain",21    "Plex": "Immeuble à revenus", "Commercial": "Commercial",22    "Fermette": "Fermette", "Chalet": "Chalet",23}242526class UbeeConnector(BaseConnector):27    source_id = "ubee"28    request_delay = 0.52930    def _search(self, body: dict) -> list[dict]:31        out, page = [], 032        while True:33            r = self.post(f"{API}?pageIndex={page}", json=body).json()34            results = r.get("results") or []35            out.extend(results)36            if len(out) >= (r.get("totalCount") or 0) or not results:37                break38            page += 139            if page > 200:   # garde-fou40                break41        return out4243    def _to_listing(self, it: dict) -> PropertyListing | None:44        lid = str(it.get("id") or "")45        slug = it.get("slugFr") or it.get("slugEn") or ""46        if not lid or not slug:47            return None48        city = (it.get("city") or "").strip()49        url = f"{SITE}/a-vendre/{it.get('citySlug') or ''}/{slug}"50        images = [im["publicUrls"]["default_size"]51                  for im in it.get("images") or []52                  if (im.get("publicUrls") or {}).get("default_size")]53        living = it.get("livingSurfaceInMeters")54        land = it.get("landSurfaceInMeters")55        ptype = _TYPES.get(it.get("inscriptionType") or "",56                           it.get("inscriptionType") or "")57        return PropertyListing(58            source=self.source_id,59            external_id=lid,60            url=url,61            title=f"{ptype} à vendre — {city}" if city else f"{ptype} à vendre",62            address=it.get("address") or "",63            city=city,64            property_type=ptype,65            price=it.get("askPrice"),66            price_label=(f"{it['askPrice']:,.0f} $".replace(",", " ")67                         if it.get("askPrice") else ""),68            bedrooms=it.get("nbBedrooms"),69            bathrooms=it.get("nbBathrooms"),70            powder_rooms=it.get("nbHalfBaths"),71            area_sqft=round(living * M2_TO_SQFT) if living else None,72            lot_sqft=round(land * M2_TO_SQFT) if land else None,73            year_built=it.get("yearBuilt"),74            details={k: it.get(k) for k in75                     ("propertyType", "buildingType", "toBuild", "taxable",76                      "openHouseDetail", "isOnlineSince") if it.get(k)},77            features=[f for f in (78                f"Type de bâtiment : {it['buildingType']}" if it.get("buildingType") else "",79                f"Sous-type : {it['propertyType']}" if it.get("propertyType") else "",80                "Neuf / à construire" if it.get("toBuild") else "",81                "Prix taxable (+tx)" if it.get("taxable") else "",82            ) if f],83            images=images,84            lat=it.get("latitude"),85            lng=it.get("longitude"),86            broker_name="Ubee",87            agency="Ubee Québec",88        )8990    def fetch(self) -> list[PropertyListing]:91        out: dict[str, PropertyListing] = {}92        for flags in ({"isResidential": True}, {"isCommercial": True}):93            body = {"sortBy": "DateDescending", "listingType": "Seller", **flags}94            for it in self._search(body):95                # Québec seulement (l'API est QC par nature, on double-vérifie)96                if (it.get("province") or "QC") != "QC":97                    continue98                lst = self._to_listing(it)99                if lst is not None:100                    out.setdefault(lst.uid, lst)101        return list(out.values())102