from abc import ABC, abstractmethod from typing import TypeVar, Generic, Type, Dict, List, Optional, Any from pydantic import BaseModel from pathlib import Path from ..common.collection import DataCollection from ..common.binding import BindingManager from ..common.sync_node import SyncNode from ..logging import get_logger from .config import StrategyConfig, OrphanAction, UpdateDirection from ..config.strategy_config import resolve_domain_option_config from .strategy_ops import ( run_delete, ) from .strategy_ops.bind_ops import ( apply_auto_bind_updates, collect_auto_bind_ready_nodes, collect_bind_entry_nodes, plan_auto_bind_updates, phase_core_state, phase_dependency_check, refresh_bind_data_id, ) from .strategy_ops.create_ops import add_create_action from .strategy_ops.update_ops import ( BoundNodePair, _build_schema_diff_validator, collect_bound_node_pairs, default_get_node_update_payload, default_needs_update, default_update_pair, emit_schema_diff_report, prepare_directional_update, ) from ..engine import ( StateMachineConfig, StateMachineRuntime, ) from .strategy_ops.compare_ops import build_data_id_normalization_map, normalized_data_for_compare T = TypeVar("T", bound=BaseModel) logger = get_logger(__name__) class BaseSyncStrategy(ABC, Generic[T]): """ 同步策略抽象接口。 子类可以通过覆盖以下类变量提供静态配置: ```python class ProjectStrategy(DefaultSyncStrategy): default_config = StrategyConfig( auto_bind_fields=["name", "code"], local_orphan_action=OrphanAction.CREATE_REMOTE ) schema = ProjectResponse ``` 跳过同步控制: skip_sync: bool - 如果为 True,则跳过该类型的 bind/create/update/sync 阶段 数据仍会被加载,但不会执行任何同步操作 适用于只需要加载数据供其他类型引用的场景(如 supplier) skip_post_check: bool - 如果为 True,则跳过该类型最终 post-check 的 reload 与一致性校验 """ default_config: StrategyConfig domain_option_model: Type[BaseModel] | None schema: Type[T] node_type: str config: StrategyConfig sm_runtime: Optional[StateMachineRuntime] @abstractmethod def __init__( self, node_type: str, local_collection: DataCollection, remote_collection: DataCollection, binding_manager: BindingManager, ): pass @abstractmethod async def bind(self): """进行绑定逻辑判定,设置 node.binding_status 和 node.action""" pass @abstractmethod async def create(self) -> List[SyncNode]: """准备 CREATE 操作""" pass @abstractmethod async def update(self) -> List[SyncNode]: """准备 UPDATE 操作""" pass @abstractmethod async def delete(self) -> List[SyncNode]: pass @property @abstractmethod def skip_sync(self) -> bool: pass @property @abstractmethod def skip_post_check(self) -> bool: pass @abstractmethod def normalize_compare_payload( self, data: Optional[Dict[str, Any]], data_id_map: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: pass class DefaultSyncStrategy(BaseSyncStrategy[T]): """ 基于 STATE_DEFINITIONS.md 实现的标准化同步策略。 采用四阶段管线:核心状态 -> 依赖检查 -> 自动匹配 -> 动作映射。 """ default_config: StrategyConfig = StrategyConfig() domain_option_model: Type[BaseModel] | None = None schema: Type[T] = None # type: ignore def __init__( self, node_type: str, local_collection: DataCollection, remote_collection: DataCollection, binding_manager: BindingManager, ): if self.schema is None: raise ValueError(f"{self.__class__.__name__} must define schema as a class variable") self.node_type = node_type self.local_collection = local_collection self.remote_collection = remote_collection self.binding_manager = binding_manager self.config = self.default_config.model_copy(deep=True) self.sm_runtime: Optional[StateMachineRuntime] = None self.config.domain_option = resolve_domain_option_config( self.domain_option_model, self.config.domain_option, ) self.config.log_logic_warnings(node_type=self.node_type, logger=logger) @classmethod def get_default_domain_option(cls) -> Dict[str, Any]: return resolve_domain_option_config(cls.domain_option_model, None) def _validate_domain_option(self, domain_option: Optional[Dict[str, Any]]) -> BaseModel | Dict[str, Any]: if self.domain_option_model is None: return dict(domain_option or {}) return self.domain_option_model.model_validate(domain_option or {}) @property def domain_option(self) -> BaseModel | Dict[str, Any]: return self._validate_domain_option(self.config.domain_option) @property def skip_sync(self) -> bool: return bool(self.config.skip_sync) @skip_sync.setter def skip_sync(self, value: bool) -> None: self.config.skip_sync = bool(value) @property def skip_post_check(self) -> bool: return bool(self.config.skip_post_check) @skip_post_check.setter def skip_post_check(self, value: bool) -> None: self.config.skip_post_check = bool(value) def ensure_runtime(self) -> StateMachineRuntime: runtime = self.sm_runtime if runtime is None: cfg_path = Path(__file__).resolve().parents[1] / "config" / "node_state_machine.yaml" runtime = StateMachineRuntime(StateMachineConfig.load(cfg_path)) self.sm_runtime = runtime self.local_collection.set_state_machine_runtime(runtime) self.remote_collection.set_state_machine_runtime(runtime) return runtime def normalize_compare_payload( self, data: Optional[Dict[str, Any]], data_id_map: Optional[Dict[str, str]] = None, ) -> Dict[str, Any]: normalized = normalized_data_for_compare( data, data_id_map, ignore_fields=set(self.config.compare_ignore_fields), ignore_list_item_fields=self.config.post_check_ignore_list_item_fields, ) return normalized async def bind(self): runtime = self.ensure_runtime() bind_local_nodes, bind_remote_nodes = collect_bind_entry_nodes( node_type=self.node_type, local_collection=self.local_collection, remote_collection=self.remote_collection, ) await phase_core_state( node_type=self.node_type, runtime=runtime, binding_manager=self.binding_manager, local_nodes=bind_local_nodes, remote_nodes=bind_remote_nodes, ) await phase_dependency_check( node_type=self.node_type, runtime=runtime, depend_fields=dict(self.config.depend_fields), local_nodes=bind_local_nodes, remote_nodes=bind_remote_nodes, local_collection=self.local_collection, remote_collection=self.remote_collection, ) auto_bind_local_nodes, auto_bind_remote_nodes = collect_auto_bind_ready_nodes( node_type=self.node_type, local_collection=self.local_collection, remote_collection=self.remote_collection, ) local_create_enabled = self.config.local_orphan_action == OrphanAction.CREATE_REMOTE remote_create_enabled = self.config.remote_orphan_action == OrphanAction.CREATE_LOCAL local_orphan_create_enabled_by_node = { node.node_id: local_create_enabled for node in auto_bind_local_nodes } remote_orphan_create_enabled_by_node = { node.node_id: remote_create_enabled for node in auto_bind_remote_nodes } pending_updates = await plan_auto_bind_updates( node_type=self.node_type, config=self.config, local_collection=self.local_collection, remote_collection=self.remote_collection, binding_manager=self.binding_manager, local_nodes=auto_bind_local_nodes, remote_nodes=auto_bind_remote_nodes, id_field_hints=dict(self.config.depend_fields), local_orphan_create_enabled_by_node=local_orphan_create_enabled_by_node, remote_orphan_create_enabled_by_node=remote_orphan_create_enabled_by_node, ) await apply_auto_bind_updates( node_type=self.node_type, runtime=runtime, binding_manager=self.binding_manager, pending_auto_bind_updates=pending_updates, ) await refresh_bind_data_id( node_type=self.node_type, local_collection=self.local_collection, remote_collection=self.remote_collection, binding_manager=self.binding_manager, local_node_ids={ *(node.node_id for node in bind_local_nodes), *(node.node_id for node in auto_bind_local_nodes), }, remote_node_ids={ *(node.node_id for node in bind_remote_nodes), *(node.node_id for node in auto_bind_remote_nodes), }, ) async def create(self) -> List[SyncNode]: self.ensure_runtime() created_nodes: List[SyncNode] = [] created_nodes.extend(await self.create_for_direction(source_is_local=True)) created_nodes.extend(await self.create_for_direction(source_is_local=False)) return created_nodes async def create_for_direction(self, *, source_is_local: bool) -> List[SyncNode]: from_collection = self.local_collection if source_is_local else self.remote_collection to_collection = self.remote_collection if source_is_local else self.local_collection return await add_create_action( self, from_collection=from_collection, to_collection=to_collection, source_is_local=source_is_local, ) async def update(self) -> List[SyncNode]: self.ensure_runtime() validator = _build_schema_diff_validator(self) self._active_schema_diff_validator = validator data_id_map = await self.build_update_data_id_map() try: pairs = await self.collect_update_pairs() updated_by_id = await self.process_update_pairs(pairs, data_id_map) finally: self._active_schema_diff_validator = None emit_schema_diff_report(self, validator) return list(updated_by_id.values()) async def build_update_data_id_map(self) -> Dict[str, str]: return await build_data_id_normalization_map( node_type=self.node_type, local_collection=self.local_collection, remote_collection=self.remote_collection, binding_manager=self.binding_manager, ) async def process_update_pairs( self, pairs: List[BoundNodePair], data_id_map: Optional[Dict[str, str]] = None, ) -> Dict[str, SyncNode]: updated_by_id: Dict[str, SyncNode] = {} for pair in pairs: for updated_node in await self.update_pair(pair.local_node, pair.remote_node, data_id_map): updated_by_id[updated_node.node_id] = updated_node return updated_by_id def get_node_update_payload(self, node: SyncNode) -> Dict[str, Any]: return default_get_node_update_payload(node) def should_update_pair( self, source_node: SyncNode, target_node: SyncNode, *, source_data: Dict[str, Any], target_data: Dict[str, Any], data_id_map: Optional[Dict[str, str]] = None, ) -> bool: return default_needs_update(self, source_data, target_data, data_id_map) async def collect_update_pairs(self): return await collect_bound_node_pairs(self) async def prepare_update_for_direction( self, local_node: SyncNode, remote_node: SyncNode, direction: UpdateDirection, data_id_map: Optional[Dict[str, str]] = None, *, include_fields: Optional[List[str]] = None, exclude_fields: Optional[List[str]] = None, ) -> Optional[SyncNode]: return await prepare_directional_update( self, local_node=local_node, remote_node=remote_node, direction=direction, data_id_map=data_id_map, include_fields=include_fields, exclude_fields=exclude_fields, ) async def update_pair( self, local_node: SyncNode, remote_node: SyncNode, data_id_map: Optional[Dict[str, str]] = None, ) -> List[SyncNode]: return await default_update_pair(self, local_node, remote_node, data_id_map) async def delete(self) -> List[SyncNode]: self.ensure_runtime() return await run_delete(self)