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__.py2. 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 panelAdvanced 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 callCommand 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 logic5. 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 hereRelated Documentation
- CLI Introduction - Overview and installation
- Commands Reference - Built-in commands
- Configuration Guide - Access configuration
- First Project - See commands in action
External Resources
- Click Documentation - Click framework
- Rich Documentation - Rich terminal formatting
- Django Management Commands - Django commands
Create powerful, beautiful CLI commands that make your Django-CFG projects even more productive! 🚀