SPB Git

spb/lou-ka Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

HTML 99.7%
3.8 KB · 97 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/base.py : classe de base des connecteurs + backends de fetch5#                      (requests direct, ou Firecrawl pour les sites JavaScript)6# -----------------------------------------------------------------------------7from __future__ import annotations89import os10import time1112import requests1314from ..schema import Listing1516USER_AGENT = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "17              "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36 "18              "LouKaBot/1.0 (+https://www.lou-ka.com/bot; contact@spboucher.ai)")1920FIRECRAWL_API = "https://api.firecrawl.dev/v1/scrape"212223class BaseConnector:24    """Un connecteur = un adaptateur propre à un site d'agence.2526    Sous-classes : définir `source_id` et implémenter `fetch()` qui retourne27    la liste complète des annonces actuellement affichées sur le site.28    Le pipeline (ingest.py) s'occupe du diff avec la base de données.29    """3031    source_id: str = ""32    request_delay: float = 0.6           # politesse entre requêtes33    timeout: int = 3034    use_detail_cache: bool = True        # cache BD des pages détail3536    def __init__(self) -> None:37        self.session = requests.Session()38        self.session.headers["User-Agent"] = USER_AGENT39        self._last_request = 0.040        self._detail_con = None4142    # -- backends -------------------------------------------------------------43    def get(self, url: str, **kw) -> requests.Response:44        """GET direct avec throttling poli."""45        wait = self.request_delay - (time.time() - self._last_request)46        if wait > 0:47            time.sleep(wait)48        resp = self.session.get(url, timeout=self.timeout, **kw)49        self._last_request = time.time()50        resp.raise_for_status()51        return resp5253    def get_rendered(self, url: str) -> str:54        """Récupère le HTML rendu (JavaScript exécuté) via Firecrawl.5556        Nécessite FIRECRAWL_API_KEY dans l'environnement (.env).57        À utiliser pour les sites SPA (Logisco, Locago, etc.).58        """59        key = os.environ.get("FIRECRAWL_API_KEY")60        if not key:61            raise RuntimeError("FIRECRAWL_API_KEY manquant (voir .env)")62        resp = requests.post(63            FIRECRAWL_API,64            json={"url": url, "formats": ["html"]},65            headers={"Authorization": f"Bearer {key}"},66            timeout=90,67        )68        resp.raise_for_status()69        data = resp.json()70        return (data.get("data") or {}).get("html", "")7172    def detail(self, external_id: str, key: str, fetch_fn) -> dict:73        """Payload « page détail » avec cache : `fetch_fn` n'est appelé que si74        l'annonce est nouvelle ou si sa clé (hash du contenu liste) a changé.7576        Permet d'extraire les champs riches (description, inclusions, contact…)77        sans revisiter chaque page détail à chaque synchronisation.78        `fetch_fn` doit retourner un dict JSON-sérialisable.79        """80        if not self.use_detail_cache:81            return fetch_fn() or {}82        from .. import db83        if self._detail_con is None:84            self._detail_con = db.connect()85        cached = db.get_cached_detail(self._detail_con, self.source_id,86                                      str(external_id), key)87        if cached is not None:88            return cached89        payload = fetch_fn() or {}90        db.put_cached_detail(self._detail_con, self.source_id,91                             str(external_id), key, payload)92        return payload9394    # -- contrat --------------------------------------------------------------95    def fetch(self) -> list[Listing]:96        raise NotImplementedError97