NODEDC_MISSION_CORE/src/k1link/sessions/models.py

222 lines
6.3 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 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
def as_dict(self) -> dict[str, Any]:
return {
"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,
}
@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 ReplayCommand:
"""Internal-only replay command. ``source_path`` never enters an API DTO."""
session_id: str
source_path: Path
allowed_root: Path
session_root: Path
replay_byte_length: int
metadata_byte_length: int
expected_source_sha256: str | None
speed: float
loop: bool
@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 LegacySessionCandidate:
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
raw_path: Path
raw_byte_length: int
replay_raw_byte_length: int
replay_metadata_byte_length: int
raw_sha256: str | None
raw_integrity_status: str
media_sources: tuple[LegacyMediaSourceCandidate, ...]
@dataclass(frozen=True, slots=True)
class LegacyMediaSourceCandidate:
source_id: str
artifact_id: str
locator: Path
byte_length: int
epoch_count: int