spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
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