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/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