first commit

This commit is contained in:
strepsiades
2026-03-09 16:31:42 +08:00
commit 4a9c444b10
486 changed files with 52479 additions and 0 deletions
+435
View File
@@ -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}"
)