"""配置加载。 凭据解析优先级:环境变量 QT_PASSWORD > Windows DPAPI 文件 > 配置文件明文 > 交互输入。 配置文件默认 config/config.json(已在 .gitignore 中)。 """ from __future__ import annotations import getpass import json import os from dataclasses import dataclass, field from pathlib import Path DEFAULT_CONFIG = "config/config.json" @dataclass class Config: # 连接 base_url: str username: str password: str = field(default="", repr=False) timeout: int = 60 # 业务 team_name: str = "当前项目" work_dir: str = "" sprint: str = "" start_time: str = "" end_time: str = "" content_desc: str = "需求开发测试" service_type: str = "DevelopService" belong_team: str = "" # 三个责任人;留空则用登录用户的真实姓名 evaluator: str = "" technical_leader: str = "" customer_leader: str = "" # TFS tfs_project: str = "Tools_Dev" tfs_team: str = "DataTechnologies" area_path: str = "" iteration_path: str = "" # 运行 workers: int = 2 db_path: str = "state/qtimport.db" def resolved_roles(self, operator: str) -> tuple[str, str, str]: """三个责任人,缺省都落到操作人(与现有脚本一致)。""" return ( self.evaluator or operator, self.technical_leader or operator, self.customer_leader or operator, ) @classmethod def load(cls, path: str | os.PathLike = DEFAULT_CONFIG, *, need_password: bool = True) -> "Config": file_path = Path(path) if not file_path.exists(): raise SystemExit(f"缺少配置文件:{file_path}(可从 config/config.example.json 复制)") with open(file_path, encoding="utf-8-sig") as handle: raw = json.load(handle) allowed = {f for f in cls.__dataclass_fields__} unknown = set(raw) - allowed if unknown: raise SystemExit(f"配置文件含未知字段:{sorted(unknown)}") cfg = cls(**raw) cfg.password = resolve_password(cfg.password, need=need_password) if not cfg.base_url or not cfg.username: raise SystemExit("配置缺少 base_url 或 username") if need_password and not cfg.password: raise SystemExit("未提供密码:设置 QT_PASSWORD 环境变量或在配置中填写") return cfg def resolve_password(configured: str, *, need: bool = True) -> str: env = os.environ.get("QT_PASSWORD") if env: return env if configured and configured != "REPLACE_ME": return configured if not need: return "" if not os.isatty(0): # 非交互(计划任务/CI) return "" return getpass.getpass("QuickTeam 密码:") def load_raw(path: str | os.PathLike = DEFAULT_CONFIG) -> dict: """读原始 JSON(plan 阶段不需要密码时用)。""" with open(Path(path), encoding="utf-8-sig") as handle: return json.load(handle)