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
3 days agolast push
HTML 98.9% Python 0.6%
6.5 KB · 157 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Lou-Ka — Location court terme3# connectors/lesversants.py : Les Versants Mont-Tremblant (lesversants.com) —4#   agence immobilière de Mont-Tremblant dont la division location propose5#   ~16 maisons et condos en location saisonnière (Mont-Tremblant,6#   Lac-Supérieur, Mont-Blanc).7#8# Méthode : le sitemap.xml est un vestige statique (2 URLs), mais le thème9#   WordPress REAL_HOMES expose tout via l'API REST :10#     GET /fr/wp-json/wp/v2/property?property-status=79&per_page=10011#   (79 = terme « À louer » de la taxonomie property-status). Chaque fiche12#   embarque property_meta REAL_HOMES_* : adresse, lat/lng, chambres, salles13#   de bain (formats « 3 + 1 »), superficie, galerie (sizes.large), prix.14#   ⚠️ PRIX SAISONNIER : le prix affiché est « Hiver 2026-27 | 18 500 $ »15#   (un forfait pour LA SAISON, pas à la nuit) → on le range dans16#   details.season_price et on laisse price_night/price_label vides pour ne17#   pas empoisonner parse_price_night().18#   Taxonomies property-type (439 Condo à louer, 440/441 maisons), city et19#   mont_tremblant_sector résolues par une passe chacune.20#21# Réglage env : LOUKA_LESVERSANTS_LIMIT (nb max d'annonces, 0 = tout).22# -----------------------------------------------------------------------------23from __future__ import annotations2425import html as _html26import os27import re2829from ..schema import StListing30from .base import StConnector3132API = "https://lesversants.com/fr/wp-json/wp/v2"33STATUS_A_LOUER = 79  # terme « À louer » (property-status)3435# id property-type → type canonique36_TYPES = {439: "Condo", 440: "Maison", 441: "Maison"}3738_TAG_RE = re.compile(r"<[^>]+>")394041def _strip_html(txt: str) -> str:42    return re.sub(r"\s+", " ", _TAG_RE.sub(" ", _html.unescape(txt or ""))).strip()434445def _rooms(v: str) -> float | None:46    """« 3 », « 3 + 1 », « 3.5 » → nombre (les « + N » sont additionnés)."""47    nums = re.findall(r"\d+(?:\.\d+)?", str(v or ""))48    return sum(float(n) for n in nums) if nums else None495051class LesVersants(StConnector):52    source_id = "lesversants"53    request_delay = 0.55455    def _get_json(self, url: str):56        return self.get(url, headers={"Accept": "application/json"}).json()5758    def _tax(self, name: str) -> dict[int, str]:59        try:60            terms = self._get_json(f"{API}/{name}?per_page=100"61                                   "&_fields=id,name")62            return {t["id"]: _html.unescape(t.get("name") or "").strip()63                    for t in terms}64        except Exception:  # noqa: BLE001 — libellés manquants ≠ blocage65            return {}6667    # -- contrat --------------------------------------------------------------68    def fetch(self) -> list[StListing]:69        limit = int(os.environ.get("LOUKA_LESVERSANTS_LIMIT", "0") or 0)70        items = self._get_json(f"{API}/property?property-status="71                               f"{STATUS_A_LOUER}&per_page=100")72        if not isinstance(items, list):73            return []74        if limit:75            items = items[:limit]7677        types = self._tax("property-type")78        cities = self._tax("city")79        sectors = self._tax("mont_tremblant_sector")8081        listings: list[StListing] = []82        seen: set[str] = set()83        for it in items:84            pid = str(it.get("id") or "").strip()85            url = (it.get("link") or "").strip()86            title = _strip_html((it.get("title") or {}).get("rendered") or "")87            if not pid or pid in seen or not url or not title:88                continue89            seen.add(pid)9091            meta = it.get("property_meta") or {}92            loc = meta.get("REAL_HOMES_property_location") or {}93            lat = float(loc["latitude"]) if loc.get("latitude") else None94            lng = float(loc["longitude"]) if loc.get("longitude") else None9596            type_ids = it.get("property-type") or []97            ptype = next((_TYPES[t] for t in type_ids if t in _TYPES), "")98            if not ptype:99                ptype = next((types[t] for t in type_ids if t in types), "")100101            city_ids = it.get("city") or []102            city = next((cities[c] for c in city_ids if c in cities),103                        "Mont-Tremblant")104            sector_ids = it.get("mont_tremblant_sector") or []105            sector = next((sectors[s] for s in sector_ids if s in sectors), "")106107            # prix SAISONNIER (« Hiver 2026-27 | 18 500 $ ») → details108            prefix = (meta.get("REAL_HOMES_property_price_prefix") or "").strip()109            raw_price = re.sub(r"[^\d.]", "",110                               str(meta.get("REAL_HOMES_property_price") or ""))111            season_price = ""112            if raw_price:113                season = prefix.rstrip("|").strip()114                season_price = (f"{season} : {raw_price} $" if season115                                else f"{raw_price} $")116117            size = (meta.get("REAL_HOMES_property_size") or "").strip()118            size_post = (meta.get("REAL_HOMES_property_size_postfix")119                         or "").strip()120121            images: list[str] = []122            for ph in meta.get("REAL_HOMES_property_images") or []:123                sizes = (ph or {}).get("sizes") or {}124                u = ((sizes.get("1536x1536") or {}).get("url")125                     or (sizes.get("large") or {}).get("url")126                     or (sizes.get("medium_large") or {}).get("url") or "")127                if u.startswith("https://") and u not in images:128                    images.append(u)129                if len(images) >= 15:130                    break131132            details = {k: v for k, v in {133                "season_price": season_price,134                "sector": sector,135                "size": f"{size} {size_post}".strip() if size else "",136            }.items() if v}137138            listings.append(StListing(139                source=self.source_id,140                external_id=pid,141                url=url,142                title=title,143                property_type=ptype,144                address=(meta.get("REAL_HOMES_property_address") or "").strip(),145                city=city,146                region="Laurentides",147                bedrooms=_rooms(meta.get("REAL_HOMES_property_bedrooms")),148                bathrooms=_rooms(meta.get("REAL_HOMES_property_bathrooms")),149                description=_strip_html((it.get("content") or {})150                                        .get("rendered") or "")[:3000],151                details=details,152                images=images,153                lat=lat,154                lng=lng,155            ))156        return listings157