| |
|
|
| |
|
|
| - 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 |
|
|
| |
|
|
| 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 |
|
|
| |
|
|
| |
|
|
| Human-readable, great for complex configurations: |
|
|
| ```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 |
| ``` |
|
|
| |
|
|
| Python-native, used by pyproject.toml: |
|
|
| ```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 |
| ``` |
|
|
| |
|
|
| Universal, machine-readable: |
|
|
| ```json |
| { |
| "app": { |
| "name": "My CLI Tool", |
| "version": "1.0.0" |
| }, |
| "api": { |
| "endpoint": "https://api.example.com", |
| "timeout": 30 |
| } |
| } |
| ``` |
|
|
| |
|
|
| Install dependencies: |
|
|
| ```bash |
| pixi add pydantic pyyaml python-dotenv tomli |
| ``` |
|
|
| |
|
|
| ```python |
| |
| 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) |
| ``` |
|
|
| |
|
|
| ```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) |
| ``` |
|
|
| |
|
|
| |
|
|
| Create `.env` file: |
|
|
| ```bash |
| |
| 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_dotenv() |
|
|
| |
| 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")) |
| ``` |
|
|
| |
|
|
| ```python |
| |
| .env |
| .env.local |
| *.secret |
|
|
| |
| API_KEY=your-key-here |
| API_ENDPOINT=https://api.example.com |
| DEBUG=false |
| ``` |
|
|
| |
|
|
| 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_" |
|
|
| |
| config = AppConfig( |
| api=APIConfig( |
| endpoint="https://api.example.com", |
| key=os.getenv("API_KEY") |
| ) |
| ) |
|
|
| |
| print(config.api.timeout) |
| print(config.data_dir) |
| ``` |
|
|
| |
|
|
| Complete configuration management system: |
|
|
| ```python |
| |
| 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_name: str = "my-cli" |
| debug: bool = False |
| |
| |
| config_dir: Path = Field(default_factory=lambda: Path.home() / ".my-cli") |
| data_dir: Path = Field(default_factory=lambda: Path.home() / ".my-cli" / "data") |
| |
| |
| api_endpoint: str = "https://api.example.com" |
| api_key: Optional[str] = None |
| api_timeout: int = 30 |
| |
| |
| 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_dotenv() |
| |
| |
| config_data = {} |
| |
| |
| if self.config_path.exists(): |
| with open(self.config_path) as f: |
| config_data = yaml.safe_load(f) or {} |
| |
| |
| env_overrides = { |
| 'debug': os.getenv('MYCLI_DEBUG', '').lower() == 'true', |
| 'api_key': os.getenv('MYCLI_API_KEY'), |
| 'api_endpoint': os.getenv('MYCLI_API_ENDPOINT'), |
| } |
| |
| |
| env_overrides = {k: v for k, v in env_overrides.items() if v is not None} |
| |
| |
| config_data.update(env_overrides) |
| |
| |
| self.config = Config(**config_data) |
| |
| |
| 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) |
| |
| |
| 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() |
|
|
| |
| _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 |
| ``` |
|
|
| |
|
|
| ```python |
| |
| 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() |
| |
| |
| 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]") |
| ``` |
|
|
| |
|
|
| 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 |
|
|
| |
| env = os.getenv("MYCLI_ENV", "development") |
| config_file = f"config.{env}.yaml" |
| ``` |
|
|
| |
|
|
| ```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 |
| ``` |
|
|
| |
|
|
| 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 |
|
|
| |
|
|
| 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" |
|
|
| |
|
|
| 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) |
| |