Configuration System Improvements: - Add automatic .env.tddai file loading without external dependencies - Implement load_dotenv_file() helper for lightweight env file parsing - Maintain configuration hierarchy: Environment → .env.tddai → Defaults - Zero breaking changes - existing setup script approach still works Documentation: - Create comprehensive CONFIG.md with configuration management guide - Document hierarchy, options, platform examples, and troubleshooting - Include migration instructions and best practices - Cover both auto-loading and manual configuration methods Benefits: - Users no longer need to manually source setup scripts - Project-agnostic configuration system remains flexible - Improved developer experience with seamless config loading - Complete documentation for configuration management 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
3.5 KiB
Python
129 lines
3.5 KiB
Python
"""
|
|
Configuration management for tddai.
|
|
|
|
The tddai framework is project-agnostic and can be configured per project
|
|
via environment variables:
|
|
|
|
- TDDAI_WORKSPACE_DIR: Workspace directory name (default: .tddai_workspace)
|
|
- TDDAI_GITEA_URL: Git platform URL
|
|
- TDDAI_REPO_OWNER: Repository owner/organization
|
|
- TDDAI_REPO_NAME: Repository name
|
|
|
|
Example .env file for a project:
|
|
```
|
|
TDDAI_WORKSPACE_DIR=.myproject_workspace
|
|
TDDAI_GITEA_URL=https://github.com
|
|
TDDAI_REPO_OWNER=myusername
|
|
TDDAI_REPO_NAME=myproject
|
|
```
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from dataclasses import dataclass
|
|
|
|
from .exceptions import ConfigurationError
|
|
|
|
|
|
def load_dotenv_file(env_file: Path) -> None:
|
|
"""Load environment variables from a .env file."""
|
|
if not env_file.exists():
|
|
return
|
|
|
|
with open(env_file, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line and not line.startswith('#') and '=' in line:
|
|
key, value = line.split('=', 1)
|
|
os.environ.setdefault(key.strip(), value.strip())
|
|
|
|
|
|
@dataclass
|
|
class TddaiConfig:
|
|
"""Configuration settings for tddai."""
|
|
|
|
# Workspace settings
|
|
workspace_dir: Path = Path(".tddai_workspace")
|
|
current_issue_file: str = "current_issue.json"
|
|
|
|
# Git repository settings (must be configured per project)
|
|
gitea_url: str = ""
|
|
repo_owner: str = ""
|
|
repo_name: str = ""
|
|
|
|
# Test settings
|
|
tests_dir: Path = Path("tests")
|
|
test_file_pattern: str = "test_issue_{issue_num}_{scenario}.py"
|
|
|
|
# AI settings
|
|
claude_code_command: str = "claude"
|
|
|
|
@property
|
|
def issues_api_url(self) -> str:
|
|
"""Get the full issues API URL."""
|
|
return f"{self.gitea_url}/api/v1/repos/{self.repo_owner}/{self.repo_name}/issues"
|
|
|
|
@property
|
|
def current_issue_path(self) -> Path:
|
|
"""Get the path to current issue file."""
|
|
return self.workspace_dir / self.current_issue_file
|
|
|
|
@classmethod
|
|
def from_environment(cls) -> "TddaiConfig":
|
|
"""Create config from environment variables and .env files."""
|
|
# Auto-load .env.tddai file if it exists
|
|
env_file = Path(".env.tddai")
|
|
load_dotenv_file(env_file)
|
|
|
|
config = cls()
|
|
|
|
# Override with environment variables if present
|
|
gitea_url = os.getenv("TDDAI_GITEA_URL")
|
|
if gitea_url:
|
|
config.gitea_url = gitea_url
|
|
|
|
repo_owner = os.getenv("TDDAI_REPO_OWNER")
|
|
if repo_owner:
|
|
config.repo_owner = repo_owner
|
|
|
|
repo_name = os.getenv("TDDAI_REPO_NAME")
|
|
if repo_name:
|
|
config.repo_name = repo_name
|
|
|
|
workspace_dir = os.getenv("TDDAI_WORKSPACE_DIR")
|
|
if workspace_dir:
|
|
config.workspace_dir = Path(workspace_dir)
|
|
|
|
return config
|
|
|
|
def validate(self) -> None:
|
|
"""Validate configuration settings."""
|
|
if not self.gitea_url:
|
|
raise ConfigurationError("gitea_url cannot be empty")
|
|
|
|
if not self.repo_owner:
|
|
raise ConfigurationError("repo_owner cannot be empty")
|
|
|
|
if not self.repo_name:
|
|
raise ConfigurationError("repo_name cannot be empty")
|
|
|
|
|
|
# Global config instance
|
|
_config: Optional[TddaiConfig] = None
|
|
|
|
|
|
def get_config() -> TddaiConfig:
|
|
"""Get the global configuration instance."""
|
|
global _config
|
|
if _config is None:
|
|
_config = TddaiConfig.from_environment()
|
|
_config.validate()
|
|
return _config
|
|
|
|
|
|
def set_config(config: TddaiConfig) -> None:
|
|
"""Set the global configuration instance."""
|
|
global _config
|
|
config.validate()
|
|
_config = config |