# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/ubee.py : Ubee (ubee.com) — plateforme immobilière québécoise # API publique anonyme : POST api.ubee.ca/api/anonymous/Search/SearchProperties # (pageIndex=N, 24 résultats/page, JSON riche : adresse, GPS, prix, pièces, # superficies m², année, galerie Cloudinary complète). # Deux volets À VENDRE (listingType=Seller) : résidentiel + commercial. # ----------------------------------------------------------------------------- from __future__ import annotations from ..schema import PropertyListing from .base import BaseConnector API = "https://api.ubee.ca/api/anonymous/Search/SearchProperties" SITE = "https://ubee.com" M2_TO_SQFT = 10.7639 _TYPES = { "Unifamiliale": "Maison", "Condo": "Condo", "Terrain": "Terrain", "Plex": "Immeuble à revenus", "Commercial": "Commercial", "Fermette": "Fermette", "Chalet": "Chalet", } class UbeeConnector(BaseConnector): source_id = "ubee" request_delay = 0.5 def _search(self, body: dict) -> list[dict]: out, page = [], 0 while True: r = self.post(f"{API}?pageIndex={page}", json=body).json() results = r.get("results") or [] out.extend(results) if len(out) >= (r.get("totalCount") or 0) or not results: break page += 1 if page > 200: # garde-fou break return out def _to_listing(self, it: dict) -> PropertyListing | None: lid = str(it.get("id") or "") slug = it.get("slugFr") or it.get("slugEn") or "" if not lid or not slug: return None city = (it.get("city") or "").strip() url = f"{SITE}/a-vendre/{it.get('citySlug') or ''}/{slug}" images = [im["publicUrls"]["default_size"] for im in it.get("images") or [] if (im.get("publicUrls") or {}).get("default_size")] living = it.get("livingSurfaceInMeters") land = it.get("landSurfaceInMeters") ptype = _TYPES.get(it.get("inscriptionType") or "", it.get("inscriptionType") or "") return PropertyListing( source=self.source_id, external_id=lid, url=url, title=f"{ptype} à vendre — {city}" if city else f"{ptype} à vendre", address=it.get("address") or "", city=city, property_type=ptype, price=it.get("askPrice"), price_label=(f"{it['askPrice']:,.0f} $".replace(",", " ") if it.get("askPrice") else ""), bedrooms=it.get("nbBedrooms"), bathrooms=it.get("nbBathrooms"), powder_rooms=it.get("nbHalfBaths"), area_sqft=round(living * M2_TO_SQFT) if living else None, lot_sqft=round(land * M2_TO_SQFT) if land else None, year_built=it.get("yearBuilt"), details={k: it.get(k) for k in ("propertyType", "buildingType", "toBuild", "taxable", "openHouseDetail", "isOnlineSince") if it.get(k)}, features=[f for f in ( f"Type de bâtiment : {it['buildingType']}" if it.get("buildingType") else "", f"Sous-type : {it['propertyType']}" if it.get("propertyType") else "", "Neuf / à construire" if it.get("toBuild") else "", "Prix taxable (+tx)" if it.get("taxable") else "", ) if f], images=images, lat=it.get("latitude"), lng=it.get("longitude"), broker_name="Ubee", agency="Ubee Québec", ) def fetch(self) -> list[PropertyListing]: out: dict[str, PropertyListing] = {} for flags in ({"isResidential": True}, {"isCommercial": True}): body = {"sortBy": "DateDescending", "listingType": "Seller", **flags} for it in self._search(body): # Québec seulement (l'API est QC par nature, on double-vérifie) if (it.get("province") or "QC") != "QC": continue lst = self._to_listing(it) if lst is not None: out.setdefault(lst.uid, lst) return list(out.values())