222 lines
6.3 KiB
Python
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
|