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%

[ka2] fix connecteur devimco: escalade Scrapfly contre le pare-feu Sucuri CloudProxy apparu sur devimco.com — même cause et même fix que brio (195f19a). Les syncs du 2026-09-13 (05:03, 15:16, 20:16... puis 23:23) rapportaient found=0 ok par INTERMITTENCE (médiane 695, dérive déclenchée, retraits suspendus) : devimco.com est passé derrière Sucuri/Cloudproxy, qui répond 307 + challenge JavaScript (cookie sucuri_cloudproxy, en-tête x-sucuri-id, ~1,4 Ko sans Location) aux clients sans navigateur selon l'humeur du filtre IP — un statut < 400 invisible de raise_for_status(), le hub sans lien projet devenait « 0 trouvé ok » silencieux via le `except Exception: return listings` de fetch(). Le diagnostic « échec transitoire » du passage ka2 de 08:08 était donc incomplet : le hoquet se répétait un sync sur deux. Vérifié live : 3 GET directs consécutifs → 307 challenge ; Scrapfly ASP + rendu JS → 200, 297 Ko, 10 pages projets. Fix minimal, pattern brio : _get_html() tente le direct (gratuit quand Sucuri laisse passer), détecte le challenge (statut 307 ou marqueur sucuri_cloudproxy) et escalade vers get_rendered() (Scrapfly ASP + rendu JS, standard du repo) ; hub ET pages projets passent par _get_html() ; le hub lève désormais une erreur franche au lieu de retourner [] (fini le zéro silencieux, idem si aucun lien projet dans la soupe) ; les pages projets gardent le continue (résultats partiels acceptables) ; l'API Planpoint (app.planpoint.io, domaine distinct) reste en direct. Testé : pytest fixtures devimco 2 passed, fetch() venv → 695 annonces (13 phases : Cosma 237, Ostral 160, Maestria A/B 121, Alexander 66... — unité 309 Alexander 2 130 $/mois, 2255 René-Lévesque O, 13 photos — médiane 695 restaurée), run.py sync devimco → found=695 ok (161 s), pm2 restart lou-ka-sync, site :8095 → 200.

Simon-Pierre Boucher committed 11 days ago (Sep 14, 2026) parent b57e64d

1 changed file +26 −5

modified louka/connectors/devimco.py +26 −5
@@ -88,12 +88,29 @@ class DevimcoConnector(BaseConnector):
88 88 request_delay = 0.6
89 89 max_projects = 15 # garde-fou
90 90
91 + def _get_html(self, url: str) -> str:
92 + """HTML de `url`, tolérant au pare-feu Sucuri CloudProxy.
93 +
94 + Vu 2026-09-13 : devimco.com est passé derrière Sucuri/Cloudproxy,
95 + qui répond par intermittence 307 + challenge JavaScript (cookie
96 + sucuri_cloudproxy) aux clients sans navigateur — un statut < 400
97 + invisible de raise_for_status(), dont la soupe sans lien projet
98 + devenait un « 0 trouvé ok » silencieux. Direct d'abord (gratuit si
99 + Sucuri laisse passer), sinon escalade Scrapfly ASP + rendu JS qui
100 + passe le challenge (pattern brio, fix 195f19a).
101 + """
102 + try:
103 + resp = self.get(url)
104 + if (resp.status_code != 307
105 + and "sucuri_cloudproxy" not in resp.text):
106 + return resp.text
107 + except Exception: # noqa: BLE001 — direct bloqué : on escalade
108 + pass
109 + return self.get_rendered(url)
110 +
91 111 def fetch(self) -> list[Listing]:
92 112 listings: list[Listing] = []
93 − try:
94 − hub = self.get(HUB_URL).text
95 − except Exception:
96 − return listings
113 + hub = self._get_html(HUB_URL)
97 114
98 115 # 1) Pages projets (a-louer/<région>/<secteur>/<projet>)
99 116 projects: dict[str, tuple[str, str]] = {}
@@ -103,6 +120,10 @@ class DevimcoConnector(BaseConnector):
103 120 # ignorer les pages de secteur (sans slug projet "…-appartements")
104 121 if proj_slug.endswith("-appartements"):
105 122 projects[url] = (sector_slug, proj_slug)
123 + if not projects:
124 + raise RuntimeError(
125 + "devimco : aucun lien projet dans le hub — challenge Sucuri "
126 + f"non passé ou site remanié ? — {HUB_URL}")
106 127
107 128 for i, (proj_url, (sector_slug, proj_slug)) in \
108 129 enumerate(projects.items()):
@@ -118,7 +139,7 @@ class DevimcoConnector(BaseConnector):
118 139 # -- un projet ---------------------------------------------------------------
119 140 def _fetch_project(self, proj_url: str, sector_slug: str) -> list[Listing]:
120 141 out: list[Listing] = []
121 − html = self.get(proj_url).text
142 + html = self._get_html(proj_url)
122 143
123 144 # iframe Planpoint : soit un projet (/<ns>/<host>), soit un groupe
124 145 # de phases. Depuis 2026-08, le widget groupe est /g/<x>/<ns> (deux
125 146