# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/humano.py : connecteur Humano District (humanodistrict.ca) # Appartements locatifs au cœur de Sherbrooke — 2 bâtiments (Galt, 1820 rue # Galt Ouest, et Maison Générale) sur l'ancien campus des sœurs. Le site WP # embarque le widget SmartCondo Plans (silo.immo) : l'API JSON # /v2/building/get-project-data?project=humano-district renvoie toutes les # unités (prix, superficie, chambres, sdb, étage, disponibilité 1=dispo / # 2=loué / 3=réservé + availability_date, plan PDF, photo). Granularité : # unité. # ----------------------------------------------------------------------------- from __future__ import annotations import re import unicodedata from .base import BaseConnector from ..schema import Listing SITE = "https://humanodistrict.ca/appartements/" API = ("https://smartcondoplans.silo.immo/v2/building/get-project-data" "?project=humano-district&language=fr") CDN = "https://smartcondoplans.silo.immo/" CITY = "Sherbrooke" ADDRESSES = { "galt": "1820, rue Galt Ouest, Sherbrooke", "maison-generale": "rue Galt Ouest, Sherbrooke", } TYPE_BY_ROOMS = {0: "Studio", 1: "3½", 2: "4½", 3: "5½"} def _slug(s: str) -> str: s = unicodedata.normalize("NFKD", str(s)).encode("ascii", "ignore").decode() return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", s.lower())).strip("-") class HumanoConnector(BaseConnector): source_id = "humano" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: data = self.get(API).json() except Exception: return listings # url de bâtiment par id (les unités référencent building via floors) bld_by_id = {b.get("id"): b for b in data.get("buildings", [])} for u in data.get("project_units", []): try: if u.get("hide") or u.get("isnonunit") or u.get("not_unit"): continue avail = u.get("availability") date = (u.get("availability_date") or "").strip() if avail == 1: availability = (f"Disponible le {date}" if date else "Disponible") elif avail == 2 and date: availability = f"Disponible le {date}" # loué → se libère else: continue # loué sans date / réservé name = str(u.get("name") or "").strip() if not name: continue bname = (u.get("building_name") or "").strip() burl = _slug(bname) if bname else "x" # retrouver l'url canonique du bâtiment si connue for b in bld_by_id.values(): if b.get("name") == bname and b.get("url"): burl = b["url"] break rooms = u.get("room") bathrooms = u.get("bathroom") unit_type = TYPE_BY_ROOMS.get(rooms, "") price = float(u["price"]) if u.get("price") else None area = float(u["area"]) if u.get("area") else None furnished = bool(re.search(r"meubl", name, re.I)) unit_no = re.sub(r"\s*-\s*Meubl[ée]s?$", "", name, flags=re.I) details: dict = {} if u.get("floor_name"): details["floor"] = u["floor_name"] if bname: details["building"] = bname if furnished: details["furnished"] = True if u.get("file"): details["plan_pdf"] = CDN + u["file"].lstrip("/") images = [] if u.get("image"): images.append(CDN + u["image"].lstrip("/")) listings.append(Listing( source=self.source_id, external_id=f"{burl}-{_slug(name)}", url=SITE, title=f"Unité {unit_no}" + (f" ({unit_type})" if unit_type else "") + (f" — {bname}, Humano District" if bname else " — Humano District"), address=ADDRESSES.get(burl, "rue Galt Ouest, Sherbrooke"), city=CITY, unit_type=unit_type, bedrooms=rooms if isinstance(rooms, int) else None, bathrooms=bathrooms if isinstance(bathrooms, int) else None, price=price, availability=availability, area_sqft=area, details=details, images=images, )) except Exception: continue return listings