Split webapp for Raspberry Pi deployment, remote generation API and sync

La webapp (carte + vignettes) et la génération de tuiles se déploient sur
deux machines : image légère Dockerfile.webapp (FastAPI + Pillow AVIF
natif + pyproj) sur Raspberry Pi, pipeline complet sur la machine de
traitement. LIDAR_GENERATION_URL délègue /api/generate, /api/preview et
/api/status ; /api/sync ramène les tuiles par rsync puis régénère
vignettes et index localement. Token partagé optionnel
(LIDAR_API_TOKEN/LIDAR_REMOTE_TOKEN). Retire du dépôt les journaux
internes (.swival, audit-findings) et les données (data/, notebooks/).
Doc : docs/DEPLOY_WEBAPP.md.
This commit is contained in:
Antoine Jacquin
2026-09-02 19:44:39 +02:00
parent ed3e90ea89
commit 422f58d772
24 changed files with 2390 additions and 329 deletions

View File

@ -96,3 +96,82 @@ def test_fetch_tiles_skips_existing_and_generated(tmp_path, monkeypatch):
result = fetch_ign.fetch_tiles(input_dir, [(1054, 6882), (1055, 6882)],
output_dir=output_dir)
assert result == []
def _make_viz_dirs(output_dir, col, row, viz_keys, resolutions=(0.5, 0.2)):
"""Crée les dossiers de visualisations d'une tuile (une entrée par résolution).
Le suffixe de résolution n'est que sur le DOSSIER : les fichiers gardent
le basename nu (cf. _expected_output_path dans le pipeline).
"""
base = f"LHD_FXX_{col:04d}_{row:04d}_PTS_LAMB93_IGN69"
for res in resolutions:
suffix = "" if res == 0.5 else "_r" + str(res).replace('.', 'p')
vis = output_dir / "visualisations" / (base + suffix)
vis.mkdir(parents=True, exist_ok=True)
for key in viz_keys:
(vis / f"{base}_{key}.avif").write_bytes(b"x")
def test_fetch_tiles_downloads_incomplete_tile(tmp_path, monkeypatch):
"""Une tuile incomplète (visualisation manquante) est téléchargée, une complète non."""
from pathlib import Path
from lidar_pipeline import fetch_ign
input_dir = tmp_path / "input"
input_dir.mkdir()
output_dir = tmp_path / "output"
_make_viz_dirs(output_dir, 1054, 6882, ["aspect"]) # slope manquant
_make_viz_dirs(output_dir, 1055, 6882, ["aspect", "slope"]) # complète
calls = []
def fake_find_tile_url(col, row, timeout=20):
calls.append((col, row))
return "https://example.org/dalle.copc.laz"
def fake_download(url, dest, timeout=120, chunk=1024 * 1024):
Path(dest).write_bytes(b"laz")
return 3
monkeypatch.setattr(fetch_ign, "find_tile_url", fake_find_tile_url)
monkeypatch.setattr(fetch_ign, "download_file", fake_download)
result = fetch_ign.fetch_tiles(input_dir, [(1054, 6882), (1055, 6882)],
output_dir=output_dir,
only_viz=["aspect", "slope"],
resolutions=(0.5, 0.2))
# Seule la tuile incomplète est retéléchargée
assert calls == [(1054, 6882)]
assert [p.name for p in result] == [fetch_ign.tile_filename(1054, 6882)]
def test_fetch_tiles_force_downloads_complete(tmp_path, monkeypatch):
"""force=True télécharge même une tuile complète (régénération avec --force)."""
from pathlib import Path
from lidar_pipeline import fetch_ign
input_dir = tmp_path / "input"
input_dir.mkdir()
output_dir = tmp_path / "output"
_make_viz_dirs(output_dir, 1054, 6882, ["aspect", "slope"])
calls = []
def fake_find_tile_url(col, row, timeout=20):
calls.append((col, row))
return "https://example.org/dalle.copc.laz"
def fake_download(url, dest, timeout=120, chunk=1024 * 1024):
Path(dest).write_bytes(b"laz")
return 3
monkeypatch.setattr(fetch_ign, "find_tile_url", fake_find_tile_url)
monkeypatch.setattr(fetch_ign, "download_file", fake_download)
result = fetch_ign.fetch_tiles(input_dir, [(1054, 6882)],
output_dir=output_dir,
only_viz=["aspect", "slope"],
resolutions=(0.5, 0.2), force=True)
assert calls == [(1054, 6882)]
assert len(result) == 1