Files
2025-11-30 09:04:14 +08:00

11 KiB

Advanced Fire CLI Patterns

This guide covers advanced patterns and techniques for building sophisticated Fire CLIs.

Pattern 1: Configuration Management with Persistence

Implement persistent configuration storage:

import fire
from rich.console import Console
from pathlib import Path
import json

console = Console()

class ConfigManager:
    """Handle configuration file I/O"""

    def __init__(self, config_path: Path):
        self.config_path = config_path
        self._ensure_exists()

    def _ensure_exists(self):
        """Create config file if missing"""
        self.config_path.parent.mkdir(parents=True, exist_ok=True)
        if not self.config_path.exists():
            self.save({})

    def load(self) -> dict:
        """Load configuration"""
        try:
            return json.loads(self.config_path.read_text())
        except Exception as e:
            console.print(f"[red]Error loading config: {e}[/red]")
            return {}

    def save(self, config: dict):
        """Save configuration"""
        self.config_path.write_text(json.dumps(config, indent=2))

class MyCLI:
    def __init__(self):
        self.config_manager = ConfigManager(
            Path.home() / ".mycli" / "config.json"
        )

    def configure(self, key, value):
        """Set configuration value"""
        config = self.config_manager.load()
        config[key] = value
        self.config_manager.save(config)
        console.print(f"[green]✓[/green] Set {key} = {value}")

Pattern 2: Environment-Based Configuration

Handle multiple environments:

from enum import Enum

class Environment(str, Enum):
    DEV = "dev"
    STAGING = "staging"
    PRODUCTION = "production"

class MyCLI:
    def __init__(self):
        self.current_env = Environment.DEV

    def set_env(self, env: Environment):
        """Switch environment

        Args:
            env: Target environment (dev, staging, production)
        """
        self.current_env = env
        console.print(f"[cyan]Environment: {env.value}[/cyan]")

    def deploy(self):
        """Deploy to current environment"""
        console.print(f"Deploying to {self.current_env.value}")

# Usage:
# python cli.py set-env staging
# python cli.py deploy

Pattern 3: Property Access for Read-Only Values

Use properties for values that should be readable but not settable:

class MyCLI:
    def __init__(self):
        self._version = "1.0.0"
        self._config_path = Path.home() / ".mycli"

    @property
    def version(self):
        """Get CLI version"""
        return self._version

    @property
    def config_path(self):
        """Get configuration path"""
        return str(self._config_path)

# Usage:
# python cli.py version      # Returns "1.0.0"
# python cli.py config-path  # Returns path

Pattern 4: Validation and Error Handling

Implement robust validation:

from pathlib import Path

class MyCLI:
    def deploy(self, path: str, confirm=False):
        """Deploy from path

        Args:
            path: Path to deployment files
            confirm: Skip confirmation prompt
        """
        deploy_path = Path(path)

        # Validate path exists
        if not deploy_path.exists():
            console.print(f"[red]Error: Path not found: {path}[/red]")
            return {"status": "error", "message": "Path not found"}

        # Validate path is directory
        if not deploy_path.is_dir():
            console.print(f"[red]Error: Not a directory: {path}[/red]")
            return {"status": "error", "message": "Not a directory"}

        # Require confirmation for sensitive operations
        if not confirm:
            console.print("[yellow]⚠ Use --confirm to proceed[/yellow]")
            return {"status": "cancelled"}

        # Perform deployment
        console.print(f"[green]Deploying from {path}...[/green]")
        return {"status": "success"}

Pattern 5: Chaining Commands

Create chainable command patterns:

class Builder:
    """Build pipeline with chaining"""

    def __init__(self):
        self.steps = []

    def clean(self):
        """Add clean step"""
        self.steps.append("clean")
        return self  # Return self for chaining

    def build(self):
        """Add build step"""
        self.steps.append("build")
        return self

    def test(self):
        """Add test step"""
        self.steps.append("test")
        return self

    def execute(self):
        """Execute pipeline"""
        for step in self.steps:
            console.print(f"[cyan]Running: {step}[/cyan]")
        return {"steps": self.steps}

class MyCLI:
    def __init__(self):
        self.builder = Builder()

# Usage:
# python cli.py builder clean
# python cli.py builder build
# python cli.py builder clean build test execute

Pattern 6: Context Managers for Resources

Use context managers for resource handling:

from contextlib import contextmanager

class MyCLI:
    @contextmanager
    def _deployment_context(self, env):
        """Context manager for deployments"""
        console.print(f"[cyan]Starting deployment to {env}...[/cyan]")
        try:
            yield
            console.print("[green]✓[/green] Deployment successful")
        except Exception as e:
            console.print(f"[red]✗ Deployment failed: {e}[/red]")
            raise
        finally:
            console.print("[dim]Cleanup complete[/dim]")

    def deploy(self, env='staging'):
        """Deploy with context management"""
        with self._deployment_context(env):
            # Deployment logic here
            console.print("  [dim]Building...[/dim]")
            console.print("  [dim]Uploading...[/dim]")

