39 lines
1.4 KiB
Python
39 lines
1.4 KiB
Python
"""Stable identifiers shared with the frontend canvas and SSE payloads."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
_NODE_RUN_ID_RE = re.compile(r"^(?P<scope>.+)-attempt(?P<attempt>\d+)$")
|
|
|
|
|
|
def make_node_run_id(run_id: str, node_id: str, attempt: int) -> str:
|
|
"""Return ``${run_id}-${node_id}-attempt{attempt}``. Random UUIDs are forbidden.
|
|
|
|
The run dimension is part of the id because the node-runs table primary
|
|
key is ``node_run_id`` alone: without it, two runs reusing the same plan
|
|
template would collide on ``${node_id}-attempt{n}``.
|
|
"""
|
|
|
|
if not run_id or run_id.strip() != run_id:
|
|
raise ValueError("run_id must be a non-empty trimmed string")
|
|
if not node_id or node_id.strip() != node_id:
|
|
raise ValueError("node_id must be a non-empty trimmed string")
|
|
if attempt < 1:
|
|
raise ValueError("attempt must be >= 1")
|
|
return f"{run_id}-{node_id}-attempt{attempt}"
|
|
|
|
|
|
def parse_node_run_id(node_run_id: str) -> tuple[str, int]:
|
|
"""Split a canonical node_run_id tail back into ``(..., attempt)``.
|
|
|
|
The returned first element is the run-scoped node portion
|
|
(``${run_id}-${node_id}``); callers that need the bare node id should
|
|
strip the known run prefix themselves.
|
|
"""
|
|
|
|
match = _NODE_RUN_ID_RE.match(node_run_id or "")
|
|
if match is None:
|
|
raise ValueError(f"invalid node_run_id: {node_run_id!r}")
|
|
return match.group("scope"), int(match.group("attempt"))
|