Skip to Content
DocumentationCLI ToolsCustom Commands

Custom CLI Commands

Learn how to create custom CLI commands for your Django-CFG projects using Click and integrate them with the existing CLI system.

Overview

Django-CFG provides a flexible system for creating custom CLI commands that integrate seamlessly with the existing CLI infrastructure. You can:

  • Extend the CLI with project-specific commands
  • Use Click decorators for rich command-line interfaces
  • Access Django-CFG configuration within commands
  • Integrate with Rich for beautiful output
  • Share commands across team members

Quick Start

1. Create Command Directory

In your Django-CFG project, create a management commands directory:

mkdir -p core/management/commands/ touch core/management/__init__.py touch core/management/commands/__init__.py

2. Basic Custom Command

Create core/management/commands/hello.py:

""" Custom Hello Command Example """ import click from django.core.management.base import BaseCommand from rich.console import Console from rich.panel import Panel console = Console() class Command(BaseCommand): help = "Say hello with Django-CFG style" def add_arguments(self, parser): parser.add_argument( '--name', type=str, default='World', help='Name to greet' ) parser.add_argument( '--style', choices=['simple', 'fancy', 'panel'], default='simple', help='Output style' ) def handle(self, *args, **options): name = options['name'] style = options['style'] if style == 'simple': self.stdout.write(f"Hello, {name}!") elif style == 'fancy': console.print(f"✨ Hello, [bold cyan]{name}[/bold cyan]! ✨") elif style == 'panel': console.print(Panel( f"Hello, [bold green]{name}[/bold green]!", title="🚀 Django-CFG Greeting", border_style="bright_blue" ))

3. Use the Command

# Basic usage python manage.py hello # With options python manage.py hello --name "Django Developer" --style fancy # With Poetry poetry run python manage.py hello --name "Team" --style panel

Advanced Command Patterns

Click Integration

Create more sophisticated commands using Click directly:

""" Advanced Click Command with Django-CFG Integration """ import click from django.core.management.base import BaseCommand from django.conf import settings from rich.console import Console from rich.table import Table from rich.progress import track from api.config import config # Your Django-CFG config console = Console() class Command(BaseCommand): help = "Advanced command with Click integration" def add_arguments(self, parser): # Use Click for argument parsing pass def handle(self, *args, **options): # Delegate to Click command cli_main() @click.group() def cli_main(): """Advanced Django-CFG command group.""" pass @cli_main.command() @click.option('--database', '-d', help='Database to analyze') @click.option('--verbose', '-v', is_flag=True, help='Verbose output') def analyze_db(database: str, verbose: bool): """Analyze database performance and structure.""" # Access Django-CFG configuration db_config = config.databases.get(database or 'default') if not db_config: console.print(f"❌ Database '{database}' not found", style="red") return console.print(f"🔍 Analyzing database: [cyan]{database or 'default'}[/cyan]") # Create rich table table = Table(title="Database Analysis") table.add_column("Metric", style="cyan") table.add_column("Value", style="green") # Simulate analysis with progress for i in track(range(10), description="Analyzing..."): # Your analysis logic here pass # Add results to table table.add_row("Engine", db_config.engine) table.add_row("Name", db_config.name) table.add_row("Tables", "42") # Your logic here console.print(table) @cli_main.command() @click.argument('service_name') @click.option('--timeout', default=30, help='Timeout in seconds') def test_service(service_name: str, timeout: int): """Test external service connectivity.""" console.print(f"🧪 Testing service: [yellow]{service_name}[/yellow]") # Access service configuration from Django-CFG services = { 'twilio': config.twilio if hasattr(config, 'twilio') else None, 'sendgrid': config.email if hasattr(config, 'email') else None, 'telegram': config.telegram if hasattr(config, 'telegram') else None, } service_config = services.get(service_name.lower()) if not service_config: console.print(f"❌ Service '{service_name}' not configured", style="red") return # Test service (implement your logic) with console.status(f"Testing {service_name}..."): # Your testing logic here import time time.sleep(2) # Simulate test console.print(f"✅ Service '{service_name}' is working!", style="green")

Configuration Access

