Files
Neo-ZQYY/gui/utils/app_settings.py

838 lines
31 KiB
Python

# -*- coding: utf-8 -*-
# AI_CHANGELOG [2026-02-13] 移除 index_intimacy_check 属性
"""应用程序设置管理"""
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_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()