SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
6.4 KB · 152 lines python
Raw Blame History
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