ProvenPin Install

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
pinned versions3
click@8.5.0rich@15.0.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
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.

requirements.txt
click==8.5.0
rich==15.0.0
pytest==9.1.1
cli.py
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()
tests/test_cli.py
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.ini
[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.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.