SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
19 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%

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 27 days ago (Aug 28, 2026) parent b59158a

3 changed files +113 −14

modified data/canada_agencies.json +37 −0
@@ -107,5 +107,42 @@
107 107 "site": "https://cbnorthbay.com",
108 108 "note": "Recensement ON 2026-08-27 : ~307 fiches (North Bay). RealtyPress/DDF.",
109 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 285 "status": "actif",
286 286 "type": "reseau",
287 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 72 province = "" # libellé exact pour location_search_field
73 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 78 request_delay = 0.35
75 79
76 80 # ------------------------------------------------------------------ API --
77 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 84 params = dict(BASE_PARAMS)
80 − params["location_search_field"] = self.province
85 + params["location_search_field"] = location or self.province
81 86 params["pgsize"] = str(pgsize)
82 87 params["startidx"] = str(startidx)
83 88 if pricemin is not None and pricemin > 0:
@@ -91,18 +96,20 @@ class _C21Province(BaseConnector):
91 96 raise RuntimeError(f"c21 API: {d.get('message', d.get('status'))}")
92 97 return d["data"]
93 98
94 − def _count(self, lo: int | None, hi: int | None) -> int:
95 − d = self._query(lo, hi, startidx=0, pgsize=1)
99 + def _count(self, lo: int | None, hi: int | None,
100 + location: str | None = None) -> int:
101 + d = self._query(lo, hi, startidx=0, pgsize=1, location=location)
96 102 return int(d.get("number_found") or 0)
97 103
98 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 107 """Tranches [lo, hi] dont number_found ≤ SLICE_MAX (scission dichotomique)."""
101 108 out: list[tuple[int | None, int | None]] = []
102 109 stack: list[tuple[int, int]] = [(0, PRICE_CEIL)]
103 110 while stack:
104 111 lo, hi = stack.pop()
105 − n = self._count(lo or None, hi)
112 + n = self._count(lo or None, hi, location=location)
106 113 time.sleep(self.request_delay)
107 114 if n == 0:
108 115 continue
@@ -189,13 +196,14 @@ class _C21Province(BaseConnector):
189 196 # --------------------------------------------------------------- fetch --
190 197 def fetch(self) -> list[PropertyListing]:
191 198 by_id: dict[str, PropertyListing] = {}
192 − for lo, hi in self._slices():
193 − d = self._query(lo, hi, pgsize=PGSIZE)
194 − for r in (d.get("result_list") or []):
195 − lst = self._to_listing(r)
196 − if lst is not None and lst.external_id not in by_id:
197 − by_id[lst.external_id] = lst
198 − time.sleep(self.request_delay)
199 + for loc in (self.locations or (None,)):
200 + for lo, hi in self._slices(location=loc):
201 + d = self._query(lo, hi, pgsize=PGSIZE, location=loc)
202 + for r in (d.get("result_list") or []):
203 + lst = self._to_listing(r)
204 + if lst is not None and lst.external_id not in by_id:
205 + by_id[lst.external_id] = lst
206 + time.sleep(self.request_delay)
199 207 return list(by_id.values())
200 208
201 209
@@ -214,9 +222,19 @@ _PROVINCES = [
214 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 234 for _sid, _prov, _code in _PROVINCES:
235 + _extra = {"locations": _SK_CITIES} if _code == "SK" else {}
218 236 globals()[f"C21_{_code}"] = type(
219 237 f"C21{_code}",
220 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