CheddahBot/clickup_runner/config.py

130 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/Cora72-for-macro"
poll_interval_seconds: int = 120
@dataclass
class BLMConfig:
blm_dir: str = "E:/dev/Big-Link-Man"
cora_inbox: str = "//PennQnap1/SHARE1/cora-inbox"
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