# Implementation Templates Complete Python implementation patterns for CLI tools. ## CLI Skeleton (Typer) ```python # src//cli.py from __future__ import annotations import json from typing import Annotated, Optional import typer from rich.console import Console from rich.table import Table from . import __version__ from .client import Client from .config import get_token app = typer.Typer( name="", help="", no_args_is_help=True, ) # stderr for human output console = Console(stderr=True) # Exit codes EXIT_SUCCESS = 0 EXIT_ERROR = 1 EXIT_AUTH_REQUIRED = 2 EXIT_NOT_FOUND = 3 EXIT_VALIDATION = 4 EXIT_FORBIDDEN = 5 EXIT_RATE_LIMITED = 6 EXIT_CONFLICT = 7 def _output_json(data) -> None: """Output JSON to stdout.""" print(json.dumps(data, indent=2, default=str)) def _error( message: str, code: str = "ERROR", exit_code: int = EXIT_ERROR, details: dict = None, as_json: bool = False, ): """Output error and exit.""" error_obj = {"error": {"code": code, "message": message}} if details: error_obj["error"]["details"] = details if as_json: _output_json(error_obj) console.print(f"[red]Error:[/red] {message}") raise typer.Exit(exit_code) def _require_auth(as_json: bool = False): """Check authentication, exit if not authenticated.""" if not get_token(): _error( "Not authenticated. Run: auth login", "AUTH_REQUIRED", EXIT_AUTH_REQUIRED, as_json=as_json, ) # Version callback def version_callback(value: bool): if value: print(f" {__version__}") raise typer.Exit() @app.callback() def main( version: Annotated[ Optional[bool], typer.Option("--version", "-V", callback=version_callback, is_eager=True), ] = None, ): """""" pass # ============================================================ # AUTH COMMANDS # ============================================================ auth_app = typer.Typer(help="Authentication") app.add_typer(auth_app, name="auth") @auth_app.command("login") def auth_login(): """Authenticate with service.""" # Implementation... console.print("[green]Authenticated[/green]") @auth_app.command("status") def auth_status( json_output: Annotated[bool, typer.Option("--json")] = False, ): """ Check authentication status. Examples: auth status auth status --json """ token = get_token() status = {"authenticated": token is not None} if json_output: _output_json(status) return if status["authenticated"]: console.print("Authenticated: [green]yes[/green]") else: console.print("Authenticated: [red]no[/red]") @auth_app.command("logout") def auth_logout(): """Clear stored credentials.""" # Implementation... console.print("[green]Logged out[/green]") # ============================================================ # RESOURCE COMMANDS # ============================================================ items_app = typer.Typer(help="Item operations") app.add_typer(items_app, name="items") @items_app.command("list") def items_list( status: Annotated[ Optional[str], typer.Option("--status", "-s", help="Filter by status"), ] = None, limit: Annotated[ int, typer.Option("--limit", "-n", help="Max results"), ] = 20, json_output: Annotated[bool, typer.Option("--json")] = False, ): """ List items with optional filtering. Examples: items list items list --status active items list --limit 50 --json items list --json | jq '.data[].name' """ _require_auth(json_output) client = Client() items = client.list_items(status=status, limit=limit) if json_output: _output_json({ "data": items, "meta": {"count": len(items)}, }) return table = Table(title="Items") table.add_column("ID") table.add_column("Name") table.add_column("Status") for item in items: table.add_row(item["id"], item["name"], item.get("status", "")) console.print(table) @items_app.command("get") def items_get( item_id: Annotated[str, typer.Argument(help="Item ID")], json_output: Annotated[bool, typer.Option("--json")] = False, ): """ Get a specific item by ID. Examples: items get abc123 items get abc123 --json """ _require_auth(json_output) client = Client() item = client.get_item(item_id) if item is None: _error( f"Item not found: {item_id}", "NOT_FOUND", EXIT_NOT_FOUND, {"item_id": item_id}, json_output, ) if json_output: _output_json({"data": item}) return console.print(f"[bold]{item['name']}[/bold]") console.print(f" ID: {item['id']}") console.print(f" Status: {item.get('status', 'N/A')}") if __name__ == "__main__": app() ``` ## Client Pattern ```python # src//client.py from typing import Optional import httpx from .config import get_token class Client: """API client.""" BASE_URL = "https://api.example.com/v1" TIMEOUT = 30 def __init__(self): self.token = get_token() def _headers(self) -> dict: return { "Authorization": f"Bearer {self.token}", "Accept": "application/json", "Content-Type": "application/json", } def _get(self, endpoint: str, params: dict = None) -> Optional[dict]: """Make GET request.""" response = httpx.get( f"{self.BASE_URL}/{endpoint}", headers=self._headers(), params=params, timeout=self.TIMEOUT, ) response.raise_for_status() return response.json() def _post(self, endpoint: str, data: dict) -> Optional[dict]: """Make POST request.""" response = httpx.post( f"{self.BASE_URL}/{endpoint}", headers=self._headers(), json=data, timeout=self.TIMEOUT, ) response.raise_for_status() return response.json() def list_items(self, status: str = None, limit: int = 20) -> list: """List items with optional filters.""" params = {"limit": limit} if status: params["status"] = status data = self._get("items", params) return data.get("items", []) def get_item(self, item_id: str) -> Optional[dict]: """Get single item by ID.""" try: data = self._get(f"items/{item_id}") return data.get("item") except httpx.HTTPStatusError as e: if e.response.status_code == 404: return None raise ``` ## Config & Token Storage ### Recommended: OS Keyring with Fallbacks Use OS keyring for secure credential storage with fallbacks: ```python # src//config.py import os from pathlib import Path import keyring from dotenv import load_dotenv # Load .env file if it exists load_dotenv() SERVICE_NAME = "mytool" TOKEN_KEY = "api_token" def get_token() -> str | None: """ Get API token with priority: 1. Environment variable (CI/CD, testing) 2. OS keyring (secure storage) 3. .env file (local development fallback) """ # 1. Environment variable (highest priority) token = os.getenv("MYTOOL_API_TOKEN") if token: return token # 2. OS keyring (Windows Credential Manager, macOS Keychain, Linux Secret Service) try: token = keyring.get_password(SERVICE_NAME, TOKEN_KEY) if token: return token except Exception: # Keyring not available (headless, CI, etc.) pass # 3. .env file fallback # Already loaded by load_dotenv() above, so check env again token = os.getenv("MYTOOL_API_TOKEN") if token: return token return None def save_token(token: str) -> None: """Save API token to OS keyring.""" try: keyring.set_password(SERVICE_NAME, TOKEN_KEY, token) except Exception as e: # Keyring not available, fallback to .env file _save_to_dotenv(token) raise RuntimeWarning( f"Keyring unavailable, saved to .env file instead: {e}" ) def clear_token() -> None: """Remove stored token from all locations.""" # Clear from keyring try: keyring.delete_password(SERVICE_NAME, TOKEN_KEY) except Exception: pass # Clear from .env file env_file = Path.cwd() / ".env" if env_file.exists(): lines = env_file.read_text().splitlines() lines = [l for l in lines if not l.startswith("MYTOOL_API_TOKEN=")] env_file.write_text("\n".join(lines)) def get_token_source() -> str: """Get where the token is stored: 'environment', 'keyring', 'dotenv', or 'none'.""" if os.getenv("MYTOOL_API_TOKEN"): # Could be from env or .env, check if .env exists env_file = Path.cwd() / ".env" if env_file.exists() and "MYTOOL_API_TOKEN" in env_file.read_text(): return "dotenv" return "environment" try: token = keyring.get_password(SERVICE_NAME, TOKEN_KEY) if token: return "keyring" except Exception: pass return "none" def _save_to_dotenv(token: str) -> None: """Fallback: save to .env file.""" env_file = Path.cwd() / ".env" # Read existing content if env_file.exists(): lines = env_file.read_text().splitlines() # Remove existing MYTOOL_API_TOKEN lines lines = [l for l in lines if not l.startswith("MYTOOL_API_TOKEN=")] else: lines = [] # Add new token lines.append(f"MYTOOL_API_TOKEN={token}") # Write back env_file.write_text("\n".join(lines) + "\n") env_file.chmod(0o600) # Restrict permissions ``` **Dependencies:** ```toml # pyproject.toml dependencies = [ "keyring>=24.0.0", "python-dotenv>=1.0.0", ] ``` ### Simple: Config File Only For tools that don't need OS keyring: ```python # src//config.py import os from pathlib import Path def get_token() -> str | None: """Get API token from environment or config file.""" # 1. Environment variable (highest priority) token = os.getenv("MYTOOL_API_TOKEN") if token: return token # 2. Config file config_file = Path.home() / ".config" / "mytool" / "token" if config_file.exists(): return config_file.read_text().strip() return None def save_token(token: str) -> None: """Save API token to config file.""" config_dir = Path.home() / ".config" / "mytool" config_dir.mkdir(parents=True, exist_ok=True) config_file = config_dir / "token" config_file.write_text(token) config_file.chmod(0o600) # Restrict permissions def clear_token() -> None: """Remove stored token.""" config_file = Path.home() / ".config" / "mytool" / "token" if config_file.exists(): config_file.unlink() ``` ## Testing Pattern ```python # tests/test_cli.py import json from typer.testing import CliRunner from .cli import app runner = CliRunner() def test_help(): """--help shows usage.""" result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "" in result.stdout def test_version(): """--version shows version.""" result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 assert "0.1.0" in result.stdout def test_list_json(): """list --json outputs valid JSON.""" result = runner.invoke(app, ["items", "list", "--json"]) assert result.exit_code == 0 data = json.loads(result.stdout) assert "data" in data def test_not_found(): """get nonexistent returns exit code 3.""" result = runner.invoke(app, ["items", "get", "nonexistent-id"]) assert result.exit_code == 3 def test_json_error(): """Errors output valid JSON with --json.""" result = runner.invoke(app, ["items", "get", "bad-id", "--json"]) assert result.exit_code == 3 data = json.loads(result.stdout) assert "error" in data assert data["error"]["code"] == "NOT_FOUND" ``` ## Project Structure ``` / ├── README.md # User documentation ├── pyproject.toml # Package config ├── src// │ ├── __init__.py # Version │ ├── cli.py # Typer CLI entry point │ ├── client.py # API client │ ├── config.py # Settings & token storage │ └── models.py # Pydantic models (optional) └── tests/ ├── conftest.py ├── test_cli.py └── test_client.py ``` ## pyproject.toml ```toml [project] name = "-cli" version = "0.1.0" description = "What this tool does" readme = "README.md" requires-python = ">=3.11" dependencies = [ "typer>=0.9.0", "rich>=13.0.0", "httpx>=0.25.0", ] [project.optional-dependencies] dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.23.0", "ruff>=0.3.0", ] [project.scripts] = ".cli:app" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src/"] [tool.ruff] line-length = 100 target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" ```