Files
presenton--presenton/servers/fastapi/templates/layout_code_validation.py
T
2026-07-13 13:08:41 +08:00

131 lines
4.1 KiB
Python

from __future__ import annotations
import os
from typing import Any
import aiohttp
from pydantic import BaseModel
class ValidatedLayoutCode(BaseModel):
layout_code: str
layout_id: str
layout_name: str
layout_description: str
schema_json_value: Any
class LayoutCodeValidationError(Exception):
def __init__(
self,
error: str,
*,
line: int | None = None,
column: int | None = None,
) -> None:
super().__init__(error)
self.error = error
self.line = line
self.column = column
def to_detail(self) -> dict[str, Any]:
detail: dict[str, Any] = {"error": self.error}
if self.line is not None:
detail["line"] = self.line
if self.column is not None:
detail["column"] = self.column
return detail
class LayoutCodeValidationServiceError(Exception):
pass
def _endpoint_from_base(base_url: str) -> str:
base = base_url.strip().rstrip("/")
if base.endswith("/api/validate-layout-code"):
return base
return f"{base}/api/validate-layout-code"
def _validation_url_candidates() -> list[str]:
explicit_endpoint = (os.getenv("LAYOUT_CODE_VALIDATION_URL") or "").strip()
if explicit_endpoint:
return [_endpoint_from_base(explicit_endpoint)]
candidate_bases = [
os.getenv("NEXT_PUBLIC_URL"),
os.getenv("PRESENTON_NEXT_URL"),
"http://localhost",
"http://localhost:3000",
]
seen: set[str] = set()
urls: list[str] = []
for base in candidate_bases:
if not base:
continue
url = _endpoint_from_base(base)
if url in seen:
continue
seen.add(url)
urls.append(url)
return urls
def _coerce_validation_error(payload: Any, fallback: str) -> LayoutCodeValidationError:
if not isinstance(payload, dict):
return LayoutCodeValidationError(fallback)
return LayoutCodeValidationError(
str(payload.get("error") or fallback),
line=payload.get("line") if isinstance(payload.get("line"), int) else None,
column=payload.get("column")
if isinstance(payload.get("column"), int)
else None,
)
async def validate_layout_code(layout_code: str) -> ValidatedLayoutCode:
last_service_error: str | None = None
timeout = aiohttp.ClientTimeout(total=20)
for url in _validation_url_candidates():
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(
url,
json={"layout_code": layout_code},
headers={"Content-Type": "application/json"},
) as response:
payload = await response.json(content_type=None)
if response.status == 200 and isinstance(payload, dict):
return ValidatedLayoutCode(
layout_code=str(payload.get("layout_code") or ""),
layout_id=str(payload.get("layoutId") or ""),
layout_name=str(payload.get("layoutName") or ""),
layout_description=str(
payload.get("layoutDescription") or ""
),
schema_json_value=payload.get("schemaJSON"),
)
if response.status == 400:
raise _coerce_validation_error(
payload, "Layout code validation failed"
)
last_service_error = (
f"Layout validation service returned HTTP {response.status}"
)
except LayoutCodeValidationError:
raise
except (aiohttp.ClientError, TimeoutError) as exc:
last_service_error = str(exc) or exc.__class__.__name__
except Exception as exc: # noqa: BLE001
last_service_error = str(exc) or exc.__class__.__name__
raise LayoutCodeValidationServiceError(
last_service_error or "Layout validation service is unavailable"
)