feat(lab): verify and expose real evidence
This commit is contained in:
@@ -34,6 +34,7 @@ from k1link.sessions import (
|
||||
)
|
||||
from k1link.web.device_plugin_composition import load_installed_device_plugins
|
||||
from k1link.web.environment_api import build_environment_router
|
||||
from k1link.web.laboratory_api import build_laboratory_router
|
||||
from k1link.web.lidar_api import build_lidar_router
|
||||
from k1link.web.plugin_catalog import DevicePluginCatalog, PluginCatalogError
|
||||
from k1link.web.plugin_runtime import (
|
||||
@@ -464,6 +465,34 @@ app.include_router(
|
||||
),
|
||||
)
|
||||
)
|
||||
app.include_router(
|
||||
build_laboratory_router(
|
||||
e29_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e29" / "results"
|
||||
),
|
||||
local_surface_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "k1-local-surface-v1"
|
||||
/ "models"
|
||||
),
|
||||
source_pack_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e10"
|
||||
/ "lidar-packs"
|
||||
),
|
||||
source_result_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e10"
|
||||
/ "worker-results"
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
frontend_dist = REPOSITORY_ROOT / "apps" / "control-station" / "dist"
|
||||
|
||||
@@ -0,0 +1,497 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from collections.abc import Callable, Mapping
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from k1link.compute.semantic_geometry_fusion import (
|
||||
CAMERA_GEOMETRY_FRAME_SCHEMA,
|
||||
CAMERA_GEOMETRY_FRAMES_NAME,
|
||||
CAMERA_GEOMETRY_FUSION_SCHEMA,
|
||||
CAMERA_GEOMETRY_MANIFEST_NAME,
|
||||
CAMERA_GEOMETRY_REPORT_NAME,
|
||||
CAMERA_GEOMETRY_REPORT_SCHEMA,
|
||||
)
|
||||
|
||||
LABORATORY_E29_CATALOG_SCHEMA: Final = (
|
||||
"missioncore.laboratory-e29-catalog/v1"
|
||||
)
|
||||
LABORATORY_E29_DETAIL_SCHEMA: Final = "missioncore.laboratory-e29-detail/v1"
|
||||
LABORATORY_E29_FRAME_SCHEMA: Final = "missioncore.laboratory-e29-frame/v1"
|
||||
|
||||
_SHA256 = re.compile(r"^[a-f0-9]{64}$")
|
||||
_E29_RESULT_ID = re.compile(r"^e29-camera-geometry-[a-f0-9]{64}$")
|
||||
_LOCAL_SURFACE_ID = re.compile(r"^k1-local-surface-[a-f0-9]{64}$")
|
||||
_SOURCE_PACK_ID = re.compile(r"^e10-lidar-pack-[a-f0-9]{64}$")
|
||||
_SOURCE_RESULT_ID = re.compile(r"^e10-integrated-perception-[a-f0-9]{64}$")
|
||||
_SESSION_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,159}$")
|
||||
|
||||
_MAX_MANIFEST_BYTES: Final = 256 * 1024
|
||||
_MAX_REPORT_BYTES: Final = 2 * 1024 * 1024
|
||||
_MAX_FRAME_BYTES: Final = 2 * 1024 * 1024
|
||||
_MAX_REVIEW_FRAMES: Final = 64
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
|
||||
class LaboratoryEvidenceError(ValueError):
|
||||
"""Raised when published laboratory evidence is incomplete or changed."""
|
||||
|
||||
|
||||
def _canonical_json(value: object) -> bytes:
|
||||
return json.dumps(
|
||||
value,
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
separators=(",", ":"),
|
||||
allow_nan=False,
|
||||
).encode()
|
||||
|
||||
|
||||
def _read_json(path: Path, *, maximum_bytes: int) -> dict[str, Any]:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise LaboratoryEvidenceError("laboratory JSON artifact is unavailable")
|
||||
size = path.stat().st_size
|
||||
if size <= 0 or size > maximum_bytes:
|
||||
raise LaboratoryEvidenceError("laboratory JSON artifact is out of bounds")
|
||||
try:
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise LaboratoryEvidenceError("laboratory JSON artifact is invalid") from exc
|
||||
if not isinstance(value, dict):
|
||||
raise LaboratoryEvidenceError("laboratory JSON artifact must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
while chunk := stream.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _safe_artifact_path(root: Path, value: object) -> Path:
|
||||
if not isinstance(value, str) or not value or Path(value).name != value:
|
||||
raise LaboratoryEvidenceError("laboratory artifact path is invalid")
|
||||
path = root / value
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise LaboratoryEvidenceError("laboratory artifact is unavailable")
|
||||
return path
|
||||
|
||||
|
||||
def _artifact_map(
|
||||
root: Path,
|
||||
manifest: Mapping[str, Any],
|
||||
) -> dict[str, tuple[Path, Mapping[str, Any]]]:
|
||||
raw = manifest.get("artifacts")
|
||||
if not isinstance(raw, list) or len(raw) != 2:
|
||||
raise LaboratoryEvidenceError("laboratory artifact list is invalid")
|
||||
artifacts: dict[str, tuple[Path, Mapping[str, Any]]] = {}
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
raise LaboratoryEvidenceError("laboratory artifact entry is invalid")
|
||||
role = item.get("role")
|
||||
byte_length = item.get("byte_length")
|
||||
digest = item.get("sha256")
|
||||
if (
|
||||
not isinstance(role, str)
|
||||
or role in artifacts
|
||||
or not isinstance(byte_length, int)
|
||||
or byte_length <= 0
|
||||
or not isinstance(digest, str)
|
||||
or _SHA256.fullmatch(digest) is None
|
||||
):
|
||||
raise LaboratoryEvidenceError("laboratory artifact metadata is invalid")
|
||||
path = _safe_artifact_path(root, item.get("path"))
|
||||
if path.stat().st_size != byte_length:
|
||||
raise LaboratoryEvidenceError("laboratory artifact size changed")
|
||||
artifacts[role] = (path, item)
|
||||
if set(artifacts) != {"camera-geometry-frames", "camera-geometry-report"}:
|
||||
raise LaboratoryEvidenceError("laboratory artifact roles are invalid")
|
||||
return artifacts
|
||||
|
||||
|
||||
def _result_signature(root: Path) -> tuple[int, ...]:
|
||||
paths = (
|
||||
root / CAMERA_GEOMETRY_MANIFEST_NAME,
|
||||
root / CAMERA_GEOMETRY_REPORT_NAME,
|
||||
root / CAMERA_GEOMETRY_FRAMES_NAME,
|
||||
)
|
||||
signature: list[int] = []
|
||||
for path in paths:
|
||||
stat = path.stat()
|
||||
signature.extend((stat.st_size, stat.st_mtime_ns))
|
||||
return tuple(signature)
|
||||
|
||||
|
||||
def _review_frame(row: Mapping[str, Any]) -> dict[str, object] | None:
|
||||
observations = row.get("semantic_observations")
|
||||
geometry = row.get("geometry_only_occupied")
|
||||
if not isinstance(observations, list) or not isinstance(geometry, list):
|
||||
raise LaboratoryEvidenceError("E29 frame collections are invalid")
|
||||
conflicts = sum(
|
||||
1
|
||||
for observation in observations
|
||||
if isinstance(observation, dict)
|
||||
and observation.get("geometry_status") == "conflict"
|
||||
)
|
||||
if conflicts == 0:
|
||||
return None
|
||||
frame_index = row.get("frame_index")
|
||||
session_seconds = row.get("session_seconds")
|
||||
if (
|
||||
not isinstance(frame_index, int)
|
||||
or frame_index < 0
|
||||
or not isinstance(session_seconds, (int, float))
|
||||
):
|
||||
raise LaboratoryEvidenceError("E29 review frame identity is invalid")
|
||||
return {
|
||||
"frame_index": frame_index,
|
||||
"source_frame_index": row.get("source_frame_index"),
|
||||
"session_seconds": float(session_seconds),
|
||||
"conflict_count": conflicts,
|
||||
"semantic_observation_count": len(observations),
|
||||
"geometry_only_cluster_count": len(geometry),
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _read_result_cached(
|
||||
root_text: str,
|
||||
signature: tuple[int, ...],
|
||||
) -> tuple[dict[str, Any], tuple[int, ...]]:
|
||||
del signature
|
||||
root = Path(root_text)
|
||||
if root.is_symlink() or not root.is_dir() or _E29_RESULT_ID.fullmatch(root.name) is None:
|
||||
raise LaboratoryEvidenceError("E29 result id is invalid")
|
||||
manifest = _read_json(
|
||||
root / CAMERA_GEOMETRY_MANIFEST_NAME,
|
||||
maximum_bytes=_MAX_MANIFEST_BYTES,
|
||||
)
|
||||
artifacts = _artifact_map(root, manifest)
|
||||
report_path, report_artifact = artifacts["camera-geometry-report"]
|
||||
frames_path, frames_artifact = artifacts["camera-geometry-frames"]
|
||||
report = _read_json(report_path, maximum_bytes=_MAX_REPORT_BYTES)
|
||||
identity = manifest.get("identity")
|
||||
identity_sha256 = manifest.get("identity_sha256")
|
||||
if (
|
||||
manifest.get("schema_version") != CAMERA_GEOMETRY_FUSION_SCHEMA
|
||||
or manifest.get("result_id") != root.name
|
||||
or not isinstance(identity, dict)
|
||||
or identity.get("schema_version") != CAMERA_GEOMETRY_FUSION_SCHEMA
|
||||
or not isinstance(identity_sha256, str)
|
||||
or _SHA256.fullmatch(identity_sha256) is None
|
||||
or hashlib.sha256(_canonical_json(identity)).hexdigest() != identity_sha256
|
||||
or root.name != f"e29-camera-geometry-{identity_sha256}"
|
||||
or report.get("schema_version") != CAMERA_GEOMETRY_REPORT_SCHEMA
|
||||
or report.get("result_id") != root.name
|
||||
or report.get("identity") != identity
|
||||
or report.get("status") != "diagnostic-replay-complete"
|
||||
or report.get("ground_truth") is not False
|
||||
):
|
||||
raise LaboratoryEvidenceError("E29 result identity is invalid")
|
||||
if _sha256(report_path) != report_artifact.get("sha256"):
|
||||
raise LaboratoryEvidenceError("E29 report digest changed")
|
||||
|
||||
expected_frames = identity.get("frame_count")
|
||||
if not isinstance(expected_frames, int) or expected_frames <= 0:
|
||||
raise LaboratoryEvidenceError("E29 frame count is invalid")
|
||||
offsets: list[int] = []
|
||||
review_frames: list[dict[str, object]] = []
|
||||
digest = hashlib.sha256()
|
||||
with frames_path.open("rb") as stream:
|
||||
frame_index = 0
|
||||
while True:
|
||||
offset = stream.tell()
|
||||
raw = stream.readline(_MAX_FRAME_BYTES + 1)
|
||||
if not raw:
|
||||
break
|
||||
if len(raw) > _MAX_FRAME_BYTES:
|
||||
raise LaboratoryEvidenceError("E29 frame is out of bounds")
|
||||
digest.update(raw)
|
||||
try:
|
||||
row = json.loads(raw)
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||
raise LaboratoryEvidenceError("E29 frame is invalid") from exc
|
||||
if (
|
||||
not isinstance(row, dict)
|
||||
or row.get("schema_version") != CAMERA_GEOMETRY_FRAME_SCHEMA
|
||||
or row.get("frame_index") != frame_index
|
||||
):
|
||||
raise LaboratoryEvidenceError("E29 frame sequence changed")
|
||||
offsets.append(offset)
|
||||
if len(review_frames) < _MAX_REVIEW_FRAMES:
|
||||
review = _review_frame(row)
|
||||
if review is not None:
|
||||
review_frames.append(review)
|
||||
frame_index += 1
|
||||
if (
|
||||
len(offsets) != expected_frames
|
||||
or digest.hexdigest() != frames_artifact.get("sha256")
|
||||
):
|
||||
raise LaboratoryEvidenceError("E29 frame evidence changed")
|
||||
|
||||
item = {
|
||||
"result_id": root.name,
|
||||
"created_at_utc": manifest.get("created_at_utc"),
|
||||
"status": report["status"],
|
||||
"identity": identity,
|
||||
"metrics": report.get("metrics"),
|
||||
"decision": report.get("decision"),
|
||||
"limitations": report.get("limitations"),
|
||||
"authority": report.get("authority"),
|
||||
"review_frames": review_frames,
|
||||
"ground_truth": False,
|
||||
"access": "read-only",
|
||||
}
|
||||
return item, tuple(offsets)
|
||||
|
||||
|
||||
def _linked_evidence(
|
||||
item: Mapping[str, Any],
|
||||
*,
|
||||
local_surface_root: Path,
|
||||
source_pack_root: Path,
|
||||
source_result_root: Path,
|
||||
) -> dict[str, object]:
|
||||
identity = item.get("identity")
|
||||
if not isinstance(identity, dict):
|
||||
raise LaboratoryEvidenceError("E29 identity is unavailable")
|
||||
model_id = identity.get("local_surface_model_id")
|
||||
pack_id = identity.get("source_pack_id")
|
||||
source_result_id = identity.get("source_result_id")
|
||||
if (
|
||||
not isinstance(model_id, str)
|
||||
or _LOCAL_SURFACE_ID.fullmatch(model_id) is None
|
||||
or not isinstance(pack_id, str)
|
||||
or _SOURCE_PACK_ID.fullmatch(pack_id) is None
|
||||
or not isinstance(source_result_id, str)
|
||||
or _SOURCE_RESULT_ID.fullmatch(source_result_id) is None
|
||||
):
|
||||
raise LaboratoryEvidenceError("E29 linked evidence ids are invalid")
|
||||
model_root = local_surface_root / model_id
|
||||
pack_root = source_pack_root / pack_id
|
||||
result_root = source_result_root / source_result_id
|
||||
required = (
|
||||
model_root / "manifest.json",
|
||||
model_root / "local-surface.json",
|
||||
model_root / "local-surface.npz",
|
||||
pack_root / "manifest.json",
|
||||
pack_root / "lidar-pack.npz",
|
||||
result_root / "result.json",
|
||||
result_root / "fusion-frames.jsonl",
|
||||
)
|
||||
if any(path.is_symlink() or not path.is_file() for path in required):
|
||||
raise LaboratoryEvidenceError("E29 linked evidence is incomplete")
|
||||
local_surface_report = _read_json(
|
||||
model_root / "local-surface.json",
|
||||
maximum_bytes=_MAX_REPORT_BYTES,
|
||||
)
|
||||
session_id = local_surface_report.get("session_id")
|
||||
if (
|
||||
local_surface_report.get("model_id") != model_id
|
||||
or not isinstance(session_id, str)
|
||||
or _SESSION_ID.fullmatch(session_id) is None
|
||||
):
|
||||
raise LaboratoryEvidenceError("E29 source session is invalid")
|
||||
return {
|
||||
"source_session_id": session_id,
|
||||
"local_surface_model_id": model_id,
|
||||
"source_pack_id": pack_id,
|
||||
"source_result_id": source_result_id,
|
||||
"published_on_control_plane": True,
|
||||
"worker_copy_required": False,
|
||||
}
|
||||
|
||||
|
||||
def _roots(
|
||||
*,
|
||||
e29_root_provider: RootProvider,
|
||||
local_surface_root_provider: RootProvider,
|
||||
source_pack_root_provider: RootProvider,
|
||||
source_result_root_provider: RootProvider,
|
||||
) -> tuple[Path, Path, Path, Path] | None:
|
||||
values = (
|
||||
e29_root_provider(),
|
||||
local_surface_root_provider(),
|
||||
source_pack_root_provider(),
|
||||
source_result_root_provider(),
|
||||
)
|
||||
if any(value is None for value in values):
|
||||
return None
|
||||
roots = tuple(value.resolve() for value in values if value is not None)
|
||||
if len(roots) != 4 or any(not root.is_dir() for root in roots):
|
||||
return None
|
||||
return roots[0], roots[1], roots[2], roots[3]
|
||||
|
||||
|
||||
def build_laboratory_router(
|
||||
*,
|
||||
e29_root_provider: RootProvider = lambda: None,
|
||||
local_surface_root_provider: RootProvider = lambda: None,
|
||||
source_pack_root_provider: RootProvider = lambda: None,
|
||||
source_result_root_provider: RootProvider = lambda: None,
|
||||
) -> APIRouter:
|
||||
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
|
||||
|
||||
@router.get("/e29/results")
|
||||
def list_e29_results(
|
||||
limit: int = Query(default=1, ge=1, le=10),
|
||||
) -> dict[str, object]:
|
||||
roots = _roots(
|
||||
e29_root_provider=e29_root_provider,
|
||||
local_surface_root_provider=local_surface_root_provider,
|
||||
source_pack_root_provider=source_pack_root_provider,
|
||||
source_result_root_provider=source_result_root_provider,
|
||||
)
|
||||
if roots is None:
|
||||
return {
|
||||
"schema_version": LABORATORY_E29_CATALOG_SCHEMA,
|
||||
"configured": False,
|
||||
"items": [],
|
||||
"candidate_total": 0,
|
||||
"invalid_total": 0,
|
||||
"access": "read-only",
|
||||
}
|
||||
e29_root, local_surface_root, source_pack_root, source_result_root = roots
|
||||
candidates = sorted(
|
||||
(
|
||||
candidate
|
||||
for candidate in e29_root.iterdir()
|
||||
if candidate.is_dir()
|
||||
and _E29_RESULT_ID.fullmatch(candidate.name) is not None
|
||||
),
|
||||
key=lambda candidate: candidate.stat().st_mtime_ns,
|
||||
reverse=True,
|
||||
)
|
||||
items: list[dict[str, object]] = []
|
||||
invalid_total = 0
|
||||
for candidate in candidates:
|
||||
try:
|
||||
item, _ = _read_result_cached(
|
||||
str(candidate.resolve()),
|
||||
_result_signature(candidate),
|
||||
)
|
||||
document = copy.deepcopy(item)
|
||||
document["linked_evidence"] = _linked_evidence(
|
||||
document,
|
||||
local_surface_root=local_surface_root,
|
||||
source_pack_root=source_pack_root,
|
||||
source_result_root=source_result_root,
|
||||
)
|
||||
if len(items) < limit:
|
||||
items.append(document)
|
||||
except (LaboratoryEvidenceError, OSError):
|
||||
invalid_total += 1
|
||||
return {
|
||||
"schema_version": LABORATORY_E29_CATALOG_SCHEMA,
|
||||
"configured": True,
|
||||
"items": items,
|
||||
"candidate_total": len(candidates),
|
||||
"invalid_total": invalid_total,
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
@router.get("/e29/results/{result_id}")
|
||||
def get_e29_result(result_id: str) -> dict[str, object]:
|
||||
roots = _roots(
|
||||
e29_root_provider=e29_root_provider,
|
||||
local_surface_root_provider=local_surface_root_provider,
|
||||
source_pack_root_provider=source_pack_root_provider,
|
||||
source_result_root_provider=source_result_root_provider,
|
||||
)
|
||||
if roots is None or _E29_RESULT_ID.fullmatch(result_id) is None:
|
||||
raise HTTPException(status_code=404, detail="LAB E29 result не найден")
|
||||
e29_root, local_surface_root, source_pack_root, source_result_root = roots
|
||||
candidate = e29_root / result_id
|
||||
if not candidate.is_dir():
|
||||
raise HTTPException(status_code=404, detail="LAB E29 result не найден")
|
||||
try:
|
||||
item, _ = _read_result_cached(
|
||||
str(candidate.resolve()),
|
||||
_result_signature(candidate),
|
||||
)
|
||||
document = copy.deepcopy(item)
|
||||
document["linked_evidence"] = _linked_evidence(
|
||||
document,
|
||||
local_surface_root=local_surface_root,
|
||||
source_pack_root=source_pack_root,
|
||||
source_result_root=source_result_root,
|
||||
)
|
||||
return {
|
||||
"schema_version": LABORATORY_E29_DETAIL_SCHEMA,
|
||||
"result": document,
|
||||
}
|
||||
except (LaboratoryEvidenceError, OSError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="LAB E29 evidence не прошло проверку целостности",
|
||||
) from exc
|
||||
|
||||
@router.get("/e29/results/{result_id}/frames/{frame_index}")
|
||||
def get_e29_frame(result_id: str, frame_index: int) -> dict[str, object]:
|
||||
roots = _roots(
|
||||
e29_root_provider=e29_root_provider,
|
||||
local_surface_root_provider=local_surface_root_provider,
|
||||
source_pack_root_provider=source_pack_root_provider,
|
||||
source_result_root_provider=source_result_root_provider,
|
||||
)
|
||||
if (
|
||||
roots is None
|
||||
or _E29_RESULT_ID.fullmatch(result_id) is None
|
||||
or frame_index < 0
|
||||
):
|
||||
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
|
||||
e29_root, local_surface_root, source_pack_root, source_result_root = roots
|
||||
candidate = e29_root / result_id
|
||||
if not candidate.is_dir():
|
||||
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
|
||||
try:
|
||||
item, offsets = _read_result_cached(
|
||||
str(candidate.resolve()),
|
||||
_result_signature(candidate),
|
||||
)
|
||||
_linked_evidence(
|
||||
item,
|
||||
local_surface_root=local_surface_root,
|
||||
source_pack_root=source_pack_root,
|
||||
source_result_root=source_result_root,
|
||||
)
|
||||
if frame_index >= len(offsets):
|
||||
raise HTTPException(status_code=404, detail="LAB E29 frame не найден")
|
||||
frames_path = candidate / CAMERA_GEOMETRY_FRAMES_NAME
|
||||
with frames_path.open("rb") as stream:
|
||||
stream.seek(offsets[frame_index])
|
||||
raw = stream.readline(_MAX_FRAME_BYTES + 1)
|
||||
if not raw or len(raw) > _MAX_FRAME_BYTES:
|
||||
raise LaboratoryEvidenceError("E29 frame is out of bounds")
|
||||
row = json.loads(raw)
|
||||
if (
|
||||
not isinstance(row, dict)
|
||||
or row.get("schema_version") != CAMERA_GEOMETRY_FRAME_SCHEMA
|
||||
or row.get("frame_index") != frame_index
|
||||
):
|
||||
raise LaboratoryEvidenceError("E29 frame identity changed")
|
||||
return {
|
||||
"schema_version": LABORATORY_E29_FRAME_SCHEMA,
|
||||
"result_id": result_id,
|
||||
"frame": row,
|
||||
"access": "read-only",
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except (LaboratoryEvidenceError, OSError, json.JSONDecodeError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="LAB E29 frame не прошёл проверку целостности",
|
||||
) from exc
|
||||
|
||||
return router
|
||||
@@ -528,6 +528,8 @@ def build_lidar_router(
|
||||
reverse=True,
|
||||
)
|
||||
for candidate in candidates:
|
||||
if len(items) >= limit:
|
||||
break
|
||||
try:
|
||||
model = K1LocalSurfaceV1(candidate)
|
||||
try:
|
||||
@@ -539,9 +541,11 @@ def build_lidar_router(
|
||||
return {
|
||||
"schema_version": K1_LOCAL_SURFACE_CATALOG_SCHEMA,
|
||||
"configured": True,
|
||||
"items": items[:limit],
|
||||
"items": items,
|
||||
"valid_total": len(items),
|
||||
"invalid_total": invalid_total,
|
||||
"candidate_total": len(candidates),
|
||||
"scan_complete": len(items) + invalid_total == len(candidates),
|
||||
"access": "read-only",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user