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,233 @@
"""Advanced validation and callbacks template.
This template demonstrates:
- Custom validators with callbacks
- Complex validation logic
- Interdependent parameter validation
- Rich error messages
"""
import typer
from typing import Optional
from pathlib import Path
import re
app = typer.Typer()
# Custom validators
def validate_email(value: str) -> str:
"""Validate email format."""
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(pattern, value):
raise typer.BadParameter("Invalid email format")
return value
def validate_port(value: int) -> int:
"""Validate port number range."""
if not 1024 <= value <= 65535:
raise typer.BadParameter("Port must be between 1024-65535")
return value
def validate_path_exists(value: Path) -> Path:
"""Validate that path exists."""
if not value.exists():
raise typer.BadParameter(f"Path does not exist: {value}")
return value
def validate_percentage(value: float) -> float:
"""Validate percentage range."""
if not 0.0 <= value <= 100.0:
raise typer.BadParameter("Percentage must be between 0-100")
return value
def validate_url(value: str) -> str:
"""Validate URL format."""
pattern = r"^https?://[^\s/$.?#].[^\s]*$"
if not re.match(pattern, value):
raise typer.BadParameter("Invalid URL format (must start with http:// or https://)")
return value
# Context manager for complex validation
class ValidationContext:
"""Context for cross-parameter validation."""
def __init__(self) -> None:
self.params: dict = {}
def add(self, key: str, value: any) -> None:
"""Add parameter to context."""
self.params[key] = value
def validate_dependencies(self) -> None:
"""Validate parameter dependencies."""
# Example: if ssl is enabled, cert and key must be provided
if self.params.get("ssl") and not (
self.params.get("cert") and self.params.get("key")
):
raise typer.BadParameter("SSL requires both --cert and --key")
# Global validation context
validation_context = ValidationContext()
@app.command()
def server(
host: str = typer.Option(
"127.0.0.1",
"--host",
"-h",
help="Server host",
),
port: int = typer.Option(
8000,
"--port",
"-p",
help="Server port",
callback=lambda _, value: validate_port(value),
),
ssl: bool = typer.Option(
False,
"--ssl",
help="Enable SSL/TLS",
),
cert: Optional[Path] = typer.Option(
None,
"--cert",
help="SSL certificate file",
callback=lambda _, value: validate_path_exists(value) if value else None,
),
key: Optional[Path] = typer.Option(
None,
"--key",
help="SSL private key file",
callback=lambda _, value: validate_path_exists(value) if value else None,
),
) -> None:
"""Start server with validated parameters.
Example:
$ python cli.py server --port 8443 --ssl --cert cert.pem --key key.pem
"""
# Store params for cross-validation
validation_context.add("ssl", ssl)
validation_context.add("cert", cert)
validation_context.add("key", key)
# Validate dependencies
try:
validation_context.validate_dependencies()
except typer.BadParameter as e:
typer.secho(f"✗ Validation error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
# Start server
protocol = "https" if ssl else "http"
typer.echo(f"Starting server at {protocol}://{host}:{port}")
@app.command()
def user_create(
username: str = typer.Argument(..., help="Username (alphanumeric only)"),
email: str = typer.Option(
...,
"--email",
"-e",
help="User email",
callback=lambda _, value: validate_email(value),
),
age: Optional[int] = typer.Option(
None,
"--age",
help="User age",
min=13,
max=120,
),
) -> None:
"""Create user with validated inputs.
Example:
$ python cli.py user-create john --email john@example.com --age 25
"""
# Additional username validation
if not username.isalnum():
typer.secho(
"✗ Username must be alphanumeric", fg=typer.colors.RED, err=True
)
raise typer.Exit(1)
typer.secho(f"✓ User created: {username}", fg=typer.colors.GREEN)
@app.command()
def deploy(
url: str = typer.Option(
...,
"--url",
help="Deployment URL",
callback=lambda _, value: validate_url(value),
),
threshold: float = typer.Option(
95.0,
"--threshold",
help="Success threshold percentage",
callback=lambda _, value: validate_percentage(value),
),
rollback_on_error: bool = typer.Option(
True, "--rollback/--no-rollback", help="Rollback on error"
),
) -> None:
"""Deploy with validated URL and threshold.
Example:
$ python cli.py deploy --url https://example.com --threshold 99.5
"""
typer.echo(f"Deploying to: {url}")
typer.echo(f"Success threshold: {threshold}%")
typer.echo(f"Rollback on error: {rollback_on_error}")
@app.command()
def batch_process(
input_dir: Path = typer.Argument(
...,
help="Input directory",
callback=lambda _, value: validate_path_exists(value),
),
pattern: str = typer.Option(
"*.txt", "--pattern", "-p", help="File pattern"
),
workers: int = typer.Option(
4,
"--workers",
"-w",
help="Number of worker threads",
min=1,
max=32,
),
) -> None:
"""Batch process files with validation.
Example:
$ python cli.py batch-process ./data --pattern "*.json" --workers 8
"""
if not input_dir.is_dir():
typer.secho(
f"✗ Not a directory: {input_dir}", fg=typer.colors.RED, err=True
)
raise typer.Exit(1)
typer.echo(f"Processing files in: {input_dir}")
typer.echo(f"Pattern: {pattern}")
typer.echo(f"Workers: {workers}")
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,68 @@
"""Basic type-safe Typer command template.
This template demonstrates modern Typer usage with:
- Full type hints on all parameters
- Path type for file operations
- Optional parameters with defaults
- Typed return hints
"""
import typer
from pathlib import Path
from typing import Optional
app = typer.Typer()
@app.command()
def process(
input_file: Path = typer.Argument(
...,
help="Input file to process",
exists=True,
file_okay=True,
dir_okay=False,
readable=True,
),
output_file: Optional[Path] = typer.Option(
None,
"--output",
"-o",
help="Output file path (optional)",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-v",
help="Enable verbose output",
),
count: int = typer.Option(
10,
"--count",
"-c",
help="Number of items to process",
min=1,
max=1000,
),
) -> None:
"""Process input file with type-safe parameters.
Example:
$ python cli.py input.txt --output result.txt --verbose --count 50
"""
if verbose:
typer.echo(f"Processing {input_file}")
typer.echo(f"Count: {count}")
# Your processing logic here
content = input_file.read_text()
if output_file:
output_file.write_text(content)
typer.secho(f"✓ Saved to {output_file}", fg=typer.colors.GREEN)
else:
typer.echo(content)
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,100 @@
"""Enum-based options template for Typer.
This template demonstrates:
- Enum usage for constrained choices
- Multiple enum types
- Enum with autocomplete
- Type-safe enum handling
"""
import typer
from enum import Enum
from typing import Optional
class LogLevel(str, Enum):
"""Log level choices."""
debug = "debug"
info = "info"
warning = "warning"
error = "error"
class OutputFormat(str, Enum):
"""Output format choices."""
json = "json"
yaml = "yaml"
text = "text"
csv = "csv"
class Environment(str, Enum):
"""Deployment environment choices."""
development = "development"
staging = "staging"
production = "production"
app = typer.Typer()
@app.command()
def deploy(
environment: Environment = typer.Argument(
..., help="Target deployment environment"
),
format: OutputFormat = typer.Option(
OutputFormat.json, "--format", "-f", help="Output format for logs"
),
log_level: LogLevel = typer.Option(
LogLevel.info, "--log-level", "-l", help="Logging level"
),
force: bool = typer.Option(False, "--force", help="Force deployment"),
) -> None:
"""Deploy application with enum-based options.
Example:
$ python cli.py production --format yaml --log-level debug
"""
typer.echo(f"Deploying to: {environment.value}")
typer.echo(f"Output format: {format.value}")
typer.echo(f"Log level: {log_level.value}")
if force:
typer.secho("⚠ Force deployment enabled", fg=typer.colors.YELLOW)
# Deployment logic here
# The enum values are guaranteed to be valid
@app.command()
def export(
format: OutputFormat = typer.Argument(
OutputFormat.json, help="Export format"
),
output: Optional[str] = typer.Option(None, "--output", "-o"),
) -> None:
"""Export data in specified format.
Example:
$ python cli.py export yaml --output data.yaml
"""
typer.echo(f"Exporting as {format.value}")
# Export logic based on format
match format:
case OutputFormat.json:
typer.echo("Generating JSON...")
case OutputFormat.yaml:
typer.echo("Generating YAML...")
case OutputFormat.text:
typer.echo("Generating plain text...")
case OutputFormat.csv:
typer.echo("Generating CSV...")
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,164 @@
"""Sub-application structure template.
This template demonstrates:
- Multiple sub-apps for command organization
- Shared context between commands
- Hierarchical command structure
- Clean separation of concerns
"""
import typer
from typing import Optional
from pathlib import Path
# Main application
app = typer.Typer(
name="mycli",
help="Example CLI with sub-applications",
add_completion=True,
)
# Database sub-app
db_app = typer.Typer(help="Database management commands")
app.add_typer(db_app, name="db")
# Server sub-app
server_app = typer.Typer(help="Server management commands")
app.add_typer(server_app, name="server")
# User sub-app
user_app = typer.Typer(help="User management commands")
app.add_typer(user_app, name="user")
# Main app callback for global options
@app.callback()
def main(
ctx: typer.Context,
config: Optional[Path] = typer.Option(
None, "--config", "-c", help="Config file path"
),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Global options for all commands."""
# Store in context for sub-commands
ctx.obj = {"config": config, "verbose": verbose}
if verbose:
typer.echo(f"Config: {config or 'default'}")
# Database commands
@db_app.command("migrate")
def db_migrate(
ctx: typer.Context,
direction: str = typer.Argument("up", help="Migration direction: up/down"),
steps: int = typer.Option(1, help="Number of migration steps"),
) -> None:
"""Run database migrations."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Running {steps} migration(s) {direction}")
typer.secho("✓ Migrations complete", fg=typer.colors.GREEN)
@db_app.command("seed")
def db_seed(
ctx: typer.Context, file: Optional[Path] = typer.Option(None, "--file", "-f")
) -> None:
"""Seed database with test data."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Seeding from: {file or 'default seed'}")
typer.secho("✓ Database seeded", fg=typer.colors.GREEN)
@db_app.command("backup")
def db_backup(ctx: typer.Context, output: Path = typer.Argument(...)) -> None:
"""Backup database to file."""
typer.echo(f"Backing up database to {output}")
typer.secho("✓ Backup complete", fg=typer.colors.GREEN)
# Server commands
@server_app.command("start")
def server_start(
ctx: typer.Context,
port: int = typer.Option(8000, help="Server port"),
host: str = typer.Option("127.0.0.1", help="Server host"),
reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"),
) -> None:
"""Start the application server."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Starting server on {host}:{port}")
if reload:
typer.echo("Auto-reload enabled")
typer.secho("✓ Server started", fg=typer.colors.GREEN)
@server_app.command("stop")
def server_stop(ctx: typer.Context) -> None:
"""Stop the application server."""
typer.echo("Stopping server...")
typer.secho("✓ Server stopped", fg=typer.colors.GREEN)
@server_app.command("status")
def server_status(ctx: typer.Context) -> None:
"""Check server status."""
typer.echo("Server status: Running")
# User commands
@user_app.command("create")
def user_create(
ctx: typer.Context,
username: str = typer.Argument(..., help="Username"),
email: str = typer.Argument(..., help="Email address"),
admin: bool = typer.Option(False, "--admin", help="Create as admin"),
) -> None:
"""Create a new user."""
verbose = ctx.obj.get("verbose", False)
if verbose:
typer.echo(f"Creating user: {username} ({email})")
if admin:
typer.echo("Creating with admin privileges")
typer.secho(f"✓ User {username} created", fg=typer.colors.GREEN)
@user_app.command("delete")
def user_delete(
ctx: typer.Context,
username: str = typer.Argument(..., help="Username"),
force: bool = typer.Option(False, "--force", help="Force deletion"),
) -> None:
"""Delete a user."""
if not force:
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."""
typer.echo("Listing users...")
# List logic here
if __name__ == "__main__":
app()

View File

@@ -0,0 +1,143 @@
"""Typer instance factory pattern template.
This template demonstrates:
- Factory function for creating Typer apps
- Better testability
- Configuration injection
- Dependency management
"""
import typer
from typing import Optional, Protocol
from pathlib import Path
from dataclasses import dataclass
# Configuration
@dataclass
class Config:
"""Application configuration."""
verbose: bool = False
debug: bool = False
config_file: Optional[Path] = None
# Service protocol (dependency injection)
class StorageService(Protocol):
"""Storage service interface."""
def save(self, data: str) -> None:
"""Save data."""
...
def load(self) -> str:
"""Load data."""
...
class FileStorage:
"""File-based storage implementation."""
def __init__(self, base_path: Path) -> None:
self.base_path = base_path
def save(self, data: str) -> None:
"""Save data to file."""
self.base_path.write_text(data)
def load(self) -> str:
"""Load data from file."""
return self.base_path.read_text()
def create_app(
config: Optional[Config] = None, storage: Optional[StorageService] = None
) -> typer.Typer:
"""Factory function for creating Typer application.
This pattern allows for:
- Easy testing with mocked dependencies
- Configuration injection
- Multiple app instances with different configs
Args:
config: Application configuration
storage: Storage service implementation
Returns:
Configured Typer application
"""
config = config or Config()
storage = storage or FileStorage(Path("data.txt"))
app = typer.Typer(
name="myapp",
help="Example CLI with factory pattern",
add_completion=True,
no_args_is_help=True,
rich_markup_mode="rich",
)
@app.command()
def save(
data: str = typer.Argument(..., help="Data to save"),
force: bool = typer.Option(False, "--force", help="Overwrite existing"),
) -> None:
"""Save data using injected storage."""
if config.verbose:
typer.echo(f"Saving: {data}")
try:
storage.save(data)
typer.secho("✓ Data saved successfully", fg=typer.colors.GREEN)
except Exception as e:
if config.debug:
raise
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def load() -> None:
"""Load data using injected storage."""
try:
data = storage.load()
typer.echo(data)
except FileNotFoundError:
typer.secho("✗ No data found", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
except Exception as e:
if config.debug:
raise
typer.secho(f"✗ Error: {e}", fg=typer.colors.RED, err=True)
raise typer.Exit(1)
@app.command()
def status() -> None:
"""Show application status."""
typer.echo("Application Status:")
typer.echo(f" Verbose: {config.verbose}")
typer.echo(f" Debug: {config.debug}")
typer.echo(f" Config: {config.config_file or 'default'}")
return app
def main() -> None:
"""Main entry point with configuration setup."""
# Parse global options
import sys
verbose = "--verbose" in sys.argv or "-v" in sys.argv
debug = "--debug" in sys.argv
# Create configuration
config = Config(verbose=verbose, debug=debug)
# Create and run app
app = create_app(config=config)
app()
if __name__ == "__main__":
main()