# -*- coding: utf-8 -*- """应用程序设置管理""" import json import logging import os import sys from pathlib import Path from typing import Any, Dict, Optional class AppSettings: """应用程序设置单例""" _instance: Optional["AppSettings"] = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialized = False return cls._instance def __init__(self): if self._initialized: return self._initialized = True # 配置文件路径 self._settings_file = self._get_settings_path() # 默认设置 self._settings = { "etl_project_path": "", # ETL 项目路径 "env_file_path": "", # .env 文件路径 # 窗口状态 "window_state": { "geometry": None, # 窗口位置和大小 [x, y, width, height] "maximized": False, # 是否最大化 "current_panel": 0, # 当前选中的面板索引 "splitter_sizes": None, # 分割器大小 }, # 任务管理状态 "task_manager_state": { "scheduler_enabled": False, # 调度器是否启用 "auto_run_enabled": False, # 自动执行是否启用 "current_tab": 0, # 当前选项卡索引 }, # 任务面板状态 "task_panel_state": { "advanced_expanded": False, # 高级选项是否展开 "current_tab": 0, # 当前选项卡 "dwd_tasks": [], # DWD 任务选择 "dws_tasks": [], # DWS 任务选择 "build_tasks": [], # 数据建设任务选择 "window_split": "day", "window_split_days": 10, "build_window_mode": "lookback", "build_lookback_hours": 24, "build_window_start": "", "build_window_end": "", "build_window_split": "day", "build_window_split_days": 10, "ml_manual_file_path": "", "index_relation_check": True, }, # 自动更新配置 "auto_update": { "hours": 24, "overlap_seconds": 600, "include_dwd": True, "auto_verify": False, "selected_tasks": [], }, # 数据校验配置 "integrity_check": { "mode": "history", "history_start": "", "history_end": "", "lookback_hours": 24, "include_dimensions": True, "auto_backfill": False, "ods_tasks": "", }, # 高级配置 "advanced": { "pipeline_flow": "FULL", "dry_run": False, "window_start": "", "window_end": "", "window_split": "none", "window_compensation": 0, "ingest_source": "", "store_id": "", "pg_dsn": "", "api_token": "", }, } # 加载设置 self._load() # 如果没有配置,尝试自动检测 if not self._settings["etl_project_path"]: self._auto_detect_paths() def _get_settings_path(self) -> Path: """获取设置文件路径""" # 优先使用用户目录 if sys.platform == "win32": app_data = os.environ.get("APPDATA", "") if app_data: settings_dir = Path(app_data) / "ETL管理系统" else: settings_dir = Path.home() / ".etl_gui" else: settings_dir = Path.home() / ".etl_gui" settings_dir.mkdir(parents=True, exist_ok=True) return settings_dir / "settings.json" def _auto_detect_paths(self): """自动检测 ETL 项目路径""" # 方法1: 检查是否从源码目录运行 try: source_dir = Path(__file__).resolve().parents[2] cli_main = source_dir / "cli" / "main.py" if cli_main.exists(): rel_source = Path(os.path.relpath(source_dir, Path.cwd())) self._settings["etl_project_path"] = str(rel_source) env_file = rel_source / ".env" if env_file.exists(): self._settings["env_file_path"] = str(env_file) self._save() return except Exception: pass # 方法2: 检查常见位置 common_paths = [ Path("."), ] for path in common_paths: if path.exists() and (path / "cli" / "main.py").exists(): self._settings["etl_project_path"] = str(path) env_file = path / ".env" if env_file.exists(): self._settings["env_file_path"] = str(env_file) self._save() return def _load(self): """加载设置""" if self._settings_file.exists(): try: data = json.loads(self._settings_file.read_text(encoding="utf-8")) self._settings.update(data) except Exception: pass def _save(self): """保存设置""" try: self._settings_file.write_text( json.dumps(self._settings, ensure_ascii=False, indent=2), encoding="utf-8" ) except Exception: pass @property def etl_project_path(self) -> str: """获取 ETL 项目路径""" return self._settings.get("etl_project_path", "") @etl_project_path.setter def etl_project_path(self, value: str): """设置 ETL 项目路径""" self._settings["etl_project_path"] = value # 同时更新 .env 路径 if value: env_path = Path(value) / ".env" if env_path.exists(): self._settings["env_file_path"] = str(env_path) self._save() @property def env_file_path(self) -> str: """获取 .env 文件路径""" path = self._settings.get("env_file_path", "") if not path and self.etl_project_path: path = str(Path(self.etl_project_path) / ".env") return path @env_file_path.setter def env_file_path(self, value: str): """设置 .env 文件路径""" self._settings["env_file_path"] = value self._save() def is_configured(self) -> bool: """检查是否已配置""" path = self.etl_project_path if not path: return False return Path(path).exists() and (Path(path) / "cli" / "main.py").exists() def validate(self) -> tuple[bool, str]: """验证配置""" path = self.etl_project_path if not path: return False, "未配置 ETL 项目路径" project_path = Path(path) if not project_path.exists(): return False, f"ETL 项目路径不存在: {path}" cli_main = project_path / "cli" / "main.py" if not cli_main.exists(): return False, f"找不到 CLI 入口: {cli_main}" return True, "配置有效" # ==================== 自动更新配置 ==================== @property def auto_update_hours(self) -> int: return self._settings.get("auto_update", {}).get("hours", 24) @auto_update_hours.setter def auto_update_hours(self, value: int): self._settings.setdefault("auto_update", {})["hours"] = value self._save() @property def auto_update_overlap_seconds(self) -> int: return self._settings.get("auto_update", {}).get("overlap_seconds", 3600) @auto_update_overlap_seconds.setter def auto_update_overlap_seconds(self, value: int): self._settings.setdefault("auto_update", {})["overlap_seconds"] = value self._save() @property def auto_update_include_dwd(self) -> bool: return self._settings.get("auto_update", {}).get("include_dwd", True) @auto_update_include_dwd.setter def auto_update_include_dwd(self, value: bool): self._settings.setdefault("auto_update", {})["include_dwd"] = value self._save() @property def auto_update_auto_verify(self) -> bool: return self._settings.get("auto_update", {}).get("auto_verify", False) @auto_update_auto_verify.setter def auto_update_auto_verify(self, value: bool): self._settings.setdefault("auto_update", {})["auto_verify"] = value self._save() @property def auto_update_selected_tasks(self) -> list: return self._settings.get("auto_update", {}).get("selected_tasks", []) @auto_update_selected_tasks.setter def auto_update_selected_tasks(self, value: list): self._settings.setdefault("auto_update", {})["selected_tasks"] = value self._save() # ==================== 数据校验配置 ==================== @property def integrity_mode(self) -> str: return self._settings.get("integrity_check", {}).get("mode", "history") @integrity_mode.setter def integrity_mode(self, value: str): self._settings.setdefault("integrity_check", {})["mode"] = value self._save() @property def integrity_history_start(self) -> str: return self._settings.get("integrity_check", {}).get("history_start", "") @integrity_history_start.setter def integrity_history_start(self, value: str): self._settings.setdefault("integrity_check", {})["history_start"] = value self._save() @property def integrity_history_end(self) -> str: return self._settings.get("integrity_check", {}).get("history_end", "") @integrity_history_end.setter def integrity_history_end(self, value: str): self._settings.setdefault("integrity_check", {})["history_end"] = value self._save() @property def integrity_lookback_hours(self) -> int: return self._settings.get("integrity_check", {}).get("lookback_hours", 24) @integrity_lookback_hours.setter def integrity_lookback_hours(self, value: int): self._settings.setdefault("integrity_check", {})["lookback_hours"] = value self._save() @property def integrity_include_dimensions(self) -> bool: return self._settings.get("integrity_check", {}).get("include_dimensions", True) @integrity_include_dimensions.setter def integrity_include_dimensions(self, value: bool): self._settings.setdefault("integrity_check", {})["include_dimensions"] = value self._save() @property def integrity_auto_backfill(self) -> bool: return self._settings.get("integrity_check", {}).get("auto_backfill", False) @integrity_auto_backfill.setter def integrity_auto_backfill(self, value: bool): self._settings.setdefault("integrity_check", {})["auto_backfill"] = value self._save() @property def integrity_ods_tasks(self) -> str: return self._settings.get("integrity_check", {}).get("ods_tasks", "") @integrity_ods_tasks.setter def integrity_ods_tasks(self, value: str): self._settings.setdefault("integrity_check", {})["ods_tasks"] = value self._save() # ==================== 高级配置 ==================== @property def advanced_pipeline_flow(self) -> str: return self._settings.get("advanced", {}).get("pipeline_flow", "FULL") @advanced_pipeline_flow.setter def advanced_pipeline_flow(self, value: str): self._settings.setdefault("advanced", {})["pipeline_flow"] = value self._save() @property def advanced_dry_run(self) -> bool: return self._settings.get("advanced", {}).get("dry_run", False) @advanced_dry_run.setter def advanced_dry_run(self, value: bool): self._settings.setdefault("advanced", {})["dry_run"] = value self._save() @property def advanced_window_start(self) -> str: return self._settings.get("advanced", {}).get("window_start", "") @advanced_window_start.setter def advanced_window_start(self, value: str): self._settings.setdefault("advanced", {})["window_start"] = value self._save() @property def advanced_window_end(self) -> str: return self._settings.get("advanced", {}).get("window_end", "") @advanced_window_end.setter def advanced_window_end(self, value: str): self._settings.setdefault("advanced", {})["window_end"] = value self._save() @property def advanced_ingest_source(self) -> str: return self._settings.get("advanced", {}).get("ingest_source", "") @advanced_ingest_source.setter def advanced_ingest_source(self, value: str): self._settings.setdefault("advanced", {})["ingest_source"] = value self._save() @property def advanced_window_split(self) -> str: return self._settings.get("advanced", {}).get("window_split", "none") @advanced_window_split.setter def advanced_window_split(self, value: str): self._settings.setdefault("advanced", {})["window_split"] = value self._save() @property def advanced_window_compensation(self) -> int: return self._settings.get("advanced", {}).get("window_compensation", 0) @advanced_window_compensation.setter def advanced_window_compensation(self, value: int): self._settings.setdefault("advanced", {})["window_compensation"] = value self._save() def get_all_settings(self) -> Dict[str, Any]: """获取所有设置(用于调试)""" return self._settings.copy() def save_all(self): """强制保存所有设置""" self._save() # ==================== 窗口状态 ==================== @property def window_geometry(self) -> Optional[list]: """获取窗口几何信息 [x, y, width, height]""" return self._settings.get("window_state", {}).get("geometry") @window_geometry.setter def window_geometry(self, value: list): """设置窗口几何信息""" self._settings.setdefault("window_state", {})["geometry"] = value self._save() @property def window_maximized(self) -> bool: """获取窗口是否最大化""" return self._settings.get("window_state", {}).get("maximized", False) @window_maximized.setter def window_maximized(self, value: bool): """设置窗口是否最大化""" self._settings.setdefault("window_state", {})["maximized"] = value self._save() @property def current_panel(self) -> int: """获取当前面板索引""" return self._settings.get("window_state", {}).get("current_panel", 0) @current_panel.setter def current_panel(self, value: int): """设置当前面板索引""" self._settings.setdefault("window_state", {})["current_panel"] = value self._save() @property def splitter_sizes(self) -> Optional[list]: """获取分割器大小""" return self._settings.get("window_state", {}).get("splitter_sizes") @splitter_sizes.setter def splitter_sizes(self, value: list): """设置分割器大小""" self._settings.setdefault("window_state", {})["splitter_sizes"] = value self._save() # ==================== 任务管理状态 ==================== @property def scheduler_enabled(self) -> bool: """获取调度器是否启用""" return self._settings.get("task_manager_state", {}).get("scheduler_enabled", False) @scheduler_enabled.setter def scheduler_enabled(self, value: bool): """设置调度器是否启用""" self._settings.setdefault("task_manager_state", {})["scheduler_enabled"] = value self._save() @property def auto_run_enabled(self) -> bool: """获取自动执行是否启用""" return self._settings.get("task_manager_state", {}).get("auto_run_enabled", False) @auto_run_enabled.setter def auto_run_enabled(self, value: bool): """设置自动执行是否启用""" self._settings.setdefault("task_manager_state", {})["auto_run_enabled"] = value self._save() @property def task_manager_tab(self) -> int: """获取任务管理当前选项卡""" return self._settings.get("task_manager_state", {}).get("current_tab", 0) @task_manager_tab.setter def task_manager_tab(self, value: int): """设置任务管理当前选项卡""" self._settings.setdefault("task_manager_state", {})["current_tab"] = value self._save() # ==================== 任务面板状态 ==================== @property def advanced_expanded(self) -> bool: """获取高级选项是否展开""" return self._settings.get("task_panel_state", {}).get("advanced_expanded", False) @advanced_expanded.setter def advanced_expanded(self, value: bool): """设置高级选项是否展开""" self._settings.setdefault("task_panel_state", {})["advanced_expanded"] = value self._save() @property def task_panel_tab(self) -> int: """获取任务面板当前选项卡""" return self._settings.get("task_panel_state", {}).get("current_tab", 0) @task_panel_tab.setter def task_panel_tab(self, value: int): """设置任务面板当前选项卡""" self._settings.setdefault("task_panel_state", {})["current_tab"] = value self._save() # ==================== 统一任务配置状态 ==================== @property def unified_pipeline(self) -> str: """获取管道类型""" return self._settings.get("task_panel_state", {}).get("pipeline", "api_ods_dwd") @unified_pipeline.setter def unified_pipeline(self, value: str): """设置管道类型""" self._settings.setdefault("task_panel_state", {})["pipeline"] = value self._save() @property def unified_processing_mode(self) -> str: """获取处理模式""" return self._settings.get("task_panel_state", {}).get("processing_mode", "increment_only") @unified_processing_mode.setter def unified_processing_mode(self, value: str): """设置处理模式""" self._settings.setdefault("task_panel_state", {})["processing_mode"] = value self._save() @property def unified_fetch_before_verify(self) -> bool: """获取校验前是否从 API 获取数据""" return self._settings.get("task_panel_state", {}).get("fetch_before_verify", False) @unified_fetch_before_verify.setter def unified_fetch_before_verify(self, value: bool): """设置校验前是否从 API 获取数据""" self._settings.setdefault("task_panel_state", {})["fetch_before_verify"] = value self._save() @property def unified_window_mode(self) -> str: """获取时间窗口模式""" return self._settings.get("task_panel_state", {}).get("window_mode", "lookback") @unified_window_mode.setter def unified_window_mode(self, value: str): """设置时间窗口模式""" self._settings.setdefault("task_panel_state", {})["window_mode"] = value self._save() @property def unified_lookback_hours(self) -> int: """获取回溯小时数""" return self._settings.get("task_panel_state", {}).get("lookback_hours", 24) @unified_lookback_hours.setter def unified_lookback_hours(self, value: int): """设置回溯小时数""" self._settings.setdefault("task_panel_state", {})["lookback_hours"] = value self._save() @property def unified_overlap_seconds(self) -> int: """获取冗余秒数""" return self._settings.get("task_panel_state", {}).get("overlap_seconds", 600) @unified_overlap_seconds.setter def unified_overlap_seconds(self, value: int): """设置冗余秒数""" self._settings.setdefault("task_panel_state", {})["overlap_seconds"] = value self._save() @property def unified_window_split(self) -> str: """获取窗口切分方式""" return self._settings.get("task_panel_state", {}).get("window_split", "day") @unified_window_split.setter def unified_window_split(self, value: str): """设置窗口切分方式""" self._settings.setdefault("task_panel_state", {})["window_split"] = value self._save() @property def unified_window_split_days(self) -> int: """获取窗口切分天数(按天时生效)""" return self._settings.get("task_panel_state", {}).get("window_split_days", 10) @unified_window_split_days.setter def unified_window_split_days(self, value: int): """设置窗口切分天数(按天时生效)""" self._settings.setdefault("task_panel_state", {})["window_split_days"] = value self._save() @property def unified_ods_tasks(self) -> list: """获取 ODS 任务选择""" return self._settings.get("task_panel_state", {}).get("ods_tasks", []) @unified_ods_tasks.setter def unified_ods_tasks(self, value: list): """设置 ODS 任务选择""" self._settings.setdefault("task_panel_state", {})["ods_tasks"] = value self._save() @property def unified_dws_tasks(self) -> list: """获取 DWS 任务选择""" return self._settings.get("task_panel_state", {}).get("dws_tasks", []) @unified_dws_tasks.setter def unified_dws_tasks(self, value: list): """设置 DWS 任务选择""" self._settings.setdefault("task_panel_state", {})["dws_tasks"] = value self._save() @property def unified_dwd_tasks(self) -> list: """获取 DWD 任务选择""" return self._settings.get("task_panel_state", {}).get("dwd_tasks", []) @unified_dwd_tasks.setter def unified_dwd_tasks(self, value: list): """设置 DWD 任务选择""" self._settings.setdefault("task_panel_state", {})["dwd_tasks"] = value self._save() @property def build_tasks(self) -> list: """获取数据建设任务选择""" return self._settings.get("task_panel_state", {}).get("build_tasks", []) @build_tasks.setter def build_tasks(self, value: list): """设置数据建设任务选择""" self._settings.setdefault("task_panel_state", {})["build_tasks"] = value self._save() @property def build_window_mode(self) -> str: """获取数据建设时间窗口模式""" return self._settings.get("task_panel_state", {}).get("build_window_mode", "lookback") @build_window_mode.setter def build_window_mode(self, value: str): """设置数据建设时间窗口模式""" self._settings.setdefault("task_panel_state", {})["build_window_mode"] = value self._save() @property def build_lookback_hours(self) -> int: """获取数据建设回溯小时数""" return self._settings.get("task_panel_state", {}).get("build_lookback_hours", 24) @build_lookback_hours.setter def build_lookback_hours(self, value: int): """设置数据建设回溯小时数""" self._settings.setdefault("task_panel_state", {})["build_lookback_hours"] = value self._save() @property def build_window_start(self) -> str: """获取数据建设窗口开始""" return self._settings.get("task_panel_state", {}).get("build_window_start", "") @build_window_start.setter def build_window_start(self, value: str): """设置数据建设窗口开始""" self._settings.setdefault("task_panel_state", {})["build_window_start"] = value self._save() @property def build_window_end(self) -> str: """获取数据建设窗口结束""" return self._settings.get("task_panel_state", {}).get("build_window_end", "") @build_window_end.setter def build_window_end(self, value: str): """设置数据建设窗口结束""" self._settings.setdefault("task_panel_state", {})["build_window_end"] = value self._save() @property def build_window_split(self) -> str: """获取数据建设窗口切分方式""" return self._settings.get("task_panel_state", {}).get("build_window_split", "day") @build_window_split.setter def build_window_split(self, value: str): """设置数据建设窗口切分方式""" self._settings.setdefault("task_panel_state", {})["build_window_split"] = value self._save() @property def build_window_split_days(self) -> int: """获取数据建设窗口切分天数(按天时生效)""" return self._settings.get("task_panel_state", {}).get("build_window_split_days", 10) @build_window_split_days.setter def build_window_split_days(self, value: int): """设置数据建设窗口切分天数(按天时生效)""" self._settings.setdefault("task_panel_state", {})["build_window_split_days"] = value self._save() @property def index_recall_check(self) -> bool: """获取召回指数复选框状态""" return self._settings.get("task_panel_state", {}).get("index_recall_check", False) @index_recall_check.setter def index_recall_check(self, value: bool): """设置召回指数复选框状态""" self._settings.setdefault("task_panel_state", {})["index_recall_check"] = value self._save() @property def index_winback_check(self) -> bool: """获取老客挽回指数复选框状态""" return self._settings.get("task_panel_state", {}).get("index_winback_check", True) @index_winback_check.setter def index_winback_check(self, value: bool): """设置老客挽回指数复选框状态""" self._settings.setdefault("task_panel_state", {})["index_winback_check"] = value self._save() @property def index_newconv_check(self) -> bool: """获取新客转化指数复选框状态""" return self._settings.get("task_panel_state", {}).get("index_newconv_check", True) @index_newconv_check.setter def index_newconv_check(self, value: bool): """设置新客转化指数复选框状态""" self._settings.setdefault("task_panel_state", {})["index_newconv_check"] = value self._save() @property def index_intimacy_check(self) -> bool: """获取亲密度指数复选框状态""" return self._settings.get("task_panel_state", {}).get("index_intimacy_check", True) @index_intimacy_check.setter def index_intimacy_check(self, value: bool): """设置亲密度指数复选框状态""" self._settings.setdefault("task_panel_state", {})["index_intimacy_check"] = value self._save() @property def index_relation_check(self) -> bool: """获取关系指数复选框状态""" return self._settings.get("task_panel_state", {}).get("index_relation_check", True) @index_relation_check.setter def index_relation_check(self, value: bool): """设置关系指数复选框状态""" self._settings.setdefault("task_panel_state", {})["index_relation_check"] = value self._save() @property def ml_manual_file_path(self) -> str: """获取 ML 人工台账文件路径""" return self._settings.get("task_panel_state", {}).get("ml_manual_file_path", "") @ml_manual_file_path.setter def ml_manual_file_path(self, value: str): """设置 ML 人工台账文件路径""" self._settings.setdefault("task_panel_state", {})["ml_manual_file_path"] = value self._save() @property def index_lookback_days(self) -> int: """获取指数回溯天数""" return self._settings.get("task_panel_state", {}).get("index_lookback_days", 60) @index_lookback_days.setter def index_lookback_days(self, value: int): """设置指数回溯天数""" self._settings.setdefault("task_panel_state", {})["index_lookback_days"] = value self._save() # ==================== 任务历史存储 ==================== def _get_history_path(self) -> Path: """获取任务历史文件路径""" return self._settings_file.parent / "task_history.json" def save_task_history(self, history_list: list): """保存任务历史到文件""" try: history_path = self._get_history_path() # 序列化任务历史 serialized = [] for task in history_list[:100]: # 最多保存100条 try: task_data = { "id": task.id, "tasks": task.config.tasks if hasattr(task, 'config') else [], "status": task.status.value if hasattr(task.status, 'value') else str(task.status), "created_at": task.created_at.isoformat() if task.created_at else None, "started_at": task.started_at.isoformat() if task.started_at else None, "finished_at": task.finished_at.isoformat() if task.finished_at else None, "exit_code": task.exit_code, "error": task.error[:500] if task.error else "", # 限制长度 "output_preview": task.output[:1000] if task.output else "", # 输出预览 # 保存配置信息 "pipeline_flow": task.config.pipeline_flow if hasattr(task, 'config') else "FULL", "window_start": task.config.window_start if hasattr(task, 'config') else None, "window_end": task.config.window_end if hasattr(task, 'config') else None, } serialized.append(task_data) except Exception: continue history_path.write_text( json.dumps(serialized, ensure_ascii=False, indent=2), encoding="utf-8" ) except Exception as e: logging.getLogger(__name__).warning("保存任务历史失败: %s", e) def load_task_history(self) -> list: """从文件加载任务历史""" try: history_path = self._get_history_path() if not history_path.exists(): return [] data = json.loads(history_path.read_text(encoding="utf-8")) return data except Exception as e: logging.getLogger(__name__).warning("加载任务历史失败: %s", e) return [] # 全局单例 app_settings = AppSettings()