[{"data":1,"prerenderedAt":3213},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks\u002F":3,"content-directory":2369},{"id":4,"title":5,"body":6,"date":2353,"description":2354,"difficulty":2355,"draft":2356,"extension":2357,"meta":2358,"navigation":137,"path":2359,"seo":2360,"stem":2361,"tags":2362,"updated":2353,"__hash__":2368},"content\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks\u002Findex.md","Showing Progress for Concurrent Tasks in a Python CLI",{"type":7,"value":8,"toc":2332},"minimark",[9,40,45,68,72,76,92,96,1504,1509,1537,1541,1555,1559,1572,1575,1579,1604,1608,1652,1655,1659,1666,2201,2204,2208,2217,2221,2225,2231,2235,2256,2260,2267,2271,2282,2286,2296,2300,2329],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,39],"p",{},"A progress bar for sequential work is straightforward: one item at a time, advance after each. Concurrent work breaks the picture. Eight uploads run at once, each at its own speed; some finish in a second, one takes a minute, two fail halfway. A single bar that jumps when items complete hides which files are in flight and why the last 10% takes forever. A bar per item, all forty of them, floods the screen. And none of it should appear when the output goes to a CI log, where animated bars become thousands of lines of escape codes. This guide builds a progress display for concurrent tasks that shows the right things: one ",[14,15,16],"strong",{},"overall"," bar with counts and an ETA, one transient ",[14,19,20],{},"row per in-flight item"," that disappears when the item finishes, ",[14,23,24],{},"failures printed permanently above"," the live area as they happen, a ",[14,27,28],{},"final summary",", and ",[14,31,32],{},"plain periodic lines"," instead of animation when stdout is not a terminal. It belongs to the ",[35,36,38],"a",{"href":37},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002F","concurrency and async topic",".",[41,42,44],"h2",{"id":43},"prerequisites","Prerequisites",[46,47,48,56],"ul",{},[49,50,51,55],"li",{},[52,53,54],"code",{},"uv add rich"," (examples checked with Rich 15 and Python 3.13).",[49,57,58,62,63,67],{},[35,59,61],{"href":60},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis\u002F","Adding progress bars and spinners to Python CLIs"," for Rich progress basics, and ",[35,64,66],{"href":65},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis\u002F","structured concurrency with TaskGroups"," for the concurrency side.",[41,69,71],{"id":70},"what-to-show","What to show",[73,74],"inline-diagram",{"name":75},"cprog-layout",[10,77,78,79,83,84,87,88,91],{},"The layout answers the questions a user has while waiting, in order of importance. ",[80,81,82],"em",{},"How far along is it, and how long will it take?"," — the overall bar with “23\u002F40” and a time estimate. ",[80,85,86],{},"Is it stuck?"," — the in-flight rows, each with its own small bar and byte count, which show that work is moving and which item is slow. ",[80,89,90],{},"Did anything go wrong?"," — failure lines that stay on screen after the live area is gone, so the user does not have to scroll through a log at the end. The number of in-flight rows equals the concurrency limit, so the display has a fixed height no matter how many items there are.",[41,93,95],{"id":94},"the-recipe","The recipe",[97,98,103],"pre",{"className":99,"code":100,"language":101,"meta":102,"style":102},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fconcurrent_progress.py\nfrom __future__ import annotations\n\nimport asyncio\nimport time\nfrom collections.abc import Awaitable, Callable, Sequence\n\nfrom rich.console import Console, Group\nfrom rich.live import Live\nfrom rich.progress import (BarColumn, DownloadColumn, MofNCompleteColumn, Progress, SpinnerColumn,\n                           TaskID, TextColumn, TimeRemainingColumn)\n\n\nclass ConcurrentProgress:\n    \"\"\"One overall bar plus a row per in-flight item; plain periodic lines when not a terminal.\"\"\"\n\n    def __init__(self, total: int, console: Console, *, label: str = \"items\",\n                 plain_every: float = 5.0) -> None:\n        self.total, self.label, self.console = total, label, console\n        self.done = self.failed = 0\n        self.interactive = console.is_terminal\n        self.plain_every, self._last_plain = plain_every, time.monotonic()\n        self.progress = Progress(\n            SpinnerColumn(), TextColumn(\"{task.description}\", style=\"bold\"),\n            BarColumn(), MofNCompleteColumn(), TimeRemainingColumn(),\n            console=console, transient=False, disable=not self.interactive,\n        )\n        self.items = Progress(\n            TextColumn(\"  {task.description}\"), BarColumn(bar_width=20), DownloadColumn(),\n            console=console, transient=True, disable=not self.interactive,\n        )\n        self.overall: TaskID = self.progress.add_task(label, total=total)\n\n    def __enter__(self) -> ConcurrentProgress:\n        if self.interactive:\n            self.live = Live(Group(self.progress, self.items), console=self.console,\n                             refresh_per_second=10, transient=False)\n            self.live.__enter__()\n        return self\n\n    def __exit__(self, *exc) -> None:\n        if self.interactive:\n            self.live.__exit__(*exc)\n        self.console.print(f\"{self.done - self.failed} of {self.total} {self.label} done\"\n                           + (f\", {self.failed} failed\" if self.failed else \"\"))\n\n    def start_item(self, name: str, size: int | None) -> TaskID:\n        return self.items.add_task(name, total=size)\n\n    def advance_item(self, item: TaskID, amount: int) -> None:\n        self.items.update(item, advance=amount)\n\n    def finish_item(self, item: TaskID, name: str, error: str | None = None) -> None:\n        self.items.remove_task(item)\n        self.done += 1\n        if error:\n            self.failed += 1\n            self.console.print(f\"[red]✗[\u002Fred] {name}: {error}\")   # stays above the live area\n        self.progress.update(self.overall, advance=1,\n                             description=f\"{self.label} ({self.failed} failed)\" if self.failed else self.label)\n        self._maybe_plain()\n\n    def _maybe_plain(self) -> None:\n        now = time.monotonic()\n        if not self.interactive and (now - self._last_plain >= self.plain_every or self.done == self.total):\n            self._last_plain = now\n            self.console.print(f\"progress: {self.done}\u002F{self.total} {self.label}, {self.failed} failed\")\n\n\nasync def run_all(items: Sequence[tuple[str, int]], worker: Callable[..., Awaitable[None]],\n                  console: Console, *, jobs: int = 4) -> ConcurrentProgress:\n    gate = asyncio.Semaphore(jobs)\n    with ConcurrentProgress(len(items), console, label=\"files\") as ui:\n\n        async def one(name: str, size: int) -> None:\n            async with gate:\n                row = ui.start_item(name, size)\n                try:\n                    await worker(name, size, lambda n: ui.advance_item(row, n))\n                except OSError as exc:\n                    ui.finish_item(row, name, str(exc))\n                else:\n                    ui.finish_item(row, name)\n\n        async with asyncio.TaskGroup() as tg:\n            for name, size in items:\n                tg.create_task(one(name, size))\n    return ui\n","python","",[52,104,105,114,132,139,148,156,169,174,187,200,213,219,224,229,242,249,254,290,312,338,359,372,390,403,432,438,470,476,488,515,541,546,569,574,585,596,630,652,666,675,680,700,709,727,777,816,821,848,865,870,889,905,910,942,950,963,971,982,1020,1042,1084,1092,1097,1112,1123,1170,1182,1228,1233,1238,1273,1294,1305,1337,1342,1368,1380,1391,1399,1414,1429,1440,1448,1454,1459,1474,1489,1495],{"__ignoreMap":102},[106,107,110],"span",{"class":108,"line":109},"line",1,[106,111,113],{"class":112},"sJ8bj","# src\u002Fmytool\u002Fconcurrent_progress.py\n",[106,115,117,121,125,128],{"class":108,"line":116},2,[106,118,120],{"class":119},"szBVR","from",[106,122,124],{"class":123},"sj4cs"," __future__",[106,126,127],{"class":119}," import",[106,129,131],{"class":130},"sVt8B"," annotations\n",[106,133,135],{"class":108,"line":134},3,[106,136,138],{"emptyLinePlaceholder":137},true,"\n",[106,140,142,145],{"class":108,"line":141},4,[106,143,144],{"class":119},"import",[106,146,147],{"class":130}," asyncio\n",[106,149,151,153],{"class":108,"line":150},5,[106,152,144],{"class":119},[106,154,155],{"class":130}," time\n",[106,157,159,161,164,166],{"class":108,"line":158},6,[106,160,120],{"class":119},[106,162,163],{"class":130}," collections.abc ",[106,165,144],{"class":119},[106,167,168],{"class":130}," Awaitable, Callable, Sequence\n",[106,170,172],{"class":108,"line":171},7,[106,173,138],{"emptyLinePlaceholder":137},[106,175,177,179,182,184],{"class":108,"line":176},8,[106,178,120],{"class":119},[106,180,181],{"class":130}," rich.console ",[106,183,144],{"class":119},[106,185,186],{"class":130}," Console, Group\n",[106,188,190,192,195,197],{"class":108,"line":189},9,[106,191,120],{"class":119},[106,193,194],{"class":130}," rich.live ",[106,196,144],{"class":119},[106,198,199],{"class":130}," Live\n",[106,201,203,205,208,210],{"class":108,"line":202},10,[106,204,120],{"class":119},[106,206,207],{"class":130}," rich.progress ",[106,209,144],{"class":119},[106,211,212],{"class":130}," (BarColumn, DownloadColumn, MofNCompleteColumn, Progress, SpinnerColumn,\n",[106,214,216],{"class":108,"line":215},11,[106,217,218],{"class":130},"                           TaskID, TextColumn, TimeRemainingColumn)\n",[106,220,222],{"class":108,"line":221},12,[106,223,138],{"emptyLinePlaceholder":137},[106,225,227],{"class":108,"line":226},13,[106,228,138],{"emptyLinePlaceholder":137},[106,230,232,235,239],{"class":108,"line":231},14,[106,233,234],{"class":119},"class",[106,236,238],{"class":237},"sScJk"," ConcurrentProgress",[106,240,241],{"class":130},":\n",[106,243,245],{"class":108,"line":244},15,[106,246,248],{"class":247},"sZZnC","    \"\"\"One overall bar plus a row per in-flight item; plain periodic lines when not a terminal.\"\"\"\n",[106,250,252],{"class":108,"line":251},16,[106,253,138],{"emptyLinePlaceholder":137},[106,255,257,260,263,266,269,272,275,278,281,284,287],{"class":108,"line":256},17,[106,258,259],{"class":119},"    def",[106,261,262],{"class":123}," __init__",[106,264,265],{"class":130},"(self, total: ",[106,267,268],{"class":123},"int",[106,270,271],{"class":130},", console: Console, ",[106,273,274],{"class":119},"*",[106,276,277],{"class":130},", label: ",[106,279,280],{"class":123},"str",[106,282,283],{"class":119}," =",[106,285,286],{"class":247}," \"items\"",[106,288,289],{"class":130},",\n",[106,291,293,296,299,301,304,307,310],{"class":108,"line":292},18,[106,294,295],{"class":130},"                 plain_every: ",[106,297,298],{"class":123},"float",[106,300,283],{"class":119},[106,302,303],{"class":123}," 5.0",[106,305,306],{"class":130},") -> ",[106,308,309],{"class":123},"None",[106,311,241],{"class":130},[106,313,315,318,321,324,327,329,332,335],{"class":108,"line":314},19,[106,316,317],{"class":123},"        self",[106,319,320],{"class":130},".total, ",[106,322,323],{"class":123},"self",[106,325,326],{"class":130},".label, ",[106,328,323],{"class":123},[106,330,331],{"class":130},".console ",[106,333,334],{"class":119},"=",[106,336,337],{"class":130}," total, label, console\n",[106,339,341,343,346,348,351,354,356],{"class":108,"line":340},20,[106,342,317],{"class":123},[106,344,345],{"class":130},".done ",[106,347,334],{"class":119},[106,349,350],{"class":123}," self",[106,352,353],{"class":130},".failed ",[106,355,334],{"class":119},[106,357,358],{"class":123}," 0\n",[106,360,362,364,367,369],{"class":108,"line":361},21,[106,363,317],{"class":123},[106,365,366],{"class":130},".interactive ",[106,368,334],{"class":119},[106,370,371],{"class":130}," console.is_terminal\n",[106,373,375,377,380,382,385,387],{"class":108,"line":374},22,[106,376,317],{"class":123},[106,378,379],{"class":130},".plain_every, ",[106,381,323],{"class":123},[106,383,384],{"class":130},"._last_plain ",[106,386,334],{"class":119},[106,388,389],{"class":130}," plain_every, time.monotonic()\n",[106,391,393,395,398,400],{"class":108,"line":392},23,[106,394,317],{"class":123},[106,396,397],{"class":130},".progress ",[106,399,334],{"class":119},[106,401,402],{"class":130}," Progress(\n",[106,404,406,409,412,415,417,420,424,426,429],{"class":108,"line":405},24,[106,407,408],{"class":130},"            SpinnerColumn(), TextColumn(",[106,410,411],{"class":247},"\"",[106,413,414],{"class":123},"{task.description}",[106,416,411],{"class":247},[106,418,419],{"class":130},", ",[106,421,423],{"class":422},"s4XuR","style",[106,425,334],{"class":119},[106,427,428],{"class":247},"\"bold\"",[106,430,431],{"class":130},"),\n",[106,433,435],{"class":108,"line":434},25,[106,436,437],{"class":130},"            BarColumn(), MofNCompleteColumn(), TimeRemainingColumn(),\n",[106,439,441,444,446,449,452,454,457,459,462,465,467],{"class":108,"line":440},26,[106,442,443],{"class":422},"            console",[106,445,334],{"class":119},[106,447,448],{"class":130},"console, ",[106,450,451],{"class":422},"transient",[106,453,334],{"class":119},[106,455,456],{"class":123},"False",[106,458,419],{"class":130},[106,460,461],{"class":422},"disable",[106,463,464],{"class":119},"=not",[106,466,350],{"class":123},[106,468,469],{"class":130},".interactive,\n",[106,471,473],{"class":108,"line":472},27,[106,474,475],{"class":130},"        )\n",[106,477,479,481,484,486],{"class":108,"line":478},28,[106,480,317],{"class":123},[106,482,483],{"class":130},".items ",[106,485,334],{"class":119},[106,487,402],{"class":130},[106,489,491,494,497,499,501,504,507,509,512],{"class":108,"line":490},29,[106,492,493],{"class":130},"            TextColumn(",[106,495,496],{"class":247},"\"  ",[106,498,414],{"class":123},[106,500,411],{"class":247},[106,502,503],{"class":130},"), BarColumn(",[106,505,506],{"class":422},"bar_width",[106,508,334],{"class":119},[106,510,511],{"class":123},"20",[106,513,514],{"class":130},"), DownloadColumn(),\n",[106,516,518,520,522,524,526,528,531,533,535,537,539],{"class":108,"line":517},30,[106,519,443],{"class":422},[106,521,334],{"class":119},[106,523,448],{"class":130},[106,525,451],{"class":422},[106,527,334],{"class":119},[106,529,530],{"class":123},"True",[106,532,419],{"class":130},[106,534,461],{"class":422},[106,536,464],{"class":119},[106,538,350],{"class":123},[106,540,469],{"class":130},[106,542,544],{"class":108,"line":543},31,[106,545,475],{"class":130},[106,547,549,551,554,556,558,561,564,566],{"class":108,"line":548},32,[106,550,317],{"class":123},[106,552,553],{"class":130},".overall: TaskID ",[106,555,334],{"class":119},[106,557,350],{"class":123},[106,559,560],{"class":130},".progress.add_task(label, ",[106,562,563],{"class":422},"total",[106,565,334],{"class":119},[106,567,568],{"class":130},"total)\n",[106,570,572],{"class":108,"line":571},33,[106,573,138],{"emptyLinePlaceholder":137},[106,575,577,579,582],{"class":108,"line":576},34,[106,578,259],{"class":119},[106,580,581],{"class":123}," __enter__",[106,583,584],{"class":130},"(self) -> ConcurrentProgress:\n",[106,586,588,591,593],{"class":108,"line":587},35,[106,589,590],{"class":119},"        if",[106,592,350],{"class":123},[106,594,595],{"class":130},".interactive:\n",[106,597,599,602,605,607,610,612,615,617,620,623,625,627],{"class":108,"line":598},36,[106,600,601],{"class":123},"            self",[106,603,604],{"class":130},".live ",[106,606,334],{"class":119},[106,608,609],{"class":130}," Live(Group(",[106,611,323],{"class":123},[106,613,614],{"class":130},".progress, ",[106,616,323],{"class":123},[106,618,619],{"class":130},".items), ",[106,621,622],{"class":422},"console",[106,624,334],{"class":119},[106,626,323],{"class":123},[106,628,629],{"class":130},".console,\n",[106,631,633,636,638,641,643,645,647,649],{"class":108,"line":632},37,[106,634,635],{"class":422},"                             refresh_per_second",[106,637,334],{"class":119},[106,639,640],{"class":123},"10",[106,642,419],{"class":130},[106,644,451],{"class":422},[106,646,334],{"class":119},[106,648,456],{"class":123},[106,650,651],{"class":130},")\n",[106,653,655,657,660,663],{"class":108,"line":654},38,[106,656,601],{"class":123},[106,658,659],{"class":130},".live.",[106,661,662],{"class":123},"__enter__",[106,664,665],{"class":130},"()\n",[106,667,669,672],{"class":108,"line":668},39,[106,670,671],{"class":119},"        return",[106,673,674],{"class":123}," self\n",[106,676,678],{"class":108,"line":677},40,[106,679,138],{"emptyLinePlaceholder":137},[106,681,683,685,688,691,693,696,698],{"class":108,"line":682},41,[106,684,259],{"class":119},[106,686,687],{"class":123}," __exit__",[106,689,690],{"class":130},"(self, ",[106,692,274],{"class":119},[106,694,695],{"class":130},"exc) -> ",[106,697,309],{"class":123},[106,699,241],{"class":130},[106,701,703,705,707],{"class":108,"line":702},42,[106,704,590],{"class":119},[106,706,350],{"class":123},[106,708,595],{"class":130},[106,710,712,714,716,719,722,724],{"class":108,"line":711},43,[106,713,601],{"class":123},[106,715,659],{"class":130},[106,717,718],{"class":123},"__exit__",[106,720,721],{"class":130},"(",[106,723,274],{"class":119},[106,725,726],{"class":130},"exc)\n",[106,728,730,732,735,738,740,743,745,748,750,753,756,759,761,764,766,769,772,774],{"class":108,"line":729},44,[106,731,317],{"class":123},[106,733,734],{"class":130},".console.print(",[106,736,737],{"class":119},"f",[106,739,411],{"class":247},[106,741,742],{"class":123},"{self",[106,744,345],{"class":130},[106,746,747],{"class":119},"-",[106,749,350],{"class":123},[106,751,752],{"class":130},".failed",[106,754,755],{"class":123},"}",[106,757,758],{"class":247}," of ",[106,760,742],{"class":123},[106,762,763],{"class":130},".total",[106,765,755],{"class":123},[106,767,768],{"class":123}," {self",[106,770,771],{"class":130},".label",[106,773,755],{"class":123},[106,775,776],{"class":247}," done\"\n",[106,778,780,783,786,788,791,793,795,797,800,803,805,807,810,813],{"class":108,"line":779},45,[106,781,782],{"class":119},"                           +",[106,784,785],{"class":130}," (",[106,787,737],{"class":119},[106,789,790],{"class":247},"\", ",[106,792,742],{"class":123},[106,794,752],{"class":130},[106,796,755],{"class":123},[106,798,799],{"class":247}," failed\"",[106,801,802],{"class":119}," if",[106,804,350],{"class":123},[106,806,353],{"class":130},[106,808,809],{"class":119},"else",[106,811,812],{"class":247}," \"\"",[106,814,815],{"class":130},"))\n",[106,817,819],{"class":108,"line":818},46,[106,820,138],{"emptyLinePlaceholder":137},[106,822,824,826,829,832,834,837,839,842,845],{"class":108,"line":823},47,[106,825,259],{"class":119},[106,827,828],{"class":237}," start_item",[106,830,831],{"class":130},"(self, name: ",[106,833,280],{"class":123},[106,835,836],{"class":130},", size: ",[106,838,268],{"class":123},[106,840,841],{"class":119}," |",[106,843,844],{"class":123}," None",[106,846,847],{"class":130},") -> TaskID:\n",[106,849,851,853,855,858,860,862],{"class":108,"line":850},48,[106,852,671],{"class":119},[106,854,350],{"class":123},[106,856,857],{"class":130},".items.add_task(name, ",[106,859,563],{"class":422},[106,861,334],{"class":119},[106,863,864],{"class":130},"size)\n",[106,866,868],{"class":108,"line":867},49,[106,869,138],{"emptyLinePlaceholder":137},[106,871,873,875,878,881,883,885,887],{"class":108,"line":872},50,[106,874,259],{"class":119},[106,876,877],{"class":237}," advance_item",[106,879,880],{"class":130},"(self, item: TaskID, amount: ",[106,882,268],{"class":123},[106,884,306],{"class":130},[106,886,309],{"class":123},[106,888,241],{"class":130},[106,890,892,894,897,900,902],{"class":108,"line":891},51,[106,893,317],{"class":123},[106,895,896],{"class":130},".items.update(item, ",[106,898,899],{"class":422},"advance",[106,901,334],{"class":119},[106,903,904],{"class":130},"amount)\n",[106,906,908],{"class":108,"line":907},52,[106,909,138],{"emptyLinePlaceholder":137},[106,911,913,915,918,921,923,926,928,930,932,934,936,938,940],{"class":108,"line":912},53,[106,914,259],{"class":119},[106,916,917],{"class":237}," finish_item",[106,919,920],{"class":130},"(self, item: TaskID, name: ",[106,922,280],{"class":123},[106,924,925],{"class":130},", error: ",[106,927,280],{"class":123},[106,929,841],{"class":119},[106,931,844],{"class":123},[106,933,283],{"class":119},[106,935,844],{"class":123},[106,937,306],{"class":130},[106,939,309],{"class":123},[106,941,241],{"class":130},[106,943,945,947],{"class":108,"line":944},54,[106,946,317],{"class":123},[106,948,949],{"class":130},".items.remove_task(item)\n",[106,951,953,955,957,960],{"class":108,"line":952},55,[106,954,317],{"class":123},[106,956,345],{"class":130},[106,958,959],{"class":119},"+=",[106,961,962],{"class":123}," 1\n",[106,964,966,968],{"class":108,"line":965},56,[106,967,590],{"class":119},[106,969,970],{"class":130}," error:\n",[106,972,974,976,978,980],{"class":108,"line":973},57,[106,975,601],{"class":123},[106,977,353],{"class":130},[106,979,959],{"class":119},[106,981,962],{"class":123},[106,983,985,987,989,991,994,997,1000,1002,1005,1007,1010,1012,1014,1017],{"class":108,"line":984},58,[106,986,601],{"class":123},[106,988,734],{"class":130},[106,990,737],{"class":119},[106,992,993],{"class":247},"\"[red]✗[\u002Fred] ",[106,995,996],{"class":123},"{",[106,998,999],{"class":130},"name",[106,1001,755],{"class":123},[106,1003,1004],{"class":247},": ",[106,1006,996],{"class":123},[106,1008,1009],{"class":130},"error",[106,1011,755],{"class":123},[106,1013,411],{"class":247},[106,1015,1016],{"class":130},")   ",[106,1018,1019],{"class":112},"# stays above the live area\n",[106,1021,1023,1025,1028,1030,1033,1035,1037,1040],{"class":108,"line":1022},59,[106,1024,317],{"class":123},[106,1026,1027],{"class":130},".progress.update(",[106,1029,323],{"class":123},[106,1031,1032],{"class":130},".overall, ",[106,1034,899],{"class":422},[106,1036,334],{"class":119},[106,1038,1039],{"class":123},"1",[106,1041,289],{"class":130},[106,1043,1045,1048,1050,1052,1054,1056,1058,1060,1062,1064,1066,1068,1071,1073,1075,1077,1079,1081],{"class":108,"line":1044},60,[106,1046,1047],{"class":422},"                             description",[106,1049,334],{"class":119},[106,1051,737],{"class":119},[106,1053,411],{"class":247},[106,1055,742],{"class":123},[106,1057,771],{"class":130},[106,1059,755],{"class":123},[106,1061,785],{"class":247},[106,1063,742],{"class":123},[106,1065,752],{"class":130},[106,1067,755],{"class":123},[106,1069,1070],{"class":247}," failed)\"",[106,1072,802],{"class":119},[106,1074,350],{"class":123},[106,1076,353],{"class":130},[106,1078,809],{"class":119},[106,1080,350],{"class":123},[106,1082,1083],{"class":130},".label)\n",[106,1085,1087,1089],{"class":108,"line":1086},61,[106,1088,317],{"class":123},[106,1090,1091],{"class":130},"._maybe_plain()\n",[106,1093,1095],{"class":108,"line":1094},62,[106,1096,138],{"emptyLinePlaceholder":137},[106,1098,1100,1102,1105,1108,1110],{"class":108,"line":1099},63,[106,1101,259],{"class":119},[106,1103,1104],{"class":237}," _maybe_plain",[106,1106,1107],{"class":130},"(self) -> ",[106,1109,309],{"class":123},[106,1111,241],{"class":130},[106,1113,1115,1118,1120],{"class":108,"line":1114},64,[106,1116,1117],{"class":130},"        now ",[106,1119,334],{"class":119},[106,1121,1122],{"class":130}," time.monotonic()\n",[106,1124,1126,1128,1131,1133,1135,1138,1141,1143,1145,1147,1150,1152,1155,1158,1160,1162,1165,1167],{"class":108,"line":1125},65,[106,1127,590],{"class":119},[106,1129,1130],{"class":119}," not",[106,1132,350],{"class":123},[106,1134,366],{"class":130},[106,1136,1137],{"class":119},"and",[106,1139,1140],{"class":130}," (now ",[106,1142,747],{"class":119},[106,1144,350],{"class":123},[106,1146,384],{"class":130},[106,1148,1149],{"class":119},">=",[106,1151,350],{"class":123},[106,1153,1154],{"class":130},".plain_every ",[106,1156,1157],{"class":119},"or",[106,1159,350],{"class":123},[106,1161,345],{"class":130},[106,1163,1164],{"class":119},"==",[106,1166,350],{"class":123},[106,1168,1169],{"class":130},".total):\n",[106,1171,1173,1175,1177,1179],{"class":108,"line":1172},66,[106,1174,601],{"class":123},[106,1176,384],{"class":130},[106,1178,334],{"class":119},[106,1180,1181],{"class":130}," now\n",[106,1183,1185,1187,1189,1191,1194,1196,1199,1201,1204,1206,1208,1210,1212,1214,1216,1218,1220,1222,1224,1226],{"class":108,"line":1184},67,[106,1186,601],{"class":123},[106,1188,734],{"class":130},[106,1190,737],{"class":119},[106,1192,1193],{"class":247},"\"progress: ",[106,1195,742],{"class":123},[106,1197,1198],{"class":130},".done",[106,1200,755],{"class":123},[106,1202,1203],{"class":247},"\u002F",[106,1205,742],{"class":123},[106,1207,763],{"class":130},[106,1209,755],{"class":123},[106,1211,768],{"class":123},[106,1213,771],{"class":130},[106,1215,755],{"class":123},[106,1217,419],{"class":247},[106,1219,742],{"class":123},[106,1221,752],{"class":130},[106,1223,755],{"class":123},[106,1225,799],{"class":247},[106,1227,651],{"class":130},[106,1229,1231],{"class":108,"line":1230},68,[106,1232,138],{"emptyLinePlaceholder":137},[106,1234,1236],{"class":108,"line":1235},69,[106,1237,138],{"emptyLinePlaceholder":137},[106,1239,1241,1244,1247,1250,1253,1255,1257,1259,1262,1265,1268,1270],{"class":108,"line":1240},70,[106,1242,1243],{"class":119},"async",[106,1245,1246],{"class":119}," def",[106,1248,1249],{"class":237}," run_all",[106,1251,1252],{"class":130},"(items: Sequence[tuple[",[106,1254,280],{"class":123},[106,1256,419],{"class":130},[106,1258,268],{"class":123},[106,1260,1261],{"class":130},"]], worker: Callable[",[106,1263,1264],{"class":123},"...",[106,1266,1267],{"class":130},", Awaitable[",[106,1269,309],{"class":123},[106,1271,1272],{"class":130},"]],\n",[106,1274,1276,1279,1281,1284,1286,1288,1291],{"class":108,"line":1275},71,[106,1277,1278],{"class":130},"                  console: Console, ",[106,1280,274],{"class":119},[106,1282,1283],{"class":130},", jobs: ",[106,1285,268],{"class":123},[106,1287,283],{"class":119},[106,1289,1290],{"class":123}," 4",[106,1292,1293],{"class":130},") -> ConcurrentProgress:\n",[106,1295,1297,1300,1302],{"class":108,"line":1296},72,[106,1298,1299],{"class":130},"    gate ",[106,1301,334],{"class":119},[106,1303,1304],{"class":130}," asyncio.Semaphore(jobs)\n",[106,1306,1308,1311,1314,1317,1320,1323,1325,1328,1331,1334],{"class":108,"line":1307},73,[106,1309,1310],{"class":119},"    with",[106,1312,1313],{"class":130}," ConcurrentProgress(",[106,1315,1316],{"class":123},"len",[106,1318,1319],{"class":130},"(items), console, ",[106,1321,1322],{"class":422},"label",[106,1324,334],{"class":119},[106,1326,1327],{"class":247},"\"files\"",[106,1329,1330],{"class":130},") ",[106,1332,1333],{"class":119},"as",[106,1335,1336],{"class":130}," ui:\n",[106,1338,1340],{"class":108,"line":1339},74,[106,1341,138],{"emptyLinePlaceholder":137},[106,1343,1345,1348,1350,1353,1356,1358,1360,1362,1364,1366],{"class":108,"line":1344},75,[106,1346,1347],{"class":119},"        async",[106,1349,1246],{"class":119},[106,1351,1352],{"class":237}," one",[106,1354,1355],{"class":130},"(name: ",[106,1357,280],{"class":123},[106,1359,836],{"class":130},[106,1361,268],{"class":123},[106,1363,306],{"class":130},[106,1365,309],{"class":123},[106,1367,241],{"class":130},[106,1369,1371,1374,1377],{"class":108,"line":1370},76,[106,1372,1373],{"class":119},"            async",[106,1375,1376],{"class":119}," with",[106,1378,1379],{"class":130}," gate:\n",[106,1381,1383,1386,1388],{"class":108,"line":1382},77,[106,1384,1385],{"class":130},"                row ",[106,1387,334],{"class":119},[106,1389,1390],{"class":130}," ui.start_item(name, size)\n",[106,1392,1394,1397],{"class":108,"line":1393},78,[106,1395,1396],{"class":119},"                try",[106,1398,241],{"class":130},[106,1400,1402,1405,1408,1411],{"class":108,"line":1401},79,[106,1403,1404],{"class":119},"                    await",[106,1406,1407],{"class":130}," worker(name, size, ",[106,1409,1410],{"class":119},"lambda",[106,1412,1413],{"class":130}," n: ui.advance_item(row, n))\n",[106,1415,1417,1420,1423,1426],{"class":108,"line":1416},80,[106,1418,1419],{"class":119},"                except",[106,1421,1422],{"class":123}," OSError",[106,1424,1425],{"class":119}," as",[106,1427,1428],{"class":130}," exc:\n",[106,1430,1432,1435,1437],{"class":108,"line":1431},81,[106,1433,1434],{"class":130},"                    ui.finish_item(row, name, ",[106,1436,280],{"class":123},[106,1438,1439],{"class":130},"(exc))\n",[106,1441,1443,1446],{"class":108,"line":1442},82,[106,1444,1445],{"class":119},"                else",[106,1447,241],{"class":130},[106,1449,1451],{"class":108,"line":1450},83,[106,1452,1453],{"class":130},"                    ui.finish_item(row, name)\n",[106,1455,1457],{"class":108,"line":1456},84,[106,1458,138],{"emptyLinePlaceholder":137},[106,1460,1462,1464,1466,1469,1471],{"class":108,"line":1461},85,[106,1463,1347],{"class":119},[106,1465,1376],{"class":119},[106,1467,1468],{"class":130}," asyncio.TaskGroup() ",[106,1470,1333],{"class":119},[106,1472,1473],{"class":130}," tg:\n",[106,1475,1477,1480,1483,1486],{"class":108,"line":1476},86,[106,1478,1479],{"class":119},"            for",[106,1481,1482],{"class":130}," name, size ",[106,1484,1485],{"class":119},"in",[106,1487,1488],{"class":130}," items:\n",[106,1490,1492],{"class":108,"line":1491},87,[106,1493,1494],{"class":130},"                tg.create_task(one(name, size))\n",[106,1496,1498,1501],{"class":108,"line":1497},88,[106,1499,1500],{"class":119},"    return",[106,1502,1503],{"class":130}," ui\n",[1505,1506,1508],"h3",{"id":1507},"two-progress-objects-in-one-live-display","Two Progress objects in one Live display",[10,1510,1511,1512,1515,1516,1518,1519,1522,1523,1526,1527,1529,1530,1532,1533,1536],{},"Rich’s ",[52,1513,1514],{},"Progress"," renders all of its tasks with the same columns, but the overall bar and the per-item rows need different columns: counts and ETA for the first, bytes for the second. The recipe uses two ",[52,1517,1514],{}," instances and combines them in a ",[52,1520,1521],{},"Group"," inside a single ",[52,1524,1525],{},"Live"," display, which owns the terminal and refreshes both at ten frames per second. The item ",[52,1528,1514],{}," is ",[52,1531,451],{},", and rows are removed with ",[52,1534,1535],{},"remove_task"," the moment an item finishes, so only in-flight work is visible.",[1505,1538,1540],{"id":1539},"rows-appear-only-while-work-is-running","Rows appear only while work is running",[10,1542,1543,1544,1547,1548,1550,1551,39],{},"The worker acquires the semaphore ",[80,1545,1546],{},"before"," adding its row. Items waiting for a slot have no row, so the in-flight list never shows forty idle entries — just the eight that are actually transferring. The byte-level ",[52,1549,899],{}," callback is passed down to the worker, which calls it as chunks are sent; how a worker reports bytes from an HTTP client is shown in ",[35,1552,1554],{"href":1553},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python\u002F","downloading files with progress in Python",[1505,1556,1558],{"id":1557},"failures-go-above-the-live-area","Failures go above the live area",[10,1560,1561,1564,1565,1567,1568,1571],{},[52,1562,1563],{},"console.print"," called while a ",[52,1566,1525],{}," display is active prints ",[80,1569,1570],{},"above"," it, and the line stays in the scrollback when the display ends. That makes it the right place for failures — “✗ file-3.bin: connection reset” — as they happen, instead of collecting them for the end. The overall bar’s description also changes to “files (1 failed)” so the count is visible at a glance.",[73,1573],{"name":1574},"cprog-terminal",[1505,1576,1578],{"id":1577},"pipes-and-ci-plain-lines-not-animation","Pipes and CI: plain lines, not animation",[10,1580,1581,1582,1584,1585,1588,1589,1591,1592,1595,1596,1599,1600,39],{},"When the console is not a terminal, both ",[52,1583,1514],{}," objects are created with ",[52,1586,1587],{},"disable=True"," and no ",[52,1590,1525],{}," display starts. Instead, ",[52,1593,1594],{},"_maybe_plain"," prints a short status line at most every five seconds and once at the end: ",[52,1597,1598],{},"progress: 23\u002F40 files, 1 failed",". CI logs get a readable record of how the run progressed, with no escape codes and no thousands of redraw lines; failure lines and the final summary are printed in both modes. The same principle is discussed in ",[35,1601,1603],{"href":1602},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells\u002F","detecting CI environments and non-interactive shells",[41,1605,1607],{"id":1606},"ux-considerations","UX considerations",[46,1609,1610,1624,1630,1636,1642],{},[49,1611,1612,1615,1616,1619,1620,39],{},[14,1613,1614],{},"Progress belongs on stderr."," Pass a ",[52,1617,1618],{},"Console(stderr=True)"," so stdout stays free for results, as in ",[35,1621,1623],{"href":1622},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output\u002F","separating logs from program output",[49,1625,1626,1629],{},[14,1627,1628],{},"Estimate from bytes when sizes vary."," An ETA based on item counts is wrong when one item is a 2 GB file; if sizes are known up front, give the overall bar a byte total instead.",[49,1631,1632,1635],{},[14,1633,1634],{},"Keep refreshes modest."," Ten refreshes per second is smooth; updating per chunk from hundreds of tasks is fine because Rich renders on its own schedule, not on every update.",[49,1637,1638,1641],{},[14,1639,1640],{},"Do not hide the slow item."," If one transfer stalls, its row staying put while others churn is exactly the information the user needs.",[49,1643,1644,1647,1648,1651],{},[14,1645,1646],{},"Finish with a summary line,"," including failures and a hint how to retry only those (“retry with ",[52,1649,1650],{},"--only-failed","”).",[73,1653],{"name":1654},"cprog-modes",[41,1656,1658],{"id":1657},"testing-the-behaviour","Testing the behaviour",[10,1660,1661,1662,1665],{},"Drive the display with fake workers and two consoles writing to ",[52,1663,1664],{},"StringIO",": one forced into terminal mode to exercise the live display, one plain to exercise the pipe path:",[97,1667,1669],{"className":99,"code":1668,"language":101,"meta":102,"style":102},"# tests\u002Ftest_concurrent_progress.py\nimport asyncio\nimport io\n\nfrom rich.console import Console\n\nfrom mytool.concurrent_progress import run_all\n\nITEMS = [(f\"file-{n}.bin\", 4096) for n in range(10)]\n\n\nasync def fake_upload(name, size, advance):\n    for _ in range(4):\n        await asyncio.sleep(0.001)\n        advance(size \u002F\u002F 4)\n    if name == \"file-3.bin\":\n        raise OSError(\"connection reset\")\n\n\ndef console(terminal: bool) -> Console:\n    return Console(file=io.StringIO(), force_terminal=terminal, width=80, color_system=None)\n\n\ndef test_counts_and_summary_in_a_pipe():\n    out = console(terminal=False)\n    ui = asyncio.run(run_all(ITEMS, fake_upload, out, jobs=3))\n    assert (ui.done, ui.failed) == (10, 1)\n    text = out.file.getvalue()\n    assert \"✗ file-3.bin: connection reset\" in text\n    assert text.rstrip().endswith(\"9 of 10 files done, 1 failed\")\n    assert \"progress: 10\u002F10 files, 1 failed\" in text            # final plain progress line\n    assert \"\\x1b[\" not in text                                  # no live-display escape codes\n\n\ndef test_live_display_on_a_terminal():\n    out = console(terminal=True)\n    ui = asyncio.run(run_all(ITEMS, fake_upload, out, jobs=3))\n    text = out.file.getvalue()\n    assert ui.done == 10\n    assert \"10\u002F10\" in text and \"files (1 failed)\" in text\n    assert \"progress:\" not in text                              # plain lines only without a TTY\n",[52,1670,1671,1676,1682,1689,1693,1704,1708,1720,1724,1774,1778,1782,1794,1814,1827,1839,1854,1868,1872,1876,1893,1935,1939,1943,1953,1972,1997,2017,2027,2040,2052,2067,2090,2094,2098,2107,2123,2143,2151,2163,2184],{"__ignoreMap":102},[106,1672,1673],{"class":108,"line":109},[106,1674,1675],{"class":112},"# tests\u002Ftest_concurrent_progress.py\n",[106,1677,1678,1680],{"class":108,"line":116},[106,1679,144],{"class":119},[106,1681,147],{"class":130},[106,1683,1684,1686],{"class":108,"line":134},[106,1685,144],{"class":119},[106,1687,1688],{"class":130}," io\n",[106,1690,1691],{"class":108,"line":141},[106,1692,138],{"emptyLinePlaceholder":137},[106,1694,1695,1697,1699,1701],{"class":108,"line":150},[106,1696,120],{"class":119},[106,1698,181],{"class":130},[106,1700,144],{"class":119},[106,1702,1703],{"class":130}," Console\n",[106,1705,1706],{"class":108,"line":158},[106,1707,138],{"emptyLinePlaceholder":137},[106,1709,1710,1712,1715,1717],{"class":108,"line":171},[106,1711,120],{"class":119},[106,1713,1714],{"class":130}," mytool.concurrent_progress ",[106,1716,144],{"class":119},[106,1718,1719],{"class":130}," run_all\n",[106,1721,1722],{"class":108,"line":176},[106,1723,138],{"emptyLinePlaceholder":137},[106,1725,1726,1729,1731,1734,1736,1739,1741,1744,1746,1749,1751,1754,1756,1759,1762,1764,1767,1769,1771],{"class":108,"line":189},[106,1727,1728],{"class":123},"ITEMS",[106,1730,283],{"class":119},[106,1732,1733],{"class":130}," [(",[106,1735,737],{"class":119},[106,1737,1738],{"class":247},"\"file-",[106,1740,996],{"class":123},[106,1742,1743],{"class":130},"n",[106,1745,755],{"class":123},[106,1747,1748],{"class":247},".bin\"",[106,1750,419],{"class":130},[106,1752,1753],{"class":123},"4096",[106,1755,1330],{"class":130},[106,1757,1758],{"class":119},"for",[106,1760,1761],{"class":130}," n ",[106,1763,1485],{"class":119},[106,1765,1766],{"class":123}," range",[106,1768,721],{"class":130},[106,1770,640],{"class":123},[106,1772,1773],{"class":130},")]\n",[106,1775,1776],{"class":108,"line":202},[106,1777,138],{"emptyLinePlaceholder":137},[106,1779,1780],{"class":108,"line":215},[106,1781,138],{"emptyLinePlaceholder":137},[106,1783,1784,1786,1788,1791],{"class":108,"line":221},[106,1785,1243],{"class":119},[106,1787,1246],{"class":119},[106,1789,1790],{"class":237}," fake_upload",[106,1792,1793],{"class":130},"(name, size, advance):\n",[106,1795,1796,1799,1802,1804,1806,1808,1811],{"class":108,"line":226},[106,1797,1798],{"class":119},"    for",[106,1800,1801],{"class":130}," _ ",[106,1803,1485],{"class":119},[106,1805,1766],{"class":123},[106,1807,721],{"class":130},[106,1809,1810],{"class":123},"4",[106,1812,1813],{"class":130},"):\n",[106,1815,1816,1819,1822,1825],{"class":108,"line":231},[106,1817,1818],{"class":119},"        await",[106,1820,1821],{"class":130}," asyncio.sleep(",[106,1823,1824],{"class":123},"0.001",[106,1826,651],{"class":130},[106,1828,1829,1832,1835,1837],{"class":108,"line":244},[106,1830,1831],{"class":130},"        advance(size ",[106,1833,1834],{"class":119},"\u002F\u002F",[106,1836,1290],{"class":123},[106,1838,651],{"class":130},[106,1840,1841,1844,1847,1849,1852],{"class":108,"line":251},[106,1842,1843],{"class":119},"    if",[106,1845,1846],{"class":130}," name ",[106,1848,1164],{"class":119},[106,1850,1851],{"class":247}," \"file-3.bin\"",[106,1853,241],{"class":130},[106,1855,1856,1859,1861,1863,1866],{"class":108,"line":256},[106,1857,1858],{"class":119},"        raise",[106,1860,1422],{"class":123},[106,1862,721],{"class":130},[106,1864,1865],{"class":247},"\"connection reset\"",[106,1867,651],{"class":130},[106,1869,1870],{"class":108,"line":292},[106,1871,138],{"emptyLinePlaceholder":137},[106,1873,1874],{"class":108,"line":314},[106,1875,138],{"emptyLinePlaceholder":137},[106,1877,1878,1881,1884,1887,1890],{"class":108,"line":340},[106,1879,1880],{"class":119},"def",[106,1882,1883],{"class":237}," console",[106,1885,1886],{"class":130},"(terminal: ",[106,1888,1889],{"class":123},"bool",[106,1891,1892],{"class":130},") -> Console:\n",[106,1894,1895,1897,1900,1903,1905,1908,1911,1913,1916,1919,1921,1924,1926,1929,1931,1933],{"class":108,"line":361},[106,1896,1500],{"class":119},[106,1898,1899],{"class":130}," Console(",[106,1901,1902],{"class":422},"file",[106,1904,334],{"class":119},[106,1906,1907],{"class":130},"io.StringIO(), ",[106,1909,1910],{"class":422},"force_terminal",[106,1912,334],{"class":119},[106,1914,1915],{"class":130},"terminal, ",[106,1917,1918],{"class":422},"width",[106,1920,334],{"class":119},[106,1922,1923],{"class":123},"80",[106,1925,419],{"class":130},[106,1927,1928],{"class":422},"color_system",[106,1930,334],{"class":119},[106,1932,309],{"class":123},[106,1934,651],{"class":130},[106,1936,1937],{"class":108,"line":374},[106,1938,138],{"emptyLinePlaceholder":137},[106,1940,1941],{"class":108,"line":392},[106,1942,138],{"emptyLinePlaceholder":137},[106,1944,1945,1947,1950],{"class":108,"line":405},[106,1946,1880],{"class":119},[106,1948,1949],{"class":237}," test_counts_and_summary_in_a_pipe",[106,1951,1952],{"class":130},"():\n",[106,1954,1955,1958,1960,1963,1966,1968,1970],{"class":108,"line":434},[106,1956,1957],{"class":130},"    out ",[106,1959,334],{"class":119},[106,1961,1962],{"class":130}," console(",[106,1964,1965],{"class":422},"terminal",[106,1967,334],{"class":119},[106,1969,456],{"class":123},[106,1971,651],{"class":130},[106,1973,1974,1977,1979,1982,1984,1987,1990,1992,1995],{"class":108,"line":440},[106,1975,1976],{"class":130},"    ui ",[106,1978,334],{"class":119},[106,1980,1981],{"class":130}," asyncio.run(run_all(",[106,1983,1728],{"class":123},[106,1985,1986],{"class":130},", fake_upload, out, ",[106,1988,1989],{"class":422},"jobs",[106,1991,334],{"class":119},[106,1993,1994],{"class":123},"3",[106,1996,815],{"class":130},[106,1998,1999,2002,2005,2007,2009,2011,2013,2015],{"class":108,"line":472},[106,2000,2001],{"class":119},"    assert",[106,2003,2004],{"class":130}," (ui.done, ui.failed) ",[106,2006,1164],{"class":119},[106,2008,785],{"class":130},[106,2010,640],{"class":123},[106,2012,419],{"class":130},[106,2014,1039],{"class":123},[106,2016,651],{"class":130},[106,2018,2019,2022,2024],{"class":108,"line":478},[106,2020,2021],{"class":130},"    text ",[106,2023,334],{"class":119},[106,2025,2026],{"class":130}," out.file.getvalue()\n",[106,2028,2029,2031,2034,2037],{"class":108,"line":490},[106,2030,2001],{"class":119},[106,2032,2033],{"class":247}," \"✗ file-3.bin: connection reset\"",[106,2035,2036],{"class":119}," in",[106,2038,2039],{"class":130}," text\n",[106,2041,2042,2044,2047,2050],{"class":108,"line":517},[106,2043,2001],{"class":119},[106,2045,2046],{"class":130}," text.rstrip().endswith(",[106,2048,2049],{"class":247},"\"9 of 10 files done, 1 failed\"",[106,2051,651],{"class":130},[106,2053,2054,2056,2059,2061,2064],{"class":108,"line":543},[106,2055,2001],{"class":119},[106,2057,2058],{"class":247}," \"progress: 10\u002F10 files, 1 failed\"",[106,2060,2036],{"class":119},[106,2062,2063],{"class":130}," text            ",[106,2065,2066],{"class":112},"# final plain progress line\n",[106,2068,2069,2071,2074,2077,2080,2082,2084,2087],{"class":108,"line":548},[106,2070,2001],{"class":119},[106,2072,2073],{"class":247}," \"",[106,2075,2076],{"class":123},"\\x1b",[106,2078,2079],{"class":247},"[\"",[106,2081,1130],{"class":119},[106,2083,2036],{"class":119},[106,2085,2086],{"class":130}," text                                  ",[106,2088,2089],{"class":112},"# no live-display escape codes\n",[106,2091,2092],{"class":108,"line":571},[106,2093,138],{"emptyLinePlaceholder":137},[106,2095,2096],{"class":108,"line":576},[106,2097,138],{"emptyLinePlaceholder":137},[106,2099,2100,2102,2105],{"class":108,"line":587},[106,2101,1880],{"class":119},[106,2103,2104],{"class":237}," test_live_display_on_a_terminal",[106,2106,1952],{"class":130},[106,2108,2109,2111,2113,2115,2117,2119,2121],{"class":108,"line":598},[106,2110,1957],{"class":130},[106,2112,334],{"class":119},[106,2114,1962],{"class":130},[106,2116,1965],{"class":422},[106,2118,334],{"class":119},[106,2120,530],{"class":123},[106,2122,651],{"class":130},[106,2124,2125,2127,2129,2131,2133,2135,2137,2139,2141],{"class":108,"line":632},[106,2126,1976],{"class":130},[106,2128,334],{"class":119},[106,2130,1981],{"class":130},[106,2132,1728],{"class":123},[106,2134,1986],{"class":130},[106,2136,1989],{"class":422},[106,2138,334],{"class":119},[106,2140,1994],{"class":123},[106,2142,815],{"class":130},[106,2144,2145,2147,2149],{"class":108,"line":654},[106,2146,2021],{"class":130},[106,2148,334],{"class":119},[106,2150,2026],{"class":130},[106,2152,2153,2155,2158,2160],{"class":108,"line":668},[106,2154,2001],{"class":119},[106,2156,2157],{"class":130}," ui.done ",[106,2159,1164],{"class":119},[106,2161,2162],{"class":123}," 10\n",[106,2164,2165,2167,2170,2172,2175,2177,2180,2182],{"class":108,"line":677},[106,2166,2001],{"class":119},[106,2168,2169],{"class":247}," \"10\u002F10\"",[106,2171,2036],{"class":119},[106,2173,2174],{"class":130}," text ",[106,2176,1137],{"class":119},[106,2178,2179],{"class":247}," \"files (1 failed)\"",[106,2181,2036],{"class":119},[106,2183,2039],{"class":130},[106,2185,2186,2188,2191,2193,2195,2198],{"class":108,"line":682},[106,2187,2001],{"class":119},[106,2189,2190],{"class":247}," \"progress:\"",[106,2192,1130],{"class":119},[106,2194,2036],{"class":119},[106,2196,2197],{"class":130}," text                              ",[106,2199,2200],{"class":112},"# plain lines only without a TTY\n",[10,2202,2203],{},"Assertions focus on what the user must be able to rely on — counts, the failure line, the final summary, no escape codes in plain mode, no plain progress lines in terminal mode — rather than the exact frames, which depend on timing. The fake worker’s tiny sleeps keep the suite fast while still interleaving the tasks.",[41,2205,2207],{"id":2206},"conclusion","Conclusion",[10,2209,2210,2211,2213,2214,2216],{},"Concurrent work needs a progress display with structure: an overall bar with counts and ETA, a transient row per in-flight item created after the concurrency slot is acquired, failures printed above the live area as they happen, and a summary at the end. Combine two ",[52,2212,1514],{}," instances in one ",[52,2215,1525],{}," group, send it to stderr, switch to periodic plain lines when the console is not a terminal, and test both modes with fake workers and string-backed consoles.",[41,2218,2220],{"id":2219},"frequently-asked-questions","Frequently asked questions",[1505,2222,2224],{"id":2223},"is-richs-progress-safe-to-update-from-many-asyncio-tasks","Is Rich’s Progress safe to update from many asyncio tasks?",[10,2226,2227,2228,2230],{},"Yes. Asyncio tasks run in one thread, and ",[52,2229,1514],{}," methods are also protected by a lock, so updates from threads work too — which matters when work runs in a thread pool.",[1505,2232,2234],{"id":2233},"how-do-i-show-progress-for-thread-pool-work","How do I show progress for thread-pool work?",[10,2236,2237,2238,419,2241,2244,2245,2248,2249,2251,2252,39],{},"The same class works: call ",[52,2239,2240],{},"start_item",[52,2242,2243],{},"advance_item"," and ",[52,2246,2247],{},"finish_item"," from worker threads. ",[52,2250,1525],{}," refreshes from its own thread, so the display stays smooth while workers block. See ",[35,2253,2255],{"href":2254},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools\u002F","parallelising CLI work with thread pools",[1505,2257,2259],{"id":2258},"what-if-item-sizes-are-unknown","What if item sizes are unknown?",[10,2261,2262,2263,2266],{},"Pass ",[52,2264,2265],{},"total=None"," for the row; Rich then shows a pulsing bar and the byte count without a percentage. The overall bar still counts items.",[1505,2268,2270],{"id":2269},"should-verbose-mode-change-the-display","Should verbose mode change the display?",[10,2272,2273,2274,2277,2278,2281],{},"At ",[52,2275,2276],{},"-v",", also print a line for each ",[80,2279,2280],{},"successful"," item above the live area. Without it, only failures are printed, which keeps the scrollback short.",[1505,2283,2285],{"id":2284},"can-i-use-tqdm-instead","Can I use tqdm instead?",[10,2287,2288,2289,2292,2293,2295],{},"tqdm supports multiple bars with ",[52,2290,2291],{},"position=",", and works well for a fixed number of workers. Rich’s ",[52,2294,1525],{}," group makes the dynamic rows and printing above the display easier, and fits better if the CLI already uses Rich.",[41,2297,2299],{"id":2298},"related","Related",[46,2301,2302,2308,2313,2317,2323],{},[49,2303,2304,2305],{},"Up: ",[35,2306,2307],{"href":37},"Concurrency and async in Python CLIs",[49,2309,2310],{},[35,2311,2312],{"href":65},"Structured concurrency with TaskGroups in CLIs",[49,2314,2315],{},[35,2316,61],{"href":60},[49,2318,2319],{},[35,2320,2322],{"href":2321},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live\u002F","Live dashboards with Rich Live",[49,2324,2325],{},[35,2326,2328],{"href":2327},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress\u002F","Uploading files with multipart and progress",[423,2330,2331],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":102,"searchDepth":116,"depth":116,"links":2333},[2334,2335,2336,2342,2343,2344,2345,2352],{"id":43,"depth":116,"text":44},{"id":70,"depth":116,"text":71},{"id":94,"depth":116,"text":95,"children":2337},[2338,2339,2340,2341],{"id":1507,"depth":134,"text":1508},{"id":1539,"depth":134,"text":1540},{"id":1557,"depth":134,"text":1558},{"id":1577,"depth":134,"text":1578},{"id":1606,"depth":116,"text":1607},{"id":1657,"depth":116,"text":1658},{"id":2206,"depth":116,"text":2207},{"id":2219,"depth":116,"text":2220,"children":2346},[2347,2348,2349,2350,2351],{"id":2223,"depth":134,"text":2224},{"id":2233,"depth":134,"text":2234},{"id":2258,"depth":134,"text":2259},{"id":2269,"depth":134,"text":2270},{"id":2284,"depth":134,"text":2285},{"id":2298,"depth":116,"text":2299},"2026-10-02","Show progress for concurrent async work with Rich: an overall bar, a row per in-flight item, failures printed above, plain lines in pipes and CI, and tests.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks",{"title":5,"description":2354},"cli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks\u002Findex",[2363,2364,2365,2366,2367],"progress","rich","asyncio","concurrency","ux","siH02G-g64j7LF9gxjRPNrxVQ8bHv0s2cMDW8ev8tOg",[2370,2373,2376,2379,2382,2385,2388,2391,2394,2397,2400,2403,2406,2409,2412,2415,2418,2421,2424,2427,2430,2433,2436,2439,2442,2445,2448,2451,2454,2457,2460,2463,2466,2469,2472,2475,2478,2481,2484,2487,2490,2493,2496,2499,2502,2505,2508,2511,2514,2517,2520,2523,2526,2529,2532,2535,2538,2541,2544,2547,2550,2553,2556,2559,2562,2565,2568,2571,2574,2577,2580,2583,2586,2589,2592,2595,2598,2601,2604,2607,2610,2613,2616,2619,2622,2625,2628,2631,2634,2637,2640,2643,2646,2649,2652,2655,2656,2659,2662,2665,2668,2671,2674,2677,2680,2683,2686,2689,2692,2695,2698,2701,2704,2707,2710,2713,2716,2719,2722,2725,2728,2731,2734,2737,2740,2743,2746,2749,2752,2755,2758,2761,2764,2767,2770,2773,2776,2779,2782,2785,2787,2790,2793,2796,2799,2802,2805,2808,2811,2814,2817,2820,2823,2826,2829,2832,2835,2838,2841,2844,2847,2850,2853,2856,2859,2862,2865,2868,2871,2874,2877,2880,2883,2886,2889,2892,2895,2898,2901,2904,2907,2910,2913,2916,2919,2922,2925,2928,2931,2934,2937,2940,2943,2946,2949,2952,2955,2958,2961,2964,2967,2970,2973,2976,2979,2982,2985,2988,2991,2994,2997,3000,3003,3006,3009,3012,3015,3018,3021,3024,3027,3030,3033,3036,3039,3042,3045,3048,3051,3054,3057,3060,3063,3066,3069,3072,3075,3078,3081,3084,3087,3090,3093,3096,3099,3102,3105,3108,3111,3114,3117,3120,3123,3126,3129,3132,3135,3138,3141,3144,3147,3150,3153,3156,3159,3162,3165,3168,3171,3174,3177,3180,3183,3186,3189,3192,3195,3198,3201,3204,3207,3210],{"path":2371,"title":2372},"\u002Fabout","About Python CLI Toolcraft",{"path":2374,"title":2375},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2377,"title":2378},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2380,"title":2381},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":2383,"title":2384},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2386,"title":2387},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2389,"title":2390},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2392,"title":2393},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2395,"title":2396},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2398,"title":2399},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":2401,"title":2402},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":2404,"title":2405},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2407,"title":2408},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":2410,"title":2411},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2413,"title":2414},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2416,"title":2417},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":2419,"title":2420},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2422,"title":2423},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":2425,"title":2426},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2428,"title":2429},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":2431,"title":2432},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2434,"title":2435},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2437,"title":2438},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2440,"title":2441},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2443,"title":2444},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":2446,"title":2447},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2449,"title":2450},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2452,"title":2453},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":2455,"title":2456},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":2458,"title":2459},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2461,"title":2462},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":2464,"title":2465},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":2467,"title":2468},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2470,"title":2471},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2473,"title":2474},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2476,"title":2477},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":2479,"title":2480},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2482,"title":2483},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2485,"title":2486},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":2488,"title":2489},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2491,"title":2492},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":2494,"title":2495},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2497,"title":2498},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2500,"title":2501},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":2503,"title":2504},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":2506,"title":2507},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2509,"title":2510},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":2512,"title":2513},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2515,"title":2516},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":2518,"title":2519},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2521,"title":2522},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2524,"title":2525},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":2527,"title":2528},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2530,"title":2531},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2533,"title":2534},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":2536,"title":2537},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":2539,"title":2540},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files","Exporting CLI Results to Files from a Python CLI",{"path":2542,"title":2543},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2545,"title":2546},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":2548,"title":2549},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":2551,"title":2552},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2554,"title":2555},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2557,"title":2558},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":2560,"title":2561},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2563,"title":2564},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":2566,"title":2567},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2569,"title":2570},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2572,"title":2573},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2575,"title":2576},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2578,"title":2579},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2581,"title":2582},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2584,"title":2585},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":2587,"title":2588},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2590,"title":2591},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2593,"title":2594},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2596,"title":2597},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2599,"title":2600},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2602,"title":2603},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2605,"title":2606},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":2608,"title":2609},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2611,"title":2612},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2614,"title":2615},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2617,"title":2618},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2620,"title":2621},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2623,"title":2624},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":2626,"title":2627},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2629,"title":2630},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2632,"title":2633},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2635,"title":2636},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":2638,"title":2639},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2641,"title":2642},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2644,"title":2645},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2647,"title":2648},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2650,"title":2651},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2653,"title":2654},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2359,"title":5},{"path":2657,"title":2658},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2660,"title":2661},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2663,"title":2664},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2666,"title":2667},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":2669,"title":2670},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2672,"title":2673},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2675,"title":2676},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2678,"title":2679},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":2681,"title":2682},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2684,"title":2685},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2687,"title":2688},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":2690,"title":2691},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":2693,"title":2694},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":2696,"title":2697},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":2699,"title":2700},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":2702,"title":2703},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2705,"title":2706},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2708,"title":2709},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2711,"title":2712},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2714,"title":2715},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2717,"title":2718},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":2720,"title":2721},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2723,"title":2724},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":2726,"title":2727},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2729,"title":2730},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2732,"title":2733},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2735,"title":2736},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2738,"title":2739},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":2741,"title":2742},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":2744,"title":2745},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2747,"title":2748},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2750,"title":2751},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2753,"title":2754},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2756,"title":2757},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2759,"title":2760},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2762,"title":2763},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":2765,"title":2766},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2768,"title":2769},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2771,"title":2772},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":2774,"title":2775},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":2777,"title":2778},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":2780,"title":2781},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":2783,"title":2784},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":1203,"title":2786},"Python CLI Toolcraft",{"path":2788,"title":2789},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":2791,"title":2792},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":2794,"title":2795},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":2797,"title":2798},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":2800,"title":2801},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":2803,"title":2804},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":2806,"title":2807},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2809,"title":2810},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":2812,"title":2813},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2815,"title":2816},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2818,"title":2819},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2821,"title":2822},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2824,"title":2825},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2827,"title":2828},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2830,"title":2831},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":2833,"title":2834},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2836,"title":2837},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2839,"title":2840},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2842,"title":2843},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":2845,"title":2846},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2848,"title":2849},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2851,"title":2852},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":2854,"title":2855},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2857,"title":2858},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2860,"title":2861},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2863,"title":2864},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2866,"title":2867},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":2869,"title":2870},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2872,"title":2873},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2875,"title":2876},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2878,"title":2879},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2881,"title":2882},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":2884,"title":2885},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":2887,"title":2888},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2890,"title":2891},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":2893,"title":2894},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":2896,"title":2897},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2899,"title":2900},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":2902,"title":2903},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2905,"title":2906},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":2908,"title":2909},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":2911,"title":2912},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2914,"title":2915},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2917,"title":2918},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2920,"title":2921},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2923,"title":2924},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2926,"title":2927},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":2929,"title":2930},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2932,"title":2933},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2935,"title":2936},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2938,"title":2939},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":2941,"title":2942},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2944,"title":2945},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":2947,"title":2948},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":2950,"title":2951},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2953,"title":2954},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":2956,"title":2957},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":2959,"title":2960},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":2962,"title":2963},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2965,"title":2966},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":2968,"title":2969},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2971,"title":2972},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2974,"title":2975},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2977,"title":2978},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2980,"title":2981},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2983,"title":2984},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":2986,"title":2987},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2989,"title":2990},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":2992,"title":2993},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2995,"title":2996},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2998,"title":2999},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3001,"title":3002},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3004,"title":3005},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3007,"title":3008},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3010,"title":3011},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3013,"title":3014},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3016,"title":3017},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3019,"title":3020},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3022,"title":3023},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3025,"title":3026},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3028,"title":3029},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3031,"title":3032},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3034,"title":3035},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3037,"title":3038},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3040,"title":3041},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":3043,"title":3044},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3046,"title":3047},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3049,"title":3050},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":3052,"title":3053},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":3055,"title":3056},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3058,"title":3059},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":3061,"title":3062},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":3064,"title":3065},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":3067,"title":3068},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3070,"title":3071},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3073,"title":3074},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":3076,"title":3077},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3079,"title":3080},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3082,"title":3083},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3085,"title":3086},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":3088,"title":3089},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3091,"title":3092},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3094,"title":3095},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":3097,"title":3098},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3100,"title":3101},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3103,"title":3104},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3106,"title":3107},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3109,"title":3110},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":3112,"title":3113},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3115,"title":3116},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3118,"title":3119},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3121,"title":3122},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":3124,"title":3125},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3127,"title":3128},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3130,"title":3131},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3133,"title":3134},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3136,"title":3137},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":3139,"title":3140},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":3142,"title":3143},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":3145,"title":3146},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":3148,"title":3149},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":3151,"title":3152},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":3154,"title":3155},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":3157,"title":3158},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":3160,"title":3161},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3163,"title":3164},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3166,"title":3167},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3169,"title":3170},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":3172,"title":3173},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3175,"title":3176},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":3178,"title":3179},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":3181,"title":3182},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":3184,"title":3185},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":3187,"title":3188},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":3190,"title":3191},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3193,"title":3194},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":3196,"title":3197},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":3199,"title":3200},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3202,"title":3203},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3205,"title":3206},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3208,"title":3209},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3211,"title":3212},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967539721]