""" 节点处理器 V2 (Node Handler - Pure Execution Layer) Handler 职责: 1. 执行后端 API 调用(load/create/update/delete) 2. 返回执行结果(TaskResult) 3. 提供异步任务轮询(poll) 4. 提供数据质量检查(validate) Handler 不负责: - ❌ 状态管理(由 DataSource 负责) - ❌ ID 映射(由 Strategy 负责,已保存在 node.data) - ❌ 依赖关系(由 Strategy 负责) """ from __future__ import annotations from typing import ( TypeVar, Generic, Type, Protocol, Dict, List, Any, Optional, runtime_checkable, TYPE_CHECKING ) from abc import ABC, abstractmethod from pydantic import BaseModel from .task_result import TaskResult if TYPE_CHECKING: from ..common.sync_node import SyncNode from ..common.collection import DataCollection from .api.client import ApiClient T = TypeVar("T", bound=BaseModel) class ValidationResult: """数据质量检查结果""" def __init__(self, is_valid: bool, errors: Optional[List[str]] = None): self.is_valid = is_valid self.errors = errors or [] @runtime_checkable class NodeHandler(Protocol[T]): """ 节点处理器协议(纯执行层) Handler 只负责调用后端 API,不管理状态。 """ @property def node_type(self) -> str: """节点类型(如 'contract')""" ... @property def node_class(self) -> Type["SyncNode[T]"]: """SyncNode 类型""" ... @property def schema(self) -> Type[T]: """Schema 类型""" ... def set_collection(self, collection: "DataCollection") -> None: """ 设置 Collection 引用 Args: collection: 数据集合 """ ... def set_handler_config(self, config: Dict[str, Any]) -> None: """注入 handler 级别配置。默认可忽略。""" ... def set_target_project_ids(self, target_project_ids: List[str]) -> None: """注入项目过滤列表。默认可忽略。""" ... def set_api_client(self, client: "ApiClient") -> None: """注入 API 客户端。默认可忽略。""" ... def set_poll_mode(self, mode: str) -> None: """设置轮询模式。默认可忽略。""" ... def get_update_fields(self, *, exclude: frozenset[str] = frozenset({"id"})) -> List[str]: """返回 post-check 可比较字段列表。默认空。""" ... async def load(self) -> List['SyncNode']: """ 从后端加载全部数据并创建节点 注意:调用前需先 set_collection() Returns: SyncNode 列表 示例: # Project Handler(无依赖) async def load(self): response = await self.api.get_projects() nodes = [] for data in response["data"]: node = self.create_node(data) node.context["project_id"] = data["id"] nodes.append(node) return nodes # Contract Handler(依赖 Project) async def load(self): # 使用 self._collection 查询依赖 projects = self._collection.filter(node_type="project") project_ids = [p.data_id for p in projects if p.data_id] nodes = [] for project_id in project_ids: response = await self.api.get_contracts(project_id=project_id) for data in response["data"]: node = self.create_node(data) node.context["project_id"] = project_id node.context["contract_id"] = data["id"] nodes.append(node) return nodes """ ... async def create_all( self, nodes: List["SyncNode"] ) -> List[TaskResult]: """ 批量创建节点 Args: nodes: 需要创建的节点列表(DataSource 已筛选为 CREATE + PENDING) Returns: List[TaskResult]: 每个 TaskResult 必须包含 node_id,用于定位节点: - TaskResult(status=SUCCESS, node_id="xxx", data_id="yyy") - TaskResult(status=FAILED, node_id="xxx", error="...") - TaskResult(status=IN_PROGRESS, node_id="xxx", task_id="...") 示例: # JSONL Handler async def create_all(self, nodes): results = [] for node in nodes: try: new_id = str(uuid4()) data = node.get_data() # ... 存储逻辑 ... results.append(TaskResult.success( node_id=node.node_id, data_id=new_id )) except Exception as e: results.append(TaskResult.failed( node_id=node.node_id, error=str(e) )) return results # API Handler - 查询依赖 async def create_all(self, nodes): results = [] for node in nodes: # 查询依赖数据(如合同创建时查项目) project_id = node.context.get("project_id") projects = self._collection.filter( node_type="project", node_filter={"data_id": project_id} ) if not projects: results.append(TaskResult.failed( node_id=node.node_id, error=f"Project {project_id} not found" )) continue # 构造请求数据 data = node.get_data() if not data: results.append(TaskResult.failed( node_id=node.node_id, error="node data is None" )) continue contract_data = { "project_id": projects[0].data_id, **data } # 调用 API response = await self.api.create_contract(contract_data) results.append(TaskResult.success( node_id=node.node_id, data_id=response["id"] )) return results """ ... async def update_all( self, nodes: List["SyncNode"] ) -> List[TaskResult]: """ 批量更新节点 Args: nodes: 需要更新的节点列表(DataSource 已筛选为 UPDATE + PENDING) Returns: List[TaskResult](同 create_all) """ ... async def delete_all( self, nodes: List["SyncNode"] ) -> List[TaskResult]: """ 批量删除节点 Args: nodes: 需要删除的节点列表(DataSource 已筛选为 DELETE + PENDING) Args: collection: Collection 上下文 Returns: List[TaskResult](同 create_all) """ ... async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]: """ 批量轮询异步任务状态 Args: task_ids: 任务 ID 列表 Returns: {task_id: TaskResult} 字典 - SUCCESS: 任务完成,返回 data_id - FAILED: 任务失败,返回 error - IN_PROGRESS: 仍在进行中 示例: async def poll_tasks(self, task_ids): response = await self.api.batch_query_tasks(task_ids) results = {} for task in response["tasks"]: if task["status"] == "completed": results[task["id"]] = TaskResult.success( data_id=task["result"]["contract_id"] ) elif task["status"] == "failed": results[task["id"]] = TaskResult.failed( error=task["error"] ) else: # running results[task["id"]] = TaskResult.in_progress( task_id=task["id"] ) return results """ ... async def validate(self, data: Dict[str, Any]) -> ValidationResult: """ 数据质量检查 Args: data: 原始数据 Returns: ValidationResult(是否有效 + 错误列表) 示例: async def validate(self, data): errors = [] if not data.get("code"): errors.append("合同编码为空") if not data.get("project_id"): errors.append("项目 ID 为空") return ValidationResult( is_valid=len(errors) == 0, errors=errors ) """ ... def extract_id(self, data: Dict[str, Any]) -> str: """ 从原始数据提取 ID Args: data: 原始数据 Returns: 业务主键 """ ... def _create_node(self, data: Dict[str, Any]) -> "SyncNode[T]": """ 从原始数据创建 SyncNode Args: data: 原始数据 Returns: 类型化的 SyncNode """ ... class BaseNodeHandler(ABC, Generic[T]): """ 节点处理器基类 子类需实现: - load() - create() - update() - delete()(可选) - poll_tasks()(如果支持异步) - validate()(可选) - extract_id() """ def __init__( self, node_type: str, node_class: Type["SyncNode[T]"], schema: Type[T] ): self._node_type = node_type self._node_class = node_class self._schema = schema self._collection: Optional["DataCollection"] = None self.handler_config: Dict[str, Any] = {} def set_collection(self, collection: "DataCollection") -> None: """ 设置 Collection 引用 Args: collection: 数据集合 """ self._collection = collection def set_target_project_ids(self, target_project_ids: List[str]) -> None: """默认忽略项目过滤能力。""" return def set_handler_config(self, config: Dict[str, Any]) -> None: """默认保存 handler 级别配置,供子类按需读取。""" self.handler_config = dict(config) def set_api_client(self, client: "ApiClient") -> None: """默认忽略 API 客户端注入。""" return def set_poll_mode(self, mode: str) -> None: """默认忽略轮询模式。""" return def get_update_fields(self, *, exclude: frozenset[str] = frozenset({"id"})) -> List[str]: """默认无 post-check 字段白名单。""" return [] @property def node_type(self) -> str: return self._node_type @property def node_class(self) -> Type["SyncNode[T]"]: return self._node_class @property def schema(self) -> Type[T]: return self._schema # ========== 必须实现 ========== @abstractmethod async def load(self) -> List['SyncNode']: """ 从数据源加载数据并创建节点 Handler 负责: 1. 加载原始数据 2. 创建 SyncNode(调用 create_node) 3. 设置 context(如 project_id, contract_id 等) Returns: SyncNode 列表 """ pass @abstractmethod async def create_all( self, nodes: List["SyncNode"] ) -> List[TaskResult]: """批量创建节点""" pass @abstractmethod async def update_all( self, nodes: List["SyncNode"] ) -> List[TaskResult]: """批量更新节点""" pass @abstractmethod def extract_id(self, data: Dict[str, Any]) -> str: """提取 ID""" pass # ========== 可选实现 ========== async def delete_all( self, nodes: List["SyncNode"] ) -> List[TaskResult]: """批量删除节点(默认不支持)""" return [ TaskResult.failed(node_id=node.node_id, error="Delete operation not supported") for node in nodes ] async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]: """轮询异步任务(默认不支持)""" return { task_id: TaskResult.failed(error="Async tasks not supported") for task_id in task_ids } async def validate(self, data: Dict[str, Any]) -> ValidationResult: """数据质量检查(默认不检查)""" return ValidationResult(is_valid=True) def create_node(self, data: Dict[str, Any]) -> "SyncNode[T]": """创建或复用 SyncNode(默认实现)""" data_id = self.extract_id(data) if self._collection is None: raise RuntimeError("Collection not set. Call set_collection() before create_node().") if data_id: existing_node = self._collection.get_by_data_id(self.node_type, data_id) if existing_node is not None: existing_node.set_data(data.copy()) existing_node.set_origin_data(data.copy()) return existing_node node_id = self._collection.generate_node_id() return self.node_class( node_id=node_id, data_id=data_id, data=data.copy() ) def _create_node(self, data: Dict[str, Any]) -> "SyncNode[T]": """兼容 NodeHandler 协议的默认实现""" return self.create_node(data) class CompatibleNodeHandler(Generic[T]): """兼容旧式 Handler,实现扩展后的可选接口并委托给原对象。""" def __init__(self, handler: NodeHandler[T]): self._handler = handler @property def node_type(self) -> str: return self._handler.node_type @property def node_class(self) -> Type["SyncNode[T]"]: return self._handler.node_class @property def schema(self) -> Type[T]: return self._handler.schema def set_collection(self, collection: "DataCollection") -> None: self._handler.set_collection(collection) def set_handler_config(self, config: Dict[str, Any]) -> None: method = getattr(self._handler, "set_handler_config", None) if callable(method): method(config) def set_target_project_ids(self, target_project_ids: List[str]) -> None: method = getattr(self._handler, "set_target_project_ids", None) if callable(method): method(target_project_ids) def set_api_client(self, client: "ApiClient") -> None: method = getattr(self._handler, "set_api_client", None) if callable(method): method(client) def set_poll_mode(self, mode: str) -> None: method = getattr(self._handler, "set_poll_mode", None) if callable(method): method(mode) def get_update_fields(self, *, exclude: frozenset[str] = frozenset({"id"})) -> List[str]: method = getattr(self._handler, "get_update_fields", None) if callable(method): return method(exclude=exclude) return [] async def load(self) -> List["SyncNode"]: return await self._handler.load() async def create_all(self, nodes: List["SyncNode"]) -> List[TaskResult]: return await self._handler.create_all(nodes) async def update_all(self, nodes: List["SyncNode"]) -> List[TaskResult]: return await self._handler.update_all(nodes) async def delete_all(self, nodes: List["SyncNode"]) -> List[TaskResult]: return await self._handler.delete_all(nodes) async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]: return await self._handler.poll_tasks(task_ids) async def validate(self, data: Dict[str, Any]) -> ValidationResult: return await self._handler.validate(data) def extract_id(self, data: Dict[str, Any]) -> str: return self._handler.extract_id(data) def _create_node(self, data: Dict[str, Any]) -> "SyncNode[T]": return self._handler._create_node(data) def __getattr__(self, item: str) -> Any: return getattr(self._handler, item) def ensure_handler_compat(handler: NodeHandler[T]) -> NodeHandler[T]: if isinstance(handler, BaseNodeHandler) or isinstance(handler, CompatibleNodeHandler): return handler return CompatibleNodeHandler(handler) class NodeHandlerRegistry: """节点处理器注册表""" _handlers: Dict[str, NodeHandler] = {} @classmethod def register(cls, node_type: str, handler: NodeHandler) -> None: """注册 handler""" cls._handlers[node_type] = handler @classmethod def get(cls, node_type: str) -> NodeHandler: """获取 handler""" if node_type not in cls._handlers: raise KeyError(f"Handler for '{node_type}' not registered") return cls._handlers[node_type] @classmethod def has(cls, node_type: str) -> bool: """检查是否已注册""" return node_type in cls._handlers @classmethod def clear(cls) -> None: """清空注册表""" cls._handlers.clear() @classmethod def list_handlers(cls) -> List[str]: """列出所有已注册的 node_type""" return list(cls._handlers.keys())