Files
NODEDC_MISSION_CORE/src/k1link/sessions/models.py
T

289 lines
8.2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal
SessionStatus = Literal["ready", "interrupted", "failed"]
SessionModality = Literal["point-cloud", "trajectory", "video"]
class SessionStoreError(RuntimeError):
"""Base error for the host-owned observation session store."""
class SessionNotFoundError(SessionStoreError):
"""The requested opaque session identifier is not registered."""
class SessionNotReplayableError(SessionStoreError):
"""The session has no reviewed replay source."""
class SessionIntegrityError(SessionStoreError):
"""A stored artifact no longer satisfies its confinement/integrity boundary."""
class LayoutConflictError(SessionStoreError):
"""A workspace layout revision changed since the caller loaded it."""
@dataclass(frozen=True, slots=True)
class LabSessionBinding:
"""Immutable provenance for one derived laboratory replay."""
session_id: str
source_session_id: str
lab_id: str
result_kind: str
result_id: str
source_result_id: str | None
config_sha256: str | None
run_created_at_utc: str
published_at_utc: str
provenance: dict[str, Any]
def as_dict(self) -> dict[str, Any]:
return {
"lab_id": self.lab_id,
"source_session_id": self.source_session_id,
"result_kind": self.result_kind,
"result_id": self.result_id,
"source_result_id": self.source_result_id,
"config_sha256": self.config_sha256,
"run_created_at_utc": self.run_created_at_utc,
"published_at_utc": self.published_at_utc,
"provenance": self.provenance,
}
@dataclass(frozen=True, slots=True)
class SessionSource:
source_id: str
semantic_channel_id: str
modality: SessionModality
status: str
seekable: bool
artifact_id: str
def as_dict(self) -> dict[str, Any]:
return {
"source_id": self.source_id,
"semantic_channel_id": self.semantic_channel_id,
"modality": self.modality,
"status": self.status,
"seekable": self.seekable,
"artifact_id": self.artifact_id,
}
@dataclass(frozen=True, slots=True)
class SessionArtifact:
artifact_id: str
kind: str
media_type: str
byte_length: int
sha256: str | None
integrity_status: str
def as_dict(self) -> dict[str, Any]:
return {
"artifact_id": self.artifact_id,
"kind": self.kind,
"media_type": self.media_type,
"byte_length": self.byte_length,
"sha256": self.sha256,
"integrity_status": self.integrity_status,
}
@dataclass(frozen=True, slots=True)
class SessionSummary:
session_id: str
display_name: str
status: SessionStatus
started_at_utc: str | None
completed_at_utc: str | None
duration_seconds: float | None
modalities: tuple[SessionModality, ...]
source_count: int
total_bytes: int
replayable: bool
origin: str
lab: LabSessionBinding | None = None
def as_dict(self) -> dict[str, Any]:
document: dict[str, Any] = {
"schema_version": "missioncore.observation-session-summary/v1",
"session_id": self.session_id,
"display_name": self.display_name,
"status": self.status,
"started_at_utc": self.started_at_utc,
"completed_at_utc": self.completed_at_utc,
"duration_seconds": self.duration_seconds,
"modalities": list(self.modalities),
"source_count": self.source_count,
"total_bytes": self.total_bytes,
"replayable": self.replayable,
"origin": self.origin,
}
if self.lab is not None:
document["lab"] = self.lab.as_dict()
return document
@dataclass(frozen=True, slots=True)
class SessionDetail:
summary: SessionSummary
sources: tuple[SessionSource, ...]
artifacts: tuple[SessionArtifact, ...]
def as_dict(self) -> dict[str, Any]:
duration = self.summary.duration_seconds
return {
"schema_version": "missioncore.observation-session/v1",
**{
key: value
for key, value in self.summary.as_dict().items()
if key != "schema_version"
},
"timeline": {
"mode": "recorded" if self.summary.replayable else "unavailable",
"seekable": self.summary.replayable,
"start_seconds": 0.0 if self.summary.replayable else None,
"end_seconds": duration if self.summary.replayable else None,
"synchronization": "host-arrival-best-effort",
},
"sources": [source.as_dict() for source in self.sources],
"artifacts": [artifact.as_dict() for artifact in self.artifacts],
}
@dataclass(frozen=True, slots=True)
class SessionPage:
items: tuple[SessionSummary, ...]
next_cursor: str | None
def as_dict(self) -> dict[str, Any]:
return {
"schema_version": "missioncore.observation-session-list/v1",
"items": [item.as_dict() for item in self.items],
"next_cursor": self.next_cursor,
}
@dataclass(frozen=True, slots=True)
class WorkspaceLayout:
workspace_id: str
schema_version: int
revision: int
name: str
layout: dict[str, Any]
updated_at_utc: str
def as_dict(self) -> dict[str, Any]:
return {
"schema_version": "missioncore.workspace-layout/v1",
"workspace_id": self.workspace_id,
"layout_schema_version": self.schema_version,
"revision": self.revision,
"name": self.name,
"layout": self.layout,
"updated_at_utc": self.updated_at_utc,
}
@dataclass(frozen=True, slots=True)
class ReplayArtifact:
"""One confined input artifact selected for plugin-owned preparation."""
artifact_id: str
path: Path
media_type: str
file_byte_length: int
replay_byte_length: int
expected_sha256: str | None
@dataclass(frozen=True, slots=True)
class ReplayCommand:
"""Internal replay request containing no vendor format or channel names."""
session_id: str
plugin_id: str
allowed_root: Path
session_root: Path
primary_artifact_id: str
artifacts: tuple[ReplayArtifact, ...]
timeline_origin_epoch_ns: int
timeline_origin_monotonic_ns: int
speed: float
loop: bool
@property
def primary_artifact(self) -> ReplayArtifact:
matches = tuple(
artifact
for artifact in self.artifacts
if artifact.artifact_id == self.primary_artifact_id
)
if len(matches) != 1:
raise SessionIntegrityError("replay command has no unique primary artifact")
return matches[0]
@dataclass(frozen=True, slots=True)
class RecordedMediaArtifact:
"""Internal handle for one confined archived camera source.
The physical source id and filesystem locator never enter the public API.
``public_source_id`` and ``artifact_id`` are opaque catalog identifiers.
"""
session_id: str
public_source_id: str
artifact_id: str
source_path: Path
byte_length: int
@dataclass(frozen=True, slots=True)
class ObservationArtifactCandidate:
artifact_id: str
kind: str
media_type: str
locator: Path
byte_length: int
replay_byte_length: int
sha256: str | None
integrity_status: str
@dataclass(frozen=True, slots=True)
class ObservationSessionCandidate:
session_id: str
display_name: str
status: SessionStatus
started_at_utc: str | None
completed_at_utc: str | None
duration_seconds: float | None
modalities: tuple[SessionModality, ...]
replayable: bool
total_bytes: int
allowed_root: Path
session_root: Path
primary_replay_artifact_id: str | None
timeline_origin_epoch_ns: int | None
timeline_origin_monotonic_ns: int | None
sources: tuple[SessionSource, ...]
artifacts: tuple[ObservationArtifactCandidate, ...]
@dataclass(frozen=True, slots=True)
class LegacyMediaSourceCandidate:
source_id: str
artifact_id: str
locator: Path
byte_length: int
epoch_count: int