Pattern 7: Plugin Architecture

Create extensible CLI with plugins:

from abc import ABC, abstractmethod
from typing import List

class Plugin(ABC):
    """Base plugin class"""

    @abstractmethod
    def execute(self, *args, **kwargs):
        """Execute plugin"""
        pass

class DatabasePlugin(Plugin):
    """Database operations plugin"""

    def execute(self, operation):
        console.print(f"[cyan]Database: {operation}[/cyan]")

class CachePlugin(Plugin):
    """Cache operations plugin"""

    def execute(self, operation):
        console.print(f"[yellow]Cache: {operation}[/yellow]")

class MyCLI:
    def __init__(self):
        self.plugins: List[Plugin] = [
            DatabasePlugin(),
            CachePlugin()
        ]

    def run_plugins(self, operation):
        """Execute operation on all plugins

        Args:
            operation: Operation to run
        """
        for plugin in self.plugins:
            plugin.execute(operation)

Pattern 8: Async Operations

Handle async operations in Fire CLI:

import asyncio
from typing import List

class MyCLI:
    def fetch(self, urls: List[str]):
        """Fetch multiple URLs concurrently

        Args:
            urls: List of URLs to fetch
        """
        async def fetch_all():
            tasks = [self._fetch_one(url) for url in urls]
            return await asyncio.gather(*tasks)

        results = asyncio.run(fetch_all())
        return {"fetched": len(results)}

    async def _fetch_one(self, url):
        """Fetch single URL"""
        # Simulated async fetch
        await asyncio.sleep(0.1)
        return url

# Usage:
# python cli.py fetch https://example.com https://google.com

Pattern 9: Dry Run Mode

Implement dry-run capability:

class MyCLI:
    def __init__(self):
        self.dry_run = False

    def deploy(self, env, dry_run=False):
        """Deploy to environment

        Args:
            env: Target environment
            dry_run: Show what would happen without executing
        """
        self.dry_run = dry_run

        if self.dry_run:
            console.print("[yellow]DRY RUN MODE[/yellow]")

        self._execute("Build project", lambda: self._build())
        self._execute("Run tests", lambda: self._test())
        self._execute("Upload files", lambda: self._upload(env))

    def _execute(self, description, action):
        """Execute or simulate action"""
        if self.dry_run:
            console.print(f"[dim]Would: {description}[/dim]")
        else:
            console.print(f"[cyan]{description}...[/cyan]")
            action()

    def _build(self):
        pass  # Build logic

    def _test(self):
        pass  # Test logic

    def _upload(self, env):
        pass  # Upload logic

Pattern 10: Logging Integration

Integrate structured logging:

import logging
from pathlib import Path

class MyCLI:
    def __init__(self):
        self._setup_logging()

    def _setup_logging(self):
        """Configure logging"""
        log_file = Path.home() / ".mycli" / "cli.log"
        log_file.parent.mkdir(parents=True, exist_ok=True)

        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler(log_file),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger(__name__)

    def deploy(self, env):
        """Deploy with logging"""
        self.logger.info(f"Starting deployment to {env}")
        try:
            console.print(f"[cyan]Deploying to {env}...[/cyan]")
            # Deployment logic
            self.logger.info("Deployment successful")
            console.print("[green]✓[/green] Deployed!")
        except Exception as e:
            self.logger.error(f"Deployment failed: {e}")
            console.print(f"[red]✗ Error: {e}[/red]")
            raise

Pattern 11: Interactive Confirmation

Add interactive prompts:

class MyCLI:
    def delete(self, resource, force=False):
        """Delete resource with confirmation

        Args:
            resource: Resource to delete
            force: Skip confirmation
        """
        if not force:
            console.print(f"[yellow]⚠ Delete {resource}?[/yellow]")
            console.print("[dim]Use --force to skip this prompt[/dim]")
            return

        console.print(f"[red]Deleting {resource}...[/red]")
        # Delete logic here
        console.print("[green]✓[/green] Deleted")

Best Practices Summary

  1. Error Handling: Always validate inputs and handle errors gracefully
  2. Confirmation: Require confirmation for destructive operations
  3. Dry Run: Implement dry-run mode for risky operations
  4. Logging: Log important actions to file for audit trail
  5. Type Hints: Use type hints for better IDE support
  6. Properties: Use properties for read-only values
  7. Context Managers: Use context managers for resource cleanup
  8. Enums: Use Enums for constrained choices
  9. Async: Use asyncio for concurrent operations
  10. Plugins: Design for extensibility with plugin architecture