# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/cosoltec.py : connecteur Cosoltec (cosoltec.com — projets locatifs # Evado à Sainte-Thérèse, Le Monroe à Blainville, Natür à Saint-Jérôme). # Les sites vitrines (evado.ca, lemonroe.ca, naturcondos.ca) affichent leurs # unités via le widget Planpoint : on interroge la même API JSON publique # (app.planpoint.io/api/{groups,projects}/find, POST namespace+hostName, # 3 requêtes/sync) — prix mensuel, pi², chambres, statut, plans et photos # par unité ; seules les unités « Available » sont retenues. # ----------------------------------------------------------------------------- from __future__ import annotations import time from ..schema import Listing, normalize_unit_type from .base import BaseConnector PLANPOINT_API = "https://app.planpoint.io/api" # les trois vitrines locatives de Cosoltec branchées sur Planpoint _SITES = [ {"kind": "groups", "namespace": "evado", "hostName": "evado", "page": "https://www.evado.ca/plans", "city": "Sainte-Thérèse"}, {"kind": "groups", "namespace": "monroe", "hostName": "monroe", "page": "https://www.lemonroe.ca/plans", "city": "Blainville"}, {"kind": "projects", "namespace": "cosolte", "hostName": "Natur", "page": "https://www.naturcondos.ca/", "city": "Saint-Jérôme"}, ] class CosoltecConnector(BaseConnector): source_id = "cosoltec" request_delay = 0.8 max_pages = 3 # 1 appel API par vitrine (evado, monroe, natur) # -- POST poli (même throttling/session que get(), donc record/replay ok) -- def _post_json(self, url: str, payload: dict) -> dict: wait = self.request_delay - (time.time() - self._last_request) if wait > 0: time.sleep(wait) resp = self.session.post(url, json=payload, timeout=self.timeout) self._last_request = time.time() resp.raise_for_status() return resp.json() def fetch(self) -> list[Listing]: listings: list[Listing] = [] for site in _SITES: try: data = self._post_json( f"{PLANPOINT_API}/{site['kind']}/find", {"namespace": site["namespace"], "hostName": site["hostName"]}) except Exception: continue # groups/find -> {projects: [...]} ; projects/find -> un seul projet projects = data.get("projects") if site["kind"] == "groups" else [data] for project in projects or []: listings.extend(self._parse_project(project, site)) return listings # -- un projet Planpoint = un immeuble --------------------------------------- def _parse_project(self, project: dict, site: dict) -> list[Listing]: name = (project.get("name") or "").strip() # adresse « 350 Place Fabien-Drapeau, Sainte-Thérèse, Quebec J7E 0C4 » addr_parts = [p.strip() for p in (project.get("address") or "").split(",") if p.strip()] street = addr_parts[0] if addr_parts else "" city = addr_parts[1] if len(addr_parts) > 1 else site["city"] lat = project.get("lat") lng = project.get("lon") res: list[Listing] = [] for floor in project.get("floors") or []: for u in floor.get("units") or []: # seules les unités affichées « Disponible » chez la source if (u.get("availability") or "").lower() != "available": continue uid = u.get("_id") or "" if not uid: continue price = u.get("price") price = float(price) if isinstance(price, (int, float)) and price > 0 else None area = u.get("squareFeet") area = float(area) if isinstance(area, (int, float)) and area > 0 else None images = [i for i in (u.get("images") or []) if isinstance(i, str)] images += [i for i in (u.get("layoutGallery") or []) if isinstance(i, str) and i not in images] details: dict = {} if isinstance(u.get("bathrooms"), (int, float)): details["bathrooms"] = u["bathrooms"] if floor.get("name"): details["floor"] = floor["name"] res.append(Listing( source=self.source_id, external_id=uid, # ObjectId Planpoint de l'unité url=site["page"], # pas de page par unité publiée title=f"{name} — Unité {u.get('name', '')}".strip(" —"), address=street, city=city, unit_type=normalize_unit_type(u.get("bedrooms") or ""), price=price, price_label=f"{price:.0f} $ /mois" if price else "", availability=u.get("availability") or "", area_sqft=area, furnished=u["furnished"] if isinstance(u.get("furnished"), bool) else None, details=details, images=images[:30], lat=lat if (lat is not None and lng is not None) else None, lng=lng if (lat is not None and lng is not None) else None, )) return res