Access Django-CFG configuration within commands:

""" Command that uses Django-CFG configuration """ from django.core.management.base import BaseCommand from rich.console import Console from rich.json import JSON from api.config import config console = Console() class Command(BaseCommand): help = "Show Django-CFG configuration" def add_arguments(self, parser): parser.add_argument( '--section', help='Configuration section to show' ) parser.add_argument( '--format', choices=['json', 'yaml', 'table'], default='json', help='Output format' ) def handle(self, *args, **options): section = options.get('section') format_type = options['format'] if section: # Show specific section if hasattr(config, section): data = getattr(config, section) if hasattr(data, 'model_dump'): data = data.model_dump() else: console.print(f"❌ Section '{section}' not found", style="red") return else: # Show all configuration data = config.model_dump() if format_type == 'json': console.print(JSON.from_data(data)) elif format_type == 'yaml': import yaml console.print(yaml.dump(data, default_flow_style=False)) elif format_type == 'table': # Create table representation from rich.table import Table table = Table(title="Django-CFG Configuration") table.add_column("Key", style="cyan") table.add_column("Value", style="green") def add_to_table(obj, prefix=""): if isinstance(obj, dict): for key, value in obj.items(): full_key = f"{prefix}.{key}" if prefix else key if isinstance(value, (dict, list)): add_to_table(value, full_key) else: table.add_row(full_key, str(value)) elif isinstance(obj, list): for i, item in enumerate(obj): add_to_table(item, f"{prefix}[{i}]") add_to_table(data) console.print(table)

Rich Output Examples

Progress Bars

from rich.progress import Progress, SpinnerColumn, TextColumn def handle(self, *args, **options): with Progress( SpinnerColumn(), TextColumn("[progress.description]{task.description}"), transient=True, ) as progress: task = progress.add_task("Processing...", total=100) for i in range(100): # Your work here progress.update(task, advance=1) time.sleep(0.01) console.print("✅ Processing complete!", style="green")

Interactive Prompts

import click def handle(self, *args, **options): # Simple confirmation if click.confirm('Do you want to continue?'): console.print("Continuing...", style="green") # Text input with validation email = click.prompt( 'Email address', type=click.STRING, default='[email protected]' ) # Password input (hidden) password = click.prompt( 'Password', type=click.STRING, hide_input=True, confirmation_prompt=True ) # Choice selection environment = click.prompt( 'Environment', type=click.Choice(['dev', 'staging', 'prod']), default='dev' )

Status and Spinners

from rich.console import Console from rich.status import Status import time console = Console() def handle(self, *args, **options): with console.status("[bold green]Working on tasks...") as status: time.sleep(2) status.update("[bold blue]Almost done...") time.sleep(2) console.print("✅ All done!", style="green")

🔌 Integration Patterns

Database Operations

""" Command for database operations """ from django.core.management.base import BaseCommand from django.db import connections from rich.console import Console from rich.table import Table console = Console() class Command(BaseCommand): help = "Database utilities" def add_arguments(self, parser): parser.add_argument('action', choices=['list', 'test', 'info']) parser.add_argument('--database', help='Database alias') def handle(self, *args, **options): action = options['action'] database = options.get('database', 'default') if action == 'list': self.list_databases() elif action == 'test': self.test_connection(database) elif action == 'info': self.show_info(database) def list_databases(self): """List all configured databases.""" table = Table(title="Configured Databases") table.add_column("Alias", style="cyan") table.add_column("Engine", style="green") table.add_column("Name", style="yellow") for alias in connections: conn = connections[alias] table.add_row( alias, conn.settings_dict['ENGINE'], conn.settings_dict['NAME'] ) console.print(table) def test_connection(self, database: str): """Test database connection.""" try: conn = connections[database] conn.ensure_connection() console.print(f"✅ Connection to '{database}' successful!", style="green") except Exception as e: console.print(f"❌ Connection to '{database}' failed: {e}", style="red") def show_info(self, database: str): """Show database information.""" conn = connections[database] settings = conn.settings_dict table = Table(title=f"Database Info: {database}") table.add_column("Setting", style="cyan") table.add_column("Value", style="green") for key, value in settings.items(): if key == 'PASSWORD': value = '***' if value else 'Not set' table.add_row(key, str(value)) console.print(table)

