SPB Git forge

spb/immo-ka

Public

Immo-Ka — agrégateur des propriétés à vendre au Québec (73 connecteurs, ~40 000 annonces, React+FastAPI)

112commits 1branches 0releases
125.4 MBsize
maindefault branch
13 days agolast push
Python 47.5% HTML 27.9% TypeScript 15.5% CSS 7.2% JavaScript 2%

Risque d inondation (BDZI) + criminalité SPVM détaillée par catégorie

- immoka/inondation.py : lookup point-dans-polygone sur la base BDZI locale
  (25 k polygones + R*Tree, copiée de lou-ka), run.py inondation-build,
  GET /api/inondation, bloc fiche avec badge de sévérité (grand courant
  0-20 ans / faible courant 20-100 ans) et couverture cartographique
- quartier : _crime_mtl ventile les actes SPVM par catégorie ; barres par
  catégorie avec variation 12 mois vs 12 précédents dans le bloc quartier

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 24, 2026) parent 057e5d6

10 changed files +358 −11

modified .gitignore +1 −0
@@ -12,3 +12,4 @@ frontend/tsconfig.tsbuildinfo
12 12 .DS_Store
13 13 data/vraiprix.db
14 14 *.bak-*
15 +data/inondation.db
modified frontend/src/api.ts +18 −1
@@ -230,7 +230,8 @@ export interface Quartier {
230 230 defavorisation?: { quintile_materiel: number | null; quintile_social: number | null };
231 231 chaleur?: { classe: number; ecart: number | null };
232 232 crime?:
233 − | { type: "points"; rayon_m: number; douze_mois: number; douze_mois_precedents: number }
233 + | { type: "points"; rayon_m: number; douze_mois: number; douze_mois_precedents: number;
234 + categories?: { nom: string; n: number; n_prec: number }[] }
234 235 | { type: "igc"; ville: string; annee: number; indice: number; indice_canada: number | null };
235 236 }
236 237
@@ -303,3 +304,19 @@ export async function toggleFavorite(on: boolean, item: FavItem) {
303 304 if (!res.ok) throw new Error(`favoris ${res.status}`);
304 305 return (await res.json()) as { ok: boolean; on: boolean };
305 306 }
307 +
308 +export interface InondationZone {
309 + type: string; severite: "eleve" | "modere" | "present";
310 + recurrence: string; distance_m: number; date_rapport: string | null;
311 +}
312 +
313 +export interface Inondation {
314 + statut: "en_zone" | "a_proximite" | "hors_zone" | "non_cartographie";
315 + severite: "eleve" | "modere" | "present" | null;
316 + couvert: boolean;
317 + zones: InondationZone[];
318 +}
319 +
320 +/** Risque d'inondation BDZI (gouv. du Québec) au point de la propriété. */
321 +export const fetchInondation = (lat: number, lng: number) =>
322 + get<Inondation>(`/api/inondation?lat=${lat}&lng=${lng}`);
modified frontend/src/components/QuartierBlock.tsx +36 −8
@@ -95,15 +95,43 @@ export default function QuartierBlock({ q }: { q: Quartier }) {
95 95 return <span className={b.cls}><Ico name={b.ico} size={14} /> {b.txt}</span>;
96 96 })()}
97 97 {q.crime?.type === "points" && (
98 − <span className="q-badge neutral">
99 − <Ico name="shield" size={14} /> {q.crime.douze_mois} acte{q.crime.douze_mois > 1 ? "s" : ""} criminel{q.crime.douze_mois > 1 ? "s" : ""} à
100 − moins de 500 m (12 mois)
101 − {q.crime.douze_mois_precedents > 0 && (
102 − q.crime.douze_mois <= q.crime.douze_mois_precedents
103 − ? ` · en baisse (${q.crime.douze_mois_precedents} l'année d'avant)`
104 − : ` · en hausse (${q.crime.douze_mois_precedents} l'année d'avant)`
98 + <div className="q-crime">
99 + <span className="q-badge neutral">
100 + <Ico name="shield" size={14} /> {q.crime.douze_mois} acte{q.crime.douze_mois > 1 ? "s" : ""} criminel{q.crime.douze_mois > 1 ? "s" : ""} à
101 + moins de 500 m (12 mois)
102 + {q.crime.douze_mois_precedents > 0 && (
103 + q.crime.douze_mois <= q.crime.douze_mois_precedents
104 + ? ` · en baisse (${q.crime.douze_mois_precedents} l'année d'avant)`
105 + : ` · en hausse (${q.crime.douze_mois_precedents} l'année d'avant)`
106 + )}
107 + </span>
108 + {(q.crime.categories?.length ?? 0) > 0 && (
109 + <ul className="q-crime-cats">
110 + {q.crime.categories!.filter((c) => c.n + c.n_prec > 0).map((c) => {
111 + const max = Math.max(...q.crime!.type === "points"
112 + ? q.crime!.categories!.map((x) => x.n) : [1], 1);
113 + const delta = c.n - c.n_prec;
114 + return (
115 + <li key={c.nom}>
116 + <span className="q-crime-nom">{c.nom}</span>
117 + <span className="q-crime-barre" aria-hidden="true">
118 + <i style={{ width: `${Math.max(3, (c.n / max) * 100)}%` }} />
119 + </span>
120 + <span className="q-crime-n">{c.n}
121 + <small>{delta === 0 ? " =" : delta > 0
122 + ? ` ▲${delta}` : ` ▼${-delta}`}</small>
123 + </span>
124 + </li>
125 + );
126 + })}
127 + </ul>
105 128 )}
106 − </span>
129 + <p className="fine q-crime-src">
130 + Actes criminels enregistrés par le SPVM (données ouvertes,
131 + position approximée à l'intersection) — 12 derniers mois,
132 + variation vs les 12 précédents.
133 + </p>
134 + </div>
107 135 )}
108 136 {q.crime?.type === "igc" && (() => {
109 137 const c = q.crime;
added frontend/src/components/RisqueInondation.tsx +76 −0
@@ -0,0 +1,76 @@
1 +// -----------------------------------------------------------------------------
2 +// Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)
3 +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 +// components/RisqueInondation.tsx : bloc « Risque d'inondation » (fiche)
5 +// Position de l'adresse vis-à-vis des zones inondables officielles (BDZI,
6 +// gouvernement du Québec) : dans une zone, à proximité (≤ 100 m), hors
7 +// zone d'un secteur cartographié, ou secteur non couvert par la
8 +// cartographie. Indicatif seulement — la carte officielle fait foi.
9 +// -----------------------------------------------------------------------------
10 +import { useEffect, useState } from "react";
11 +import { fetchInondation, Inondation } from "../api";
12 +
13 +const BADGE: Record<string, [string, string]> = {
14 + eleve: ["zi-eleve", "Risque élevé"],
15 + modere: ["zi-modere", "Risque modéré"],
16 + present: ["zi-present", "Zone inondable"],
17 + hors_zone: ["zi-ok", "Hors zone inondable"],
18 + non_cartographie: ["zi-nc", "Secteur non cartographié"],
19 +};
20 +
21 +function libelle(d: Inondation): string {
22 + const z = d.zones[0];
23 + if (d.statut === "en_zone" && z)
24 + return `L'adresse se trouve dans une ${z.type.toLowerCase()}` +
25 + (z.recurrence ? ` (${z.recurrence})` : "") + ".";
26 + if (d.statut === "a_proximite" && z)
27 + return `Une ${z.type.toLowerCase()} se trouve à environ ${z.distance_m} m` +
28 + (z.recurrence ? ` (${z.recurrence})` : "") + ".";
29 + if (d.statut === "hors_zone")
30 + return "L'adresse est à l'extérieur des zones inondables cartographiées " +
31 + "de ce secteur.";
32 + return "Ce secteur n'est pas couvert par la cartographie officielle des " +
33 + "zones inondables — l'absence de zone ne signifie pas une absence " +
34 + "de risque.";
35 +}
36 +
37 +export default function RisqueInondation({ lat, lng }:
38 + { lat: number | null; lng: number | null }) {
39 + const [d, setD] = useState<Inondation | null>(null);
40 + useEffect(() => {
41 + setD(null);
42 + if (lat == null || lng == null) return;
43 + fetchInondation(lat, lng).then(setD).catch(() => setD(null));
44 + }, [lat, lng]);
45 + if (lat == null || lng == null || !d) return null;
46 +
47 + const key = d.statut === "en_zone" || d.statut === "a_proximite"
48 + ? (d.severite ?? "present") : d.statut;
49 + const [cls, label] = BADGE[key] ?? BADGE.non_cartographie;
50 + return (
51 + <section className="f-bloc f-zi" id="inondation">
52 + <h2>Risque d'inondation</h2>
53 + <div className="zi-head">
54 + <span className={`zi-badge ${cls}`}>{label}</span>
55 + </div>
56 + <p className="zi-texte">{libelle(d)}</p>
57 + {d.zones.length > 1 && (
58 + <ul className="zi-liste">
59 + {d.zones.slice(1, 3).map((z, i) => (
60 + <li key={i}>
61 + {z.type}{z.recurrence ? ` (${z.recurrence})` : ""} —{" "}
62 + {z.distance_m === 0 ? "à l'adresse" : `à ~${z.distance_m} m`}
63 + </li>
64 + ))}
65 + </ul>
66 + )}
67 + <p className="fine">
68 + Base de données des zones à risque d'inondation (BDZI), gouvernement
69 + du Québec — indicatif seulement, selon la position géocodée ;{" "}
70 + <a href="https://www.quebec.ca/agriculture-environnement-et-ressources-naturelles/eau/zones-inondables-mobilite-rives-littoral/cartographies"
71 + target="_blank" rel="noopener noreferrer">
72 + la cartographie officielle fait foi</a>.
73 + </p>
74 + </section>
75 + );
76 +}
modified frontend/src/pages/Listing.tsx +3 −0
@@ -14,6 +14,7 @@ import {
14 14
15 15 const PropertyMap = lazy(() => import("../components/PropertyMap"));
16 16 import QuartierBlock from "../components/QuartierBlock";
17 +import RisqueInondation from "../components/RisqueInondation";
17 18 import { Ico } from "../components/Icons";
18 19 import { TypeFallback } from "../components/PropertyImg";
19 20
@@ -451,6 +452,8 @@ export default function ListingPage() {
451 452 </div>
452 453
453 454 {/* quartier : pleine largeur, APRÈS les infos de propriété (ordre mobile correct) */}
455 + <RisqueInondation lat={l.lat} lng={l.lng} />
456 +
454 457 {l.quartier && <QuartierBlock q={l.quartier} />}
455 458
456 459 <div className="fine f-foot">
modified frontend/src/styles.css +30 −0
@@ -850,3 +850,33 @@ table.rooms tr:nth-child(even) td { background: var(--surface-2); }
850 850 .q-table .num { text-align: right; font-family: var(--font-mono); }
851 851 .q-meter { display: inline-block; width: 74px; height: 9px; background: var(--surface-2); border: 1px solid var(--line-strong); border-radius: 999px; overflow: hidden; vertical-align: -1px; margin-right: 7px; }
852 852 .q-meter i { display: block; height: 100%; background: var(--green); }
853 +
854 +
855 +/* ---- Criminalité détaillée SPVM (bloc quartier) ---- */
856 +.q-crime { width: 100%; }
857 +.q-crime-cats { list-style: none; margin: 8px 0 0; padding: 0; }
858 +.q-crime-cats li { display: flex; align-items: center; gap: 8px;
859 + padding: 3px 0; font-size: 12.5px; }
860 +.q-crime-nom { flex: 0 0 46%; color: var(--ink-2, #555); overflow: hidden;
861 + text-overflow: ellipsis; white-space: nowrap; }
862 +.q-crime-barre { flex: 1 1 auto; height: 10px; background: var(--surface-2, #f0ede8);
863 + border-radius: 5px; overflow: hidden; }
864 +.q-crime-barre i { display: block; height: 100%; border-radius: 5px;
865 + background: var(--accent, #e23744); opacity: 0.55; }
866 +.q-crime-n { flex: 0 0 52px; text-align: right; font-weight: 600;
867 + color: var(--ink, #222); white-space: nowrap; }
868 +.q-crime-n small { font-weight: 400; color: var(--ink-3, #888); }
869 +.q-crime-src { margin-top: 8px; }
870 +
871 +
872 +/* ---- Risque d'inondation (fiche) — BDZI gouv. du Québec ---- */
873 +.f-zi .zi-head { display: flex; align-items: center; gap: 10px; }
874 +.zi-badge { display: inline-block; padding: 3px 10px; border-radius: 999px;
875 + font-size: 12.5px; font-weight: 700; }
876 +.zi-eleve { background: #fde8e8; color: #a12622; }
877 +.zi-modere { background: #fdf3e0; color: #8a5a00; }
878 +.zi-present { background: #fdf3e0; color: #8a5a00; }
879 +.zi-ok { background: #e7f4ea; color: #1e6b34; }
880 +.zi-nc { background: var(--surface-2, #f0ede8); color: var(--ink-3, #888); }
881 +.zi-liste { margin: 8px 0 0; padding: 0; list-style: none; font-size: 13px; }
882 +.zi-liste li { padding: 4px 0; border-top: 1px solid var(--line, #e6e4df); }
added immoka/inondation.py +171 −0
@@ -0,0 +1,171 @@
1 +# -----------------------------------------------------------------------------
2 +# Immo-Ka — Agrégateur de propriétés à vendre (province de Québec)
3 +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 +# inondation.py : risque d'inondation à l'adresse — BDZI (gouv. du Québec)
5 +#
6 +# Source : Base de données des zones à risque d'inondation (BDZI, MELCCFP),
7 +# donnée ouverte officielle (Données Québec), GeoPackage EPSG:3857.
8 +# `build()` éclate les multipolygones de la couche ZOI_s (pleine précision)
9 +# en polygones simples indexés R*Tree dans data/inondation.db (+ la couche
10 +# Carto_ZI_S : périmètres couverts par une cartographie — sans elle,
11 +# « aucune zone » ne veut rien dire), puis `lookup(lat, lng)` fait un test
12 +# point-dans-polygone exact (shapely) sur les seuls candidats de la boîte.
13 +#
14 +# Types BDZI : « Zone de grand courant » (récurrence 0-20 ans, risque
15 +# élevé), « Zone de faible courant » (20-100 ans), « Zone de crue
16 +# 0-100 ans », variantes « - Pont » et « Autre zone inondable ».
17 +#
18 +# Usage : python run.py inondation-build (une fois, GPKG requis)
19 +# lookup(lat, lng) -> dict (fiche, /api/inondation)
20 +# -----------------------------------------------------------------------------
21 +from __future__ import annotations
22 +
23 +import math
24 +import sqlite3
25 +from pathlib import Path
26 +
27 +DATA = Path(__file__).resolve().parent.parent / "data"
28 +DB_PATH = DATA / "inondation.db"
29 +GPKG = DATA / "BDZI_GPK.gpkg"
30 +
31 +# rayon de tolérance : géocodage + emprise du bâtiment
32 +NEAR_M = 30.0
33 +# au-delà, on signale quand même une zone toute proche (information utile)
34 +WARN_M = 100.0
35 +
36 +_SEVERITE = {
37 + "Zone de grand courant": ("eleve", "récurrence 0-20 ans"),
38 + "Zone de grand courant - Pont": ("eleve", "récurrence 0-20 ans"),
39 + "Zone de faible courant": ("modere", "récurrence 20-100 ans"),
40 + "Zone de faible courant - Pont": ("modere", "récurrence 20-100 ans"),
41 + "Zone de crue 0-100 ans": ("present", "récurrence 0-100 ans"),
42 + "Zone de crue 0-100 ans - Pont": ("present", "récurrence 0-100 ans"),
43 + "Autre zone inondable": ("present", ""),
44 +}
45 +
46 +_R = 20037508.342789244
47 +
48 +
49 +def _to_3857(lat: float, lng: float) -> tuple[float, float]:
50 + x = lng * _R / 180.0
51 + y = math.log(math.tan((90 + lat) * math.pi / 360.0)) * _R / math.pi
52 + return x, y
53 +
54 +
55 +def _gpkg_wkb(blob: bytes) -> bytes:
56 + """Retire l'en-tête GeoPackage (magic GP + drapeaux + enveloppe)."""
57 + if blob[:2] != b"GP":
58 + return blob
59 + flags = blob[3]
60 + env = (flags >> 1) & 0x07
61 + env_len = {0: 0, 1: 32, 2: 48, 3: 48, 4: 64}.get(env, 0)
62 + return blob[8 + env_len:]
63 +
64 +
65 +def build(gpkg: Path = GPKG) -> None:
66 + """Construit data/inondation.db à partir du GeoPackage BDZI."""
67 + from shapely import wkb as _swkb
68 +
69 + src = sqlite3.connect(gpkg)
70 + con = sqlite3.connect(DB_PATH)
71 + con.executescript("""
72 + DROP TABLE IF EXISTS zi; DROP TABLE IF EXISTS zi_rtree;
73 + DROP TABLE IF EXISTS couverture; DROP TABLE IF EXISTS couv_rtree;
74 + CREATE TABLE zi (id INTEGER PRIMARY KEY, description TEXT,
75 + rapport TEXT, date_rapport TEXT, wkb BLOB);
76 + CREATE VIRTUAL TABLE zi_rtree USING rtree(id, xmin, xmax, ymin, ymax);
77 + CREATE TABLE couverture (id INTEGER PRIMARY KEY, nom TEXT, wkb BLOB);
78 + CREATE VIRTUAL TABLE couv_rtree USING rtree(id, xmin, xmax, ymin, ymax);
79 + """)
80 + nid = 0
81 + for desc, rapport, date_r, blob in src.execute(
82 + "SELECT Description, Nm_rapport, Date_rapport, Shape FROM ZOI_s"):
83 + geom = _swkb.loads(_gpkg_wkb(blob))
84 + polys = geom.geoms if geom.geom_type == "MultiPolygon" else [geom]
85 + for poly in polys:
86 + if poly.is_empty:
87 + continue
88 + nid += 1
89 + con.execute("INSERT INTO zi VALUES (?,?,?,?,?)",
90 + (nid, desc, rapport, date_r, poly.wkb))
91 + x0, y0, x1, y1 = poly.bounds
92 + con.execute("INSERT INTO zi_rtree VALUES (?,?,?,?,?)",
93 + (nid, x0, x1, y0, y1))
94 + if nid % 500 < len(polys):
95 + print(f"[inondation] {nid} polygones…", flush=True)
96 + cid = 0
97 + for nom, blob in src.execute("SELECT Nom_Carte, Shape FROM Carto_ZI_S"):
98 + geom = _swkb.loads(_gpkg_wkb(blob))
99 + polys = geom.geoms if geom.geom_type == "MultiPolygon" else [geom]
100 + for poly in polys:
101 + if poly.is_empty:
102 + continue
103 + cid += 1
104 + con.execute("INSERT INTO couverture VALUES (?,?,?)",
105 + (cid, nom, poly.wkb))
106 + x0, y0, x1, y1 = poly.bounds
107 + con.execute("INSERT INTO couv_rtree VALUES (?,?,?,?,?)",
108 + (cid, x0, x1, y0, y1))
109 + con.commit()
110 + con.execute("VACUUM")
111 + print(f"[inondation] {nid} polygones de zones, {cid} périmètres "
112 + f"cartographiés -> {DB_PATH}")
113 + con.close()
114 + src.close()
115 +
116 +
117 +def lookup(lat: float, lng: float) -> dict | None:
118 + """Risque d'inondation BDZI au point (WGS84). None si base absente."""
119 + if not DB_PATH.exists():
120 + return None
121 + from shapely import wkb as _swkb
122 + from shapely.geometry import Point
123 +
124 + x, y = _to_3857(lat, lng)
125 + # les distances 3857 sont dilatées d'un facteur 1/cos(lat)
126 + scale = 1.0 / max(0.2, math.cos(math.radians(lat)))
127 + pad = WARN_M * scale
128 + pt = Point(x, y)
129 +
130 + con = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
131 + zones: list[dict] = []
132 + for zid, desc, rapport, date_r, blob in con.execute(
133 + "SELECT z.id, z.description, z.rapport, z.date_rapport, z.wkb "
134 + "FROM zi z JOIN zi_rtree r ON z.id = r.id "
135 + "WHERE r.xmax >= ? AND r.xmin <= ? AND r.ymax >= ? AND r.ymin <= ?",
136 + (x - pad, x + pad, y - pad, y + pad)):
137 + poly = _swkb.loads(blob)
138 + d = poly.distance(pt) / scale # ~mètres réels
139 + if poly.contains(pt):
140 + d = 0.0
141 + elif d > WARN_M:
142 + continue
143 + sev, rec = _SEVERITE.get(desc, ("present", ""))
144 + zones.append({"type": desc, "severite": sev, "recurrence": rec,
145 + "distance_m": round(d),
146 + "date_rapport": (date_r or "")[:10] or None})
147 + couvert = False
148 + for (blob,) in con.execute(
149 + "SELECT c.wkb FROM couverture c JOIN couv_rtree r ON c.id = r.id "
150 + "WHERE r.xmax >= ? AND r.xmin <= ? AND r.ymax >= ? AND r.ymin <= ?",
151 + (x, x, y, y)):
152 + if _swkb.loads(blob).contains(pt):
153 + couvert = True
154 + break
155 + con.close()
156 +
157 + zones.sort(key=lambda z: (z["distance_m"],
158 + {"eleve": 0, "modere": 1, "present": 2}
159 + .get(z["severite"], 3)))
160 + dans = [z for z in zones if z["distance_m"] <= NEAR_M]
161 + if dans:
162 + statut = "en_zone"
163 + pire = dans[0]["severite"]
164 + elif zones:
165 + statut, pire = "a_proximite", zones[0]["severite"]
166 + elif couvert:
167 + statut, pire = "hors_zone", None
168 + else:
169 + statut, pire = "non_cartographie", None
170 + return {"statut": statut, "severite": pire, "couvert": couvert,
171 + "zones": zones[:5]}
modified immoka/quartier.py +9 −2
@@ -96,24 +96,31 @@ def _crime_mtl(qcon: sqlite3.Connection, lat: float, lng: float) -> dict | None:
96 96 dlng = 500 / (111000.0 * max(0.2, math.cos(math.radians(lat))))
97 97 now = time.time()
98 98 rows = qcon.execute(
99 − "SELECT lat, lng, ts FROM crime_mtl WHERE lat BETWEEN ? AND ?"
99 + "SELECT lat, lng, ts, categorie FROM crime_mtl WHERE lat BETWEEN ? AND ?"
100 100 " AND lng BETWEEN ? AND ? AND ts >= ?",
101 101 (lat - dlat, lat + dlat, lng - dlng, lng + dlng, now - 730 * 86400)).fetchall()
102 102 recent = avant = 0
103 + cats: dict[str, list[int]] = {} # categorie -> [12 mois, 12 prec.]
103 104 for r in rows:
104 105 # distance exacte (le bbox est un carré)
105 106 d = math.hypot((r["lat"] - lat) * 111000.0,
106 107 (r["lng"] - lng) * 111000.0 * math.cos(math.radians(lat)))
107 108 if d > 500:
108 109 continue
110 + c = cats.setdefault(r["categorie"] or "Autre", [0, 0])
109 111 if r["ts"] >= now - 365 * 86400:
110 112 recent += 1
113 + c[0] += 1
111 114 else:
112 115 avant += 1
116 + c[1] += 1
113 117 if recent == 0 and avant == 0:
114 118 return None
119 + categories = [{"nom": k, "n": v[0], "n_prec": v[1]}
120 + for k, v in sorted(cats.items(),
121 + key=lambda kv: -(kv[1][0] + kv[1][1]))]
115 122 return {"type": "points", "rayon_m": 500, "douze_mois": recent,
116 − "douze_mois_precedents": avant}
123 + "douze_mois_precedents": avant, "categories": categories}
117 124
118 125
119 126 def _crime_igc(qcon: sqlite3.Connection, city: str) -> dict | None:
modified immoka/web.py +11 −0
@@ -133,6 +133,17 @@ def list_listings(
133 133 return {"total": total, "count": len(rows), "listings": rows}
134 134
135 135
136 +@app.get("/api/inondation")
137 +def inondation_at(lat: float, lng: float):
138 + """Risque d'inondation à l'adresse (BDZI, gouv. du Québec) —
139 + bloc « Risque d'inondation » de la fiche."""
140 + from . import inondation
141 + d = inondation.lookup(lat, lng)
142 + if d is None:
143 + raise HTTPException(404, "Base des zones inondables non disponible")
144 + return d
145 +
146 +
136 147 @app.get("/api/listings/{uid}")
137 148 def get_listing(uid: str):
138 149 con = db.connect()
modified run.py +3 −0
@@ -52,6 +52,9 @@ def main() -> None:
52 52 elif cmd == "poi":
53 53 from immoka import poi
54 54 poi.run(int(sys.argv[2]) if len(sys.argv) > 2 else None)
55 + elif cmd == "inondation-build":
56 + from immoka import inondation
57 + inondation.build()
55 58 elif cmd == "quartier":
56 59 from immoka import quartier
57 60 quartier.enrich(int(sys.argv[2]) if len(sys.argv) > 2 else None)
58 61