Skip to main content

A Django-like command system with Pydantic model integration for automatic argparse generation

Project description

Pydantic Commands

A Django-like command system with Pydantic model integration for automatic argparse generation.

Features

  • 🎯 Django-inspired API: Familiar command structure similar to Django's management commands
  • 🔒 Type-Safe: Leverage Pydantic models for automatic validation and type conversion
  • 🚀 Auto-generated argparse: Convert Pydantic models to argparse arguments automatically
  • 🎨 Flexible: Support both class-based and function-based (decorator) commands
  • 📦 Registry System: Built on any-registries for pluggable command architecture
  • Rich Type Support: Handles strings, integers, floats, booleans, enums, lists, Path objects, and more

Installation

pip install pydantic-commands

Quick Start

Class-Based Commands

from pydantic import BaseModel, Field
from pydantic_commands import BaseCommand

class CreateUserCommand(BaseCommand):
    """Create a new user in the system."""

    name = "createuser"
    help = "Create a new user"

    class Arguments(BaseModel):
        username: str = Field(..., description="Username for the new user")
        email: str = Field(..., description="Email address")
        age: int = Field(default=18, description="User age")
        is_admin: bool = Field(default=False, description="Admin privileges")

    def handle(self, args: Arguments) -> None:
        print(f"Creating user: {args.username}")
        print(f"Email: {args.email}")
        print(f"Age: {args.age}")
        print(f"Admin: {args.is_admin}")

Function-Based Commands (Decorator)

from pydantic import BaseModel
from pydantic_commands import command

class GreetArgs(BaseModel):
    name: str
    greeting: str = "Hello"
    enthusiastic: bool = False

@command(name="greet", help="Greet someone", arguments=GreetArgs)
def greet_command(args: GreetArgs) -> None:
    message = f"{args.greeting}, {args.name}!"
    if args.enthusiastic:
        message = message.upper() + "!!!"
    print(message)

# The decorator automatically creates a command class
# and registers it in the command registry

CLI Application Entry Point (host_cli)

The simplest way to create a CLI application is using host_cli(), similar to Django's manage.py:

# cli.py
from pydantic import BaseModel, Field
from pydantic_commands import BaseCommand, command_registry, host_cli

class CreateUserCommand(BaseCommand):
    name = "createuser"
    help = "Create a new user"

    class Arguments(BaseModel):
        username: str = Field(..., description="Username")
        email: str = Field(..., description="Email address")

    def handle(self, args: Arguments) -> None:
        print(f"Creating user: {args.username}")

# Register your commands
command_registry.register(CreateUserCommand())

# This is your entry point - that's it!
if __name__ == "__main__":
    host_cli()

Run your CLI:

# Show help
python cli.py --help

# List commands
python cli.py

# Run a command
python cli.py createuser --username john --email john@example.com

# Get command help
python cli.py createuser --help

Alternative: Manual CommandExecutor

For more control, use CommandExecutor directly:

from pydantic_commands import CommandExecutor, command_registry

# Register your commands
cmd = CreateUserCommand()
command_registry.register(cmd)

# Execute from command line
if __name__ == "__main__":
    executor = CommandExecutor(prog_name="myapp")
    exit_code = executor.execute()  # Uses sys.argv
    sys.exit(exit_code)

Run your CLI:

# Show help
python myapp.py --help

# List commands
python myapp.py

# Run a command
python myapp.py createuser --username john --email john@example.com

# Get command help
python myapp.py createuser --help

Advanced Usage

Enum Fields

from enum import Enum
from pydantic import BaseModel
from pydantic_commands import BaseCommand

class Status(str, Enum):
    ACTIVE = "active"
    INACTIVE = "inactive"
    PENDING = "pending"

class UpdateStatusCommand(BaseCommand):
    name = "updatestatus"

    class Arguments(BaseModel):
        user_id: int
        status: Status

    def handle(self, args: Arguments) -> None:
        print(f"Updating user {args.user_id} to {args.status.value}")
python myapp.py updatestatus --user-id 123 --status active

List Fields

class TagCommand(BaseCommand):
    name = "tag"

    class Arguments(BaseModel):
        tags: list[str]
        resource_ids: list[int]

    def handle(self, args: Arguments) -> None:
        print(f"Tagging resources {args.resource_ids} with {args.tags}")
python myapp.py tag --tags python cli tool --resource-ids 1 2 3

Path Fields

from pathlib import Path

class ConvertCommand(BaseCommand):
    name = "convert"

    class Arguments(BaseModel):
        input_file: Path
        output_file: Path
        overwrite: bool = False

    def handle(self, args: Arguments) -> None:
        if args.output_file.exists() and not args.overwrite:
            raise CommandError("Output file exists, use --overwrite")
        # Process files...

Custom Argument Parsing

You can extend the add_arguments method to add custom argparse arguments:

class CustomCommand(BaseCommand):
    name = "custom"

    def add_arguments(self, parser):
        parser.add_argument(
            "--custom-flag",
            action="store_true",
            help="A custom flag"
        )

    def handle(self, args):
        print(f"Custom flag: {args.custom_flag}")

Error Handling

from pydantic_commands import BaseCommand, CommandError

class RiskyCommand(BaseCommand):
    name = "risky"

    def handle(self, args):
        if some_condition:
            raise CommandError("Something went wrong!")
        # Normal execution...

Auto-Loading Commands

