deerflow-code/offline-backend-20260512/backend/app/report_collaboration/contracts/ids.py
2026-09-07 18:24:55 +08:00

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"))