""" 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', '')}" ) 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 ''} → {new_data_id or ''}{reason_text}") import logging logger = logging.getLogger(__name__) logger.debug( f"[{self.node_type}] 节点 {self.node_id}: " f"data_id {old_data_id or ''} → {new_data_id or ''}{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 ''}" ) old_status = self.binding_status self.binding_status = new_status reason_text = reason or "" # 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}" )