Skip to content

crimson206/runpy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

31 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Runpy - Convert Python Functions to CLI Commands

PyPI version Python 3.10+ License: MIT

Runpy automatically converts your Python functions into command-line interfaces (CLIs) with minimal code changes. Simply decorate your functions, and Runpy generates a fully-featured CLI with argument parsing, help text, and type validation.

Features

  • πŸš€ Zero Configuration: Convert functions to CLI commands with a single decorator
  • πŸ“ Automatic Help Generation: Docstrings become help text automatically
  • πŸ” Type Validation: Automatic type checking based on type hints
  • πŸ“Š Rich Output: Support for structured output (JSON, tables, etc.)
  • πŸ—οΈ Command Groups: Organize commands in hierarchical groups
  • πŸ”§ Flexible Configuration: YAML/JSON config file support
  • πŸ“¦ Pydantic Integration: Full support for Pydantic models as parameters
  • 🌐 Multiple Input Formats: Accept JSON, Python dict, and TypeScript object notation
  • 🎨 Customizable: Extensive customization options for advanced use cases

Installation

pip install runpycli

Quick Start

Create a file mycli.py:

from runpycli import Runpy

# Create a Runpy instance
cli = Runpy()

# Register functions as commands
@cli.register
def hello(name: str = "World") -> str:
    """Say hello to someone"""
    return f"Hello, {name}!"

@cli.register
def add(x: int, y: int) -> int:
    """Add two numbers"""
    return x + y

if __name__ == "__main__":
    cli.app()

Run your CLI:

python mycli.py hello --name Alice
# Output: Hello, Alice!

python mycli.py add --x 5 --y 3
# Output: 8

Advanced Features

Pydantic Models as Parameters

from pydantic import BaseModel, Field
from typing import List

class UserInput(BaseModel):
    """User information"""
    name: str = Field(..., description="User's full name")
    age: int = Field(..., ge=0, le=150, description="User's age")
    emails: List[str] = Field(default_factory=list, description="Email addresses")

@cli.register
def create_user(user: UserInput) -> dict:
    """Create a new user from the provided data"""
    return {"status": "created", "user": user.model_dump()}

Usage with multiple input formats:

# JSON format (standard)
python mycli.py create-user --user '{"name": "John Doe", "age": 30, "emails": ["john@example.com"]}'

# Python dict format
python mycli.py create-user --user "{'name': 'John Doe', 'age': 30, 'emails': ['john@example.com']}"

# TypeScript/JavaScript object format
python mycli.py create-user --user '{name: "John Doe", age: 30, emails: ["john@example.com"]}'

Command Groups

# Create command groups
cli = Runpy(name="myapp/db")

@cli.register
def migrate():
    """Run database migrations"""
    pass

@cli.register
def seed():
    """Seed the database"""
    pass

Configuration Files

Create a config.json:

{
  "defaults": {
    "environment": "development",
    "debug": true
  },
  "shortcuts": {
    "env": "e",
    "debug": "d"
  }
}

Use in your CLI:

cli = Runpy(config_file="config.json")

@cli.register
def deploy(environment: str, debug: bool = False):
    """Deploy the application"""
    print(f"Deploying to {environment} (debug: {debug})")

Register Multiple Functions

import math

# Register all public functions from a module
cli.register_module(math)

# Or selectively register functions
cli.register(math.sin)
cli.register(math.cos)

Documentation Commands

Runpy automatically adds two special commands:

docs Command

View detailed documentation for any command:

python mycli.py docs create-user

schema Command

Generate OpenAPI-style schema documentation:

python mycli.py schema
python mycli.py schema --json  # JSON output
python mycli.py schema --save api-docs.json  # Save to file

Type Support

Runpy supports a wide range of Python types:

  • Basic types: str, int, float, bool
  • Container types: List[T], Dict[K, V], Set[T], Tuple[T, ...]
  • Optional types: Optional[T], Union[T1, T2]
  • Pydantic models: Any class inheriting from pydantic.BaseModel
  • Enums: Enum subclasses
  • File types: Path, FilePath, DirectoryPath

Boolean Parameters

Boolean parameters are handled as regular options, not flags:

# Correct usage
python mycli.py command --bool-param true
python mycli.py command --bool-param false

# NOT as flags (this is not supported)
python mycli.py command --bool-param  # ❌

Optional Parameters

Parameters with default values (including None) are automatically optional:

def process(
    required_param: str,  # Required: must provide --required-param
    optional_str: Optional[str] = None,  # Optional: can omit
    optional_int: Optional[int] = None,  # Optional: can omit
    optional_with_default: str = "default"  # Optional: uses default if omitted
):
    pass

Best Practices

  1. Use Type Hints: Always add type hints to get automatic type validation
  2. Write Docstrings: Function and parameter docstrings become help text
  3. Set Defaults: Default values make parameters optional
  4. Return Values: Return values are automatically displayed
  5. Use Pydantic: For complex inputs, Pydantic models provide validation

Examples

Simple Calculator

from runpycli import Runpy
import math

cli = Runpy(name="calc", version="1.0.0")

cli.register(math.sin)
cli.register(math.cos)
cli.register(math.sqrt)

@cli.register
def divide(x: float, y: float) -> float:
    """Divide x by y"""
    if y == 0:
        raise ValueError("Cannot divide by zero")
    return x / y

if __name__ == "__main__":
    cli.app()

Task Manager

from runpycli import Runpy
from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime

cli = Runpy(name="tasks")

class Task(BaseModel):
    title: str
    description: Optional[str] = None
    due_date: Optional[datetime] = None
    tags: List[str] = []

tasks_db = []

@cli.register
def add(task: Task) -> dict:
    """Add a new task"""
    task_dict = task.model_dump()
    task_dict["id"] = len(tasks_db) + 1
    tasks_db.append(task_dict)
    return task_dict

@cli.register
def list_tasks(tag: Optional[str] = None) -> List[dict]:
    """List all tasks, optionally filtered by tag"""
    if tag:
        return [t for t in tasks_db if tag in t.get("tags", [])]
    return tasks_db

if __name__ == "__main__":
    cli.app()

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Links

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages