Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/_planpoint.py : helper PARTAGÉ pour le widget Planpoint5# (app.planpoint.io — même API JSON publique que cosoltec/devimco :6# POST /api/{groups,projects}/find avec {namespace, hostName}).7# Utilisé par espace_w, m3_laval, quartier_7 et lac_jerome (Rive-Nord).8# Pas un connecteur : aucun source_id, ignoré par le registre auto-découvrant.9# -----------------------------------------------------------------------------10from __future__ import annotations1112import time1314from ..schema import Listing, normalize_unit_type1516PLANPOINT_API = "https://app.planpoint.io/api"171819def planpoint_projects(conn, kind: str, namespace: str, host_name: str) -> list[dict]:20 """Projets d'un compte Planpoint (kind = « groups » ou « projects »).2122 POST throttlé via la session du connecteur ; `groups/find` renvoie23 {projects: [...]}, `projects/find` renvoie un projet unique.24 """25 wait = conn.request_delay - (time.time() - conn._last_request)26 if wait > 0:27 time.sleep(wait)28 resp = conn.session.post(f"{PLANPOINT_API}/{kind}/find",29 json={"namespace": namespace, "hostName": host_name},30 timeout=conn.timeout)31 conn._last_request = time.time()32 resp.raise_for_status()33 data = resp.json()34 if kind == "groups":35 return data.get("projects") or []36 return [data] if isinstance(data, dict) else []373839def project_images(project: dict) -> list[str]:40 """Galerie du projet (photos communes) + image de couverture."""41 imgs = [i for i in (project.get("images") or [])42 if isinstance(i, str) and i.startswith("http")]43 cover = project.get("projectImageUrl")44 if isinstance(cover, str) and cover.startswith("http") and cover not in imgs:45 imgs.append(cover)46 return imgs474849def unit_listing(source_id: str, project: dict, floor: dict, unit: dict, *,50 page_url: str, default_city: str,51 building: str = "", extra_amenities: list[str] | None = None,52 ) -> Listing | None:53 """Une unité Planpoint « Available » -> Listing standard (sinon None).5455 Champs communs aux sites Planpoint : external_id = ObjectId de l'unité56 (stable), prix mensuel s'il est publié (>0, jamais inventé), pi²,57 inclusions françaises (inclusionsArr), plans + photos d'unité puis58 galerie du projet, date de livraison -> disponibilité.59 """60 if (unit.get("availability") or "").lower() != "available":61 return None62 uid = unit.get("_id") or ""63 if not uid:64 return None65 name = (project.get("name") or "").strip()66 raw_addr = (project.get("address") or "").strip()67 addr_parts = [p.strip() for p in raw_addr.split(",") if p.strip()]68 street = addr_parts[0] if addr_parts else ""69 city = addr_parts[1] if len(addr_parts) > 1 else default_city70 lat, lng = project.get("lat"), project.get("lon")7172 price = unit.get("price")73 price = float(price) if isinstance(price, (int, float)) and price > 0 else None74 area = unit.get("squareFeet")75 area = float(area) if isinstance(area, (int, float)) and area > 0 else None7677 # type d'unité : champ `type` s'il ressemble à « 4 1/2 » / « 4,5-G »,78 # sinon dérivé du nombre de chambres (« 2 bedrooms » -> 4½)79 unit_type = normalize_unit_type(unit.get("type") or "")80 if not unit_type.endswith("½") and unit_type != "6½+":81 unit_type = normalize_unit_type(unit.get("bedrooms") or "") or unit_type8283 images = [i for i in (unit.get("images") or [])84 if isinstance(i, str) and i.startswith("http")]85 images += [i for i in (unit.get("layoutGallery") or [])86 if isinstance(i, str) and i.startswith("http") and i not in images]87 images += [i for i in project_images(project) if i not in images]8889 amenities: list[str] = []90 for inc in unit.get("inclusionsArr") or []:91 lbl = ((inc.get("fr") or inc.get("en") or "").strip()92 if isinstance(inc, dict) else str(inc).strip())93 if lbl and lbl not in amenities:94 amenities.append(lbl[0].upper() + lbl[1:])95 for lbl in extra_amenities or []:96 if lbl not in amenities:97 amenities.append(lbl)9899 details: dict = {}100 if isinstance(unit.get("bathrooms"), (int, float)):101 details["bathrooms"] = unit["bathrooms"]102 if floor.get("name"):103 details["floor"] = floor["name"]104 if unit.get("orientation"):105 details["orientation"] = unit["orientation"]106 if unit.get("type"):107 details["model"] = unit["type"]108109 delivery = str(unit.get("deliveryDate") or "")[:10]110 availability = f"Disponible le {delivery}" if delivery else "Disponible"111112 facts: list[str] = []113 where = building or name114 if unit.get("name"):115 facts.append(f"Unité {unit['name']}"116 + (f" — {where}" if where else "") + f", {city}.")117 bits: list[str] = []118 if unit.get("type"):119 bits.append(f"modèle {unit['type']}")120 if floor.get("name"):121 bits.append(f"étage {floor['name']}")122 if area:123 bits.append(f"{area:g} pi²")124 if isinstance(unit.get("bathrooms"), (int, float)):125 bits.append(f"{unit['bathrooms']:g} salle(s) de bain")126 if bits:127 facts.append(", ".join(bits).capitalize() + ".")128 if amenities:129 facts.append("Inclusions : " + ", ".join(amenities) + ".")130131 return Listing(132 source=source_id,133 external_id=uid, # ObjectId Planpoint de l'unité134 url=page_url, # pas de page publique par unité135 title=f"{name} — Unité {unit.get('name', '')}".strip(" —"),136 address=street,137 city=city,138 unit_type=unit_type,139 price=price,140 price_label=f"{price:.0f} $ /mois" if price else "",141 availability=availability,142 availability_date=delivery or None,143 area_sqft=area,144 furnished=unit["furnished"] if isinstance(unit.get("furnished"), bool) else None,145 description=" ".join(facts)[:2000],146 amenities=amenities,147 details=details,148 images=images[:30],149 lat=lat if (lat is not None and lng is not None) else None,150 lng=lng if (lat is not None and lng is not None) else None,151 )152