Python 67%
TypeScript 18.2%
CSS 14.4%
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# mortgage/providers/eq_bank.py : Banque EQ / Équitable — dictionnaire JSON5# global embarqué dans le payload Next.js de la page des taux (marqueur6# \"rates\":{ échappé). Clés CMS stables ; les clés « Adjustable » sont des7# ÉCARTS vs prime (jamais interprétées comme des taux).8# -----------------------------------------------------------------------------9from __future__ import annotations1011import json1213from .base import RateProvider1415# clé CMS -> (rate_type, terme mois, kind, nom, clé APR éventuelle)16KEY_MAP: dict[str, tuple] = {17 "mortgage-rate-fixed-5-year": ("fixed", 60, "special",18 "Fixe 5 ans (vitrine EQ)",19 "mortgage-rate-fixed-5-year-APR"),20 "mortgage-rate-variable-5-year": ("variable", 60, "special",21 "Variable 5 ans (vitrine EQ)",22 "mortgage-rate-variable-5-year-APR"),23 "es-fixed-12-month": ("fixed", 12, "special", "Evolution Suite — fixe 1 an", None),24 "es-fixed-24-month": ("fixed", 24, "special", "Evolution Suite — fixe 2 ans", None),25 "es-fixed-36-month": ("fixed", 36, "special", "Evolution Suite — fixe 3 ans", None),26 "es-fixed-48-month": ("fixed", 48, "special", "Evolution Suite — fixe 4 ans", None),27 "es-fixed-60-month": ("fixed", 60, "special", "Evolution Suite — fixe 5 ans", None),28 "Standard-Mortgage-Rate-1-Year-Fixed": ("fixed", 12, "posted", "Fixe affiché 1 an", None),29 "Standard-Mortgage-Rate-2-Year-Fixed": ("fixed", 24, "posted", "Fixe affiché 2 ans", None),30 "Standard-Mortgage-Rate-3-Year-Fixed": ("fixed", 36, "posted", "Fixe affiché 3 ans", None),31 "Standard-Mortgage-Rate-4-Year-Fixed": ("fixed", 48, "posted", "Fixe affiché 4 ans", None),32 "Standard-Mortgage-Rate-5-Year-Fixed": ("fixed", 60, "posted", "Fixe affiché 5 ans", None),33}34PRIME_KEY = "equitable-prime-rate"353637def extract_rates_blob(html: str) -> dict:38 """Isole le dictionnaire {clé: {name, rate}} du payload Next.js.39 Le JSON est échappé dans le HTML (\\\" -> \")."""40 marker = '\\"rates\\":{'41 i = html.find(marker)42 if i < 0:43 marker = '"rates":{'44 i = html.find(marker)45 if i < 0:46 raise ValueError("marqueur rates introuvable (structure changée)")47 seg = html[i:i + 400_000].replace('\\"', '"')48 start = seg.find("{")49 depth = 050 for j, ch in enumerate(seg[start:], start):51 if ch == "{":52 depth += 153 elif ch == "}":54 depth -= 155 if depth == 0:56 return json.loads(seg[start:j + 1])57 raise ValueError("JSON rates non équilibré (structure changée)")585960class EqBankProvider(RateProvider):61 provider_id = "eq_bank"62 institution = "Banque EQ"63 source_url = "https://www.eqbank.ca/residential/mortgage-rates"64 request_delay = 1.06566 def fetch(self) -> list[dict]:67 return self.parse(self.get(self.source_url).text)6869 def parse(self, payload: str) -> list[dict]:70 rates = extract_rates_blob(payload)7172 def val(key: str) -> float | None:73 cell = rates.get(key)74 if isinstance(cell, dict) and isinstance(cell.get("rate"), (int, float)):75 return float(cell["rate"])76 return None7778 out: list[dict] = []79 for key, (rtype, term, kind, name, apr_key) in KEY_MAP.items():80 rate = val(key)81 if rate is None:82 continue # clé absente : produit omis, jamais deviné83 apr = val(apr_key) if apr_key else None84 out.append(self.make_product(85 rate=rate, rate_type=rtype, term_months=term, kind=kind,86 product_name=name, apr=apr,87 raw={"key": key, "rate": rate, "apr": apr}))88 prime = val(PRIME_KEY)89 if prime is not None:90 out.append(self.make_product(91 rate=prime, rate_type="other", term_months=12, kind="posted",92 product_name="Taux préférentiel Banque Équitable",93 purpose="unknown", raw={"key": PRIME_KEY, "rate": prime}))94 return out95