# Configuration and Settings Management

## Learning Objectives

- Read and write configuration files (YAML, TOML, JSON)
- Manage environment variables securely
- Implement user preferences and defaults
- Validate configuration with Pydantic
- Handle configuration across different environments

## Why Configuration Management?

Good configuration management allows users to:
- Customize tool behavior without changing code
- Store API keys and secrets securely
- Maintain different settings for dev/prod environments
- Share configuration across team members

## Configuration File Formats

### YAML

Human-readable, great for complex configurations:

```yaml
# config.yaml
app:
  name: "My CLI Tool"
  version: "1.0.0"
  
api:
  endpoint: "https://api.example.com"
  timeout: 30
  
features:
  ai_enabled: true
  max_retries: 3
```

### TOML

Python-native, used by pyproject.toml:

```toml
# config.toml
[app]
name = "My CLI Tool"
version = "1.0.0"

[api]
endpoint = "https://api.example.com"
timeout = 30

[features]
ai_enabled = true
max_retries = 3
```

### JSON

Universal, machine-readable:

```json
{
  "app": {
    "name": "My CLI Tool",
    "version": "1.0.0"
  },
  "api": {
    "endpoint": "https://api.example.com",
    "timeout": 30
  }
}
```

## Reading Configuration Files

Install dependencies:

```bash
pixi add pydantic pyyaml python-dotenv tomli
```

### YAML Configuration

```python
# src/my_cli/config.py
import yaml
from pathlib import Path
from typing import Optional

def load_yaml_config(config_path: Path) -> dict:
    """Load configuration from YAML file."""
    if not config_path.exists():
        raise FileNotFoundError(f"Config file not found: {config_path}")
    
    with open(config_path) as f:
        return yaml.safe_load(f)

def save_yaml_config(config: dict, config_path: Path):
    """Save configuration to YAML file."""
    with open(config_path, 'w') as f:
        yaml.dump(config, f, default_flow_style=False)
```

### TOML Configuration

```python
import tomli
import tomli_w

def load_toml_config(config_path: Path) -> dict:
    """Load configuration from TOML file."""
    with open(config_path, 'rb') as f:
        return tomli.load(f)

def save_toml_config(config: dict, config_path: Path):
    """Save configuration to TOML file."""
    with open(config_path, 'wb') as f:
        tomli_w.dump(config, f)
```

## Environment Variables

### Using python-dotenv

Create `.env` file:

```bash
# .env
API_KEY=your-secret-key-here
API_ENDPOINT=https://api.example.com
DEBUG=true
MAX_RETRIES=3
```

Load in your application:

```python
import os
from dotenv import load_dotenv

# Load .env file
load_dotenv()

# Access variables
api_key = os.getenv("API_KEY")
api_endpoint = os.getenv("API_ENDPOINT", "https://default.api.com")
debug = os.getenv("DEBUG", "false").lower() == "true"
max_retries = int(os.getenv("MAX_RETRIES", "3"))
```

### Security Best Practices

```python
# .gitignore
.env
.env.local
*.secret

# .env.example (commit this)
API_KEY=your-key-here
API_ENDPOINT=https://api.example.com
DEBUG=false
```

## Configuration with Pydantic

Pydantic provides type-safe configuration with validation:

```python
from pydantic import BaseModel, Field, validator
from typing import Optional
from pathlib import Path

class APIConfig(BaseModel):
    """API configuration."""
    endpoint: str = Field(..., description="API endpoint URL")
    key: str = Field(..., description="API key")
    timeout: int = Field(30, ge=1, le=300, description="Request timeout in seconds")
    max_retries: int = Field(3, ge=0, le=10)
    
    @validator('endpoint')
    def validate_endpoint(cls, v):
        if not v.startswith(('http://', 'https://')):
            raise ValueError('Endpoint must start with http:// or https://')
        return v

class AppConfig(BaseModel):
    """Application configuration."""
    name: str = "My CLI Tool"
    version: str = "1.0.0"
    debug: bool = False
    data_dir: Path = Field(default_factory=lambda: Path.home() / ".my-cli")
    
    api: APIConfig
    
    class Config:
        env_prefix = "MYCLI_"  # Environment variables start with MYCLI_

# Usage
config = AppConfig(
    api=APIConfig(
        endpoint="https://api.example.com",
        key=os.getenv("API_KEY")
    )
)

# Validation happens automatically
print(config.api.timeout)  # 30
print(config.data_dir)     # /home/user/.my-cli
```

## Configuration Manager

Complete configuration management system:

