225 lines
7.0 KiB
Python
225 lines
7.0 KiB
Python
"""Immutable lifecycle and result documents for the reference perception graph."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
from typing import Final
|
|
|
|
from .contracts import LocalObstacleMap, ThreatAssessment
|
|
|
|
GRAPH_RESULT_SCHEMA: Final = "missioncore.reference-perception-graph-result/v1"
|
|
GRAPH_RESULT_SCHEMA_V2: Final = "missioncore.reference-perception-graph-result/v2"
|
|
TERMINAL_OUTCOME_SCHEMA: Final = "missioncore.perception-terminal-outcome/v1"
|
|
REFERENCE_GRAPH_ID: Final = "reference-perception-graph/v1"
|
|
REFERENCE_GRAPH_ID_V2: Final = "reference-perception-graph/v2"
|
|
_SAFE_IDENTIFIER: Final = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,159}$")
|
|
|
|
|
|
class GraphExecutionError(RuntimeError):
|
|
"""The graph cannot preserve its bounded lifecycle or product accounting."""
|
|
|
|
|
|
class GraphState(StrEnum):
|
|
CREATED = "created"
|
|
STARTING = "starting"
|
|
RUNNING = "running"
|
|
STOPPING = "stopping"
|
|
STOPPED = "stopped"
|
|
CANCELLED = "cancelled"
|
|
FAILED = "failed"
|
|
|
|
|
|
class GraphRunMode(StrEnum):
|
|
"""Bounded queue behavior for realtime and deterministic replay."""
|
|
|
|
SOURCE_PACED_LATEST_WINS = "source-paced-latest-wins"
|
|
LOSSLESS_REPLAY = "lossless-replay"
|
|
|
|
|
|
class TerminalOutcomeType(StrEnum):
|
|
DELIVERED = "delivered"
|
|
SUPERSEDED = "superseded"
|
|
STALE = "stale"
|
|
REJECTED = "rejected"
|
|
FAILED = "failed"
|
|
UNAVAILABLE = "unavailable"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class TerminalOutcome:
|
|
source_id: str
|
|
session_id: str
|
|
frame_id: str
|
|
sequence: int
|
|
outcome: TerminalOutcomeType
|
|
stage_id: str
|
|
reason: str
|
|
|
|
def __post_init__(self) -> None:
|
|
for value in (self.source_id, self.session_id, self.frame_id, self.stage_id, self.reason):
|
|
if _SAFE_IDENTIFIER.fullmatch(value) is None:
|
|
raise GraphExecutionError("terminal outcome contains an unsafe identifier")
|
|
if self.sequence < 0:
|
|
raise GraphExecutionError("terminal sequence must be nonnegative")
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
return {
|
|
"schema_version": TERMINAL_OUTCOME_SCHEMA,
|
|
"source_id": self.source_id,
|
|
"session_id": self.session_id,
|
|
"frame_id": self.frame_id,
|
|
"sequence": self.sequence,
|
|
"outcome": self.outcome.value,
|
|
"stage_id": self.stage_id,
|
|
"reason": self.reason,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DeliveredFrame:
|
|
sequence: int
|
|
obstacle_map: LocalObstacleMap
|
|
threats: tuple[ThreatAssessment, ...]
|
|
|
|
def canonical_dict(self) -> dict[str, object]:
|
|
obstacle_map = self.obstacle_map.to_dict()
|
|
obstacle_map.pop("generated_monotonic_ns")
|
|
obstacle_map.pop("output_age_ns")
|
|
return {
|
|
"sequence": self.sequence,
|
|
"obstacle_map": obstacle_map,
|
|
"threats": [item.to_dict() for item in self.threats],
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GraphRunResult:
|
|
graph_id: str
|
|
source_profile_id: str
|
|
state: GraphState
|
|
admitted_count: int
|
|
terminal_outcomes: tuple[TerminalOutcome, ...]
|
|
deliveries: tuple[DeliveredFrame, ...]
|
|
canonical_payload_sha256: str
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
return {
|
|
"schema_version": GRAPH_RESULT_SCHEMA,
|
|
"graph_id": self.graph_id,
|
|
"source_profile_id": self.source_profile_id,
|
|
"state": self.state.value,
|
|
"admitted_count": self.admitted_count,
|
|
"terminal_outcomes": [item.to_dict() for item in self.terminal_outcomes],
|
|
"deliveries": [item.canonical_dict() for item in self.deliveries],
|
|
"canonical_payload_sha256": self.canonical_payload_sha256,
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class GraphRunResultV2:
|
|
graph_id: str
|
|
source_profile_id: str
|
|
run_mode: GraphRunMode
|
|
state: GraphState
|
|
admitted_count: int
|
|
terminal_outcomes: tuple[TerminalOutcome, ...]
|
|
deliveries: tuple[DeliveredFrame, ...]
|
|
queue_high_watermarks: tuple[tuple[str, int], ...]
|
|
canonical_payload_sha256: str
|
|
|
|
def to_dict(self) -> dict[str, object]:
|
|
return {
|
|
"schema_version": GRAPH_RESULT_SCHEMA_V2,
|
|
"graph_id": self.graph_id,
|
|
"source_profile_id": self.source_profile_id,
|
|
"run_mode": self.run_mode.value,
|
|
"state": self.state.value,
|
|
"admitted_count": self.admitted_count,
|
|
"terminal_outcomes": [item.to_dict() for item in self.terminal_outcomes],
|
|
"deliveries": [item.canonical_dict() for item in self.deliveries],
|
|
"queue_high_watermarks": dict(self.queue_high_watermarks),
|
|
"canonical_payload_sha256": self.canonical_payload_sha256,
|
|
}
|
|
|
|
|
|
def build_graph_run_result(
|
|
*,
|
|
graph_id: str,
|
|
source_profile_id: str,
|
|
state: GraphState,
|
|
admitted_count: int,
|
|
outcomes: tuple[TerminalOutcome, ...],
|
|
deliveries: tuple[DeliveredFrame, ...],
|
|
) -> GraphRunResult:
|
|
canonical = {
|
|
"graph_id": graph_id,
|
|
"source_profile_id": source_profile_id,
|
|
"terminal_outcomes": [item.to_dict() for item in outcomes],
|
|
"deliveries": [item.canonical_dict() for item in deliveries],
|
|
}
|
|
payload = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode()
|
|
return GraphRunResult(
|
|
graph_id=graph_id,
|
|
source_profile_id=source_profile_id,
|
|
state=state,
|
|
admitted_count=admitted_count,
|
|
terminal_outcomes=outcomes,
|
|
deliveries=deliveries,
|
|
canonical_payload_sha256=hashlib.sha256(payload).hexdigest(),
|
|
)
|
|
|
|
|
|
def build_graph_run_result_v2(
|
|
*,
|
|
graph_id: str,
|
|
source_profile_id: str,
|
|
run_mode: GraphRunMode,
|
|
state: GraphState,
|
|
admitted_count: int,
|
|
outcomes: tuple[TerminalOutcome, ...],
|
|
deliveries: tuple[DeliveredFrame, ...],
|
|
queue_high_watermarks: tuple[tuple[str, int], ...],
|
|
) -> GraphRunResultV2:
|
|
canonical = {
|
|
"graph_id": graph_id,
|
|
"source_profile_id": source_profile_id,
|
|
"run_mode": run_mode.value,
|
|
"terminal_outcomes": [item.to_dict() for item in outcomes],
|
|
"deliveries": [item.canonical_dict() for item in deliveries],
|
|
}
|
|
payload = json.dumps(canonical, sort_keys=True, separators=(",", ":")).encode()
|
|
return GraphRunResultV2(
|
|
graph_id=graph_id,
|
|
source_profile_id=source_profile_id,
|
|
run_mode=run_mode,
|
|
state=state,
|
|
admitted_count=admitted_count,
|
|
terminal_outcomes=outcomes,
|
|
deliveries=deliveries,
|
|
queue_high_watermarks=queue_high_watermarks,
|
|
canonical_payload_sha256=hashlib.sha256(payload).hexdigest(),
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"GRAPH_RESULT_SCHEMA",
|
|
"GRAPH_RESULT_SCHEMA_V2",
|
|
"REFERENCE_GRAPH_ID",
|
|
"REFERENCE_GRAPH_ID_V2",
|
|
"TERMINAL_OUTCOME_SCHEMA",
|
|
"DeliveredFrame",
|
|
"GraphExecutionError",
|
|
"GraphRunResult",
|
|
"GraphRunResultV2",
|
|
"GraphRunMode",
|
|
"GraphState",
|
|
"TerminalOutcome",
|
|
"TerminalOutcomeType",
|
|
"build_graph_run_result",
|
|
"build_graph_run_result_v2",
|
|
]
|