The command registry automatically discovers and loads command files using a configurable pattern matching system:

from pydantic_commands import command_registry

# By default, all files matching "*/commands.py" are auto-loaded
# This means any file named "commands.py" in any directory will be discovered

Default Pattern: */commands.py

The registry will automatically find and import all Python files matching this pattern, making any command classes defined in those files available in your CLI application.

Custom Pattern with Environment Variable:

You can customize the discovery pattern using the COMMANDS_PATTERN environment variable:

# Load commands from specific patterns
export COMMANDS_PATTERN="myapp/management/commands/*.py"
python cli.py

# Load from multiple app directories
export COMMANDS_PATTERN="apps/*/commands.py"
python cli.py

# Load from nested command directories
export COMMANDS_PATTERN="**/commands/*.py"
python cli.py

Example Directory Structure:

myproject/
├── cli.py
├── user_management/
│   └── commands.py          # Auto-loaded (contains UserCommands)
├── data_processing/
│   └── commands.py          # Auto-loaded (contains DataCommands)
└── reporting/
    └── commands.py          # Auto-loaded (contains ReportCommands)

How It Works:

  1. When the command registry is initialized, it scans for files matching the pattern
  2. Each matching file is automatically imported
  3. Any BaseCommand subclasses or @command decorated functions in those files are registered
  4. Commands become available immediately without manual registration

Manual Registration (if you prefer explicit control):

from pydantic_commands import command_registry
from myapp.commands import MyCommand

# Disable auto-loading and register manually
command_registry.register(MyCommand())

This auto-discovery system makes it easy to organize commands across multiple files and modules while keeping your CLI application simple and maintainable.

Comparison with Django Commands

Feature Django Commands Pydantic Commands
Base Class BaseCommand BaseCommand
Argument Definition add_arguments() Pydantic model
Type Validation Manual Automatic (Pydantic)
Argument Types argparse types Python types + Pydantic
Error Handling CommandError CommandError
Registry Django app system any-registries
Django Required Yes No

API Reference

BaseCommand

The base class for all commands.

Attributes:

  • name (str): Command name (auto-inferred from class name if not set)
  • help (str): Short help text
  • description (str): Detailed description
  • Arguments (Type[BaseModel]): Pydantic model for arguments

Methods:

  • handle(args): Main command logic (must be implemented)
  • add_arguments(parser): Hook for adding custom arguments
  • execute(argv): Execute the command with given arguments
  • create_parser(): Create an ArgumentParser for this command

@command Decorator

Create commands from functions.

Parameters:

  • name (str): Command name
  • help (str): Help text
  • description (str): Detailed description
  • arguments (Type[BaseModel]): Pydantic model for arguments
  • register (bool): Auto-register in registry (default: True)

host_cli()

The main entry point for CLI applications (recommended).

Function Signature:

def host_cli(
    argv: Optional[list[str]] = None,
    prog_name: Optional[str] = None
) -> None

Parameters:

  • argv (list[str], optional): Command-line arguments (defaults to sys.argv)
  • prog_name (str, optional): Program name for help text (defaults to sys.argv[0])

Usage:

# cli.py
from pydantic_commands import host_cli

if __name__ == "__main__":
    host_cli()

Features:

  • Automatically discovers and executes registered commands
  • Handles all argument parsing and validation
  • Exits with appropriate exit code
  • Similar to Django's manage.py pattern

CommandExecutor

Execute commands from CLI arguments (for advanced usage).

Methods:

  • execute(argv=None): Execute command from arguments
  • list_commands(): Get list of registered commands

command_registry

Global registry for commands (instance of any_registries.Registry).

Methods:

  • register(command): Register a command
  • get(name): Get command by name
  • list(): List all command names

Development

Setup

# Clone the repository
git clone https://github.com/Starscribers/python-packages.git
cd python-packages/pydantic-commands

# Install dependencies
pip install -e ".[dev]"

# Install pre-commit hooks
pre-commit install

Running Tests

pytest tests/

Code Quality

# Format code
ruff format .

# Lint
ruff check .

# Type check
mypy src/

License

MIT License - see LICENSE file for details.

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Links

Credits

Built with:

Inspired by Django's management command system.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pydantic_commands-0.1.0.tar.gz (22.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pydantic_commands-0.1.0-py3-none-any.whl (16.3 kB view details)

Uploaded Python 3

File details

Details for the file pydantic_commands-0.1.0.tar.gz.

File metadata

  • Download URL: pydantic_commands-0.1.0.tar.gz
  • Upload date:
  • Size: 22.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.5

File hashes

Hashes for pydantic_commands-0.1.0.tar.gz
Algorithm Hash digest
SHA256 5d29ae05f0b7729a1e854e19b71690cdb46b1109729a722d24ee2f90fccde3b6
MD5 9a61d4c9a2672bc36785fc9dc4d9d84d
BLAKE2b-256 3eac35b3a35bab96aaeaa15ba599de0b74f2077a3cf11ee8d0bc36c712b9c7b6

See more details on using hashes here.

File details

Details for the file pydantic_commands-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pydantic_commands-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 41bd31d19b813e755927f3cf98f2bfbe5ad90af7892e41ee191f715809b9880d
MD5 c85dd3308558f7f6bb1299946cd40cb3
BLAKE2b-256 f78359cf39685d6c7b730928145122eaeecf172cd0ba413d59081d36e33fafdd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page