refactor(lab): canonize selected evidence reports

This commit is contained in:
DCCONSTRUCTIONS
2026-08-05 11:49:44 +03:00
parent 4c763bd8aa
commit de12e96297
30 changed files with 2876 additions and 134 deletions
+22
View File
@@ -6,10 +6,32 @@ from k1link.laboratory.evidence_registry import (
LaboratoryEvidenceRegistry,
LaboratoryRegistryError,
)
from k1link.laboratory.evidence_report import (
LABORATORY_EVIDENCE_REPORT_SCHEMA,
LaboratoryEvidenceReportError,
LaboratoryEvidenceReportNotFound,
LaboratoryEvidenceReportService,
)
from k1link.laboratory.value_review_registry import (
LABORATORY_VALUE_REVIEW_INDEX_SCHEMA,
LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA,
LaboratoryValueReviewEntry,
LaboratoryValueReviewRegistry,
LaboratoryValueReviewRegistryError,
)
__all__ = [
"LABORATORY_EVIDENCE_DEFINITION_SCHEMA",
"LABORATORY_EVIDENCE_REPORT_SCHEMA",
"LaboratoryEvidenceDefinition",
"LaboratoryEvidenceRegistry",
"LaboratoryEvidenceReportError",
"LaboratoryEvidenceReportNotFound",
"LaboratoryEvidenceReportService",
"LaboratoryRegistryError",
"LABORATORY_VALUE_REVIEW_INDEX_SCHEMA",
"LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA",
"LaboratoryValueReviewEntry",
"LaboratoryValueReviewRegistry",
"LaboratoryValueReviewRegistryError",
]
+443
View File
@@ -0,0 +1,443 @@
from __future__ import annotations
import hashlib
import json
from collections.abc import Callable
from pathlib import Path, PurePosixPath
from typing import Any, Final
from k1link.laboratory.evidence_registry import (
LaboratoryEvidenceDefinition,
LaboratoryEvidenceRegistry,
)
LABORATORY_EVIDENCE_REPORT_SCHEMA: Final = "missioncore.laboratory-evidence-report/v1"
_DOCUMENT_MAX_BYTES: Final = 1024 * 1024
_ARTIFACT_LIMIT: Final = 256
_HASH_CHUNK_BYTES: Final = 1024 * 1024
RuntimeRootProvider = Callable[[], Path | None]
class LaboratoryEvidenceReportError(ValueError):
"""Raised when immutable LAB evidence cannot be verified or projected."""
class LaboratoryEvidenceReportNotFound(LaboratoryEvidenceReportError):
"""Raised when the requested evidence identity is not available."""
class LaboratoryEvidenceReportService:
def __init__(
self,
registry: LaboratoryEvidenceRegistry,
runtime_root_provider: RuntimeRootProvider,
) -> None:
self._definitions = {
definition.work_id: definition for definition in registry.definitions
}
self._runtime_root_provider = runtime_root_provider
def read(self, work_id: str, result_id: str) -> dict[str, object]:
definition = self._definitions.get(work_id)
if definition is None or definition.result_id_pattern.fullmatch(result_id) is None:
raise LaboratoryEvidenceReportNotFound("LAB evidence identity is unknown")
result_root = self._result_root(definition, result_id)
document_path = _safe_file(result_root, definition.document_name)
document_bytes = _read_bounded(document_path, _DOCUMENT_MAX_BYTES, "LAB document")
document = _json_object(document_bytes, "LAB document")
_validate_document(document, definition, result_id)
identity = _object_or_none(document.get("identity"))
identity_sha256 = document.get("identity_sha256")
if identity is None or not isinstance(identity_sha256, str):
raise LaboratoryEvidenceReportError("LAB identity proof is missing")
actual_identity_sha256 = _canonical_sha256(identity)
if actual_identity_sha256 != identity_sha256 or not result_id.endswith(identity_sha256):
raise LaboratoryEvidenceReportError("LAB identity proof is invalid")
artifacts = _verified_artifacts(result_root, document.get("artifacts"))
report_descriptor = _report_descriptor(artifacts)
report = (
_read_json_artifact(result_root, report_descriptor, "LAB report")
if report_descriptor is not None
else document
)
runtime_descriptor = _runtime_descriptor(artifacts)
runtime = (
_read_json_artifact(result_root, runtime_descriptor, "LAB runtime")
if runtime_descriptor is not None
else None
)
source = _first_object(
report.get("source"),
identity.get("source"),
_nested(identity, "profile", "source"),
) or _source_projection(report, identity)
configuration = _configuration_projection(report, identity)
method = _first_object(
report.get("method"),
identity.get("method"),
identity.get("profile"),
)
execution = _first_object(
report.get("execution"),
runtime,
identity.get("execution"),
identity.get("worker"),
report.get("worker"),
)
metrics = _first_object(report.get("metrics"), _nested(runtime, "metrics"))
resources = _first_object(
_nested(report, "metrics", "resources"),
_nested(runtime, "metrics", "resources"),
_nested(runtime, "metrics", "gpu"),
)
gates = _first_object(
report.get("acceptance"),
report.get("quality_gate"),
report.get("acceptance_requirements"),
_nested(runtime, "acceptance"),
)
decision = _json_value_or_none(report.get("decision"))
limitations = _json_value_or_none(report.get("limitations"))
authority = _first_object(
report.get("authority"),
identity.get("authority"),
document.get("authority"),
)
visual_review = _first_object(
report.get("visual_review"),
report.get("visual_evidence"),
)
visual_artifacts = [
artifact
for artifact in artifacts
if _is_visual_artifact(artifact)
]
completeness_values: dict[str, object | None] = {
"identity": identity,
"source": source,
"configuration": configuration,
"method": method,
"execution": execution,
"resources": resources,
"metrics": metrics,
"gates": gates,
"decision": decision,
"limitations": limitations,
"authority": authority,
"artifacts": artifacts or None,
"visual_evidence": visual_review or (visual_artifacts or None),
}
return {
"schema_version": LABORATORY_EVIDENCE_REPORT_SCHEMA,
"work_id": work_id,
"result_id": result_id,
"created_at_utc": _optional_text(document.get("created_at_utc")),
"access": "read-only",
"proof": {
"document_schema_version": document.get("schema_version"),
"document_sha256": hashlib.sha256(document_bytes).hexdigest(),
"identity_sha256": identity_sha256,
"report_schema_version": report.get("schema_version"),
"report_sha256": (
report_descriptor["sha256"] if report_descriptor is not None else None
),
"artifact_count": len(artifacts),
"verified_artifact_count": len(artifacts),
},
"completeness": {
key: "recorded" if value is not None else "not-recorded"
for key, value in completeness_values.items()
},
"identity": identity,
"source": source,
"configuration": configuration,
"method": method,
"execution": execution,
"resources": resources,
"metrics": metrics,
"gates": gates,
"decision": decision,
"limitations": limitations,
"authority": authority,
"artifacts": artifacts,
"visual_evidence": {
"review": visual_review,
"artifacts": visual_artifacts,
},
"raw_report": report,
}
def _result_root(
self,
definition: LaboratoryEvidenceDefinition,
result_id: str,
) -> Path:
configured = self._runtime_root_provider()
if configured is None:
raise LaboratoryEvidenceReportNotFound("LAB runtime root is unavailable")
runtime_root = configured.expanduser().absolute()
if runtime_root.is_symlink():
raise LaboratoryEvidenceReportError("LAB runtime root must not be a symlink")
try:
runtime_root = runtime_root.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportNotFound("LAB runtime root is unavailable") from exc
candidate = definition.result_root(runtime_root) / result_id
if candidate.is_symlink():
raise LaboratoryEvidenceReportError("LAB result must not be a symlink")
try:
result_root = candidate.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportNotFound("LAB evidence result is unavailable") from exc
if not result_root.is_dir() or not result_root.is_relative_to(runtime_root):
raise LaboratoryEvidenceReportError("LAB evidence result path is invalid")
return result_root
def _validate_document(
document: dict[str, Any],
definition: LaboratoryEvidenceDefinition,
result_id: str,
) -> None:
if document.get("schema_version") != definition.result_schema_version:
raise LaboratoryEvidenceReportError("LAB document schema is invalid")
if document.get("result_id") != result_id:
raise LaboratoryEvidenceReportError("LAB result identity is invalid")
def _verified_artifacts(result_root: Path, value: object) -> list[dict[str, object]]:
if value is None:
return []
if not isinstance(value, list) or len(value) > _ARTIFACT_LIMIT:
raise LaboratoryEvidenceReportError("LAB artifact manifest is invalid")
verified: list[dict[str, object]] = []
for index, item in enumerate(value):
descriptor = _object_or_none(item)
if descriptor is None:
raise LaboratoryEvidenceReportError(f"LAB artifact {index} is invalid")
path_value = descriptor.get("path")
byte_length = descriptor.get("byte_length")
sha256 = descriptor.get("sha256")
if (
not isinstance(path_value, str)
or not isinstance(byte_length, int)
or isinstance(byte_length, bool)
or byte_length < 0
or not isinstance(sha256, str)
or len(sha256) != 64
):
raise LaboratoryEvidenceReportError(f"LAB artifact {index} proof is invalid")
path = _safe_file(result_root, path_value)
if path.stat().st_size != byte_length or _file_sha256(path) != sha256:
raise LaboratoryEvidenceReportError(f"LAB artifact {index} proof does not match")
verified.append(
{
"kind": _optional_text(descriptor.get("role") or descriptor.get("kind")),
"path": path_value,
"byte_length": byte_length,
"sha256": sha256,
"schema_version": _optional_text(descriptor.get("schema_version")),
"media_type": _optional_text(descriptor.get("media_type")),
"verified": True,
}
)
return verified
def _safe_file(root: Path, relative: str) -> Path:
if not isinstance(relative, str) or "\\" in relative:
raise LaboratoryEvidenceReportError("LAB artifact path is invalid")
posix = PurePosixPath(relative)
if (
posix.is_absolute()
or not posix.parts
or str(posix) != relative
or any(part in {"", ".", ".."} for part in posix.parts)
):
raise LaboratoryEvidenceReportError("LAB artifact path is invalid")
candidate = root.joinpath(*posix.parts)
current = root
for part in posix.parts:
current = current / part
if current.is_symlink():
raise LaboratoryEvidenceReportError("LAB artifact must not use symlinks")
try:
resolved = candidate.resolve(strict=True)
except OSError as exc:
raise LaboratoryEvidenceReportError("LAB artifact is missing") from exc
if not resolved.is_file() or not resolved.is_relative_to(root):
raise LaboratoryEvidenceReportError("LAB artifact path escaped its result")
return resolved
def _read_bounded(path: Path, maximum: int, label: str) -> bytes:
if path.stat().st_size > maximum:
raise LaboratoryEvidenceReportError(f"{label} is too large")
try:
return path.read_bytes()
except OSError as exc:
raise LaboratoryEvidenceReportError(f"{label} is unreadable") from exc
def _json_object(value: bytes, label: str) -> dict[str, Any]:
try:
document = json.loads(value)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise LaboratoryEvidenceReportError(f"{label} is invalid JSON") from exc
if not isinstance(document, dict) or not all(isinstance(key, str) for key in document):
raise LaboratoryEvidenceReportError(f"{label} must be an object")
return document
def _read_json_artifact(
root: Path,
descriptor: dict[str, object],
label: str,
) -> dict[str, Any]:
path_value = descriptor["path"]
if not isinstance(path_value, str):
raise LaboratoryEvidenceReportError(f"{label} path is invalid")
encoded = _read_bounded(
_safe_file(root, path_value),
_DOCUMENT_MAX_BYTES,
label,
)
return _json_object(encoded, label)
def _report_descriptor(
artifacts: list[dict[str, object]],
) -> dict[str, object] | None:
return next(
(
artifact
for artifact in artifacts
if "report" in str(artifact.get("kind") or "").lower()
and str(artifact.get("path") or "").lower().endswith(".json")
),
None,
)
def _runtime_descriptor(
artifacts: list[dict[str, object]],
) -> dict[str, object] | None:
return next(
(
artifact
for artifact in artifacts
if "runtime" in str(artifact.get("kind") or "").lower()
and str(artifact.get("path") or "").lower().endswith(".json")
),
None,
)
def _is_visual_artifact(artifact: dict[str, object]) -> bool:
kind = str(artifact.get("kind") or "").lower()
media_type = str(artifact.get("media_type") or "").lower()
suffix = Path(str(artifact.get("path") or "")).suffix.lower()
return (
any(token in kind for token in ("visual", "video", "overlay", "contact-sheet", "image"))
or media_type.startswith(("image/", "video/"))
or suffix in {".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm"}
)
def _canonical_sha256(value: object) -> str:
encoded = json.dumps(
value,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
allow_nan=False,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(_HASH_CHUNK_BYTES), b""):
digest.update(chunk)
return digest.hexdigest()
def _object_or_none(value: object) -> dict[str, Any] | None:
if isinstance(value, dict) and all(isinstance(key, str) for key in value):
return value
return None
def _first_object(*values: object) -> dict[str, Any] | None:
for value in values:
document = _object_or_none(value)
if document is not None:
return document
return None
def _nested(value: object, *keys: str) -> object:
current = value
for key in keys:
document = _object_or_none(current)
if document is None:
return None
current = document.get(key)
return current
def _json_value_or_none(value: object) -> object | None:
return value if value is not None else None
def _source_projection(*documents: dict[str, Any]) -> dict[str, Any] | None:
keys = {
"camera_source_id",
"frame_count",
"route_frame_count",
"session_id",
"source_display_name",
"source_id",
"source_result_id",
"source_session_id",
"source_sha256",
"stream_sha256",
"upstream",
}
result: dict[str, Any] = {}
for document in documents:
for key, value in document.items():
if key in keys or key.endswith("_source"):
result.setdefault(key, value)
return result or None
def _configuration_projection(
report: dict[str, Any],
identity: dict[str, Any],
) -> dict[str, Any] | None:
keys = {
"analysis_profile",
"configuration",
"detection",
"detector",
"parameters",
"preprocessing",
"profile",
"valid_fov",
}
result: dict[str, Any] = {}
for document in (report, identity):
for key, value in document.items():
if key in keys:
result.setdefault(key, value)
return result or None
def _optional_text(value: object) -> str | None:
return value if isinstance(value, str) and value.strip() else None
@@ -0,0 +1,156 @@
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Literal, cast
LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA: Final = (
"missioncore.laboratory-value-review-registry/v1"
)
LABORATORY_VALUE_REVIEW_INDEX_SCHEMA: Final = "missioncore.laboratory-value-review-index/v1"
_REGISTRY_MAX_BYTES: Final = 128 * 1024
_CATALOG_ID = re.compile(r"^(?:[a-z][a-z0-9-]{2,95}|session:[A-Za-z0-9._:-]{3,128})$")
_EVIDENCE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{2,191}$")
_ROOT_KEYS: Final = frozenset({"schema_version", "reviewed_at_utc", "entries"})
_ENTRY_KEYS: Final = frozenset(
{"catalog_id", "evidence_id", "signal", "lifecycle", "visual_evidence"}
)
LaboratoryValueSignal = Literal["progress", "retained", "failed"]
LaboratoryValueLifecycle = Literal["current", "legacy"]
LaboratoryVisualEvidence = Literal["available", "partial", "missing"]
class LaboratoryValueReviewRegistryError(ValueError):
"""Raised when reviewed LAB value metadata is unsafe or ambiguous."""
@dataclass(frozen=True, slots=True)
class LaboratoryValueReviewEntry:
catalog_id: str
evidence_id: str
signal: LaboratoryValueSignal
lifecycle: LaboratoryValueLifecycle
visual_evidence: LaboratoryVisualEvidence
def as_payload(self) -> dict[str, str]:
return {
"catalog_id": self.catalog_id,
"evidence_id": self.evidence_id,
"signal": self.signal,
"lifecycle": self.lifecycle,
"visual_evidence": self.visual_evidence,
"access": "read-only",
}
@dataclass(frozen=True, slots=True)
class LaboratoryValueReviewRegistry:
reviewed_at_utc: str
entries: tuple[LaboratoryValueReviewEntry, ...]
def __post_init__(self) -> None:
_text(self.reviewed_at_utc, "reviewed_at_utc", maximum=64)
if not isinstance(self.entries, tuple) or not all(
isinstance(entry, LaboratoryValueReviewEntry) for entry in self.entries
):
raise LaboratoryValueReviewRegistryError(
"LAB value-review entries must be an immutable tuple"
)
catalog_ids = [entry.catalog_id for entry in self.entries]
if len(catalog_ids) != len(set(catalog_ids)):
raise LaboratoryValueReviewRegistryError("duplicate LAB value-review catalog_id")
@classmethod
def from_file(cls, path: Path) -> LaboratoryValueReviewRegistry:
candidate = path.expanduser().absolute()
if candidate.is_symlink() or not candidate.is_file():
raise LaboratoryValueReviewRegistryError(
"LAB value-review registry must be a regular file"
)
if candidate.stat().st_size > _REGISTRY_MAX_BYTES:
raise LaboratoryValueReviewRegistryError("LAB value-review registry is too large")
try:
payload: object = json.loads(candidate.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as exc:
raise LaboratoryValueReviewRegistryError(
"LAB value-review registry is unreadable"
) from exc
document = _object(payload, "LAB value-review registry")
_exact_keys(document, _ROOT_KEYS, "LAB value-review registry")
if document["schema_version"] != LABORATORY_VALUE_REVIEW_REGISTRY_SCHEMA:
raise LaboratoryValueReviewRegistryError(
"LAB value-review registry schema is invalid"
)
entries = document["entries"]
if not isinstance(entries, list) or len(entries) > 128:
raise LaboratoryValueReviewRegistryError("LAB value-review entries are invalid")
return cls(
reviewed_at_utc=_text(document["reviewed_at_utc"], "reviewed_at_utc", maximum=64),
entries=tuple(_entry(item, index) for index, item in enumerate(entries)),
)
def as_payload(self) -> dict[str, object]:
return {
"schema_version": LABORATORY_VALUE_REVIEW_INDEX_SCHEMA,
"reviewed_at_utc": self.reviewed_at_utc,
"items": [entry.as_payload() for entry in self.entries],
"access": "read-only",
}
def _entry(value: object, index: int) -> LaboratoryValueReviewEntry:
document = _object(value, f"LAB value-review entry {index}")
_exact_keys(document, _ENTRY_KEYS, f"LAB value-review entry {index}")
catalog_id = _text(document["catalog_id"], "catalog_id", maximum=160)
evidence_id = _text(document["evidence_id"], "evidence_id", maximum=192)
if _CATALOG_ID.fullmatch(catalog_id) is None:
raise LaboratoryValueReviewRegistryError("LAB value-review catalog_id is invalid")
if _EVIDENCE_ID.fullmatch(evidence_id) is None:
raise LaboratoryValueReviewRegistryError("LAB value-review evidence_id is invalid")
signal = document["signal"]
lifecycle = document["lifecycle"]
visual_evidence = document["visual_evidence"]
if signal not in {"progress", "retained", "failed"}:
raise LaboratoryValueReviewRegistryError("LAB value-review signal is invalid")
if lifecycle not in {"current", "legacy"}:
raise LaboratoryValueReviewRegistryError("LAB value-review lifecycle is invalid")
if visual_evidence not in {"available", "partial", "missing"}:
raise LaboratoryValueReviewRegistryError("LAB value-review visual_evidence is invalid")
return LaboratoryValueReviewEntry(
catalog_id=catalog_id,
evidence_id=evidence_id,
signal=cast(LaboratoryValueSignal, signal),
lifecycle=cast(LaboratoryValueLifecycle, lifecycle),
visual_evidence=cast(LaboratoryVisualEvidence, visual_evidence),
)
def _object(value: object, label: str) -> dict[str, object]:
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
raise LaboratoryValueReviewRegistryError(f"{label} must be an object")
return value
def _exact_keys(document: dict[str, object], expected: frozenset[str], label: str) -> None:
actual = frozenset(document)
if actual != expected:
raise LaboratoryValueReviewRegistryError(
f"{label} keys are invalid; missing={sorted(expected - actual)}, "
f"unexpected={sorted(actual - expected)}"
)
def _text(value: object, label: str, *, maximum: int = 768) -> str:
if (
not isinstance(value, str)
or not value.strip()
or value != value.strip()
or len(value) > maximum
):
raise LaboratoryValueReviewRegistryError(
f"{label} must be a bounded trimmed string"
)
return value
+10
View File
@@ -57,6 +57,7 @@ from k1link.compute.e40_perception_product_gate import (
)
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
from k1link.web.l31_pointpillars_ravnoves_api import latest_l31_identity
from k1link.web.l32_pointpillars_camera_review_api import latest_l32_identity
from k1link.web.l33_camera_first_detector_review_api import latest_l33_identity
@@ -917,6 +918,15 @@ def build_advanced_laboratory_router(
"access": "read-only",
}
)
l31_identity = latest_l31_identity(l31_ravnoves_root_provider)
if l31_identity is not None:
items.append(
{
"work_id": "l31-pointpillars-ravnoves",
**l31_identity,
"access": "read-only",
}
)
l32_identity = latest_l32_identity(l32_camera_review_root_provider)
if l32_identity is not None:
items.append(
+19 -1
View File
@@ -23,7 +23,11 @@ from k1link.compute import (
RecordedPerceptionOverlayMux,
RecordedPerceptionOverlayStore,
)
from k1link.laboratory import LaboratoryEvidenceRegistry
from k1link.laboratory import (
LaboratoryEvidenceRegistry,
LaboratoryEvidenceReportService,
LaboratoryValueReviewRegistry,
)
from k1link.sessions import (
MaterializedRecording,
RecordedMediaInspector,
@@ -104,6 +108,7 @@ from k1link.web.l34e_self_review_diagnostic_api import (
)
from k1link.web.l34f_adjudication_api import build_l34f_adjudication_router
from k1link.web.laboratory_api import build_laboratory_router
from k1link.web.laboratory_report_api import build_laboratory_report_router
from k1link.web.lidar_api import build_lidar_router
from k1link.web.lidar_local_surface_service import K1LocalSurfaceReadService
from k1link.web.map_api import (
@@ -137,6 +142,13 @@ INVALID_REQUEST_DETAIL = "Некорректные параметры запро
LABORATORY_EVIDENCE_REGISTRY = LaboratoryEvidenceRegistry.from_directory(
REPOSITORY_ROOT / "config" / "laboratories"
)
LABORATORY_VALUE_REVIEW_REGISTRY = LaboratoryValueReviewRegistry.from_file(
REPOSITORY_ROOT / "config" / "laboratory-value-review.json"
)
LABORATORY_EVIDENCE_REPORTS = LaboratoryEvidenceReportService(
LABORATORY_EVIDENCE_REGISTRY,
lambda: REPOSITORY_ROOT / ".runtime" / "compute-experiments",
)
def _resolve_media_tool(name: str) -> Path | None:
@@ -559,6 +571,12 @@ app.include_router(
),
)
)
app.include_router(
build_laboratory_report_router(
LABORATORY_VALUE_REVIEW_REGISTRY,
LABORATORY_EVIDENCE_REPORTS,
)
)
app.include_router(
build_advanced_laboratory_router(
evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
+34
View File
@@ -0,0 +1,34 @@
from __future__ import annotations
from fastapi import APIRouter, HTTPException
from k1link.laboratory import (
LaboratoryEvidenceReportError,
LaboratoryEvidenceReportNotFound,
LaboratoryEvidenceReportService,
LaboratoryValueReviewRegistry,
)
def build_laboratory_report_router(
registry: LaboratoryValueReviewRegistry,
evidence_reports: LaboratoryEvidenceReportService | None = None,
) -> APIRouter:
router = APIRouter(prefix="/api/v1/laboratory", tags=["laboratory"])
@router.get("/value-review-index")
def get_value_review_index() -> dict[str, object]:
return registry.as_payload()
@router.get("/evidence-reports/{work_id}/{result_id}")
def get_evidence_report(work_id: str, result_id: str) -> dict[str, object]:
if evidence_reports is None:
raise HTTPException(status_code=404, detail="LAB evidence report is unavailable")
try:
return evidence_reports.read(work_id, result_id)
except LaboratoryEvidenceReportNotFound as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except LaboratoryEvidenceReportError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
return router