# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # mortgage/providers/eq_bank.py : Banque EQ / Équitable — dictionnaire JSON # global embarqué dans le payload Next.js de la page des taux (marqueur # \"rates\":{ échappé). Clés CMS stables ; les clés « Adjustable » sont des # ÉCARTS vs prime (jamais interprétées comme des taux). # ----------------------------------------------------------------------------- from __future__ import annotations import json from .base import RateProvider # clé CMS -> (rate_type, terme mois, kind, nom, clé APR éventuelle) KEY_MAP: dict[str, tuple] = { "mortgage-rate-fixed-5-year": ("fixed", 60, "special", "Fixe 5 ans (vitrine EQ)", "mortgage-rate-fixed-5-year-APR"), "mortgage-rate-variable-5-year": ("variable", 60, "special", "Variable 5 ans (vitrine EQ)", "mortgage-rate-variable-5-year-APR"), "es-fixed-12-month": ("fixed", 12, "special", "Evolution Suite — fixe 1 an", None), "es-fixed-24-month": ("fixed", 24, "special", "Evolution Suite — fixe 2 ans", None), "es-fixed-36-month": ("fixed", 36, "special", "Evolution Suite — fixe 3 ans", None), "es-fixed-48-month": ("fixed", 48, "special", "Evolution Suite — fixe 4 ans", None), "es-fixed-60-month": ("fixed", 60, "special", "Evolution Suite — fixe 5 ans", None), "Standard-Mortgage-Rate-1-Year-Fixed": ("fixed", 12, "posted", "Fixe affiché 1 an", None), "Standard-Mortgage-Rate-2-Year-Fixed": ("fixed", 24, "posted", "Fixe affiché 2 ans", None), "Standard-Mortgage-Rate-3-Year-Fixed": ("fixed", 36, "posted", "Fixe affiché 3 ans", None), "Standard-Mortgage-Rate-4-Year-Fixed": ("fixed", 48, "posted", "Fixe affiché 4 ans", None), "Standard-Mortgage-Rate-5-Year-Fixed": ("fixed", 60, "posted", "Fixe affiché 5 ans", None), } PRIME_KEY = "equitable-prime-rate" def extract_rates_blob(html: str) -> dict: """Isole le dictionnaire {clé: {name, rate}} du payload Next.js. Le JSON est échappé dans le HTML (\\\" -> \").""" marker = '\\"rates\\":{' i = html.find(marker) if i < 0: marker = '"rates":{' i = html.find(marker) if i < 0: raise ValueError("marqueur rates introuvable (structure changée)") seg = html[i:i + 400_000].replace('\\"', '"') start = seg.find("{") depth = 0 for j, ch in enumerate(seg[start:], start): if ch == "{": depth += 1 elif ch == "}": depth -= 1 if depth == 0: return json.loads(seg[start:j + 1]) raise ValueError("JSON rates non équilibré (structure changée)") class EqBankProvider(RateProvider): provider_id = "eq_bank" institution = "Banque EQ" source_url = "https://www.eqbank.ca/residential/mortgage-rates" request_delay = 1.0 def fetch(self) -> list[dict]: return self.parse(self.get(self.source_url).text) def parse(self, payload: str) -> list[dict]: rates = extract_rates_blob(payload) def val(key: str) -> float | None: cell = rates.get(key) if isinstance(cell, dict) and isinstance(cell.get("rate"), (int, float)): return float(cell["rate"]) return None out: list[dict] = [] for key, (rtype, term, kind, name, apr_key) in KEY_MAP.items(): rate = val(key) if rate is None: continue # clé absente : produit omis, jamais deviné apr = val(apr_key) if apr_key else None out.append(self.make_product( rate=rate, rate_type=rtype, term_months=term, kind=kind, product_name=name, apr=apr, raw={"key": key, "rate": rate, "apr": apr})) prime = val(PRIME_KEY) if prime is not None: out.append(self.make_product( rate=prime, rate_type="other", term_months=12, kind="posted", product_name="Taux préférentiel Banque Équitable", purpose="unknown", raw={"key": PRIME_KEY, "rate": prime})) return out