Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1# -----------------------------------------------------------------------------2# Rent-Ka — Rental listings aggregator (Canada, outside Québec)3# Author: Simon-Pierre Boucher — contact@spboucher.ai4# hiver.py : « Vie quotidienne en hiver » — indicateur 0-100 pensé pour le5# Québec : peut-on vivre son quotidien À PIED quand il fait -20 °C6# et que les trottoirs sont enneigés?7#8# Calcul déterministe et explicable à partir des données DÉJÀ collectées9# (poi_cache — plus proche commodité par catégorie, OpenStreetMap) :10# épicerie, pharmacie, arrêt de bus/métro, dépanneur. Chaque sous-score est11# une décroissance linéaire de la distance de marche (vol d'oiseau × 1,3).12# Pas de données de pente/exposition disponibles -> non pris en compte, et la13# méthodologie l'assume. Sans données POI : aucun score (jamais inventé).14# -----------------------------------------------------------------------------15from __future__ import annotations1617import json1819from . import db2021VERSION = "hiver-1.0"22DETOUR = 1.3 # vol d'oiseau -> distance de marche (même facteur que POI)23VITESSE_M_MIN = 80.0 # 4,8 km/h2425# (catégorie, libellé, poids, distance pleine note (m), distance note nulle (m))26CRITERES = [27 ("epicerie", "Grocery store", 0.35, 300, 1500),28 ("pharmacie", "Pharmacy", 0.20, 300, 1500),29 ("bus", "Bus stop", 0.25, 150, 900),30 ("metro", "Subway/LRT", 0.05, 400, 1800),31 ("depanneur", "Convenience store", 0.15, 200, 1000),32]3334CLASSES = [(80, "very practical"), (60, "practical"), (40, "demanding")]35CLASSE_MIN = "difficult"363738def _sous_score(dist_m: float, plein: float, nul: float) -> float:39 d = dist_m * DETOUR40 if d <= plein:41 return 100.042 if d >= nul:43 return 0.044 return 100.0 * (nul - d) / (nul - plein)454647def score(lat: float | None, lng: float | None, con=None) -> dict | None:48 """Score hiver de l'immeuble (via poi_cache) — None si données absentes."""49 if lat is None or lng is None:50 return None51 own = con is None52 if own:53 con = db.connect()54 try:55 key = f"{round(lat, 4)},{round(lng, 4)}"56 row = con.execute("SELECT pois FROM poi_cache WHERE coord_key=?",57 (key,)).fetchone()58 if row is None:59 return None60 try:61 pois = {p["cat"]: p for p in json.loads(row["pois"] or "[]")}62 except (ValueError, TypeError):63 return None64 if not pois:65 return None6667 total = 0.068 poids_total = 0.069 detail = []70 for cat, label, poids, plein, nul in CRITERES:71 p = pois.get(cat)72 if p is None:73 # métro absent = normal hors Montréal : critère simplement omis74 if cat == "metro":75 continue76 s = 0.077 item = {"critere": label, "score": 0, "distance_m": None,78 "note": "none found within walking distance"}79 else:80 s = _sous_score(p["dist_m"], plein, nul)81 item = {"critere": label, "score": round(s),82 "distance_m": p["dist_m"],83 "minutes": max(1, round(p["dist_m"] * DETOUR84 / VITESSE_M_MIN)),85 "nom": p.get("name")}86 total += s * poids87 poids_total += poids88 detail.append(item)89 if poids_total == 0:90 return None91 note = round(total / poids_total)92 classe = CLASSE_MIN93 for seuil, nom in CLASSES:94 if note >= seuil:95 classe = nom96 break97 return {98 "score": note, "classe": classe, "detail": detail,99 "statut": "calculated", "version": VERSION,100 "methode": ("essential amenities within walking distance "101 "(grocery, pharmacy, bus/subway, convenience store — "102 "OpenStreetMap), linear decay of the estimated "103 "walking distance (as the crow flies × 1.3). Slope "104 "and exposure not available: not considered."),105 }106 finally:107 if own:108 con.close()109