181 lines
6.8 KiB
Python
181 lines
6.8 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from enum import Enum
|
|
from typing import Any, Dict, List, Mapping, Type
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
def resolve_domain_option_config(
|
|
domain_option_model: Type[BaseModel] | None,
|
|
domain_option: Mapping[str, Any] | None,
|
|
) -> Dict[str, Any]:
|
|
if domain_option_model is None:
|
|
return dict(domain_option or {})
|
|
return domain_option_model.model_validate(domain_option or {}).model_dump()
|
|
|
|
|
|
class OrphanAction(str, Enum):
|
|
CREATE_REMOTE = "create_remote"
|
|
CREATE_LOCAL = "create_local"
|
|
DELETE_LOCAL = "delete_local"
|
|
DELETE_REMOTE = "delete_remote"
|
|
NONE = "none"
|
|
|
|
|
|
class UpdateDirection(str, Enum):
|
|
PUSH = "push"
|
|
PULL = "pull"
|
|
NONE = "none"
|
|
|
|
|
|
class PreBindDataIdPair(BaseModel):
|
|
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
|
|
local_data_id: str
|
|
remote_data_id: str
|
|
|
|
|
|
class StrategyConfig(BaseModel):
|
|
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
|
|
auto_bind: bool = True
|
|
auto_bind_fields: List[str] = Field(default_factory=list)
|
|
pre_bind_data_id: List[PreBindDataIdPair] = Field(default_factory=list)
|
|
depend_fields: Dict[str, str] = Field(default_factory=dict)
|
|
|
|
local_orphan_action: OrphanAction = OrphanAction.NONE
|
|
remote_orphan_action: OrphanAction = OrphanAction.NONE
|
|
update_direction: UpdateDirection = UpdateDirection.PUSH
|
|
|
|
# post-check 一致性比较时忽略的顶层字段。
|
|
# 只影响最终一致性评估,不影响 create/update 的差异检测。
|
|
post_check_ignore_fields: List[str] = Field(default_factory=list)
|
|
|
|
# post-check 一致性比较时,忽略列表型字段内各元素的特定子键。
|
|
# 格式: {字段名: [子键, ...]},如 {"plan_node": ["is_audit"]}。
|
|
# 用于排除后端硬写死、永远与本地不一致的字段,避免误报。
|
|
post_check_ignore_list_item_fields: Dict[str, List[str]] = Field(default_factory=dict)
|
|
|
|
# schema diff / validate 时忽略的顶层字段。
|
|
compare_ignore_fields: List[str] = Field(default_factory=list)
|
|
|
|
# 当 auto-bind 命中“多对一”候选时,允许稳定选择一条进行绑定,剩余候选按 orphan/create 流程继续处理。
|
|
# 默认关闭,避免在一般场景下引入隐式匹配。
|
|
allow_many_to_one_auto_bind: bool = False
|
|
|
|
# 是否跳过该类型的 create/update 阶段。
|
|
skip_sync: bool = False
|
|
|
|
# 是否跳过该类型的 post-check reload 与一致性校验。
|
|
skip_post_check: bool = False
|
|
|
|
# 领域策略运行参数,由各 strategy 的 domain_option_model 负责解释与校验。
|
|
domain_option: Dict[str, Any] = Field(default_factory=dict)
|
|
|
|
def log_logic_warnings(self, *, node_type: str, logger: logging.Logger) -> None:
|
|
direction = self.update_direction
|
|
local_action = self.local_orphan_action
|
|
remote_action = self.remote_orphan_action
|
|
|
|
if direction == UpdateDirection.PUSH:
|
|
if local_action == OrphanAction.CREATE_LOCAL:
|
|
logger.warning(
|
|
f"[{node_type}] Config inconsistency: "
|
|
f"update_direction=PUSH but local_orphan_action=CREATE_LOCAL (本地创建)。"
|
|
f"应使用 CREATE_REMOTE (推送到远程)"
|
|
)
|
|
if remote_action == OrphanAction.CREATE_LOCAL:
|
|
logger.warning(
|
|
f"[{node_type}] Config inconsistency: "
|
|
f"update_direction=PUSH but remote_orphan_action=CREATE_LOCAL (本地创建)。"
|
|
f"PUSH 模式应设为 NONE 或 DELETE_REMOTE"
|
|
)
|
|
elif direction == UpdateDirection.PULL:
|
|
if local_action == OrphanAction.CREATE_REMOTE:
|
|
logger.warning(
|
|
f"[{node_type}] Config inconsistency: "
|
|
f"update_direction=PULL but local_orphan_action=CREATE_REMOTE (推送到远程)。"
|
|
f"PULL 模式应设为 NONE 或 DELETE_LOCAL"
|
|
)
|
|
if remote_action in {OrphanAction.CREATE_REMOTE, OrphanAction.DELETE_LOCAL}:
|
|
logger.warning(
|
|
f"[{node_type}] Config inconsistency: "
|
|
f"update_direction=PULL but remote_orphan_action={remote_action}。"
|
|
f"PULL 模式建议使用 CREATE_LOCAL 或 NONE"
|
|
)
|
|
elif direction == UpdateDirection.NONE:
|
|
if local_action != OrphanAction.NONE:
|
|
logger.warning(
|
|
f"[{node_type}] Config inconsistency: "
|
|
f"update_direction=NONE but local_orphan_action={local_action}。"
|
|
f"应设为 NONE"
|
|
)
|
|
if remote_action != OrphanAction.NONE:
|
|
logger.warning(
|
|
f"[{node_type}] Config inconsistency: "
|
|
f"update_direction=NONE but remote_orphan_action={remote_action}。"
|
|
f"应设为 NONE"
|
|
)
|
|
|
|
|
|
class StrategyRuntimeConfig(BaseModel):
|
|
"""Pipeline 层策略运行配置(按 node_type)。"""
|
|
|
|
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
|
|
node_type: str
|
|
config: StrategyConfig = Field(default_factory=StrategyConfig)
|
|
|
|
|
|
class ConfigPresets:
|
|
@staticmethod
|
|
def first_sync() -> StrategyConfig:
|
|
return StrategyConfig(
|
|
local_orphan_action=OrphanAction.CREATE_REMOTE,
|
|
remote_orphan_action=OrphanAction.CREATE_LOCAL,
|
|
update_direction=UpdateDirection.PUSH,
|
|
)
|
|
|
|
@staticmethod
|
|
def daily_sync() -> StrategyConfig:
|
|
return ConfigPresets.push_only()
|
|
|
|
@staticmethod
|
|
def repair_sync() -> StrategyConfig:
|
|
return StrategyConfig(
|
|
local_orphan_action=OrphanAction.NONE,
|
|
remote_orphan_action=OrphanAction.NONE,
|
|
update_direction=UpdateDirection.NONE,
|
|
)
|
|
|
|
@staticmethod
|
|
def pull_only() -> StrategyConfig:
|
|
return StrategyConfig(
|
|
local_orphan_action=OrphanAction.NONE,
|
|
remote_orphan_action=OrphanAction.CREATE_LOCAL,
|
|
update_direction=UpdateDirection.PULL,
|
|
)
|
|
|
|
@staticmethod
|
|
def push_only() -> StrategyConfig:
|
|
return StrategyConfig(
|
|
local_orphan_action=OrphanAction.CREATE_REMOTE,
|
|
remote_orphan_action=OrphanAction.NONE,
|
|
update_direction=UpdateDirection.PUSH,
|
|
)
|
|
|
|
@staticmethod
|
|
def get_preset(name: str) -> StrategyConfig:
|
|
presets = {
|
|
"first_sync": ConfigPresets.first_sync,
|
|
"daily_sync": ConfigPresets.daily_sync,
|
|
"repair_sync": ConfigPresets.repair_sync,
|
|
"pull_only": ConfigPresets.pull_only,
|
|
"push_only": ConfigPresets.push_only,
|
|
}
|
|
if name not in presets:
|
|
raise ValueError(f"Unknown preset: {name}. Available: {list(presets.keys())}")
|
|
return presets[name]()
|