Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:04:14 +08:00
commit 70c36b5eff
248 changed files with 47482 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
# Basic CLI Example
Simple type-safe CLI demonstrating fundamental Typer patterns.
## Features
- Type hints on all parameters
- Path validation
- Optional output file
- Boolean flags
- Verbose mode
## Usage
```bash
# Process and display
python cli.py input.txt
# Process and save
python cli.py input.txt --output result.txt
# Convert to uppercase
python cli.py input.txt --uppercase
# Verbose output
python cli.py input.txt --verbose
```
## Testing
```bash
# Create test file
echo "hello world" > test.txt
# Run CLI
python cli.py test.txt --uppercase
# Output: HELLO WORLD
# Save to file
python cli.py test.txt --output out.txt --uppercase --verbose
# Outputs: Processing: test.txt
# ✓ Written to: out.txt
```
## Key Patterns
1. **Path type**: Automatic validation of file existence
2. **Optional parameters**: Using `Optional[Path]` for optional output
3. **Boolean flags**: Simple `bool` type for flags
4. **Colored output**: Using `typer.secho()` for success messages

View File

@@ -0,0 +1,43 @@
"""Basic CLI example using type hints.
This example demonstrates:
- Simple type-safe command
- Path validation
- Optional parameters
- Verbose output
"""
import typer
from pathlib import Path
from typing import Optional
app = typer.Typer(help="Basic file processing CLI")
@app.command()
def process(
input_file: Path = typer.Argument(
..., help="Input file to process", exists=True
),
output: Optional[Path] = typer.Option(None, "--output", "-o"),
uppercase: bool = typer.Option(False, "--uppercase", "-u"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Process text file with optional transformations."""
if verbose:
typer.echo(f"Processing: {input_file}")
content = input_file.read_text()
if uppercase:
content = content.upper()
if output:
output.write_text(content)
typer.secho(f"✓ Written to: {output}", fg=typer.colors.GREEN)
else:
typer.echo(content)
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,66 @@
# Enum CLI Example
Type-safe CLI using Enums for constrained choices.
## Features
- Multiple Enum types (LogLevel, OutputFormat)
- Autocomplete support
- Type-safe matching with match/case
- Validated input values
## Usage
```bash
# Export as JSON (default)
python cli.py export
# Export as YAML
python cli.py export yaml
# Export with custom log level
python cli.py export json --log-level debug
# Save to file
python cli.py export yaml --output config.yaml
# Validate log level
python cli.py validate info
```
## Testing
```bash
# Export JSON to console
python cli.py export json
# Output: {"app": "example", "version": "1.0.0", ...}
# Export YAML to file
python cli.py export yaml --output test.yaml --log-level warning
# Creates test.yaml with YAML format
# Validate enum value
python cli.py validate error
# Output: Log Level: error
# Severity: High - error messages
# Invalid enum (will fail with helpful message)
python cli.py export invalid
# Error: Invalid value for 'FORMAT': 'invalid' is not one of 'json', 'yaml', 'text'.
```
## Key Patterns
1. **Enum as Argument**: `format: OutputFormat = typer.Argument(...)`
2. **Enum as Option**: `log_level: LogLevel = typer.Option(...)`
3. **String Enum**: Inherit from `str, Enum` for string values
4. **Match/Case**: Use pattern matching with enum values
5. **Autocomplete**: Automatic shell completion for enum values
## Benefits
- Type safety at runtime and compile time
- IDE autocomplete for enum values
- Automatic validation of inputs
- Self-documenting constrained choices
- Easy to extend with new values

View File

@@ -0,0 +1,102 @@
"""Enum-based CLI example.
This example demonstrates:
- Enum usage for constrained choices
- Multiple enum types
- Autocomplete with enums
- Match/case with enum values
"""
import typer
from enum import Enum
from typing import Optional
import json
class LogLevel(str, Enum):
"""Logging levels."""
debug = "debug"
info = "info"
warning = "warning"
error = "error"
class OutputFormat(str, Enum):
"""Output formats."""
json = "json"
yaml = "yaml"
text = "text"
app = typer.Typer(help="Configuration export CLI with Enums")
@app.command()
def export(
format: OutputFormat = typer.Argument(
OutputFormat.json, help="Export format"
),
log_level: LogLevel = typer.Option(
LogLevel.info, "--log-level", "-l", help="Logging level"
),
output: Optional[str] = typer.Option(None, "--output", "-o"),
) -> None:
"""Export configuration in specified format.
The format parameter uses an Enum, providing:
- Autocomplete in the shell
- Validation of input values
- Type safety in code
"""
# Sample data
data = {
"app": "example",
"version": "1.0.0",
"log_level": log_level.value,
"features": ["auth", "api", "cache"],
}
# Format output based on enum
match format:
case OutputFormat.json:
output_text = json.dumps(data, indent=2)
case OutputFormat.yaml:
# Simplified YAML output
output_text = "\n".join(f"{k}: {v}" for k, v in data.items())
case OutputFormat.text:
output_text = "\n".join(
f"{k.upper()}: {v}" for k, v in data.items()
)
# Output
if output:
with open(output, "w") as f:
f.write(output_text)
typer.secho(f"✓ Exported to {output}", fg=typer.colors.GREEN)
else:
typer.echo(output_text)
@app.command()
def validate(
level: LogLevel = typer.Argument(..., help="Log level to validate")
) -> None:
"""Validate and display log level information."""
typer.echo(f"Log Level: {level.value}")
# Access enum properties
match level:
case LogLevel.debug:
typer.echo("Severity: Lowest - detailed debugging information")
case LogLevel.info:
typer.echo("Severity: Low - informational messages")
case LogLevel.warning:
typer.echo("Severity: Medium - warning messages")
case LogLevel.error:
typer.echo("Severity: High - error messages")
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,128 @@
# Factory Pattern CLI Example
Testable CLI using factory pattern with dependency injection.
## Features
- Factory function for app creation
- Dependency injection via Protocol
- Multiple storage implementations
- Configuration injection
- Highly testable structure
## Usage
```bash
# Save data
python cli.py save name "John Doe"
python cli.py save email "john@example.com"
# Load data
python cli.py load name
# Output: John Doe
python cli.py load email
# Output: john@example.com
# Show configuration
python cli.py config-show
# Use verbose mode
python cli.py --verbose save status "active"
# Custom data directory
python cli.py --data-dir /tmp/mydata save test "value"
```
## Testing Example
```python
# test_cli.py
from cli import create_app, Config, MemoryStorage
from typer.testing import CliRunner
def test_save_and_load():
"""Test save and load commands."""
# Create test configuration
config = Config(verbose=True)
storage = MemoryStorage()
# Create app with test dependencies
app = create_app(config=config, storage=storage)
# Test runner
runner = CliRunner()
# Test save
result = runner.invoke(app, ["save", "test_key", "test_value"])
assert result.exit_code == 0
assert "Saved test_key" in result.output
# Test load
result = runner.invoke(app, ["load", "test_key"])
assert result.exit_code == 0
assert "test_value" in result.output
```
Run tests:
```bash
pytest test_cli.py
```
## Architecture
### Factory Function
```python
def create_app(config: Config, storage: Storage) -> typer.Typer:
"""Create app with injected dependencies."""
```
### Storage Protocol
```python
class Storage(Protocol):
def save(self, key: str, value: str) -> None: ...
def load(self, key: str) -> str: ...
```
### Implementations
- `MemoryStorage`: In-memory storage (for testing)
- `FileStorage`: File-based storage (for production)
## Key Patterns
1. **Factory Function**: Returns configured Typer app
2. **Protocol Types**: Interface for dependency injection
3. **Dataclass Config**: Type-safe configuration
4. **Dependency Injection**: Pass storage and config to factory
5. **Testability**: Easy to mock dependencies in tests
## Benefits
- Unit testable without file I/O
- Swap implementations easily
- Configuration flexibility
- Clean dependency management
- Follows SOLID principles
- Easy to extend with new storage types
## Extension Example
Add new storage type:
```python
class DatabaseStorage:
"""Database storage implementation."""
def __init__(self, connection_string: str) -> None:
self.conn = connect(connection_string)
def save(self, key: str, value: str) -> None:
self.conn.execute("INSERT INTO data VALUES (?, ?)", (key, value))
def load(self, key: str) -> str:
return self.conn.execute("SELECT value FROM data WHERE key = ?", (key,)).fetchone()
# Use it
storage = DatabaseStorage("postgresql://localhost/mydb")
app = create_app(storage=storage)
```

View File

@@ -0,0 +1,162 @@
"""Factory pattern CLI example.
This example demonstrates:
- Factory function for app creation
- Dependency injection
- Testable CLI structure
- Configuration injection
"""
import typer
from typing import Protocol, Optional
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Config:
"""Application configuration."""
verbose: bool = False
data_dir: Path = Path("./data")
max_items: int = 100
class Storage(Protocol):
"""Storage interface for dependency injection."""
def save(self, key: str, value: str) -> None:
"""Save data."""
...
def load(self, key: str) -> str:
"""Load data."""
...
class MemoryStorage:
"""In-memory storage implementation."""
def __init__(self) -> None:
self.data: dict[str, str] = {}
def save(self, key: str, value: str) -> None:
"""Save to memory."""
self.data[key] = value
def load(self, key: str) -> str:
"""Load from memory."""
return self.data.get(key, "")
class FileStorage:
"""File-based storage implementation."""
def __init__(self, base_dir: Path) -> None:
self.base_dir = base_dir
self.base_dir.mkdir(exist_ok=True)
def save(self, key: str, value: str) -> None:
"""Save to file."""
file_path = self.base_dir / f"{key}.txt"
file_path.write_text(value)
def load(self, key: str) -> str:
"""Load from file."""
file_path = self.base_dir / f"{key}.txt"
return file_path.read_text() if file_path.exists() else ""
def create_app(config: Optional[Config] = None, storage: Optional[Storage] = None) -> typer.Typer:
"""Factory function to create Typer app with dependencies.
This pattern enables:
- Dependency injection for testing
- Configuration flexibility
- Multiple app instances
- Easier unit testing
Args:
config: Application configuration
storage: Storage implementation
Returns:
Configured Typer application
"""
config = config or Config()
storage = storage or FileStorage(config.data_dir)
app = typer.Typer(
help="Data management CLI with factory pattern",
no_args_is_help=True,
)
@app.command()
def save(
key: str = typer.Argument(..., help="Data key"),
value: str = typer.Argument(..., help="Data value"),
) -> None:
"""Save data using injected storage."""
if config.verbose:
typer.echo(f"Saving {key}={value}")
try:
storage.save(key, value)
typer.secho(f"✓ Saved {key}", fg=typer.colors.GREEN)
except Exception as e:
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def load(key: str = typer.Argument(..., help="Data key")) -> None:
"""Load data using injected storage."""
if config.verbose:
typer.echo(f"Loading {key}")
try:
value = storage.load(key)
if value:
typer.echo(value)
else:
typer.secho(f"✗ Key not found: {key}", fg=typer.colors.YELLOW)
except Exception as e:
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def config_show() -> None:
"""Show current configuration."""
typer.echo("Configuration:")
typer.echo(f" Verbose: {config.verbose}")
typer.echo(f" Data dir: {config.data_dir}")
typer.echo(f" Max items: {config.max_items}")
return app
def main() -> None:
"""Main entry point with configuration."""
import sys
# Parse global flags
verbose = "--verbose" in sys.argv or "-v" in sys.argv
data_dir = Path("./data")
# Check for custom data directory
if "--data-dir" in sys.argv:
idx = sys.argv.index("--data-dir")
if idx + 1 < len(sys.argv):
data_dir = Path(sys.argv[idx + 1])
sys.argv.pop(idx) # Remove flag
sys.argv.pop(idx) # Remove value
# Create configuration
config = Config(verbose=verbose, data_dir=data_dir)
# Create and run app
app = create_app(config=config)
app()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,77 @@
# Sub-Application CLI Example
Multi-level CLI with organized command groups.
## Features
- Three sub-apps: db, server, user
- Shared context via callback
- Global options (--config, --verbose)
- Clean command hierarchy
- Logical command grouping
## Usage
```bash
# Show main help
python cli.py --help
# Show sub-app help
python cli.py db --help
python cli.py server --help
python cli.py user --help
# Database commands
python cli.py db init
python cli.py db migrate --steps 5
python cli.py db seed
# Server commands
python cli.py server start --port 8080
python cli.py server stop
python cli.py server restart
# User commands
python cli.py user create alice --email alice@example.com
python cli.py user create bob --email bob@example.com --admin
python cli.py user list
python cli.py user delete alice
# Global options
python cli.py --verbose db migrate
python cli.py --config prod.yaml server start
```
## Command Structure
```
cli.py
├── db
│ ├── init - Initialize database
│ ├── migrate - Run migrations
│ └── seed - Seed test data
├── server
│ ├── start - Start server
│ ├── stop - Stop server
│ └── restart - Restart server
└── user
├── create - Create user
├── delete - Delete user
└── list - List users
```
## Key Patterns
1. **Sub-App Creation**: `db_app = typer.Typer()`
2. **Adding Sub-Apps**: `app.add_typer(db_app, name="db")`
3. **Global Callback**: `@app.callback()` for shared options
4. **Context Sharing**: `ctx.obj` for passing data to sub-commands
5. **Command Organization**: Group related commands in sub-apps
## Benefits
- Clear command hierarchy
- Easier navigation with help text
- Logical grouping of functionality
- Shared configuration across commands
- Scalable structure for large CLIs

View File

@@ -0,0 +1,132 @@
"""Sub-application CLI example.
This example demonstrates:
- Multi-level command structure
- Sub-apps for logical grouping
- Shared context across commands
- Clean command organization
"""
import typer
from typing import Optional
from pathlib import Path
# Main app
app = typer.Typer(
help="Project management CLI with sub-commands", add_completion=True
)
# Sub-applications
db_app = typer.Typer(help="Database commands")
server_app = typer.Typer(help="Server commands")
user_app = typer.Typer(help="User management commands")
# Add sub-apps to main app
app.add_typer(db_app, name="db")
app.add_typer(server_app, name="server")
app.add_typer(user_app, name="user")
# Global callback for shared options
@app.callback()
def main(
ctx: typer.Context,
config: Optional[Path] = typer.Option(None, "--config", "-c"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Global options for all commands."""
ctx.obj = {"config": config, "verbose": verbose}
if verbose:
typer.echo(f"Config: {config or 'default'}")
# Database commands
@db_app.command("init")
def db_init(ctx: typer.Context) -> None:
"""Initialize database."""
if ctx.obj["verbose"]:
typer.echo("Initializing database...")
typer.secho("✓ Database initialized", fg=typer.colors.GREEN)
@db_app.command("migrate")
def db_migrate(ctx: typer.Context, steps: int = typer.Option(1)) -> None:
"""Run database migrations."""
if ctx.obj["verbose"]:
typer.echo(f"Running {steps} migration(s)...")
typer.secho("✓ Migrations complete", fg=typer.colors.GREEN)
@db_app.command("seed")
def db_seed(ctx: typer.Context) -> None:
"""Seed database with test data."""
if ctx.obj["verbose"]:
typer.echo("Seeding database...")
typer.secho("✓ Database seeded", fg=typer.colors.GREEN)
# Server commands
@server_app.command("start")
def server_start(
ctx: typer.Context,
port: int = typer.Option(8000, "--port", "-p"),
host: str = typer.Option("127.0.0.1", "--host"),
) -> None:
"""Start application server."""
if ctx.obj["verbose"]:
typer.echo(f"Starting server on {host}:{port}...")
typer.secho(f"✓ Server running at http://{host}:{port}", fg=typer.colors.GREEN)
@server_app.command("stop")
def server_stop(ctx: typer.Context) -> None:
"""Stop application server."""
if ctx.obj["verbose"]:
typer.echo("Stopping server...")
typer.secho("✓ Server stopped", fg=typer.colors.RED)
@server_app.command("restart")
def server_restart(ctx: typer.Context) -> None:
"""Restart application server."""
if ctx.obj["verbose"]:
typer.echo("Restarting server...")
typer.secho("✓ Server restarted", fg=typer.colors.GREEN)
# User commands
@user_app.command("create")
def user_create(
ctx: typer.Context,
username: str = typer.Argument(...),
email: str = typer.Option(..., "--email", "-e"),
admin: bool = typer.Option(False, "--admin"),
) -> None:
"""Create a new user."""
if ctx.obj["verbose"]:
typer.echo(f"Creating user: {username}")
role = "admin" if admin else "user"
typer.secho(f"✓ User {username} created as {role}", fg=typer.colors.GREEN)
@user_app.command("delete")
def user_delete(ctx: typer.Context, username: str = typer.Argument(...)) -> None:
"""Delete a user."""
confirm = typer.confirm(f"Delete user {username}?")
if not confirm:
typer.echo("Cancelled")
raise typer.Abort()
typer.secho(f"✓ User {username} deleted", fg=typer.colors.RED)
@user_app.command("list")
def user_list(ctx: typer.Context) -> None:
"""List all users."""
users = ["alice", "bob", "charlie"]
typer.echo("Users:")
for user in users:
typer.echo(f" - {user}")
if __name__ == "__main__":
app()