```python
# src/my_cli/config.py
from pathlib import Path
from typing import Optional
import os
import yaml
from pydantic import BaseModel, Field
from dotenv import load_dotenv

class Config(BaseModel):
    """Application configuration."""
    
    # App settings
    app_name: str = "my-cli"
    debug: bool = False
    
    # Paths
    config_dir: Path = Field(default_factory=lambda: Path.home() / ".my-cli")
    data_dir: Path = Field(default_factory=lambda: Path.home() / ".my-cli" / "data")
    
    # API settings
    api_endpoint: str = "https://api.example.com"
    api_key: Optional[str] = None
    api_timeout: int = 30
    
    # Feature flags
    ai_enabled: bool = True
    max_retries: int = 3
    
    class Config:
        env_prefix = "MYCLI_"

class ConfigManager:
    """Manage application configuration."""
    
    def __init__(self, config_path: Optional[Path] = None):
        self.config_path = config_path or Path.home() / ".my-cli" / "config.yaml"
        self.config: Optional[Config] = None
    
    def load(self) -> Config:
        """Load configuration from file and environment."""
        # Load .env file if it exists
        load_dotenv()
        
        # Start with defaults
        config_data = {}
        
        # Load from file if exists
        if self.config_path.exists():
            with open(self.config_path) as f:
                config_data = yaml.safe_load(f) or {}
        
        # Override with environment variables
        env_overrides = {
            'debug': os.getenv('MYCLI_DEBUG', '').lower() == 'true',
            'api_key': os.getenv('MYCLI_API_KEY'),
            'api_endpoint': os.getenv('MYCLI_API_ENDPOINT'),
        }
        
        # Remove None values
        env_overrides = {k: v for k, v in env_overrides.items() if v is not None}
        
        # Merge configurations (env takes precedence)
        config_data.update(env_overrides)
        
        # Create and validate config
        self.config = Config(**config_data)
        
        # Ensure directories exist
        self.config.config_dir.mkdir(parents=True, exist_ok=True)
        self.config.data_dir.mkdir(parents=True, exist_ok=True)
        
        return self.config
    
    def save(self):
        """Save current configuration to file."""
        if not self.config:
            raise ValueError("No configuration loaded")
        
        self.config_path.parent.mkdir(parents=True, exist_ok=True)
        
        # Convert to dict, excluding sensitive data
        config_dict = self.config.dict(exclude={'api_key'})
        
        with open(self.config_path, 'w') as f:
            yaml.dump(config_dict, f, default_flow_style=False)
    
    def get(self, key: str, default=None):
        """Get configuration value."""
        if not self.config:
            self.load()
        return getattr(self.config, key, default)
    
    def set(self, key: str, value):
        """Set configuration value."""
        if not self.config:
            self.load()
        setattr(self.config, key, value)
        self.save()

# Global config instance
_config_manager = None

def get_config() -> Config:
    """Get global configuration instance."""
    global _config_manager
    if _config_manager is None:
        _config_manager = ConfigManager()
        _config_manager.load()
    return _config_manager.config
```

## Using Configuration in CLI

```python
# src/my_cli/cli.py
import typer
from rich.console import Console
from .config import get_config, ConfigManager

app = typer.Typer()
console = Console()

@app.command()
def show_config():
    """Show current configuration."""
    config = get_config()
    
    console.print("[bold cyan]Current Configuration:[/bold cyan]\n")
    console.print(f"App Name: {config.app_name}")
    console.print(f"Debug: {config.debug}")
    console.print(f"Config Dir: {config.config_dir}")
    console.print(f"API Endpoint: {config.api_endpoint}")
    console.print(f"AI Enabled: {config.ai_enabled}")

@app.command()
def set_config(key: str, value: str):
    """Set a configuration value."""
    manager = ConfigManager()
    manager.load()
    
    # Type conversion
    if value.lower() in ('true', 'false'):
        value = value.lower() == 'true'
    elif value.isdigit():
        value = int(value)
    
    manager.set(key, value)
    console.print(f"[green]✓ Set {key} = {value}[/green]")

@app.command()
def init_config():
    """Initialize configuration with defaults."""
    manager = ConfigManager()
    manager.load()
    manager.save()
    console.print(f"[green]✓ Configuration initialized at {manager.config_path}[/green]")
```

## Multi-Environment Configuration

Support different configurations for dev/staging/prod:

```python
from enum import Enum

class Environment(str, Enum):
    DEV = "development"
    STAGING = "staging"
    PROD = "production"

class EnvironmentConfig(BaseModel):
    """Environment-specific configuration."""
    environment: Environment = Environment.DEV
    
    @property
    def is_production(self) -> bool:
        return self.environment == Environment.PROD
    
    @property
    def is_development(self) -> bool:
        return self.environment == Environment.DEV

# Load based on environment
env = os.getenv("MYCLI_ENV", "development")
config_file = f"config.{env}.yaml"
```

## Configuration Validation

```python
from pydantic import validator, root_validator

class Config(BaseModel):
    api_key: Optional[str] = None
    ai_enabled: bool = False
    
    @validator('api_key')
    def validate_api_key(cls, v, values):
        if values.get('ai_enabled') and not v:
            raise ValueError('API key required when AI is enabled')
        return v
    
    @root_validator
    def validate_config(cls, values):
        """Validate entire configuration."""
        if values.get('debug') and values.get('environment') == 'production':
            raise ValueError('Debug mode not allowed in production')
        return values
```

## Best Practices

1. **Use environment variables for secrets**: Never commit API keys or passwords
2. **Provide defaults**: Make configuration optional with sensible defaults
3. **Validate early**: Use Pydantic to catch configuration errors at startup
4. **Document configuration**: Provide example config files
5. **Support multiple formats**: Allow YAML, TOML, or JSON based on user preference

## Using Copilot

Ask Copilot to help:

- "Create a Pydantic model for API configuration with validation"
- "Generate a function to load YAML config with error handling"
- "Add environment variable support to configuration loader"
- "Create a CLI command to show and edit configuration"

## Next Steps

With configuration management in place, you're ready to integrate AI capabilities into your CLI tool in Chapter 3.

## Resources

- [Pydantic Documentation](https://docs.pydantic.dev/)
- [python-dotenv](https://github.com/theskumar/python-dotenv)
- [PyYAML Documentation](https://pyyaml.org/wiki/PyYAMLDocumentation)
