SPB Git

spb/house-ka

Public

fix(c21): Saskatchewan sans polygone API -> recherche par ~30 villes

L API MoxiWorks n a pas de GeoLayer pour la province de Saskatchewan
(« Unable to locate GeoLayer ») : le connecteur accepte desormais une liste
de localites (attribut locations) et C21_SK interroge les ~30 principales
villes SK. Registre canada_agencies restaure (4 sites RP re-fusionnes,
hanlon page_size 10).
Simon-Pierre Boucher committed (Aug 28, 2026) parent b59158a Browse files

3 changed files +113 −14

modified data/canada_agencies.json +37 −0
@@ -107,5 +107,42 @@
107 "site": "https://cbnorthbay.com", 107 "site": "https://cbnorthbay.com",
108 "note": "Recensement ON 2026-08-27 : ~307 fiches (North Bay). RealtyPress/DDF.", 108 "note": "Recensement ON 2026-08-27 : ~307 fiches (North Bay). RealtyPress/DDF.",
109 "province": "Ontario" 109 "province": "Ontario"
110+ },
111+ {
112+ "id": "rp_ag_hanlonrealty",
113+ "name": "hanlonrealty.ca (Newfoundland and Labrador)",
114+ "site": "https://hanlonrealty.ca",
115+ "archive": "listing",
116+ "max_pages": 2400,
117+ "province": "Newfoundland and Labrador",
118+ "note": "Recensement CANADA 2026-08-27 : ~22 000 fiches (board NL complet). posts_per_page plafonne a ~3 500 items -> pagination NATIVE 10/page. RealtyPress/DDF.",
119+ "page_size": 10
120+ },
121+ {
122+ "id": "rp_ag_denisedunnrealtor",
123+ "name": "denisedunnrealtor.com (British Columbia)",
124+ "site": "https://denisedunnrealtor.com",
125+ "archive": "listing",
126+ "max_pages": 400,
127+ "province": "British Columbia",
128+ "note": "Recensement CANADA 2026-08-27 : ~≥2400 fiches, provinces page 1 : {'British Columbia': 24}. RealtyPress/DDF."
129+ },
130+ {
131+ "id": "rp_ag_reddoorrealty",
132+ "name": "reddoorrealty.ca (Nova Scotia)",
133+ "site": "https://reddoorrealty.ca",
134+ "archive": "listing",
135+ "max_pages": 10,
136+ "province": "Nova Scotia",
137+ "note": "Recensement CANADA 2026-08-27 : ~≥580 fiches, provinces page 1 : {'Nova Scotia': 202}. RealtyPress/DDF."
138+ },
139+ {
140+ "id": "rp_ag_raymondanthony",
141+ "name": "raymondanthony.com (Alberta)",
142+ "site": "https://raymondanthony.com",
143+ "archive": "listing",
144+ "max_pages": 400,
145+ "province": "Alberta",
146+ "note": "Recensement CANADA 2026-08-27 : ~≥60 fiches, provinces page 1 : {'Alberta': 25}. RealtyPress/DDF."
110 } 147 }
111 ] 148 ]
\ No newline at end of file
modified data/sources.json +44 −0
@@ -285,6 +285,50 @@
285 "status": "actif", 285 "status": "actif",
286 "type": "reseau", 286 "type": "reseau",
287 "note": "API MoxiWorks corporative de c21.ca (svc.moxiworks.com listing/search_v2, JSONP). Paramètres d'attribution obligatoires, découpage adaptatif par tranches de prix (liste retournée seulement si number_found ≤ ~6k). Réponse liste complète : AUCUNE passe détail nécessaire. Rétro-conçue 2026-08-27." 287 "note": "API MoxiWorks corporative de c21.ca (svc.moxiworks.com listing/search_v2, JSONP). Paramètres d'attribution obligatoires, découpage adaptatif par tranches de prix (liste retournée seulement si number_found ≤ ~6k). Réponse liste complète : AUCUNE passe détail nécessaire. Rétro-conçue 2026-08-27."
288+ },
289+ {
290+ "id": "rp_ag_hanlonrealty",
291+ "name": "hanlonrealty.ca (Newfoundland and Labrador)",
292+ "url": "https://hanlonrealty.ca",
293+ "listing_url": "https://hanlonrealty.ca/listing/",
294+ "coverage": "Newfoundland and Labrador — ~≥22000 fiches DDF",
295+ "connector": "realtypress",
296+ "status": "actif",
297+ "type": "agence",
298+ "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)."
299+ },
300+ {
301+ "id": "rp_ag_denisedunnrealtor",
302+ "name": "denisedunnrealtor.com (British Columbia)",
303+ "url": "https://denisedunnrealtor.com",
304+ "listing_url": "https://denisedunnrealtor.com/listing/",
305+ "coverage": "British Columbia — ~≥2400 fiches DDF",
306+ "connector": "realtypress",
307+ "status": "actif",
308+ "type": "agence",
309+ "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)."
310+ },
311+ {
312+ "id": "rp_ag_reddoorrealty",
313+ "name": "reddoorrealty.ca (Nova Scotia)",
314+ "url": "https://reddoorrealty.ca",
315+ "listing_url": "https://reddoorrealty.ca/listing/",
316+ "coverage": "Nova Scotia — ~≥580 fiches DDF",
317+ "connector": "realtypress",
318+ "status": "actif",
319+ "type": "agence",
320+ "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)."
321+ },
322+ {
323+ "id": "rp_ag_raymondanthony",
324+ "name": "raymondanthony.com (Alberta)",
325+ "url": "https://raymondanthony.com",
326+ "listing_url": "https://raymondanthony.com/listing/",
327+ "coverage": "Alberta — ~≥60 fiches DDF",
328+ "connector": "realtypress",
329+ "status": "actif",
330+ "type": "agence",
331+ "note": "House-Ka — generic RealtyPress connector (registry data/canada_agencies.json). WordPress RealtyPress plugin on the CREA DDF feed. external_id ddf<id>: cross-site dedup by MIN(uid)."
288 } 332 }
289 ] 333 ]
290 } 334 }
\ No newline at end of file
modified immoka/connectors/c21_canada.py +32 −14
@@ -71,13 +71,18 @@ class _C21Province(BaseConnector):
71 71
72 province = "" # libellé exact pour location_search_field 72 province = "" # libellé exact pour location_search_field
73 state_code = "" # code à 2 lettres attendu dans location.state 73 state_code = "" # code à 2 lettres attendu dans location.state
74+ locations: tuple = () # localités interrogées (défaut : la province) —
75+ # l'API n'a PAS de polygone pour certaines
76+ # provinces (Saskatchewan : « Unable to locate
77+ # GeoLayer ») → liste de villes à la place
74 request_delay = 0.35 78 request_delay = 0.35
75 79
76 # ------------------------------------------------------------------ API -- 80 # ------------------------------------------------------------------ API --
77 def _query(self, pricemin: int | None, pricemax: int | None, 81 def _query(self, pricemin: int | None, pricemax: int | None,
78− startidx: int = 0, pgsize: int = PGSIZE) -> dict: 82+ startidx: int = 0, pgsize: int = PGSIZE,
83+ location: str | None = None) -> dict:
79 params = dict(BASE_PARAMS) 84 params = dict(BASE_PARAMS)
80− params["location_search_field"] = self.province 85+ params["location_search_field"] = location or self.province
81 params["pgsize"] = str(pgsize) 86 params["pgsize"] = str(pgsize)
82 params["startidx"] = str(startidx) 87 params["startidx"] = str(startidx)
83 if pricemin is not None and pricemin > 0: 88 if pricemin is not None and pricemin > 0:
@@ -91,18 +96,20 @@ class _C21Province(BaseConnector):
91 raise RuntimeError(f"c21 API: {d.get('message', d.get('status'))}") 96 raise RuntimeError(f"c21 API: {d.get('message', d.get('status'))}")
92 return d["data"] 97 return d["data"]
93 98
94− def _count(self, lo: int | None, hi: int | None) -> int: 99+ def _count(self, lo: int | None, hi: int | None,
95− d = self._query(lo, hi, startidx=0, pgsize=1) 100+ location: str | None = None) -> int:
101+ d = self._query(lo, hi, startidx=0, pgsize=1, location=location)
96 return int(d.get("number_found") or 0) 102 return int(d.get("number_found") or 0)
97 103
98 # ------------------------------------------------------ tranches de prix -- 104 # ------------------------------------------------------ tranches de prix --
99− def _slices(self) -> list[tuple[int | None, int | None]]: 105+ def _slices(self, location: str | None = None
106+ ) -> list[tuple[int | None, int | None]]:
100 """Tranches [lo, hi] dont number_found ≤ SLICE_MAX (scission dichotomique).""" 107 """Tranches [lo, hi] dont number_found ≤ SLICE_MAX (scission dichotomique)."""
101 out: list[tuple[int | None, int | None]] = [] 108 out: list[tuple[int | None, int | None]] = []
102 stack: list[tuple[int, int]] = [(0, PRICE_CEIL)] 109 stack: list[tuple[int, int]] = [(0, PRICE_CEIL)]
103 while stack: 110 while stack:
104 lo, hi = stack.pop() 111 lo, hi = stack.pop()
105− n = self._count(lo or None, hi) 112+ n = self._count(lo or None, hi, location=location)
106 time.sleep(self.request_delay) 113 time.sleep(self.request_delay)
107 if n == 0: 114 if n == 0:
108 continue 115 continue
@@ -189,13 +196,14 @@ class _C21Province(BaseConnector):
189 # --------------------------------------------------------------- fetch -- 196 # --------------------------------------------------------------- fetch --
190 def fetch(self) -> list[PropertyListing]: 197 def fetch(self) -> list[PropertyListing]:
191 by_id: dict[str, PropertyListing] = {} 198 by_id: dict[str, PropertyListing] = {}
192− for lo, hi in self._slices(): 199+ for loc in (self.locations or (None,)):
193− d = self._query(lo, hi, pgsize=PGSIZE) 200+ for lo, hi in self._slices(location=loc):
194− for r in (d.get("result_list") or []): 201+ d = self._query(lo, hi, pgsize=PGSIZE, location=loc)
195− lst = self._to_listing(r) 202+ for r in (d.get("result_list") or []):
196− if lst is not None and lst.external_id not in by_id: 203+ lst = self._to_listing(r)
197− by_id[lst.external_id] = lst 204+ if lst is not None and lst.external_id not in by_id:
198− time.sleep(self.request_delay) 205+ by_id[lst.external_id] = lst
206+ time.sleep(self.request_delay)
199 return list(by_id.values()) 207 return list(by_id.values())
200 208
201 209
@@ -214,9 +222,19 @@ _PROVINCES = [
214 ("c21_nt", "Northwest Territories", "NT"), 222 ("c21_nt", "Northwest Territories", "NT"),
215 ] 223 ]
216 224
225+# Saskatchewan : pas de polygone provincial côté API → villes principales
226+_SK_CITIES = tuple(f"{c}, SK" for c in (
227+ "Saskatoon", "Regina", "Prince Albert", "Moose Jaw", "Swift Current",
228+ "Yorkton", "North Battleford", "Estevan", "Weyburn", "Warman",
229+ "Martensville", "Melfort", "Humboldt", "Meadow Lake", "Kindersley",
230+ "Melville", "Nipawin", "Tisdale", "La Ronge", "Battleford", "Rosetown",
231+ "Assiniboia", "Unity", "Canora", "Esterhazy", "Outlook", "Watrous",
232+ "Moosomin", "Shellbrook", "Fort Qu'Appelle"))
233+
217 for _sid, _prov, _code in _PROVINCES: 234 for _sid, _prov, _code in _PROVINCES:
235+ _extra = {"locations": _SK_CITIES} if _code == "SK" else {}
218 globals()[f"C21_{_code}"] = type( 236 globals()[f"C21_{_code}"] = type(
219 f"C21{_code}", 237 f"C21{_code}",
220 (_C21Province,), 238 (_C21Province,),
221− {"source_id": _sid, "province": _prov, "state_code": _code}, 239+ {"source_id": _sid, "province": _prov, "state_code": _code, **_extra},
222 ) 240 )
223 241
Menu

Theme