"""PLAN 344-1 — 격자 통로(`/surface/models/{mid}/grid`) · 받을 자리(`terrain-sources` 의 `grid`). DTM 만 격자 · 평활 고르기 = 서버 표본기 · 첫 요청 때 `.bin` 저장본 · ETag 304 · DTM 아닌 표현 404 · 주소 서명 = manifest 구성 · 평활 서명. """ from __future__ import annotations import asyncio import json from contextlib import asynccontextmanager from pathlib import Path import numpy as np from starlette.requests import Request from B05_Profile import B05_Profile_Router_Terrain as terrain_router from common_util.common_util_surface_sampler import dtm_grid_bytes PID = "1bb0f876-983f-4ca5-9cc7-f59ee1f77e0a" PARAMS = {"source_filter": "csf", "method": "dtm", "smooth": True, "contour_interval_m": 1.0} def _dtm(path: Path, z: float) -> None: x = np.arange(0.0, 11.0, dtype=np.float32) np.savez( path, x=x, y=x, z=np.full((11, 11), z, dtype=np.float32), valid_mask=np.ones((11, 11), dtype=bool), ) def _project(tmp_path: Path) -> Path: models = tmp_path / "B04_PreProcess" / "models" models.mkdir(parents=True) _dtm(models / "dtm_csf.npz", 100.0) _dtm(models / "dtm_csf_smooth.npz", 101.0) manifest = { "config_signature": "cfg1", "source_filters": {"csf": {"methods": {"dtm": {"smooth": {"smoothing_signature": "sm1"}}}}}, } (models / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") return tmp_path def _patch(monkeypatch, root: Path, row: tuple | None) -> None: class _Cursor: async def execute(self, *_args): return None async def fetchone(self): return row class _Connection: @asynccontextmanager async def cursor(self): yield _Cursor() class _Pool: @asynccontextmanager async def acquire(self): yield _Connection() async def stored(_connection, _project_id): return "x" monkeypatch.setattr(terrain_router, "get_db_pool", lambda: _Pool()) monkeypatch.setattr(terrain_router, "get_project_storage_relative_path", stored) monkeypatch.setattr(terrain_router, "resolve_stored_project_path", lambda _s: root) def _request(etag: str | None = None) -> Request: headers = [(b"if-none-match", etag.encode())] if etag else [] return Request({"type": "http", "method": "GET", "path": "/", "headers": headers}) def _grid(smooth: bool, etag: str | None = None): return asyncio.run(terrain_router.read_surface_grid(_request(etag), PID, 7, smooth)) def test_평활_격자를_저장본으로_내고_같은_ETag_면_304(tmp_path, monkeypatch): root = _project(tmp_path) models = root / "B04_PreProcess" / "models" _patch(monkeypatch, root, ("dtm", "B04_PreProcess/models/dtm_csf.npz")) response = _grid(True) assert response.status_code == 200 saved = models / "dtm_csf_smooth_grid.bin" assert Path(response.path) == saved assert saved.read_bytes() == dtm_grid_bytes(models / "dtm_csf_smooth.npz") assert _grid(True, response.headers["etag"]).status_code == 304 assert Path(_grid(False).path).name == "dtm_csf_grid.bin" def test_DTM_아닌_표현은_404(tmp_path, monkeypatch): root = _project(tmp_path) _patch(monkeypatch, root, ("tin", "B04_PreProcess/models/tin_csf.npz")) assert _grid(True).status_code == 404 _patch(monkeypatch, root, None) assert _grid(True).status_code == 404 def test_받을_자리_서명은_manifest_구성과_평활(tmp_path): models = _project(tmp_path) / "B04_PreProcess" / "models" confirmed = (7, "dtm", "B04_PreProcess/models/dtm_csf.npz") grid = terrain_router._design_grid(PARAMS, models, confirmed) assert grid == {"model_id": 7, "smooth": True, "signature": "cfg1.sm1"} raw = terrain_router._design_grid({**PARAMS, "smooth": False}, models, confirmed) assert raw["signature"] == "cfg1." # 서버 표본기와 다른 파일 · DTM 아닌 표현 · 확정 없음 = 격자 안 냄(서버 표본 길) assert ( terrain_router._design_grid({**PARAMS, "source_filter": "pmf"}, models, confirmed) is None ) assert terrain_router._design_grid({**PARAMS, "method": "tin"}, models, confirmed) is None assert terrain_router._design_grid(PARAMS, models, None) is None def test_지반고_자릿수_기본_3_묶음은_6(tmp_path, monkeypatch): root = tmp_path models = root / "B04_PreProcess" / "models" models.mkdir(parents=True) x = np.arange(0.0, 11.0) np.savez( models / "dtm_csf_smooth.npz", x=x, y=x, z=np.add.outer(x * 0.0, x * 0.1234567), valid_mask=np.ones((11, 11), dtype=bool), ) _patch(monkeypatch, root, None) async def params(_connection, _project_id): return dict(PARAMS) monkeypatch.setattr(terrain_router, "get_surface_confirmation_params", params) ask = terrain_router.ElevationRequest plain = asyncio.run(terrain_router.read_route_elevations(PID, ask(points=[(5.5, 5.0)]))) fine = asyncio.run( terrain_router.read_route_elevations(PID, ask(points=[(5.5, 5.0)], decimals=6)) ) assert plain["z"] == [round(5.5 * 0.1234567, 3)] assert fine["z"] == [round(5.5 * 0.1234567, 6)]