119 lines
4.1 KiB
Python
119 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
from enum import Enum
|
|
from typing import Any, Optional, Set, Type, TypeVar, cast
|
|
|
|
from ..common.sync_node import SyncNode
|
|
from ..common.types import BindingStatus, SyncAction, SyncStatus
|
|
from .dispatcher import StateMachineRuntime
|
|
|
|
E = TypeVar("E", bound=Enum)
|
|
|
|
|
|
def _state_def(runtime: StateMachineRuntime, state_id: str) -> dict[str, Any]:
|
|
states = runtime._config.raw.get("states")
|
|
if not isinstance(states, dict):
|
|
raise RuntimeError("invalid state-machine config: missing states")
|
|
state_def = states.get(state_id)
|
|
if not isinstance(state_def, dict):
|
|
raise RuntimeError(f"unknown state in config: {state_id}")
|
|
return state_def
|
|
|
|
|
|
def _to_enum(enum_cls: Type[E], raw_value: Any, field_name: str, state_id: str) -> E:
|
|
if not isinstance(raw_value, str):
|
|
raise RuntimeError(
|
|
f"invalid {field_name} for {state_id}: expected str/list[str], got {type(raw_value).__name__}"
|
|
)
|
|
try:
|
|
return cast(E, enum_cls(raw_value))
|
|
except Exception:
|
|
raw_lower = str(raw_value).lower()
|
|
for member in enum_cls:
|
|
if str(member.value).lower() == raw_lower or str(member.name).lower() == raw_lower:
|
|
return cast(E, member)
|
|
raise RuntimeError(f"invalid {field_name} value for {state_id}: {raw_value}")
|
|
|
|
|
|
def _resolve_enum_value(
|
|
*,
|
|
state_id: str,
|
|
field_name: str,
|
|
raw_value: Any,
|
|
enum_cls: Type[E],
|
|
current_value: E,
|
|
) -> E:
|
|
if isinstance(raw_value, str):
|
|
return _to_enum(enum_cls, raw_value, field_name, state_id)
|
|
|
|
if isinstance(raw_value, list):
|
|
candidates = [_to_enum(enum_cls, v, field_name, state_id) for v in raw_value]
|
|
if not candidates:
|
|
raise RuntimeError(f"empty {field_name} candidates for {state_id}")
|
|
if current_value in candidates:
|
|
return current_value
|
|
if len(candidates) == 1:
|
|
return candidates[0]
|
|
raise RuntimeError(
|
|
f"ambiguous {field_name} for {state_id}: candidates={[c.value for c in candidates]}, "
|
|
f"current={current_value.value}"
|
|
)
|
|
|
|
raise RuntimeError(
|
|
f"invalid {field_name} for {state_id}: expected str/list[str], got {type(raw_value).__name__}"
|
|
)
|
|
|
|
|
|
def apply_state_to_node(
|
|
runtime: StateMachineRuntime,
|
|
node: SyncNode,
|
|
*,
|
|
state_id: Optional[str],
|
|
reason: str = "",
|
|
fields: Optional[Set[str]] = None,
|
|
data_id: Optional[str] = None,
|
|
) -> bool:
|
|
if not state_id:
|
|
return False
|
|
|
|
state_def = _state_def(runtime, state_id)
|
|
selected_fields = fields or {"binding_status", "action", "status"}
|
|
|
|
with node.allow_core_state_write(source="state_machine"):
|
|
if "binding_status" in selected_fields and "binding_status" in state_def:
|
|
target_binding = _resolve_enum_value(
|
|
state_id=state_id,
|
|
field_name="binding_status",
|
|
raw_value=state_def["binding_status"],
|
|
enum_cls=BindingStatus,
|
|
current_value=node.binding_status,
|
|
)
|
|
node.set_binding_status(target_binding, reason)
|
|
|
|
if "action" in selected_fields and "action" in state_def:
|
|
target_action = _resolve_enum_value(
|
|
state_id=state_id,
|
|
field_name="action",
|
|
raw_value=state_def["action"],
|
|
enum_cls=SyncAction,
|
|
current_value=node.action,
|
|
)
|
|
node.set_action(target_action, reason)
|
|
|
|
if "status" in selected_fields and "status" in state_def:
|
|
target_status = _resolve_enum_value(
|
|
state_id=state_id,
|
|
field_name="status",
|
|
raw_value=state_def["status"],
|
|
enum_cls=SyncStatus,
|
|
current_value=node.status,
|
|
)
|
|
node.set_status(target_status, reason)
|
|
|
|
if "data_id" in selected_fields:
|
|
if data_id is None:
|
|
raise RuntimeError(f"state {state_id} requested data_id update but no data_id provided")
|
|
node.set_data_id(data_id, reason)
|
|
|
|
return True
|