Rich¶
Rich is the CLI’s presentation layer, and only that. It renders what the
tdw subcommands in src/test_data_workbench/cli/ already decided to
say; it makes no decisions of its own about what data to generate or how to
classify a schema.
Tables for structured output¶
rich.table.Table is the default shape for anything list-like: discovered
tables and their entity types, detected relationships, PII columns, and
validation results. cli/analyze.py’s table output is representative:
table = Table(title="Discovered Tables")
table.add_column("Table Name", style="cyan")
table.add_column("Entity Type", style="magenta")
table.add_column("Columns", justify="right", style="green")
table.add_column("Estimated Rows", justify="right", style="yellow")
for table_info in schema_info.tables:
table.add_row(
table_info.name,
table_info.entity_type.value,
str(len(table_info.columns)),
str(row_count) if row_count is not None and row_count > 0 else "Unknown",
)
The "Unknown" fallback for an absent row count is not a Rich concern by
itself, but it is exactly the kind of detail Rich’s tabular output makes
easy to get right: a metadata-only analysis (Privacy and Data Access)
leaves row_count as None, and the table renders that as an explicit
word rather than a blank cell or a stray "None" string.
Panel for status and next-step framing¶
rich.panel.Panel wraps summary and outcome messages, analysis complete,
deployment succeeded or failed, validation verdict, each with a title and
color-coded content via Rich’s inline markup ([bold green]...[/bold
green]). cli/deploy.py uses the border color itself as a signal, not
just the text inside it:
console.print(Panel(
f"[bold green]Deployment finished {status_word} target[/bold green]\n"
f"Completed in {total_time:.1f} seconds\n...",
title="Deployment complete",
border_style="green" if status_word == "within" else "yellow",
))
Every command that ends successfully also prints a “Next steps” panel
suggesting the next tdw command to run. That is a CLI design choice the
docs are attributing correctly to the CLI, not to Rich, but Rich’s Panel
is the mechanism that makes that guidance visually distinct from the data
above it.
Progress and Live for the two operations that take real time¶
Schema analysis and deployment are the only two commands that can run long
enough to need feedback mid-operation, and each uses a different Rich
primitive suited to its shape. cli/analyze.py uses an indeterminate
spinner, because there is nothing to measure progress against until the
analysis finishes:
with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"),
console=console) as progress:
task = progress.add_task("Connecting to database...", total=None)
...
schema_info = await analyzer.analyze_production_schema(...)
cli/deploy.py instead has three known phases with target times, so it
uses rich.live.Live to keep re-rendering a phase-status table in place as
each phase completes, rather than a spinner with no structure:
with Live(render_phase_table(), refresh_per_second=4, console=console) as live:
...
live.update(render_phase_table(time.time() - start_time))
The choice between the two is deliberate: a spinner communicates “working,
duration unknown”; a live-updating table communicates “here is the plan and
here is where we are in it.” Using Live for the multi-phase deploy and
Progress for the single-phase analyze matches the shape of what is
actually happening in each command.
Confined to the CLI¶
Rich does not appear in core, adaptation, or api. The FastAPI
layer (FastAPI) returns plain Pydantic models as JSON; formatting for
a terminal is entirely the CLI’s concern, which keeps Rich out of any code
path that does not actually print to a terminal.