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%
4.5 KB · 117 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Lou-Ka — Location court terme3# connectors/rezerve.py : Rëzerve / reserver.ca (Charlevoix, Laurentides,4# Estrie, Lanaudière, Mauricie, Outaouais…)5#6# Gestionnaire québécois (~85 chalets) sur moteur Guesty. Le site reserver.ca7# est un Next.js (App Router) : le catalogue /chalets est RENDU SERVEUR et le8# payload React Flight (`self.__next_f.push([1,"…"])`) contient l'objet9# {"listings":[…]} complet — id Guesty, slug, nom, description fr, photos,10# chambres, sdb, lits, capacité, prix « à partir de » CAD, ville, adresse,11# lat/lng, commodités, numéro CITQ, heures d'arrivée/départ. UNE SEULE requête12# suffit, aucune page détail à visiter.13#   External_id = id Guesty (stable). URL publique : /chalets/<slug>.14# -----------------------------------------------------------------------------15from __future__ import annotations1617import json18import re1920from ..schema import StListing21from .base import StConnector2223SITE = "https://reserver.ca"242526def _num(v) -> float | None:27    try:28        return float(v) if v not in (None, "") else None29    except (TypeError, ValueError):30        return None313233def _flight_blob(html: str) -> str:34    """Reconstitue le payload React Flight (chunks __next_f concaténés)."""35    parts = []36    for c in re.findall(r'self\.__next_f\.push\(\[1,"((?:[^"\\]|\\.)*)"\]\)',37                        html):38        try:39            parts.append(json.loads(f'"{c}"'))40        except ValueError:41            continue42    return "".join(parts)434445class Rezerve(StConnector):46    source_id = "rezerve"4748    def _catalog(self) -> list[dict]:49        html = self.get(f"{SITE}/chalets").text50        blob = _flight_blob(html)51        i = blob.find('{"listings":[')52        if i < 0:53            return []54        obj, _ = json.JSONDecoder().raw_decode(blob[i:])55        return obj.get("listings") or []5657    @staticmethod58    def _description(it: dict) -> str:59        desc = str(it.get("description") or "")60        if not desc or desc.startswith("$"):     # référence Flight non résolue61            desc = str((it.get("descriptions") or {}).get("fr") or "")62        if desc.startswith("$"):63            desc = ""64        return re.sub(r"\s+", " ", desc).strip()[:4000]6566    # -- contrat ----------------------------------------------------------67    def fetch(self) -> list[StListing]:68        listings: list[StListing] = []69        for it in self._catalog():70            lid = str(it.get("id") or "").strip()71            slug = (it.get("slug") or "").strip()72            title = (it.get("name") or "").strip()73            if not lid or not slug or not title:74                continue7576            geo = it.get("geo") or {}77            citq = str((it.get("citq") or {}).get("number") or "").strip()78            street = (it.get("addressStreet") or "").strip()79            postal = (it.get("addressPostal") or "").strip()80            price = _num(it.get("priceFromCAD"))8182            details = {k: v for k, v in {83                "guesty_id": lid,84                "area_sqft": it.get("areaSquareFeet"),85                "check_in": it.get("checkInTime"),86                "check_out": it.get("checkOutTime"),87                "tags": it.get("tags") or None,88            }.items() if v}8990            listings.append(StListing(91                source=self.source_id,92                external_id=lid,93                url=f"{SITE}/chalets/{slug}",94                title=title,95                property_type="Chalet",96                address=" ".join(p for p in (street, postal) if p),97                city=(it.get("city") or "").strip(),98                region="",                # ville + lat/lng font foi99                price_night=price,100                price_label=(f"à partir de {price:.0f} $ / nuit"101                             if price else ""),102                capacity=_num(it.get("maxGuests")),103                bedrooms=_num(it.get("bedrooms")),104                beds=_num(it.get("beds")),105                bathrooms=_num(it.get("bathrooms")),106                citq=citq if re.fullmatch(r"\d{6}", citq) else "",107                description=self._description(it),108                amenities=[a for a in (it.get("amenities") or [])109                           if isinstance(a, str)][:80],110                details=details,111                images=[u for u in (it.get("images") or [])112                        if isinstance(u, str)][:20],113                lat=_num(geo.get("lat")),114                lng=_num(geo.get("lng")),115            ))116        return listings117