Files
Aislo/B06_Section/B06_Section_Router_Calc.py
T

93 lines
4.1 KiB
Python

"""계산 자리 「서버」 창구(PLAN 511-A · 요구 320-4) — 조작 중 계산을 서버가 같은 TS 로.
POST /api/projects/{id}/calc {run: [id…], inputs: {…}} → {results, ms, wall_ms}
브라우저가 계산 자리 판(`calc_sites.json`)에서 묶음을 「서버」로 둔 때만 부름
(`design_calc_server.ts`).
권한 = 프로젝트 접근(`protected_with_company`) · 호출량 = `main.py` 미들웨어 · 본문 상한 `MAX_BODY`.
실행 = 늘 떠 있는 Node 하나(`common_util_calc_runner.run_calc_served`) ·
정본 저장 길은 이 창구를 안 씀.
단계(`stage.*`) · 화면 길(`cross.refresh`) = 서버가 DTM 격자 경로 · 구조물 정의를 채움 —
화면이 보낸 경로는 버림(파일 경로를 화면에 맡기지 않음 · DTM 아닌 지표면 = 격자 없음 · 511-B B-4).
"""
import asyncio
import json
from pathlib import Path
from uuid import UUID
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from common_util.common_util_calc_runner import PAGE_IDS, STAGE_IDS, run_calc_served
router = APIRouter(prefix="/api/projects", tags=["B06 Calc Server"])
#: 본문 상한 — 종횡단 상세 한 벌(1.3~1.5 MB) 여유 · 넘으면 413.
MAX_BODY = 8 * 1024 * 1024
def _error(status: int, message: str) -> JSONResponse:
return JSONResponse(status_code=status, content={"status": "error", "message": message})
@router.post("/{project_id}/calc", response_model=None)
async def run_calc_endpoint(project_id: UUID, request: Request) -> dict | JSONResponse:
declared = request.headers.get("content-length")
if declared and declared.isdigit() and int(declared) > MAX_BODY:
return _error(413, "계산 입력이 너무 큼")
body = b""
async for chunk in request.stream():
body += chunk
if len(body) > MAX_BODY:
return _error(413, "계산 입력이 너무 큼")
try:
payload = json.loads(body)
except ValueError:
return _error(400, "JSON 아님")
run = payload.get("run") if isinstance(payload, dict) else None
inputs = payload.get("inputs", {}) if isinstance(payload, dict) else None
if not isinstance(run, list) or not all(isinstance(i, str) for i in run):
return _error(400, "run = id 목록")
if not isinstance(inputs, dict):
return _error(400, "inputs = 객체")
if set(run) & (STAGE_IDS | PAGE_IDS):
await _server_context(project_id, inputs)
try:
answer = await asyncio.to_thread(run_calc_served, run, inputs)
except ValueError as exc:
return _error(400, str(exc))
if answer is None:
return _error(503, "서버 계산을 못 돌림")
if "error" in answer:
return _error(422, str(answer["error"]))
return {"status": "success", "project_id": str(project_id), **answer}
async def _server_context(project_id: UUID, inputs: dict) -> None:
"""단계 · 화면 길 입력에 서버 맥락을 채움 — DTM 격자 이진 경로(없으면 None) ·
구조물 정의(M02 반영)."""
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B05_Profile.B05_Profile_Structures_Schema import load_definition
from B06_Section.B06_Section_Server_Calc_Prebuild import design_grid_file
from common_util.common_util_storage import resolve_stored_project_path
from common_util.common_util_surface_confirmation import (
design_terrain,
get_surface_confirmation_params,
)
from config.config_db import run_with_connection
stored, params = await asyncio.gather(
run_with_connection(get_project_storage_relative_path, project_id),
run_with_connection(get_surface_confirmation_params, str(project_id)),
)
root = Path(resolve_stored_project_path(stored))
grid = await asyncio.to_thread(design_grid_file, root, params)
inputs["definition"] = load_definition()
if isinstance(inputs.get("stages"), dict):
inputs["stages"]["grid"] = (
{"path": str(grid), "design": design_terrain(params)} if grid else None
)
if isinstance(inputs.get("cross_refresh"), dict):
inputs["cross_refresh"]["grid"] = str(grid) if grid else None