129 lines
3.8 KiB
Python
129 lines
3.8 KiB
Python
"""Configuration loader: env vars -> config.yaml -> defaults."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
from dotenv import load_dotenv
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parent.parent
|
|
load_dotenv(ROOT_DIR / ".env")
|
|
|
|
|
|
@dataclass
|
|
class ClickUpConfig:
|
|
api_token: str = ""
|
|
space_id: str = ""
|
|
task_type_field_name: str = "Work Category"
|
|
# Custom field names (must match ClickUp exactly)
|
|
delegate_field_name: str = "Delegate to Claude"
|
|
stage_field_name: str = "Stage"
|
|
error_field_name: str = "Error"
|
|
# Statuses
|
|
ai_working_status: str = "ai working"
|
|
review_status: str = "review"
|
|
client_review_status: str = "client review"
|
|
complete_status: str = "complete"
|
|
|
|
|
|
@dataclass
|
|
class AutoCoraConfig:
|
|
jobs_dir: str = "//PennQnap1/SHARE1/AutoCora/jobs"
|
|
results_dir: str = "//PennQnap1/SHARE1/AutoCora/results"
|
|
xlsx_dir: str = "//PennQnap1/SHARE1/Cora-For-Human"
|
|
poll_interval_seconds: int = 120
|
|
|
|
|
|
@dataclass
|
|
class BLMConfig:
|
|
blm_dir: str = "E:/dev/Big-Link-Man"
|
|
timeout_seconds: int = 1800 # 30 minutes
|
|
|
|
|
|
@dataclass
|
|
class NASConfig:
|
|
generated_dir: str = "//PennQnap1/SHARE1/generated"
|
|
|
|
|
|
@dataclass
|
|
class RunnerConfig:
|
|
poll_interval_seconds: int = 720
|
|
claude_timeout_seconds: int = 2700 # 45 minutes
|
|
max_turns_default: int = 10
|
|
temp_dir: str = "" # empty = system temp
|
|
|
|
|
|
@dataclass
|
|
class NtfyConfig:
|
|
enabled: bool = False
|
|
server: str = "https://ntfy.sh"
|
|
error_topic: str = ""
|
|
success_topic: str = ""
|
|
|
|
|
|
@dataclass
|
|
class Config:
|
|
clickup: ClickUpConfig = field(default_factory=ClickUpConfig)
|
|
autocora: AutoCoraConfig = field(default_factory=AutoCoraConfig)
|
|
blm: BLMConfig = field(default_factory=BLMConfig)
|
|
nas: NASConfig = field(default_factory=NASConfig)
|
|
runner: RunnerConfig = field(default_factory=RunnerConfig)
|
|
ntfy: NtfyConfig = field(default_factory=NtfyConfig)
|
|
|
|
# Derived paths
|
|
root_dir: Path = field(default_factory=lambda: ROOT_DIR)
|
|
skills_dir: Path = field(default_factory=lambda: ROOT_DIR / "skills")
|
|
db_path: Path = field(default_factory=lambda: ROOT_DIR / "data" / "clickup_runner.db")
|
|
|
|
|
|
def _apply_section(cfg_obj, data: dict):
|
|
"""Apply a dict of values to a dataclass instance, skipping unknown keys."""
|
|
for k, v in data.items():
|
|
if hasattr(cfg_obj, k):
|
|
setattr(cfg_obj, k, v)
|
|
|
|
|
|
def load_config(yaml_path: Path | None = None) -> Config:
|
|
"""Load config from env vars -> config.yaml -> defaults."""
|
|
cfg = Config()
|
|
|
|
# Load YAML if exists
|
|
if yaml_path is None:
|
|
yaml_path = ROOT_DIR / "clickup_runner.yaml"
|
|
if yaml_path.exists():
|
|
with open(yaml_path) as f:
|
|
data = yaml.safe_load(f) or {}
|
|
|
|
for section_name in ("clickup", "autocora", "blm", "nas", "runner", "ntfy"):
|
|
if section_name in data and isinstance(data[section_name], dict):
|
|
_apply_section(getattr(cfg, section_name), data[section_name])
|
|
|
|
# Env var overrides
|
|
if token := os.getenv("CLICKUP_API_TOKEN"):
|
|
cfg.clickup.api_token = token
|
|
if space := os.getenv("CLICKUP_SPACE_ID"):
|
|
cfg.clickup.space_id = space
|
|
|
|
# ntfy topics from env vars
|
|
if topic := os.getenv("NTFY_ERROR_TOPIC"):
|
|
cfg.ntfy.error_topic = topic
|
|
if topic := os.getenv("NTFY_SUCCESS_TOPIC"):
|
|
cfg.ntfy.success_topic = topic
|
|
|
|
# Validate required fields
|
|
if not cfg.clickup.api_token:
|
|
log.warning("CLICKUP_API_TOKEN not set -- runner will not be able to poll")
|
|
if not cfg.clickup.space_id:
|
|
log.warning("CLICKUP_SPACE_ID not set -- runner will not be able to poll")
|
|
|
|
# Ensure data dir exists
|
|
cfg.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
return cfg
|