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 douek: retry (3 essais, backoff 5/10 s) de la page d'accueil — un raté transitoire (vu 2026-08-30 ~10:00) produisait un faux « 0 trouvé ok » (médiane 10) car fetch() retournait [] en silence si l'accueil n'avait aucune carte featured_property et avalait les exceptions par immeuble; désormais erreur franche si aucune carte après 3 essais ou si des immeubles sont en erreur sans aucune annonce récoltée. Testé: run.py sync douek → 10 annonces (= médiane 10)

Simon-Pierre Boucher committed 25 days ago (Aug 30, 2026) parent e013adf

1 changed file +43 −21

modified louka/connectors/douek.py +43 −21
@@ -13,6 +13,7 @@
13 13 from __future__ import annotations
14 14
15 15 import re
16 +import time
16 17
17 18 from bs4 import BeautifulSoup
18 19
@@ -41,35 +42,56 @@ class DouekConnector(AlfidConnector):
41 42 max_buildings = 20 # garde-fou (9 immeubles à l'écriture)
42 43
43 44 def fetch(self) -> list[Listing]:
44 − home = self.get(f"{SITE}/").text
45 − soup = BeautifulSoup(home, "html.parser")
46 −
47 − # cartes d'immeubles de l'accueil : slug, nom, adresse
45 + # Un raté transitoire (vu 2026-08-30 ~10:00) produisait un faux
46 + # « 0 trouvé ok » (médiane 10) : retry de l'accueil (3 essais,
47 + # backoff 5/10 s) puis erreur franche plutôt qu'un zéro silencieux.
48 48 buildings: dict[str, dict] = {}
49 − for card in soup.select(".featured_property"):
50 − a = card.select_one('a[href*="/rent/"]')
51 − if not a:
52 − continue
53 − slug = (a.get("href") or "").rstrip("/").split("/")[-1]
54 − if not slug or slug == "cities":
55 − continue
56 − name_el = card.select_one(".building-name")
57 − addr_el = card.select_one(".building-address")
58 − buildings.setdefault(slug, {
59 − "name": _clean(name_el.get_text(" ", strip=True)
60 − if name_el else slug),
61 − "address": _clean(addr_el.get_text(" ", strip=True)
62 − if addr_el else ""),
63 − })
49 + for attempt in range(3):
50 + try:
51 + home = self.get(f"{SITE}/").text
52 + except Exception:
53 + if attempt == 2:
54 + raise
55 + home = ""
56 + soup = BeautifulSoup(home, "html.parser")
57 + # cartes d'immeubles de l'accueil : slug, nom, adresse
58 + for card in soup.select(".featured_property"):
59 + a = card.select_one('a[href*="/rent/"]')
60 + if not a:
61 + continue
62 + slug = (a.get("href") or "").rstrip("/").split("/")[-1]
63 + if not slug or slug == "cities":
64 + continue
65 + name_el = card.select_one(".building-name")
66 + addr_el = card.select_one(".building-address")
67 + buildings.setdefault(slug, {
68 + "name": _clean(name_el.get_text(" ", strip=True)
69 + if name_el else slug),
70 + "address": _clean(addr_el.get_text(" ", strip=True)
71 + if addr_el else ""),
72 + })
73 + if buildings:
74 + break
75 + if attempt < 2:
76 + time.sleep(5 * (attempt + 1))
77 + if not buildings:
78 + raise RuntimeError(
79 + "accueil douek.ca sans carte featured_property après 3 essais")
64 80
65 81 listings: list[Listing] = []
82 + errors = 0
83 + last_exc: Exception | None = None
66 84 for i, (slug, meta) in enumerate(buildings.items()):
67 85 if i >= self.max_buildings:
68 86 break
69 87 try:
70 88 listings.extend(self._parse_rent_page(slug, meta))
71 − except Exception:
72 − continue
89 + except Exception as exc:
90 + errors += 1
91 + last_exc = exc
92 + # tous les immeubles en erreur et rien récolté : erreur franche
93 + if not listings and last_exc is not None and errors:
94 + raise last_exc
73 95 return listings
74 96
75 97 # -- fiche /rent/<slug> : suites rendues serveur -----------------------------
76 98