Service Testing

""" Command for testing external services """ from django.core.management.base import BaseCommand from rich.console import Console from rich.panel import Panel import requests import time console = Console() class Command(BaseCommand): help = "Test external service integrations" def add_arguments(self, parser): parser.add_argument('service', choices=['all', 'twilio', 'sendgrid', 'telegram']) parser.add_argument('--timeout', type=int, default=10) def handle(self, *args, **options): service = options['service'] timeout = options['timeout'] if service == 'all': self.test_all_services(timeout) else: self.test_service(service, timeout) def test_service(self, service: str, timeout: int): """Test individual service.""" console.print(f"🧪 Testing {service.title()} service...") try: with console.status(f"Connecting to {service}..."): # Implement service-specific testing if service == 'twilio': self.test_twilio(timeout) elif service == 'sendgrid': self.test_sendgrid(timeout) elif service == 'telegram': self.test_telegram(timeout) console.print(Panel( f"✅ {service.title()} service is working correctly!", title="Success", border_style="green" )) except Exception as e: console.print(Panel( f"❌ {service.title()} service failed: {str(e)}", title="Error", border_style="red" )) def test_twilio(self, timeout: int): """Test Twilio service.""" from api.config import config if not hasattr(config, 'twilio'): raise ValueError("Twilio not configured") # Implement Twilio testing logic time.sleep(1) # Simulate API call def test_sendgrid(self, timeout: int): """Test SendGrid service.""" # Implement SendGrid testing logic time.sleep(1) # Simulate API call def test_telegram(self, timeout: int): """Test Telegram service.""" # Implement Telegram testing logic time.sleep(1) # Simulate API call

Command Distribution

Packaging Commands

Create reusable command packages:

# commands/package.py """ Reusable command package for Django-CFG projects """ from django.core.management.base import BaseCommand from rich.console import Console console = Console() class DjangoCfgCommand(BaseCommand): """Base class for Django-CFG commands.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.console = Console() def success(self, message: str): """Print success message.""" self.console.print(f"✅ {message}", style="green") def error(self, message: str): """Print error message.""" self.console.print(f"❌ {message}", style="red") def warning(self, message: str): """Print warning message.""" self.console.print(f"⚠️ {message}", style="yellow") def info(self, message: str): """Print info message.""" self.console.print(f"ℹ️ {message}", style="blue") # Your command inherits from DjangoCfgCommand class Command(DjangoCfgCommand): help = "Example command using base class" def handle(self, *args, **options): self.info("Starting process...") self.success("Process completed!")

Sharing Commands

Create a commands package for your team:

# team_commands/setup.py from setuptools import setup, find_packages setup( name="mycompany-django-cfg-commands", version="1.0.0", packages=find_packages(), install_requires=[ "django-cfg", "rich", "click", ], entry_points={ 'console_scripts': [ 'mycompany-deploy=team_commands.deploy:main', ], }, )

Best Practices

1. Use Rich for Output

# Good: Rich formatting console.print("✅ Success!", style="green") # Avoid: Plain text print("Success!")

2. Handle Errors Gracefully

def handle(self, *args, **options): try: # Your logic here pass except Exception as e: console.print(f"❌ Error: {e}", style="red") raise CommandError(f"Command failed: {e}")

3. Use Progress Indicators

from rich.progress import track def handle(self, *args, **options): items = range(100) for item in track(items, description="Processing..."): # Your work here time.sleep(0.01)

4. Validate Configuration

def handle(self, *args, **options): from api.config import config # Validate required configuration if not hasattr(config, 'database'): raise CommandError("Database configuration missing") # Continue with command logic

5. Use Type Hints

from typing import Any, Dict, Optional def handle(self, *args: Any, **options: Dict[str, Any]) -> None: name: Optional[str] = options.get('name') count: int = options.get('count', 10) # Your logic here

External Resources

Create powerful, beautiful CLI commands that make your Django-CFG projects even more productive! 🚀