eight-hourr/backend/config.py

209 lines
5.9 KiB
Python

from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from paths import DATA_DIR, EXTENSION_DIR, ROOT_DIR, get_dates_file_path, get_env_files
from services.settings_store import load_overrides, save_overrides
CONFIGURABLE_FIELDS = [
"gitlab_url",
"gitlab_token",
"gitlab_project_path",
"gitlab_board_id",
"gitlab_board_labels",
"gitlab_milestone_title",
"pts_url",
"pts_project_name",
"pts_default_task_name",
"auto_fill_enabled",
"auto_fill_hour",
"auto_fill_minute",
"max_hours_per_day",
"hour_step",
"xai_api_key",
"xai_base_url",
"grok_model",
"use_llm",
"use_description_variation",
"pts_username",
"pts_password",
"teams_username",
"teams_password",
"teams_calendar_enabled",
"teams_calendar_timezone",
"teams_meeting_task_name",
"teams_headless",
"teams_meeting_exclude_keywords",
# CompBase attendance punch (independent of PTS)
"compbase_list_url",
"compbase_fill_url",
"compbase_username",
"compbase_password",
"compbase_default_days",
"compbase_out_time_mode",
# legacy Graph fields kept for backward-compatible settings files
"azure_client_id",
"azure_tenant_id",
]
class EnvSettings(BaseSettings):
model_config = SettingsConfigDict(
env_file=get_env_files() or None,
env_file_encoding="utf-8",
extra="ignore",
)
gitlab_url: str = "https://gitlab.supermicro.com"
gitlab_token: str = ""
gitlab_project_path: str = "super-cloud/scc-flex"
gitlab_board_id: int = 1126
gitlab_board_labels: str = "Category::BugFix,Type::Bug"
gitlab_milestone_title: str = ""
pts_url: str = "https://tw-timesheet.supermicro.com/PTS"
pts_project_name: str = "SuperCloud Composer"
pts_default_task_name: str = "Implement"
pts_username: str = ""
pts_password: str = ""
auto_fill_enabled: bool = True
auto_fill_hour: int = 18
auto_fill_minute: int = 0
backend_host: str = "0.0.0.0"
backend_port: int = 8765
max_hours_per_day: float = 8.0
hour_step: float = 0.5
xai_api_key: str = ""
xai_base_url: str = "https://api.x.ai"
grok_model: str = "grok-3-mini"
use_llm: bool = True
use_description_variation: bool = True
teams_calendar_enabled: bool = False
teams_username: str = ""
teams_password: str = ""
teams_calendar_timezone: str = "Asia/Taipei"
teams_meeting_task_name: str = "Meeting"
teams_headless: bool = False
teams_meeting_exclude_keywords: str = "貼心的提醒,請記得下班刷退"
azure_client_id: str = ""
azure_tenant_id: str = "organizations"
compbase_list_url: str = "http://tw-compbase.supermicro.com:6699"
compbase_fill_url: str = "http://tw-compbase.supermicro.com:6671"
compbase_username: str = ""
compbase_password: str = ""
compbase_default_days: int = 14
compbase_out_time_mode: str = "expected"
class AppSettings(BaseModel):
gitlab_url: str = "https://gitlab.supermicro.com"
gitlab_token: str = ""
gitlab_project_path: str = "super-cloud/scc-flex"
gitlab_board_id: int = 1126
gitlab_board_labels: str = "Category::BugFix,Type::Bug"
gitlab_milestone_title: str = ""
pts_url: str = "https://tw-timesheet.supermicro.com/PTS"
pts_project_name: str = "SuperCloud Composer"
pts_default_task_name: str = "Implement"
pts_username: str = ""
pts_password: str = ""
auto_fill_enabled: bool = True
auto_fill_hour: int = 18
auto_fill_minute: int = 0
backend_host: str = "0.0.0.0"
backend_port: int = 8765
max_hours_per_day: float = 8.0
hour_step: float = 0.5
xai_api_key: str = ""
xai_base_url: str = "https://api.x.ai"
grok_model: str = "grok-3-mini"
use_llm: bool = True
use_description_variation: bool = True
teams_calendar_enabled: bool = False
teams_username: str = ""
teams_password: str = ""
teams_calendar_timezone: str = "Asia/Taipei"
teams_meeting_task_name: str = "Meeting"
teams_headless: bool = False
teams_meeting_exclude_keywords: str = "貼心的提醒,請記得下班刷退"
azure_client_id: str = ""
azure_tenant_id: str = "organizations"
compbase_list_url: str = "http://tw-compbase.supermicro.com:6699"
compbase_fill_url: str = "http://tw-compbase.supermicro.com:6671"
compbase_username: str = ""
compbase_password: str = ""
compbase_default_days: int = 14
compbase_out_time_mode: str = "expected"
@property
def teams_meeting_exclude_keyword_list(self) -> list[str]:
return [part.strip() for part in self.teams_meeting_exclude_keywords.split(",") if part.strip()]
@property
def gitlab_board_label_list(self) -> list[str]:
return [part.strip() for part in self.gitlab_board_labels.split(",") if part.strip()]
_env = EnvSettings()
def build_settings() -> AppSettings:
data = _env.model_dump()
overrides = load_overrides()
for key in CONFIGURABLE_FIELDS:
if key in overrides and overrides[key] is not None:
data[key] = overrides[key]
return AppSettings(**data)
def get_settings_dict() -> dict[str, Any]:
return build_settings().model_dump()
def update_settings(updates: dict[str, Any]) -> AppSettings:
current = load_overrides()
merged = {**current}
for key in CONFIGURABLE_FIELDS:
if key not in updates:
continue
value = updates[key]
if value is not None:
merged[key] = value
save_overrides(merged)
return reload_settings()
def reload_settings() -> AppSettings:
global _runtime
_runtime = build_settings()
return _runtime
_runtime = build_settings()
class SettingsProxy:
def __getattr__(self, name: str) -> Any:
return getattr(_runtime, name)
def __repr__(self) -> str:
return f"SettingsProxy({_runtime!r})"
settings = SettingsProxy()