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%
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