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%
5.6 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/devloc.py : connecteur Devloc (devloc.ca / app.devloc.ca)5#   L'inventaire vit dans l'ERP Laravel/Vue app.devloc.ca : l'API publique6#   paginée GET /properties-public?page=N (Accept: application/json) retourne7#   tout : type (TYPE_STUDIO/TYPE_3_5…), pi², loyer demandé, date de8#   disponibilité, adresse géocodée (lat/lng), quartier (region.name), photos9#   (uploads.devloc.ca), inclusions actives, tolérance chiens/chats et10#   promotion. Seules les unités is_available (province QC) sont retenues.11#   Une annonce par unité ; external_id = id numérique de l'ERP ;12#   URL publique : https://app.devloc.ca/public/<slug>.13# -----------------------------------------------------------------------------14from __future__ import annotations1516import re1718from ..schema import Listing19from .base import BaseConnector2021APP = "https://app.devloc.ca"22API_URL = f"{APP}/properties-public"2324TYPE_RE = re.compile(r"TYPE_(\d)_5")25_HEADERS = {"Accept": "application/json",26            "X-Requested-With": "XMLHttpRequest"}27_MAX_PAGES = 30                          # garde-fou pagination282930def _unit_type(t: str) -> str:31    if t == "TYPE_STUDIO":32        return "Studio"33    m = TYPE_RE.fullmatch(t or "")34    return f"{m.group(1)}½" if m else ""353637def _pets(dog: bool | None, cat: bool | None) -> str | None:38    if dog is None and cat is None:39        return None40    if dog and cat:41        return "oui"42    if not dog and not cat:43        return "non"44    return "conditions"454647class DevlocConnector(BaseConnector):48    source_id = "devloc"49    request_delay = 0.75051    def fetch(self) -> list[Listing]:52        listings: list[Listing] = []53        page, last_page = 1, 154        while page <= min(last_page, _MAX_PAGES):55            data = self.get(API_URL, params={"page": page},56                            headers=_HEADERS).json()57            last_page = int((data.get("meta") or {}).get("last_page") or 1)58            for p in data.get("data") or []:59                try:60                    lst = self._listing(p)61                    if lst:62                        listings.append(lst)63                except Exception:64                    continue65            page += 166        return listings6768    def _listing(self, p: dict) -> Listing | None:69        if not p.get("is_available"):70            return None71        adr = p.get("address") or {}72        if (adr.get("province") or "").strip().lower() != "qc":73            return None74        pid = str(p.get("id"))75        slug = p.get("slug") or pid76        street = (adr.get("address_1") or "").strip()77        unite = (adr.get("address_2") or "").strip()78        city = (adr.get("city") or "").strip() or "Montréal"79        postal = (adr.get("postal_code") or "").strip().upper()80        address = street + (f", app. {unite}" if unite else "")81        if city:82            address += f", {city}"83        if postal:84            address += f", QC {postal}"85        try:86            lat = float(adr["latitude"]) if adr.get("latitude") else None87            lng = float(adr["longitude"]) if adr.get("longitude") else None88        except (TypeError, ValueError):89            lat = lng = None9091        region = ((p.get("region") or {}).get("name") or "").strip()92        sector = "" if region.startswith("*") else region9394        ut = _unit_type(p.get("type") or "")95        price = p.get("property_status_asked_rent")96        try:97            price = float(price) if price else None98        except (TypeError, ValueError):99            price = None100        area = None101        try:102            area = float(p["square_feet_area"]) \103                if p.get("square_feet_area") else None104        except (TypeError, ValueError):105            pass106107        date = (p.get("availability_date")108                or p.get("property_status_starting_date") or "")[:10]109        availability = f"Disponible le {date}" if date else "Disponible"110111        amenities = [str(i.get("inclusion_label") or "").strip().capitalize()112                     for i in (p.get("active_inclusions") or [])113                     if i.get("inclusion_label")]114        if p.get("has_laundry_room"):115            amenities.append("Salle de lavage")116        if p.get("number_of_balconies"):117            amenities.append(f"{p['number_of_balconies']} balcon(s)")118119        desc = ""120        promo = (p.get("promotion") or "").strip()121        if promo:122            desc = f"Promotion : {promo}."123        notes = (p.get("tolerance_notes") or "").strip()124        if notes:125            desc = f"{desc} {notes}".strip()126127        titre_type = ut or "logement"128        return Listing(129            source=self.source_id,130            external_id=pid,131            url=f"{APP}/public/{slug}",132            title=f"{street} — {titre_type}" + (f" · app. {unite}"133                                                if unite else ""),134            address=address,135            sector=sector,136            city=city,137            unit_type=ut,138            price=price,139            price_label=f"{price:.0f} $" if price else "",140            availability=availability,141            availability_date=date if re.fullmatch(r"\d{4}-\d{2}-\d{2}",142                                                   date) else None,143            area_sqft=area,144            pets=_pets(p.get("dog_tolerance"), p.get("cat_tolerance")),145            description=desc,146            amenities=amenities,147            images=[f.get("url") for f in (p.get("files") or [])148                    if f.get("url")][:15],149            lat=lat,150            lng=lng,151        )152