Click CLI with rich Progress bars and Status for batch work: working versions (tested 2026-10-15)
A Click command normalizes a batch while rich draws a Progress bar and a Status spinner, with tests that pin down terminal detection, transient output and ANSI stripping.
passed6 passed · 0 failed2026-10-15 15:57 UTC- Status
- passed
- Last passed
- 2026-10-15 15:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38764699834
The code
Exactly the files that passed. Your agent can pull them with get_recipe("click-rich-progress") (install), or see them on GitHub.
click==8.5.0 rich==15.0.0 pytest==9.1.1
import click
from rich.console import Console
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TextColumn,
TimeElapsedColumn,
)
from rich.status import Status
def normalize(item: str) -> str:
if not item.strip():
raise ValueError("empty item")
return item.strip().lower().replace(" ", "-")
def make_progress(console: Console, transient: bool) -> Progress:
return Progress(
SpinnerColumn(),
TextColumn("[bold blue]{task.description}"),
BarColumn(),
TextColumn("{task.completed}/{task.total}"),
TimeElapsedColumn(),
console=console,
transient=transient,
)
def run_batch(items, console: Console, transient: bool = True):
with Status("[bold]validating[/]", console=console):
for item in items:
normalize(item)
results = []
with make_progress(console, transient) as progress:
task = progress.add_task("processing", total=len(items))
for item in items:
results.append(normalize(item))
progress.advance(task)
return results
@click.command()
@click.argument("items", nargs=-1, required=True)
@click.option("--transient/--no-transient", default=True, help="Erase bars when done.")
def main(items, transient):
console = Console()
results = run_batch(items, console, transient=transient)
console.print(f"[green]done[/] {len(results)} items")
for result in results:
console.print(result)
if __name__ == "__main__":
main()
import io
import click
from click.testing import CliRunner
from rich.console import Console
from cli import main, run_batch
def test_cli_transforms_items():
result = CliRunner().invoke(main, ["Hello World", " Foo Bar "], env={"FORCE_COLOR": ""})
assert result.exit_code == 0
assert "hello-world" in click.unstyle(result.output)
assert "foo-bar" in click.unstyle(result.output)
assert "done 2 items" in click.unstyle(result.output)
def test_non_terminal_output_stays_plain():
result = CliRunner().invoke(main, ["A B"], env={"FORCE_COLOR": ""})
assert "\x1b[" not in result.output
assert "done 1 items" in result.output
def test_ansi_codes_are_stripped_with_click_unstyle():
buf = io.StringIO()
console = Console(file=buf, force_terminal=True, color_system="standard", width=40)
run_batch(["A B"], console, transient=False)
assert "\x1b[" in buf.getvalue()
assert "1/1" in click.unstyle(buf.getvalue())
def test_transient_erases_the_bar():
buf = io.StringIO()
console = Console(file=buf, force_terminal=False, width=40)
run_batch(["a b"], console, transient=True)
assert "1/1" not in buf.getvalue()
def test_non_transient_keeps_the_bar():
buf = io.StringIO()
console = Console(file=buf, force_terminal=False, width=40)
run_batch(["a b", "c d"], console, transient=False)
assert "2/2" in buf.getvalue()
def test_bad_item_raises_and_stops_cleanly():
result = CliRunner().invoke(main, ["ok", " "], env={"FORCE_COLOR": ""})
assert result.exit_code != 0
assert isinstance(result.exception, ValueError)
assert "done" not in click.unstyle(result.output)
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Click CLI for RRULE next run times, tested with freezegun and CliRunner
A Click command that computes the next run times of an RFC 5545 rule in any IANA zone with dateutil rrule and zoneinfo, with DST gaps normalized and time frozen with freezegun in tests.
Typer CLI with Rich tables, progress and --json output
A Typer command that renders Rich tables and progress on stderr, with a --json mode for piping, tested offline with CliRunner and no-color Rich output.