Recipe: Building a Production CLI Tool
Learn how to build a production-ready CLI tool using ElectriPy AI runtime components.
When to use this: You need a CLI interface around data processing, AI pipelines, or batch workloads, and want retry, rate limiting, and health checks built in from the start.
Overview
This recipe demonstrates building a data processing CLI tool that: - Reads data from JSONL files - Processes data with retry logic - Rate limits API calls - Provides health checks
Complete Example
#!/usr/bin/env python3
"""Data processor CLI tool."""
import asyncio
from pathlib import Path
import typer
from rich.console import Console
from electripy import Config, get_logger, setup_logging
from electripy.concurrency import async_retry, AsyncTokenBucketRateLimiter
from electripy.io import read_jsonl, write_jsonl
app = typer.Typer()
console = Console()
logger = get_logger(__name__)
@async_retry(max_attempts=3, delay=1.0)
async def process_record(record: dict, limiter: AsyncTokenBucketRateLimiter) -> dict:
"""Process a single record with retry and rate limiting."""
async with limiter:
# Simulate API call or processing
await asyncio.sleep(0.1)
# Transform the record
return {
"id": record["id"],
"processed": True,
"result": record.get("value", 0) * 2,
}
async def process_file_async(
input_path: Path,
output_path: Path,
rate_limit: int,
) -> None:
"""Process file asynchronously with rate limiting."""
limiter = AsyncTokenBucketRateLimiter(rate=rate_limit, capacity=rate_limit)
# Read all records
records = list(read_jsonl(input_path))
logger.info(f"Processing {len(records)} records")
# Process with concurrency
tasks = [process_record(record, limiter) for record in records]
results = await asyncio.gather(*tasks)
# Write results
write_jsonl(output_path, results)
logger.info(f"Wrote {len(results)} results to {output_path}")
@app.command()
def process(
input_file: Path = typer.Argument(..., help="Input JSONL file"),
output_file: Path = typer.Argument(..., help="Output JSONL file"),
rate_limit: int = typer.Option(10, help="Requests per second"),
verbose: bool = typer.Option(False, "--verbose", "-v"),
) -> None:
"""Process JSONL data with rate limiting and retry."""
# Setup
setup_logging(level="DEBUG" if verbose else "INFO")
config = Config.from_env()
console.print(f"[cyan]Processing {input_file} -> {output_file}[/cyan]")
# Validate input
if not input_file.exists():
console.print(f"[red]Error: {input_file} does not exist[/red]")
raise typer.Exit(1)
# Process
try:
asyncio.run(process_file_async(input_file, output_file, rate_limit))
console.print("[green]✓ Processing complete![/green]")
except Exception as e:
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
@app.command()
def doctor() -> None:
"""Check tool health."""
from electripy.cli.app import doctor as electripy_doctor
electripy_doctor()
if __name__ == "__main__":
app()
Usage
Save the above as data_processor.py and run:
# Process a file
python data_processor.py input.jsonl output.jsonl --rate-limit 10
# With verbose output
python data_processor.py input.jsonl output.jsonl -v
# Check health
python data_processor.py doctor
Key Features
- Async Processing: Uses asyncio for concurrent operations
- Rate Limiting: Controls request rate to external services
- Retry Logic: Automatically retries failed operations
- Logging: Structured logging with configurable levels
- Error Handling: Graceful error handling with user feedback
- Health Checks: Built-in diagnostics via doctor command
Customization
Extend this recipe by: - Adding more processing steps - Implementing different data sources (CSV, databases) - Adding authentication and API client setup - Creating additional CLI commands - Adding progress bars with Rich