first commit
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
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}"
|
||||
)
|
||||
Reference in New Issue
Block a user