Workflows and status¶
Modules: rs_rich.ext.workflow, cancel, transfer, countdown,
notify (Rust: rich_ext::workflow and friends). They show what a program is
doing: a tree of tasks, commands and their output, transfers, retries and
notifications.
Times are seconds. Pass a ManualClock (or now= values) for output that
does not depend on the wall clock, as these examples do.
Task trees¶
TaskTree holds tasks by integer id. Start, finish, warn, fail, skip or
cancel them; the tree shows state, elapsed time and progress, and
summary() gives a CompletionSummary when the work is done.
from rs_rich.console import Console
from rs_rich.ext import workflow
console = Console(width=60)
clock = workflow.ManualClock()
tree = workflow.TaskTree("Deploy", clock=clock)
build = tree.add("build")
compile_ = tree.add("compile", build)
upload = tree.add("upload")
tree.start(compile_)
clock.advance(9.2)
tree.succeed(compile_)
tree.start(upload)
tree.progress(upload, 21, 50)
clock.advance(1.9)
console.print(tree.view(animate=False))
print(tree.overall(), tree.counts())
Deploy
├── ✔ ok build 9.2s
│ └── ✔ ok compile 9.2s
└── ▶ running upload 21/50 (42%) 1.9s
running {'running': 1, 'succeeded': 1}
tree.fail(upload, "403 Forbidden")
console.print(tree.summary(next_steps=["check the bucket policy"]))
✖ error Deploy 11.1s
1 failed, 1 succeeded
✖ error upload 1.9s 403 Forbidden
Next steps:
→ check the bucket policy
tree.token(id) gives the task's CancelToken; cancelling a task cancels
its children's tokens. cancel.CancelToken is also usable on its own:
child() makes a token cancelled with its parent.
Commands¶
CommandRecord is a command, its output lines, status and duration.
run_command(argv, on_update=...) runs one and records it; view() renders
it with the last lines of output, and diagnostic() turns a failure into a
Diagnostic.
record = workflow.CommandRecord(
"cargo", ["test", "-p", "demo"],
stdout="running 3 tests\ntest parse::quoted ... FAILED",
stderr="error: test failed",
status=101,
duration=4.25,
)
print(record.state, record.detail, record.command_line)
console.print(record.view(tail=2))
failed exit 101 cargo test -p demo
✖ error $ cargo test -p demo exit 101 4.2s
│ running 3 tests
│ test parse::quoted ... FAILED
! error: test failed
error: `cargo test -p demo` exited with code 101
caused by: error: test failed
Transfers¶
transfer.Transfer tracks one download or upload: bytes, rate, ETA,
retries. Transfers renders several with a summary line.
wrap_reader/wrap_writer wrap a binary file so reading or writing updates
the transfer; task_fields() gives the total/completed/description
fields for an rs_rich.progress task.
from rs_rich.ext import transfer
iso = transfer.Transfer("debian-13.iso", total=650_000_000)
for second in range(11):
iso.update(second * 21_000_000, now=second)
docs = transfer.Transfer("docs.zip", total=2_400_000)
docs.finish(now=4)
console.print(transfer.Transfers([iso, docs], summary=True))
print(iso.state, iso.eta)
↓ debian-13.iso 210.0/650.0 MB 21.0 MB/s ETA 0:00:21
↓ docs.zip 2.4/2.4 MB - ✔ done
2 transfers, 1 done 212.4/652.4 MB 21.0 MB/s
active 21.0
Retries and rate limits¶
countdown.Backoff computes retry delays (exponential, capped, with seeded
jitter) and the status line for each attempt. RetryStatus, RateLimit and
CountdownBar render waits; countdown_wait sleeps in ticks and stops early
on a cancelled token.
from rs_rich.ext import countdown
backoff = countdown.Backoff(1, factor=2.0, max=30, attempts=4)
print([backoff.delay(attempt) for attempt in range(1, 6)])
wide = Console(width=90)
wide.print(backoff.status(2, "503 Service Unavailable"))
wide.print(countdown.RateLimit(42, scope="search API", limit=30, remaining=0, bar=20))
[1.0, 2.0, 4.0, None, None]
⚠ warning attempt 2/4 failed: 503 Service Unavailable — retrying in 2s
⚠ warning rate limited (search API) — resets in 0:00:42 (0/30 left) [##################]
Notifications¶
notify.Notifications is a stack of toasts that expire; Notification
renders on its own too, as a line or (toast="panel") a panel.