436 lines
15 KiB
Python
436 lines
15 KiB
Python
"""
|
||
SyncNode - 核心状态机容器
|
||
|
||
将data存储为Dict,但支持schema验证和Pydantic模型转换。
|
||
提供统一的数据访问接口:get_data() / put_data()
|
||
"""
|
||
|
||
import copy
|
||
from contextlib import contextmanager
|
||
from typing import TypeVar, Generic, Optional, Any, Dict, List, Type
|
||
from pydantic import BaseModel, ValidationError
|
||
|
||
from .types import SyncAction, SyncStatus, BindingStatus
|
||
|
||
T = TypeVar("T", bound=BaseModel)
|
||
|
||
|
||
class SyncNode(Generic[T]):
|
||
"""
|
||
核心状态机容器
|
||
|
||
数据存储策略:
|
||
- origin_data/data 内部存储为 Dict
|
||
- 通过 get_data() 获取数据副本(深拷贝)
|
||
- 通过 set_data() 设置数据(自动验证+深拷贝)
|
||
- 通过 to_model() 转换为 Pydantic 模型实例
|
||
|
||
基类 SyncNode 不应直接实例化,应通过子类使用。
|
||
子类应设置类变量 node_type 和 schema:
|
||
```python
|
||
class ContractSyncNode(SyncNode[ContractResponse]):
|
||
node_type = "contract"
|
||
schema = ContractResponse
|
||
```
|
||
"""
|
||
|
||
# 类变量:子类必须设置
|
||
node_type: str = None # type: ignore
|
||
schema: Type[T] = None # type: ignore
|
||
_CORE_FIELDS = {"binding_status", "action", "status", "data_id"}
|
||
|
||
def __setattr__(self, name: str, value: Any) -> None:
|
||
if (
|
||
name in self._CORE_FIELDS
|
||
and self.__dict__.get("_core_guard_enabled", False)
|
||
and self.__dict__.get("_core_write_depth", 0) <= 0
|
||
):
|
||
raise RuntimeError(
|
||
f"Core field '{name}' can only be modified via event apply path for node={self.__dict__.get('node_id', '<uninitialized>')}"
|
||
)
|
||
object.__setattr__(self, name, value)
|
||
|
||
@contextmanager
|
||
def allow_core_state_write(self, source: str = "unknown"):
|
||
depth = self.__dict__.get("_core_write_depth", 0)
|
||
prev_source = self.__dict__.get("_core_write_source", "")
|
||
object.__setattr__(self, "_core_write_depth", depth + 1)
|
||
object.__setattr__(self, "_core_write_source", source)
|
||
try:
|
||
yield
|
||
finally:
|
||
object.__setattr__(self, "_core_write_depth", depth)
|
||
object.__setattr__(self, "_core_write_source", prev_source)
|
||
|
||
def __init__(
|
||
self,
|
||
node_id: str,
|
||
data_id: str = "",
|
||
depend_ids: Optional[List[str]] = None,
|
||
data: Optional[Dict[str, Any]] = None,
|
||
origin_data: Optional[Dict[str, Any]] = None,
|
||
action: SyncAction = SyncAction.NONE,
|
||
status: SyncStatus = SyncStatus.PENDING,
|
||
binding_status: BindingStatus = BindingStatus.UNCHECKED,
|
||
error: Optional[str] = None,
|
||
context: Optional[Dict[str, Any]] = None,
|
||
sync_log: Optional[str] = None
|
||
):
|
||
object.__setattr__(self, "_core_guard_enabled", False)
|
||
object.__setattr__(self, "_core_write_depth", 0)
|
||
|
||
# 检查类变量是否设置
|
||
if self.node_type is None:
|
||
raise ValueError(
|
||
f"{self.__class__.__name__} must define class variable 'node_type'. "
|
||
f"Example: node_type = 'contract'"
|
||
)
|
||
if self.schema is None:
|
||
raise ValueError(
|
||
f"{self.__class__.__name__} must define class variable 'schema'. "
|
||
f"Example: schema = YourSchema"
|
||
)
|
||
|
||
self.node_id = node_id
|
||
self.data_id = data_id
|
||
self.depend_ids = depend_ids if depend_ids is not None else []
|
||
|
||
if origin_data is not None:
|
||
self.origin_data = copy.deepcopy(origin_data)
|
||
else:
|
||
self.origin_data = None
|
||
self.action = action
|
||
self.status = status
|
||
self.binding_status = binding_status
|
||
self.error = error
|
||
self.sync_log = sync_log # Debug/info级别的日志信息
|
||
|
||
# Context 字典:存储额外的上下文信息(如 project_id, contract_id 等)
|
||
self.context: Dict[str, Any] = context if context is not None else {}
|
||
|
||
# 在初始化时验证并设置 data
|
||
if data is not None:
|
||
self._validate_and_set_data(data)
|
||
if self.origin_data is None:
|
||
self.origin_data = copy.deepcopy(self.data)
|
||
else:
|
||
self.data = None
|
||
|
||
object.__setattr__(self, "_core_guard_enabled", True)
|
||
|
||
def _validate_and_set_data(self, data: Dict[str, Any]) -> None:
|
||
"""验证并设置数据(内部方法)"""
|
||
if not isinstance(data, dict):
|
||
self.data = None
|
||
raise ValueError(f"Data validation failed for {self.node_type}: data must be a dict")
|
||
|
||
try:
|
||
# 基础兼容性:将 _id 转换为 id
|
||
data_to_validate = data.copy()
|
||
if "id" not in data_to_validate and "_id" in data_to_validate:
|
||
data_to_validate["id"] = data_to_validate["_id"]
|
||
|
||
model = self.schema.model_validate(data_to_validate)
|
||
# 使用 model_dump() 获得完整校验后的数据,去掉 schema 之外的字段
|
||
self.data = model.model_dump()
|
||
except Exception as e:
|
||
# 如果出错就存 None, 然后 error 里加个错
|
||
self.data = None
|
||
self.error = f"Validation failed: {str(e)}"
|
||
# 不要轻易降级错误。如果校验失败,应抛出 ValueError 供上层处理
|
||
raise ValueError(f"Data validation failed for {self.node_type}: {e}")
|
||
|
||
# ===== 数据访问方法 =====
|
||
|
||
def get_data(self, exclude_unset: bool = False) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
获取数据的副本。
|
||
|
||
Args:
|
||
exclude_unset: 是否排除未设置(默认值)的字段。
|
||
|
||
Returns:
|
||
数据字典的副本,如果为None则返回None
|
||
"""
|
||
if self.data is None:
|
||
return None
|
||
|
||
if not exclude_unset:
|
||
return copy.deepcopy(self.data)
|
||
|
||
try:
|
||
model = self.schema.model_validate(self.data)
|
||
return model.model_dump(exclude_unset=True)
|
||
except Exception:
|
||
return copy.deepcopy(self.data)
|
||
|
||
def set_data(self, data: Optional[Dict[str, Any]], validate: bool = True) -> None:
|
||
"""
|
||
设置数据(自动深拷贝)。
|
||
|
||
Args:
|
||
data: 要设置的数据字典
|
||
validate: 是否进行schema验证(默认True)
|
||
|
||
Raises:
|
||
ValueError: 如果数据验证失败
|
||
"""
|
||
if data is None:
|
||
self.data = None
|
||
return
|
||
|
||
if validate:
|
||
self._validate_and_set_data(data)
|
||
else:
|
||
self.data = copy.deepcopy(data)
|
||
|
||
def get_origin_data(self) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
获取原始数据的深拷贝副本。
|
||
|
||
Returns:
|
||
原始数据字典的副本,如果为None则返回None
|
||
"""
|
||
if self.origin_data is None:
|
||
return None
|
||
return copy.deepcopy(self.origin_data)
|
||
|
||
def set_origin_data(self, data: Optional[Dict[str, Any]]) -> None:
|
||
"""
|
||
设置原始数据(自动深拷贝)。
|
||
|
||
Args:
|
||
data: 要设置的原始数据字典
|
||
"""
|
||
if data is None:
|
||
self.origin_data = None
|
||
return
|
||
|
||
if self.schema is None:
|
||
raise ValueError(f"Cannot set origin_data without schema for {self.node_type}")
|
||
|
||
try:
|
||
model = self.schema.model_validate(copy.deepcopy(data))
|
||
# 使用 exclude_unset=True 确保 origin_data 也只记录实际提供/返回的字段
|
||
self.origin_data = model.model_dump(exclude_unset=True)
|
||
except AttributeError as e:
|
||
raise ValueError(
|
||
f"Origin data validation failed for {self.node_type}: model_dump not available"
|
||
) from e
|
||
except (ValidationError, TypeError, ValueError) as e:
|
||
raise ValueError(f"Origin data validation failed for {self.node_type}: {e}") from e
|
||
|
||
# ===== Schema 相关方法 =====
|
||
|
||
def get_schema(self) -> Optional[Type[T]]:
|
||
"""
|
||
获取数据的 Schema 类型。
|
||
|
||
Returns:
|
||
Pydantic Model 类型,如果未设置则返回 None
|
||
"""
|
||
return self.schema
|
||
|
||
def set_schema(self, schema: Type[T]) -> None:
|
||
"""
|
||
设置数据的 Schema 类型。
|
||
|
||
Args:
|
||
schema: Pydantic Model 类型
|
||
"""
|
||
self.schema = schema
|
||
|
||
def validate(self) -> bool:
|
||
"""
|
||
验证当前数据是否符合 schema。
|
||
|
||
Returns:
|
||
True 如果验证通过或无 schema,False 如果验证失败
|
||
"""
|
||
if self.schema is None or self.data is None:
|
||
return True
|
||
|
||
try:
|
||
self.schema.model_validate(self.data)
|
||
return True
|
||
except ValidationError:
|
||
return False
|
||
|
||
def to_model(self) -> Optional[T]:
|
||
"""
|
||
将数据转换为 Pydantic 模型实例。
|
||
|
||
Returns:
|
||
模型实例,如果无数据或无schema则返回None
|
||
|
||
Raises:
|
||
ValidationError: 如果数据验证失败
|
||
"""
|
||
if self.schema is None or self.data is None:
|
||
return None
|
||
|
||
return self.schema.model_validate(self.data)
|
||
|
||
def from_model(self, model: T) -> None:
|
||
"""
|
||
从 Pydantic 模型实例设置数据。
|
||
|
||
Args:
|
||
model: Pydantic 模型实例
|
||
"""
|
||
try:
|
||
self.data = model.model_dump()
|
||
except AttributeError as e:
|
||
raise ValueError(
|
||
f"Cannot convert model to dict: model_dump not available ({type(model)})"
|
||
) from e
|
||
|
||
# 自动设置schema
|
||
if self.schema is None:
|
||
self.schema = type(model)
|
||
|
||
# ===== 辅助方法 =====
|
||
|
||
def get_field(self, field_name: str, default: Any = None) -> Any:
|
||
"""
|
||
从data中获取指定字段的值。
|
||
|
||
Args:
|
||
field_name: 字段名
|
||
default: 默认值
|
||
|
||
Returns:
|
||
字段值,如果不存在则返回default
|
||
"""
|
||
if self.data is None:
|
||
return default
|
||
return self.data.get(field_name, default)
|
||
|
||
def set_field(self, field_name: str, value: Any) -> None:
|
||
"""
|
||
设置data中的指定字段。
|
||
|
||
Args:
|
||
field_name: 字段名
|
||
value: 字段值
|
||
"""
|
||
if self.data is None:
|
||
self.data = {}
|
||
self.data[field_name] = value
|
||
|
||
def has_field(self, field_name: str) -> bool:
|
||
"""
|
||
检查data中是否存在指定字段。
|
||
|
||
Args:
|
||
field_name: 字段名
|
||
|
||
Returns:
|
||
True 如果字段存在
|
||
"""
|
||
if self.data is None:
|
||
return False
|
||
return field_name in self.data
|
||
|
||
# ===== 状态管理方法(带日志记录) =====
|
||
|
||
def append_log(self, message: str) -> None:
|
||
"""追加日志到 sync_log(中文)"""
|
||
import time
|
||
from datetime import datetime
|
||
timestamp = datetime.fromtimestamp(time.time()).strftime("%H:%M:%S")
|
||
log_entry = f"[{timestamp}] {message}"
|
||
|
||
if self.sync_log:
|
||
self.sync_log += "\n" + log_entry
|
||
else:
|
||
self.sync_log = log_entry
|
||
|
||
def set_status(self, new_status: SyncStatus, reason: str = "") -> None:
|
||
"""
|
||
设置status并更新时间戳和日志(如果状态改变)
|
||
|
||
Args:
|
||
new_status: 新的执行状态
|
||
reason: 状态变化原因(中文)
|
||
"""
|
||
if self.status != new_status:
|
||
old_status = self.status
|
||
self.status = new_status
|
||
reason_text = f" - {reason}" if reason else ""
|
||
|
||
# DEBUG 日志
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.debug(
|
||
f"[{self.node_type}] 节点 {self.node_id}: "
|
||
f"执行状态 {old_status.value} → {new_status.value}{reason_text}"
|
||
)
|
||
|
||
def set_data_id(self, new_data_id: str, reason: str = "") -> None:
|
||
"""
|
||
设置 data_id(仅允许在 event apply 路径内调用)
|
||
"""
|
||
if self.data_id != new_data_id:
|
||
old_data_id = self.data_id
|
||
self.data_id = new_data_id
|
||
|
||
reason_text = f" - {reason}" if reason else ""
|
||
self.append_log(f"数据ID: {old_data_id or '<empty>'} → {new_data_id or '<empty>'}{reason_text}")
|
||
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.debug(
|
||
f"[{self.node_type}] 节点 {self.node_id}: "
|
||
f"data_id {old_data_id or '<empty>'} → {new_data_id or '<empty>'}{reason_text}"
|
||
)
|
||
|
||
def set_binding_status(self, new_status: BindingStatus, reason: str = "") -> None:
|
||
"""
|
||
设置binding_status并更新时间戳和日志(如果状态改变)
|
||
|
||
Args:
|
||
new_status: 新的绑定状态
|
||
reason: 状态变化原因(中文)
|
||
"""
|
||
if self.binding_status != new_status:
|
||
if self.__dict__.get("_core_guard_enabled", False):
|
||
write_source = self.__dict__.get("_core_write_source", "")
|
||
if write_source != "state_machine":
|
||
raise RuntimeError(
|
||
f"binding_status update must go through state machine for node={self.node_id}, source={write_source or '<none>'}"
|
||
)
|
||
old_status = self.binding_status
|
||
self.binding_status = new_status
|
||
|
||
reason_text = reason or "<no-reason>"
|
||
|
||
# DEBUG 日志
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.debug(
|
||
f"[{self.node_type}] 节点 {self.node_id}: "
|
||
f"绑定状态 {old_status.value} → {new_status.value} - {reason_text}"
|
||
)
|
||
|
||
def set_action(self, new_action: SyncAction, reason: str = "") -> None:
|
||
"""
|
||
设置action并更新时间戳和日志(如果动作改变)
|
||
|
||
Args:
|
||
new_action: 新的同步动作
|
||
reason: 动作变化原因(中文)
|
||
"""
|
||
if self.action != new_action:
|
||
old_action = self.action
|
||
self.action = new_action
|
||
reason_text = f" - {reason}" if reason else ""
|
||
|
||
# DEBUG 日志
|
||
import logging
|
||
logger = logging.getLogger(__name__)
|
||
logger.debug(
|
||
f"[{self.node_type}] 节点 {self.node_id}: "
|
||
f"同步动作 {old_action.value} → {new_action.value}{reason_text}"
|
||
)
|