SPB Git forge

spb/house-ka

Public
18commits 1branches 0releases
1.9 MBsize
maindefault branch
20 days agolast push
Python 67% TypeScript 18.2% CSS 14.4%
4.4 KB · 98 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Immo-Ka — Agrégateur de maisons à vendre (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# mortgage/providers/first_national.py : First National — table HTML statique5#   (server-side, aucun anti-bot). Taux fixes fermés par catégorie6#   assuré/assurable(LTV)/conventionnel, ARM « Prime - X% », prime maison.7# -----------------------------------------------------------------------------8from __future__ import annotations910import re1112from bs4 import BeautifulSoup1314from .base import RateProvider, parse_rate, term_to_months151617def _insured_status(category: str) -> str:18    c = category.lower()19    if c.startswith("insured"):20        return "insured"21    if c.startswith("insurable"):22        return "insurable"23    if c.startswith("conventional"):24        return "uninsured"25    return "unknown"262728class FirstNationalProvider(RateProvider):29    provider_id = "first_national"30    institution = "First National"31    source_url = "https://www.firstnational.ca/residential/mortgage-rates"32    request_delay = 1.03334    def fetch(self) -> list[dict]:35        return self.parse(self.get(self.source_url).text)3637    def parse(self, payload: str) -> list[dict]:38        soup = BeautifulSoup(payload, "html.parser")39        out: list[dict] = []40        prime = None41        m = re.search(r"First National Prime Rate:\s*(?:</?\w+[^>]*>\s*)*([\d.]+)",42                      payload)43        if m:44            prime = float(m.group(1))45        # -- taux fixes fermés (table avec aria-label par terme) --------------46        for h3 in soup.find_all("h3"):47            title = h3.get_text(" ", strip=True)48            table = h3.find_next("table")49            if table is None:50                continue51            if title.lower().startswith("fixed rate mortgages"):52                for tr in table.select("tbody tr"):53                    th = tr.find("th")54                    if th is None:55                        continue56                    category = re.sub(r"\s+", " ", th.get_text(" ", strip=True))57                    status = _insured_status(category)58                    for td in tr.find_all("td"):59                        term = term_to_months(td.get("aria-label") or "")60                        rate = parse_rate(td.get_text(strip=True))61                        if term is None or rate is None:62                            continue  # cellule N/A : produit non offert63                        out.append(self.make_product(64                            rate=rate, rate_type="fixed", term_months=term,65                            kind="posted",66                            product_name=f"Fixe fermé — {category}",67                            insured_status=status, conditions=category,68                            raw={"category": category,69                                 "cell": td.get_text(strip=True)}))70            elif title.lower().startswith("adjustable rate"):71                if prime is None:72                    continue  # sans prime confirmée, ne rien deviner73                for tr in table.select("tbody tr"):74                    th = tr.find("th")75                    td = tr.find("td")76                    if th is None or td is None:77                        continue78                    category = re.sub(r"\s+", " ", th.get_text(" ", strip=True))79                    cell = td.get_text(" ", strip=True)80                    dm = re.search(r"Prime\s*([+-])\s*([\d.]+)\s*%", cell)81                    if not dm:82                        continue83                    delta = float(dm.group(2)) * (1 if dm.group(1) == "+" else -1)84                    out.append(self.make_product(85                        rate=round(prime + delta, 2), rate_type="adjustable",86                        term_months=60, kind="special",87                        product_name=f"ARM 5 ans — {category}",88                        insured_status=_insured_status(category),89                        conditions=f"{cell} (prime First National {prime} %)",90                        raw={"category": category, "cell": cell,91                             "prime": prime, "delta": delta}))92        if prime is not None:93            out.append(self.make_product(94                rate=prime, rate_type="other", term_months=12, kind="posted",95                product_name="Taux préférentiel First National",96                purpose="unknown", raw={"prime": prime}))97        return out98