[{"data":1,"prerenderedAt":3177},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python\u002F":3,"content-directory":2631},{"id":4,"title":5,"body":6,"date":2617,"description":2618,"difficulty":2619,"draft":2620,"extension":2621,"meta":2622,"navigation":140,"path":2623,"seo":2624,"stem":2625,"tags":2626,"updated":2617,"__hash__":2630},"content\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python\u002Findex.md","Downloading Files with Progress in Python CLIs",{"type":7,"value":8,"toc":2601},"minimark",[9,28,33,66,70,81,85,95,99,1056,1059,1119,1122,1143,1147,1712,1718,1722,1725,1777,1781,1788,2473,2484,2488,2503,2507,2512,2515,2519,2535,2539,2550,2554,2564,2568,2597],[10,11,12,13,17,18,21,22,27],"p",{},"Your CLI fetches something big — a dataset, a model file, a release artefact, a database snapshot. The naive version, ",[14,15,16],"code",{},"open(dest, \"wb\").write(httpx.get(url).content)",", loads the entire file into memory, shows nothing for two minutes, and if the connection drops at 95% leaves a truncated file at the destination that looks perfectly valid until something tries to read it. The user's only recourse is to start again from zero. This guide builds a download function that streams in chunks, shows a Rich progress bar, writes to a temporary ",[14,19,20],{},".part"," file, verifies a checksum, renames into place only when everything checks out, and resumes interrupted downloads with HTTP range requests. It is part of the ",[23,24,26],"a",{"href":25},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002F","HTTP APIs topic",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,36,52,63],"ul",{},[37,38,39,40,43,44,47,48,51],"li",{},"Python 3.10+, ",[14,41,42],{},"httpx"," and ",[14,45,46],{},"rich"," (",[14,49,50],{},"uv add httpx rich",").",[37,53,54,55,58,59,62],{},"A server that reports ",[14,56,57],{},"Content-Length","; resuming also needs ",[14,60,61],{},"Accept-Ranges: bytes",". Most static file hosts, object stores and CDNs provide both.",[37,64,65],{},"Ideally, an expected SHA-256 for the file — published alongside releases, or returned by your API.",[29,67,69],{"id":68},"the-shape-of-a-safe-download","The shape of a safe download",[10,71,72,73,77,78,80],{},"A download that cannot corrupt anything follows the same shape as an ",[23,74,76],{"href":75},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002F","atomic write",": the destination path is only ever touched by a final rename. Everything before that happens in a ",[14,79,20],{}," file beside it.",[82,83],"inline-diagram",{"name":84},"http-download-flow",[10,86,87,88,91,92,94],{},"Streaming matters for memory: ",[14,89,90],{},"client.stream(\"GET\", url)"," returns a response whose body you iterate in chunks, so a 20 GB file costs the same memory as a 20 KB one. Writing to ",[14,93,20],{}," matters for correctness: an interrupted download leaves a clearly unfinished file that the next run can resume or discard, never a truncated file with the real name.",[29,96,98],{"id":97},"the-recipe","The recipe",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fdownload.py\nfrom __future__ import annotations\n\nimport hashlib\nimport os\nfrom pathlib import Path\n\nimport httpx\nfrom rich.progress import (BarColumn, DownloadColumn, Progress, TextColumn,\n                           TimeRemainingColumn, TransferSpeedColumn)\n\nCHUNK = 64 * 1024\n\n\nclass DownloadError(Exception):\n    pass\n\n\ndef _progress(enabled: bool) -> Progress:\n    return Progress(\n        TextColumn(\"[bold]{task.description}\"), BarColumn(), DownloadColumn(),\n        TransferSpeedColumn(), TimeRemainingColumn(),\n        disable=not enabled, transient=True,\n    )\n\n\ndef download(client: httpx.Client, url: str, dest: Path, *, sha256: str | None = None,\n             show_progress: bool = True) -> Path:\n    dest = dest.resolve()\n    part = dest.with_name(dest.name + \".part\")\n    digest = hashlib.sha256()\n    headers: dict[str, str] = {}\n    offset = part.stat().st_size if part.exists() else 0\n    if offset:\n        headers[\"Range\"] = f\"bytes={offset}-\"\n\n    with client.stream(\"GET\", url, headers=headers) as response:\n        if response.status_code == 416:            # .part already complete or stale\n            part.unlink()\n            return download(client, url, dest, sha256=sha256, show_progress=show_progress)\n        response.raise_for_status()\n        resumed = response.status_code == 206\n        if not resumed:\n            offset = 0                               # server ignored Range: start over\n        mode = \"ab\" if resumed else \"wb\"\n        if resumed:                                   # hash the bytes we already have\n            with part.open(\"rb\") as existing:\n                for block in iter(lambda: existing.read(CHUNK), b\"\"):\n                    digest.update(block)\n        length = response.headers.get(\"Content-Length\")\n        total = offset + int(length) if length else None\n\n        with _progress(show_progress) as progress, part.open(mode) as fh:\n            task = progress.add_task(dest.name, total=total, completed=offset)\n            for chunk in response.iter_bytes(CHUNK):\n                fh.write(chunk)\n                digest.update(chunk)\n                progress.update(task, advance=len(chunk))\n            fh.flush()\n            os.fsync(fh.fileno())\n\n    size = part.stat().st_size\n    if total is not None and size != total:\n        raise DownloadError(f\"incomplete download: {size} of {total} bytes (run again to resume)\")\n    if sha256 and digest.hexdigest() != sha256.lower():\n        part.unlink()\n        raise DownloadError(\"checksum mismatch — the file was corrupted or changed; removed it\")\n    os.replace(part, dest)\n    return dest\n","python","",[14,107,108,117,135,142,151,159,172,177,185,198,204,209,227,232,237,256,262,267,272,290,299,318,324,349,355,360,365,402,418,429,449,460,481,504,513,544,549,578,599,605,630,636,651,662,676,698,709,729,765,771,787,816,821,840,867,885,891,897,914,920,926,931,942,969,1005,1024,1030,1042,1048],{"__ignoreMap":105},[109,110,113],"span",{"class":111,"line":112},"line",1,[109,114,116],{"class":115},"sJ8bj","# src\u002Fmytool\u002Fdownload.py\n",[109,118,120,124,128,131],{"class":111,"line":119},2,[109,121,123],{"class":122},"szBVR","from",[109,125,127],{"class":126},"sj4cs"," __future__",[109,129,130],{"class":122}," import",[109,132,134],{"class":133},"sVt8B"," annotations\n",[109,136,138],{"class":111,"line":137},3,[109,139,141],{"emptyLinePlaceholder":140},true,"\n",[109,143,145,148],{"class":111,"line":144},4,[109,146,147],{"class":122},"import",[109,149,150],{"class":133}," hashlib\n",[109,152,154,156],{"class":111,"line":153},5,[109,155,147],{"class":122},[109,157,158],{"class":133}," os\n",[109,160,162,164,167,169],{"class":111,"line":161},6,[109,163,123],{"class":122},[109,165,166],{"class":133}," pathlib ",[109,168,147],{"class":122},[109,170,171],{"class":133}," Path\n",[109,173,175],{"class":111,"line":174},7,[109,176,141],{"emptyLinePlaceholder":140},[109,178,180,182],{"class":111,"line":179},8,[109,181,147],{"class":122},[109,183,184],{"class":133}," httpx\n",[109,186,188,190,193,195],{"class":111,"line":187},9,[109,189,123],{"class":122},[109,191,192],{"class":133}," rich.progress ",[109,194,147],{"class":122},[109,196,197],{"class":133}," (BarColumn, DownloadColumn, Progress, TextColumn,\n",[109,199,201],{"class":111,"line":200},10,[109,202,203],{"class":133},"                           TimeRemainingColumn, TransferSpeedColumn)\n",[109,205,207],{"class":111,"line":206},11,[109,208,141],{"emptyLinePlaceholder":140},[109,210,212,215,218,221,224],{"class":111,"line":211},12,[109,213,214],{"class":126},"CHUNK",[109,216,217],{"class":122}," =",[109,219,220],{"class":126}," 64",[109,222,223],{"class":122}," *",[109,225,226],{"class":126}," 1024\n",[109,228,230],{"class":111,"line":229},13,[109,231,141],{"emptyLinePlaceholder":140},[109,233,235],{"class":111,"line":234},14,[109,236,141],{"emptyLinePlaceholder":140},[109,238,240,243,247,250,253],{"class":111,"line":239},15,[109,241,242],{"class":122},"class",[109,244,246],{"class":245},"sScJk"," DownloadError",[109,248,249],{"class":133},"(",[109,251,252],{"class":126},"Exception",[109,254,255],{"class":133},"):\n",[109,257,259],{"class":111,"line":258},16,[109,260,261],{"class":122},"    pass\n",[109,263,265],{"class":111,"line":264},17,[109,266,141],{"emptyLinePlaceholder":140},[109,268,270],{"class":111,"line":269},18,[109,271,141],{"emptyLinePlaceholder":140},[109,273,275,278,281,284,287],{"class":111,"line":274},19,[109,276,277],{"class":122},"def",[109,279,280],{"class":245}," _progress",[109,282,283],{"class":133},"(enabled: ",[109,285,286],{"class":126},"bool",[109,288,289],{"class":133},") -> Progress:\n",[109,291,293,296],{"class":111,"line":292},20,[109,294,295],{"class":122},"    return",[109,297,298],{"class":133}," Progress(\n",[109,300,302,305,309,312,315],{"class":111,"line":301},21,[109,303,304],{"class":133},"        TextColumn(",[109,306,308],{"class":307},"sZZnC","\"[bold]",[109,310,311],{"class":126},"{task.description}",[109,313,314],{"class":307},"\"",[109,316,317],{"class":133},"), BarColumn(), DownloadColumn(),\n",[109,319,321],{"class":111,"line":320},22,[109,322,323],{"class":133},"        TransferSpeedColumn(), TimeRemainingColumn(),\n",[109,325,327,331,334,337,340,343,346],{"class":111,"line":326},23,[109,328,330],{"class":329},"s4XuR","        disable",[109,332,333],{"class":122},"=not",[109,335,336],{"class":133}," enabled, ",[109,338,339],{"class":329},"transient",[109,341,342],{"class":122},"=",[109,344,345],{"class":126},"True",[109,347,348],{"class":133},",\n",[109,350,352],{"class":111,"line":351},24,[109,353,354],{"class":133},"    )\n",[109,356,358],{"class":111,"line":357},25,[109,359,141],{"emptyLinePlaceholder":140},[109,361,363],{"class":111,"line":362},26,[109,364,141],{"emptyLinePlaceholder":140},[109,366,368,370,373,376,379,382,385,388,390,393,396,398,400],{"class":111,"line":367},27,[109,369,277],{"class":122},[109,371,372],{"class":245}," download",[109,374,375],{"class":133},"(client: httpx.Client, url: ",[109,377,378],{"class":126},"str",[109,380,381],{"class":133},", dest: Path, ",[109,383,384],{"class":122},"*",[109,386,387],{"class":133},", sha256: ",[109,389,378],{"class":126},[109,391,392],{"class":122}," |",[109,394,395],{"class":126}," None",[109,397,217],{"class":122},[109,399,395],{"class":126},[109,401,348],{"class":133},[109,403,405,408,410,412,415],{"class":111,"line":404},28,[109,406,407],{"class":133},"             show_progress: ",[109,409,286],{"class":126},[109,411,217],{"class":122},[109,413,414],{"class":126}," True",[109,416,417],{"class":133},") -> Path:\n",[109,419,421,424,426],{"class":111,"line":420},29,[109,422,423],{"class":133},"    dest ",[109,425,342],{"class":122},[109,427,428],{"class":133}," dest.resolve()\n",[109,430,432,435,437,440,443,446],{"class":111,"line":431},30,[109,433,434],{"class":133},"    part ",[109,436,342],{"class":122},[109,438,439],{"class":133}," dest.with_name(dest.name ",[109,441,442],{"class":122},"+",[109,444,445],{"class":307}," \".part\"",[109,447,448],{"class":133},")\n",[109,450,452,455,457],{"class":111,"line":451},31,[109,453,454],{"class":133},"    digest ",[109,456,342],{"class":122},[109,458,459],{"class":133}," hashlib.sha256()\n",[109,461,463,466,468,471,473,476,478],{"class":111,"line":462},32,[109,464,465],{"class":133},"    headers: dict[",[109,467,378],{"class":126},[109,469,470],{"class":133},", ",[109,472,378],{"class":126},[109,474,475],{"class":133},"] ",[109,477,342],{"class":122},[109,479,480],{"class":133}," {}\n",[109,482,484,487,489,492,495,498,501],{"class":111,"line":483},33,[109,485,486],{"class":133},"    offset ",[109,488,342],{"class":122},[109,490,491],{"class":133}," part.stat().st_size ",[109,493,494],{"class":122},"if",[109,496,497],{"class":133}," part.exists() ",[109,499,500],{"class":122},"else",[109,502,503],{"class":126}," 0\n",[109,505,507,510],{"class":111,"line":506},34,[109,508,509],{"class":122},"    if",[109,511,512],{"class":133}," offset:\n",[109,514,516,519,522,524,526,529,532,535,538,541],{"class":111,"line":515},35,[109,517,518],{"class":133},"        headers[",[109,520,521],{"class":307},"\"Range\"",[109,523,475],{"class":133},[109,525,342],{"class":122},[109,527,528],{"class":122}," f",[109,530,531],{"class":307},"\"bytes=",[109,533,534],{"class":126},"{",[109,536,537],{"class":133},"offset",[109,539,540],{"class":126},"}",[109,542,543],{"class":307},"-\"\n",[109,545,547],{"class":111,"line":546},36,[109,548,141],{"emptyLinePlaceholder":140},[109,550,552,555,558,561,564,567,569,572,575],{"class":111,"line":551},37,[109,553,554],{"class":122},"    with",[109,556,557],{"class":133}," client.stream(",[109,559,560],{"class":307},"\"GET\"",[109,562,563],{"class":133},", url, ",[109,565,566],{"class":329},"headers",[109,568,342],{"class":122},[109,570,571],{"class":133},"headers) ",[109,573,574],{"class":122},"as",[109,576,577],{"class":133}," response:\n",[109,579,581,584,587,590,593,596],{"class":111,"line":580},38,[109,582,583],{"class":122},"        if",[109,585,586],{"class":133}," response.status_code ",[109,588,589],{"class":122},"==",[109,591,592],{"class":126}," 416",[109,594,595],{"class":133},":            ",[109,597,598],{"class":115},"# .part already complete or stale\n",[109,600,602],{"class":111,"line":601},39,[109,603,604],{"class":133},"            part.unlink()\n",[109,606,608,611,614,617,619,622,625,627],{"class":111,"line":607},40,[109,609,610],{"class":122},"            return",[109,612,613],{"class":133}," download(client, url, dest, ",[109,615,616],{"class":329},"sha256",[109,618,342],{"class":122},[109,620,621],{"class":133},"sha256, ",[109,623,624],{"class":329},"show_progress",[109,626,342],{"class":122},[109,628,629],{"class":133},"show_progress)\n",[109,631,633],{"class":111,"line":632},41,[109,634,635],{"class":133},"        response.raise_for_status()\n",[109,637,639,642,644,646,648],{"class":111,"line":638},42,[109,640,641],{"class":133},"        resumed ",[109,643,342],{"class":122},[109,645,586],{"class":133},[109,647,589],{"class":122},[109,649,650],{"class":126}," 206\n",[109,652,654,656,659],{"class":111,"line":653},43,[109,655,583],{"class":122},[109,657,658],{"class":122}," not",[109,660,661],{"class":133}," resumed:\n",[109,663,665,668,670,673],{"class":111,"line":664},44,[109,666,667],{"class":133},"            offset ",[109,669,342],{"class":122},[109,671,672],{"class":126}," 0",[109,674,675],{"class":115},"                               # server ignored Range: start over\n",[109,677,679,682,684,687,690,693,695],{"class":111,"line":678},45,[109,680,681],{"class":133},"        mode ",[109,683,342],{"class":122},[109,685,686],{"class":307}," \"ab\"",[109,688,689],{"class":122}," if",[109,691,692],{"class":133}," resumed ",[109,694,500],{"class":122},[109,696,697],{"class":307}," \"wb\"\n",[109,699,701,703,706],{"class":111,"line":700},46,[109,702,583],{"class":122},[109,704,705],{"class":133}," resumed:                                   ",[109,707,708],{"class":115},"# hash the bytes we already have\n",[109,710,712,715,718,721,724,726],{"class":111,"line":711},47,[109,713,714],{"class":122},"            with",[109,716,717],{"class":133}," part.open(",[109,719,720],{"class":307},"\"rb\"",[109,722,723],{"class":133},") ",[109,725,574],{"class":122},[109,727,728],{"class":133}," existing:\n",[109,730,732,735,738,741,744,746,749,752,754,757,760,763],{"class":111,"line":731},48,[109,733,734],{"class":122},"                for",[109,736,737],{"class":133}," block ",[109,739,740],{"class":122},"in",[109,742,743],{"class":126}," iter",[109,745,249],{"class":133},[109,747,748],{"class":122},"lambda",[109,750,751],{"class":133},": existing.read(",[109,753,214],{"class":126},[109,755,756],{"class":133},"), ",[109,758,759],{"class":122},"b",[109,761,762],{"class":307},"\"\"",[109,764,255],{"class":133},[109,766,768],{"class":111,"line":767},49,[109,769,770],{"class":133},"                    digest.update(block)\n",[109,772,774,777,779,782,785],{"class":111,"line":773},50,[109,775,776],{"class":133},"        length ",[109,778,342],{"class":122},[109,780,781],{"class":133}," response.headers.get(",[109,783,784],{"class":307},"\"Content-Length\"",[109,786,448],{"class":133},[109,788,790,793,795,798,800,803,806,808,811,813],{"class":111,"line":789},51,[109,791,792],{"class":133},"        total ",[109,794,342],{"class":122},[109,796,797],{"class":133}," offset ",[109,799,442],{"class":122},[109,801,802],{"class":126}," int",[109,804,805],{"class":133},"(length) ",[109,807,494],{"class":122},[109,809,810],{"class":133}," length ",[109,812,500],{"class":122},[109,814,815],{"class":126}," None\n",[109,817,819],{"class":111,"line":818},52,[109,820,141],{"emptyLinePlaceholder":140},[109,822,824,827,830,832,835,837],{"class":111,"line":823},53,[109,825,826],{"class":122},"        with",[109,828,829],{"class":133}," _progress(show_progress) ",[109,831,574],{"class":122},[109,833,834],{"class":133}," progress, part.open(mode) ",[109,836,574],{"class":122},[109,838,839],{"class":133}," fh:\n",[109,841,843,846,848,851,854,856,859,862,864],{"class":111,"line":842},54,[109,844,845],{"class":133},"            task ",[109,847,342],{"class":122},[109,849,850],{"class":133}," progress.add_task(dest.name, ",[109,852,853],{"class":329},"total",[109,855,342],{"class":122},[109,857,858],{"class":133},"total, ",[109,860,861],{"class":329},"completed",[109,863,342],{"class":122},[109,865,866],{"class":133},"offset)\n",[109,868,870,873,876,878,881,883],{"class":111,"line":869},55,[109,871,872],{"class":122},"            for",[109,874,875],{"class":133}," chunk ",[109,877,740],{"class":122},[109,879,880],{"class":133}," response.iter_bytes(",[109,882,214],{"class":126},[109,884,255],{"class":133},[109,886,888],{"class":111,"line":887},56,[109,889,890],{"class":133},"                fh.write(chunk)\n",[109,892,894],{"class":111,"line":893},57,[109,895,896],{"class":133},"                digest.update(chunk)\n",[109,898,900,903,906,908,911],{"class":111,"line":899},58,[109,901,902],{"class":133},"                progress.update(task, ",[109,904,905],{"class":329},"advance",[109,907,342],{"class":122},[109,909,910],{"class":126},"len",[109,912,913],{"class":133},"(chunk))\n",[109,915,917],{"class":111,"line":916},59,[109,918,919],{"class":133},"            fh.flush()\n",[109,921,923],{"class":111,"line":922},60,[109,924,925],{"class":133},"            os.fsync(fh.fileno())\n",[109,927,929],{"class":111,"line":928},61,[109,930,141],{"emptyLinePlaceholder":140},[109,932,934,937,939],{"class":111,"line":933},62,[109,935,936],{"class":133},"    size ",[109,938,342],{"class":122},[109,940,941],{"class":133}," part.stat().st_size\n",[109,943,945,947,950,953,955,957,960,963,966],{"class":111,"line":944},63,[109,946,509],{"class":122},[109,948,949],{"class":133}," total ",[109,951,952],{"class":122},"is",[109,954,658],{"class":122},[109,956,395],{"class":126},[109,958,959],{"class":122}," and",[109,961,962],{"class":133}," size ",[109,964,965],{"class":122},"!=",[109,967,968],{"class":133}," total:\n",[109,970,972,975,978,981,984,986,989,991,994,996,998,1000,1003],{"class":111,"line":971},64,[109,973,974],{"class":122},"        raise",[109,976,977],{"class":133}," DownloadError(",[109,979,980],{"class":122},"f",[109,982,983],{"class":307},"\"incomplete download: ",[109,985,534],{"class":126},[109,987,988],{"class":133},"size",[109,990,540],{"class":126},[109,992,993],{"class":307}," of ",[109,995,534],{"class":126},[109,997,853],{"class":133},[109,999,540],{"class":126},[109,1001,1002],{"class":307}," bytes (run again to resume)\"",[109,1004,448],{"class":133},[109,1006,1008,1010,1013,1016,1019,1021],{"class":111,"line":1007},65,[109,1009,509],{"class":122},[109,1011,1012],{"class":133}," sha256 ",[109,1014,1015],{"class":122},"and",[109,1017,1018],{"class":133}," digest.hexdigest() ",[109,1020,965],{"class":122},[109,1022,1023],{"class":133}," sha256.lower():\n",[109,1025,1027],{"class":111,"line":1026},66,[109,1028,1029],{"class":133},"        part.unlink()\n",[109,1031,1033,1035,1037,1040],{"class":111,"line":1032},67,[109,1034,974],{"class":122},[109,1036,977],{"class":133},[109,1038,1039],{"class":307},"\"checksum mismatch — the file was corrupted or changed; removed it\"",[109,1041,448],{"class":133},[109,1043,1045],{"class":111,"line":1044},68,[109,1046,1047],{"class":133},"    os.replace(part, dest)\n",[109,1049,1051,1053],{"class":111,"line":1050},69,[109,1052,295],{"class":122},[109,1054,1055],{"class":133}," dest\n",[10,1057,1058],{},"Some details that are easy to get wrong:",[34,1060,1061,1085,1099,1110],{},[37,1062,1063,1073,1074,1077,1078,1081,1082,1084],{},[1064,1065,1066,1069,1070,27],"strong",{},[14,1067,1068],{},"iter_bytes",", not ",[14,1071,1072],{},"iter_raw"," ",[14,1075,1076],{},"iter_bytes()"," decodes any ",[14,1079,1080],{},"Content-Encoding"," (gzip) the server applied for transport, so you get the actual file. The trade-off is that ",[14,1083,57],{}," then describes the compressed size; static binaries are almost never transfer-compressed, so this rarely matters, but it is why the progress total can be approximate.",[37,1086,1087,1094,1095,1098],{},[1064,1088,1089,1090,1093],{},"A ",[14,1091,1092],{},"206 Partial Content"," is the only proof a resume worked."," Servers that do not support ranges simply ignore the header and send the whole file with ",[14,1096,1097],{},"200",". The code checks the status and starts over when that happens, instead of appending a complete file to a partial one.",[37,1100,1101,1106,1107,1109],{},[1064,1102,1103],{},[14,1104,1105],{},"416 Range Not Satisfiable"," means the ",[14,1108,20],{}," file is at least as big as the resource — typically a finished download whose rename never happened, or a file that shrank on the server. Deleting it and starting again is the safe response.",[37,1111,1112,1115,1116,1118],{},[1064,1113,1114],{},"The checksum covers every byte",", including those downloaded by an earlier run, which is why the existing ",[14,1117,20],{}," content is hashed before appending.",[82,1120],{"name":1121},"http-resume-decision",[10,1123,1124,1125,1128,1129,1132,1133,1135,1136,1138,1139,1142],{},"For stronger guarantees on resume, send ",[14,1126,1127],{},"If-Range"," with the ",[14,1130,1131],{},"ETag"," from the original response. If the file changed on the server, the server then returns the whole new file (",[14,1134,1097],{},") instead of splicing a range of the new version onto the old one. Store the ETag next to the ",[14,1137,20],{}," file (for example in ",[14,1140,1141],{},".part.etag",") to make that work across runs.",[29,1144,1146],{"id":1145},"the-command","The command",[100,1148,1150],{"className":102,"code":1149,"language":104,"meta":105,"style":105},"# src\u002Fmytool\u002Fcli.py\nimport sys\nfrom pathlib import Path\n\nimport httpx\nimport typer\n\nfrom mytool.download import DownloadError, download\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Dataset tools.\"\"\"\n\n\n@app.command()\ndef fetch(\n    url: str,\n    out: Path = typer.Option(None, \"--out\", \"-o\", help=\"Destination (default: name from URL).\"),\n    sha256: str = typer.Option(None, help=\"Expected SHA-256 of the file.\"),\n) -> None:\n    \"\"\"Download URL, resuming if a previous attempt was interrupted.\"\"\"\n    dest = out or Path(httpx.URL(url).path.rsplit(\"\u002F\", 1)[-1] or \"download\")\n    timeout = httpx.Timeout(60.0, connect=10.0)\n    with httpx.Client(timeout=timeout, follow_redirects=True) as client:\n        try:\n            path = download(client, url, dest, sha256=sha256, show_progress=sys.stderr.isatty())\n        except (DownloadError, httpx.HTTPError) as exc:\n            typer.secho(f\"error: {exc}\", fg=\"red\", err=True)\n            raise typer.Exit(1)\n        except KeyboardInterrupt:\n            typer.echo(\"\\ninterrupted — run the same command again to resume\", err=True)\n            raise typer.Exit(130)\n    if sha256:\n        typer.echo(f\"verified sha256 {sha256[:4]}...{sha256[-4:]}\", err=True)\n    typer.echo(f\"saved {path}\", err=True)\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1151,1152,1157,1164,1174,1178,1184,1191,1195,1207,1211,1221,1225,1229,1237,1253,1258,1262,1266,1273,1283,1292,1327,1351,1360,1365,1405,1430,1459,1466,1488,1501,1541,1553,1562,1585,1596,1603,1655,1684,1688,1692,1707],{"__ignoreMap":105},[109,1153,1154],{"class":111,"line":112},[109,1155,1156],{"class":115},"# src\u002Fmytool\u002Fcli.py\n",[109,1158,1159,1161],{"class":111,"line":119},[109,1160,147],{"class":122},[109,1162,1163],{"class":133}," sys\n",[109,1165,1166,1168,1170,1172],{"class":111,"line":137},[109,1167,123],{"class":122},[109,1169,166],{"class":133},[109,1171,147],{"class":122},[109,1173,171],{"class":133},[109,1175,1176],{"class":111,"line":144},[109,1177,141],{"emptyLinePlaceholder":140},[109,1179,1180,1182],{"class":111,"line":153},[109,1181,147],{"class":122},[109,1183,184],{"class":133},[109,1185,1186,1188],{"class":111,"line":161},[109,1187,147],{"class":122},[109,1189,1190],{"class":133}," typer\n",[109,1192,1193],{"class":111,"line":174},[109,1194,141],{"emptyLinePlaceholder":140},[109,1196,1197,1199,1202,1204],{"class":111,"line":179},[109,1198,123],{"class":122},[109,1200,1201],{"class":133}," mytool.download ",[109,1203,147],{"class":122},[109,1205,1206],{"class":133}," DownloadError, download\n",[109,1208,1209],{"class":111,"line":187},[109,1210,141],{"emptyLinePlaceholder":140},[109,1212,1213,1216,1218],{"class":111,"line":200},[109,1214,1215],{"class":133},"app ",[109,1217,342],{"class":122},[109,1219,1220],{"class":133}," typer.Typer()\n",[109,1222,1223],{"class":111,"line":206},[109,1224,141],{"emptyLinePlaceholder":140},[109,1226,1227],{"class":111,"line":211},[109,1228,141],{"emptyLinePlaceholder":140},[109,1230,1231,1234],{"class":111,"line":229},[109,1232,1233],{"class":245},"@app.callback",[109,1235,1236],{"class":133},"()\n",[109,1238,1239,1241,1244,1247,1250],{"class":111,"line":234},[109,1240,277],{"class":122},[109,1242,1243],{"class":245}," main",[109,1245,1246],{"class":133},"() -> ",[109,1248,1249],{"class":126},"None",[109,1251,1252],{"class":133},":\n",[109,1254,1255],{"class":111,"line":239},[109,1256,1257],{"class":307},"    \"\"\"Dataset tools.\"\"\"\n",[109,1259,1260],{"class":111,"line":258},[109,1261,141],{"emptyLinePlaceholder":140},[109,1263,1264],{"class":111,"line":264},[109,1265,141],{"emptyLinePlaceholder":140},[109,1267,1268,1271],{"class":111,"line":269},[109,1269,1270],{"class":245},"@app.command",[109,1272,1236],{"class":133},[109,1274,1275,1277,1280],{"class":111,"line":274},[109,1276,277],{"class":122},[109,1278,1279],{"class":245}," fetch",[109,1281,1282],{"class":133},"(\n",[109,1284,1285,1288,1290],{"class":111,"line":292},[109,1286,1287],{"class":133},"    url: ",[109,1289,378],{"class":126},[109,1291,348],{"class":133},[109,1293,1294,1297,1299,1302,1304,1306,1309,1311,1314,1316,1319,1321,1324],{"class":111,"line":301},[109,1295,1296],{"class":133},"    out: Path ",[109,1298,342],{"class":122},[109,1300,1301],{"class":133}," typer.Option(",[109,1303,1249],{"class":126},[109,1305,470],{"class":133},[109,1307,1308],{"class":307},"\"--out\"",[109,1310,470],{"class":133},[109,1312,1313],{"class":307},"\"-o\"",[109,1315,470],{"class":133},[109,1317,1318],{"class":329},"help",[109,1320,342],{"class":122},[109,1322,1323],{"class":307},"\"Destination (default: name from URL).\"",[109,1325,1326],{"class":133},"),\n",[109,1328,1329,1332,1334,1336,1338,1340,1342,1344,1346,1349],{"class":111,"line":320},[109,1330,1331],{"class":133},"    sha256: ",[109,1333,378],{"class":126},[109,1335,217],{"class":122},[109,1337,1301],{"class":133},[109,1339,1249],{"class":126},[109,1341,470],{"class":133},[109,1343,1318],{"class":329},[109,1345,342],{"class":122},[109,1347,1348],{"class":307},"\"Expected SHA-256 of the file.\"",[109,1350,1326],{"class":133},[109,1352,1353,1356,1358],{"class":111,"line":326},[109,1354,1355],{"class":133},") -> ",[109,1357,1249],{"class":126},[109,1359,1252],{"class":133},[109,1361,1362],{"class":111,"line":351},[109,1363,1364],{"class":307},"    \"\"\"Download URL, resuming if a previous attempt was interrupted.\"\"\"\n",[109,1366,1367,1369,1371,1374,1377,1380,1383,1385,1388,1391,1394,1396,1398,1400,1403],{"class":111,"line":357},[109,1368,423],{"class":133},[109,1370,342],{"class":122},[109,1372,1373],{"class":133}," out ",[109,1375,1376],{"class":122},"or",[109,1378,1379],{"class":133}," Path(httpx.URL(url).path.rsplit(",[109,1381,1382],{"class":307},"\"\u002F\"",[109,1384,470],{"class":133},[109,1386,1387],{"class":126},"1",[109,1389,1390],{"class":133},")[",[109,1392,1393],{"class":122},"-",[109,1395,1387],{"class":126},[109,1397,475],{"class":133},[109,1399,1376],{"class":122},[109,1401,1402],{"class":307}," \"download\"",[109,1404,448],{"class":133},[109,1406,1407,1410,1412,1415,1418,1420,1423,1425,1428],{"class":111,"line":362},[109,1408,1409],{"class":133},"    timeout ",[109,1411,342],{"class":122},[109,1413,1414],{"class":133}," httpx.Timeout(",[109,1416,1417],{"class":126},"60.0",[109,1419,470],{"class":133},[109,1421,1422],{"class":329},"connect",[109,1424,342],{"class":122},[109,1426,1427],{"class":126},"10.0",[109,1429,448],{"class":133},[109,1431,1432,1434,1437,1440,1442,1445,1448,1450,1452,1454,1456],{"class":111,"line":367},[109,1433,554],{"class":122},[109,1435,1436],{"class":133}," httpx.Client(",[109,1438,1439],{"class":329},"timeout",[109,1441,342],{"class":122},[109,1443,1444],{"class":133},"timeout, ",[109,1446,1447],{"class":329},"follow_redirects",[109,1449,342],{"class":122},[109,1451,345],{"class":126},[109,1453,723],{"class":133},[109,1455,574],{"class":122},[109,1457,1458],{"class":133}," client:\n",[109,1460,1461,1464],{"class":111,"line":404},[109,1462,1463],{"class":122},"        try",[109,1465,1252],{"class":133},[109,1467,1468,1471,1473,1475,1477,1479,1481,1483,1485],{"class":111,"line":420},[109,1469,1470],{"class":133},"            path ",[109,1472,342],{"class":122},[109,1474,613],{"class":133},[109,1476,616],{"class":329},[109,1478,342],{"class":122},[109,1480,621],{"class":133},[109,1482,624],{"class":329},[109,1484,342],{"class":122},[109,1486,1487],{"class":133},"sys.stderr.isatty())\n",[109,1489,1490,1493,1496,1498],{"class":111,"line":431},[109,1491,1492],{"class":122},"        except",[109,1494,1495],{"class":133}," (DownloadError, httpx.HTTPError) ",[109,1497,574],{"class":122},[109,1499,1500],{"class":133}," exc:\n",[109,1502,1503,1506,1508,1511,1513,1516,1518,1520,1522,1525,1527,1530,1532,1535,1537,1539],{"class":111,"line":451},[109,1504,1505],{"class":133},"            typer.secho(",[109,1507,980],{"class":122},[109,1509,1510],{"class":307},"\"error: ",[109,1512,534],{"class":126},[109,1514,1515],{"class":133},"exc",[109,1517,540],{"class":126},[109,1519,314],{"class":307},[109,1521,470],{"class":133},[109,1523,1524],{"class":329},"fg",[109,1526,342],{"class":122},[109,1528,1529],{"class":307},"\"red\"",[109,1531,470],{"class":133},[109,1533,1534],{"class":329},"err",[109,1536,342],{"class":122},[109,1538,345],{"class":126},[109,1540,448],{"class":133},[109,1542,1543,1546,1549,1551],{"class":111,"line":462},[109,1544,1545],{"class":122},"            raise",[109,1547,1548],{"class":133}," typer.Exit(",[109,1550,1387],{"class":126},[109,1552,448],{"class":133},[109,1554,1555,1557,1560],{"class":111,"line":483},[109,1556,1492],{"class":122},[109,1558,1559],{"class":126}," KeyboardInterrupt",[109,1561,1252],{"class":133},[109,1563,1564,1567,1569,1572,1575,1577,1579,1581,1583],{"class":111,"line":506},[109,1565,1566],{"class":133},"            typer.echo(",[109,1568,314],{"class":307},[109,1570,1571],{"class":126},"\\n",[109,1573,1574],{"class":307},"interrupted — run the same command again to resume\"",[109,1576,470],{"class":133},[109,1578,1534],{"class":329},[109,1580,342],{"class":122},[109,1582,345],{"class":126},[109,1584,448],{"class":133},[109,1586,1587,1589,1591,1594],{"class":111,"line":515},[109,1588,1545],{"class":122},[109,1590,1548],{"class":133},[109,1592,1593],{"class":126},"130",[109,1595,448],{"class":133},[109,1597,1598,1600],{"class":111,"line":546},[109,1599,509],{"class":122},[109,1601,1602],{"class":133}," sha256:\n",[109,1604,1605,1608,1610,1613,1615,1618,1621,1624,1626,1629,1631,1634,1636,1638,1641,1643,1645,1647,1649,1651,1653],{"class":111,"line":551},[109,1606,1607],{"class":133},"        typer.echo(",[109,1609,980],{"class":122},[109,1611,1612],{"class":307},"\"verified sha256 ",[109,1614,534],{"class":126},[109,1616,1617],{"class":133},"sha256[:",[109,1619,1620],{"class":126},"4",[109,1622,1623],{"class":133},"]",[109,1625,540],{"class":126},[109,1627,1628],{"class":307},"...",[109,1630,534],{"class":126},[109,1632,1633],{"class":133},"sha256[",[109,1635,1393],{"class":122},[109,1637,1620],{"class":126},[109,1639,1640],{"class":133},":]",[109,1642,540],{"class":126},[109,1644,314],{"class":307},[109,1646,470],{"class":133},[109,1648,1534],{"class":329},[109,1650,342],{"class":122},[109,1652,345],{"class":126},[109,1654,448],{"class":133},[109,1656,1657,1660,1662,1665,1667,1670,1672,1674,1676,1678,1680,1682],{"class":111,"line":580},[109,1658,1659],{"class":133},"    typer.echo(",[109,1661,980],{"class":122},[109,1663,1664],{"class":307},"\"saved ",[109,1666,534],{"class":126},[109,1668,1669],{"class":133},"path",[109,1671,540],{"class":126},[109,1673,314],{"class":307},[109,1675,470],{"class":133},[109,1677,1534],{"class":329},[109,1679,342],{"class":122},[109,1681,345],{"class":126},[109,1683,448],{"class":133},[109,1685,1686],{"class":111,"line":601},[109,1687,141],{"emptyLinePlaceholder":140},[109,1689,1690],{"class":111,"line":607},[109,1691,141],{"emptyLinePlaceholder":140},[109,1693,1694,1696,1699,1702,1705],{"class":111,"line":632},[109,1695,494],{"class":122},[109,1697,1698],{"class":126}," __name__",[109,1700,1701],{"class":122}," ==",[109,1703,1704],{"class":307}," \"__main__\"",[109,1706,1252],{"class":133},[109,1708,1709],{"class":111,"line":638},[109,1710,1711],{"class":133},"    app()\n",[10,1713,1714,1717],{},[14,1715,1716],{},"follow_redirects=True"," is important for downloads: release assets on GitHub, object-store presigned URLs and CDNs almost always redirect. The read timeout of 60 seconds limits the gap between chunks, not the total download time, so a large file on a slow link is fine as long as bytes keep arriving.",[29,1719,1721],{"id":1720},"ux-considerations","UX considerations",[82,1723],{"name":1724},"http-download-terminal",[34,1726,1727,1745,1755,1761,1767],{},[37,1728,1729,1732,1733,1736,1737,1740,1741,27],{},[1064,1730,1731],{},"Progress only on a terminal."," Rich's ",[14,1734,1735],{},"disable="," keeps the bar off when stderr is redirected, so CI logs do not fill with carriage-return redraws. ",[14,1738,1739],{},"transient=True"," removes the bar when done, leaving a clean \"saved\" line. The general rule is in ",[23,1742,1744],{"href":1743},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output\u002F","detecting a TTY and adapting output",[37,1746,1747,1750,1751,27],{},[1064,1748,1749],{},"Show size, speed and time remaining."," Those three columns answer \"is it working?\" and \"should I get coffee?\" — which is all a progress bar is for. More patterns are in ",[23,1752,1754],{"href":1753},"\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",[37,1756,1757,1760],{},[1064,1758,1759],{},"Tell the user resuming is possible."," On Ctrl+C or a dropped connection, \"run the same command again to resume\" converts a frustrating failure into a minor pause.",[37,1762,1763,1766],{},[1064,1764,1765],{},"Verify, and say so."," Printing a short form of the verified hash reassures users that the file is exactly what was published.",[37,1768,1769,1772,1773,1776],{},[1064,1770,1771],{},"Refuse to overwrite silently."," If the destination exists and differs, consider requiring ",[14,1774,1775],{},"--force",", or at least say that it was replaced.",[29,1778,1780],{"id":1779},"testing-the-behaviour","Testing the behaviour",[10,1782,1783,1784,1787],{},"A mock transport that serves a known byte string and honours ",[14,1785,1786],{},"Range"," lets you test full downloads, resumes and corruption without a network:",[100,1789,1791],{"className":102,"code":1790,"language":104,"meta":105,"style":105},"# tests\u002Ftest_download.py\nimport hashlib\n\nimport httpx\nimport pytest\n\nfrom mytool.download import DownloadError, download\n\nBLOB = bytes(range(256)) * 1000          # 256 KB of known data\nSHA = hashlib.sha256(BLOB).hexdigest()\n\n\ndef server(ranges: bool = True, body: bytes = BLOB):\n    def handler(request):\n        rng = request.headers.get(\"Range\")\n        if ranges and rng:\n            start = int(rng.removeprefix(\"bytes=\").rstrip(\"-\"))\n            if start >= len(body):\n                return httpx.Response(416)\n            chunk = body[start:]\n            return httpx.Response(206, content=chunk, headers={\"Content-Length\": str(len(chunk))})\n        return httpx.Response(200, content=body, headers={\"Content-Length\": str(len(body))})\n    return httpx.Client(transport=httpx.MockTransport(handler))\n\n\ndef test_full_download(tmp_path):\n    dest = download(server(), \"https:\u002F\u002Ffiles.test\u002Fdata.bin\", tmp_path \u002F \"data.bin\",\n                    sha256=SHA, show_progress=False)\n    assert dest.read_bytes() == BLOB\n    assert not (tmp_path \u002F \"data.bin.part\").exists()\n\n\ndef test_resume_appends_the_rest(tmp_path):\n    (tmp_path \u002F \"data.bin.part\").write_bytes(BLOB[:100_000])\n    dest = download(server(), \"https:\u002F\u002Ffiles.test\u002Fdata.bin\", tmp_path \u002F \"data.bin\",\n                    sha256=SHA, show_progress=False)\n    assert dest.read_bytes() == BLOB\n\n\ndef test_server_without_ranges_restarts(tmp_path):\n    (tmp_path \u002F \"data.bin.part\").write_bytes(b\"garbage\" * 10)\n    dest = download(server(ranges=False), \"https:\u002F\u002Ffiles.test\u002Fdata.bin\", tmp_path \u002F \"data.bin\",\n                    sha256=SHA, show_progress=False)\n    assert dest.read_bytes() == BLOB\n\n\ndef test_bad_checksum_leaves_no_file(tmp_path):\n    with pytest.raises(DownloadError, match=\"checksum\"):\n        download(server(), \"https:\u002F\u002Ffiles.test\u002Fdata.bin\", tmp_path \u002F \"data.bin\",\n                 sha256=\"0\" * 64, show_progress=False)\n    assert list(tmp_path.iterdir()) == []\n",[14,1792,1793,1798,1804,1808,1814,1821,1825,1835,1839,1870,1885,1889,1893,1922,1933,1947,1959,1983,2000,2013,2023,2062,2099,2113,2117,2121,2131,2154,2174,2187,2204,2208,2212,2221,2244,2262,2280,2290,2294,2298,2307,2329,2357,2375,2385,2389,2393,2402,2419,2434,2458],{"__ignoreMap":105},[109,1794,1795],{"class":111,"line":112},[109,1796,1797],{"class":115},"# tests\u002Ftest_download.py\n",[109,1799,1800,1802],{"class":111,"line":119},[109,1801,147],{"class":122},[109,1803,150],{"class":133},[109,1805,1806],{"class":111,"line":137},[109,1807,141],{"emptyLinePlaceholder":140},[109,1809,1810,1812],{"class":111,"line":144},[109,1811,147],{"class":122},[109,1813,184],{"class":133},[109,1815,1816,1818],{"class":111,"line":153},[109,1817,147],{"class":122},[109,1819,1820],{"class":133}," pytest\n",[109,1822,1823],{"class":111,"line":161},[109,1824,141],{"emptyLinePlaceholder":140},[109,1826,1827,1829,1831,1833],{"class":111,"line":174},[109,1828,123],{"class":122},[109,1830,1201],{"class":133},[109,1832,147],{"class":122},[109,1834,1206],{"class":133},[109,1836,1837],{"class":111,"line":179},[109,1838,141],{"emptyLinePlaceholder":140},[109,1840,1841,1844,1846,1849,1851,1854,1856,1859,1862,1864,1867],{"class":111,"line":187},[109,1842,1843],{"class":126},"BLOB",[109,1845,217],{"class":122},[109,1847,1848],{"class":126}," bytes",[109,1850,249],{"class":133},[109,1852,1853],{"class":126},"range",[109,1855,249],{"class":133},[109,1857,1858],{"class":126},"256",[109,1860,1861],{"class":133},")) ",[109,1863,384],{"class":122},[109,1865,1866],{"class":126}," 1000",[109,1868,1869],{"class":115},"          # 256 KB of known data\n",[109,1871,1872,1875,1877,1880,1882],{"class":111,"line":200},[109,1873,1874],{"class":126},"SHA",[109,1876,217],{"class":122},[109,1878,1879],{"class":133}," hashlib.sha256(",[109,1881,1843],{"class":126},[109,1883,1884],{"class":133},").hexdigest()\n",[109,1886,1887],{"class":111,"line":206},[109,1888,141],{"emptyLinePlaceholder":140},[109,1890,1891],{"class":111,"line":211},[109,1892,141],{"emptyLinePlaceholder":140},[109,1894,1895,1897,1900,1903,1905,1907,1909,1912,1915,1917,1920],{"class":111,"line":229},[109,1896,277],{"class":122},[109,1898,1899],{"class":245}," server",[109,1901,1902],{"class":133},"(ranges: ",[109,1904,286],{"class":126},[109,1906,217],{"class":122},[109,1908,414],{"class":126},[109,1910,1911],{"class":133},", body: ",[109,1913,1914],{"class":126},"bytes",[109,1916,217],{"class":122},[109,1918,1919],{"class":126}," BLOB",[109,1921,255],{"class":133},[109,1923,1924,1927,1930],{"class":111,"line":234},[109,1925,1926],{"class":122},"    def",[109,1928,1929],{"class":245}," handler",[109,1931,1932],{"class":133},"(request):\n",[109,1934,1935,1938,1940,1943,1945],{"class":111,"line":239},[109,1936,1937],{"class":133},"        rng ",[109,1939,342],{"class":122},[109,1941,1942],{"class":133}," request.headers.get(",[109,1944,521],{"class":307},[109,1946,448],{"class":133},[109,1948,1949,1951,1954,1956],{"class":111,"line":258},[109,1950,583],{"class":122},[109,1952,1953],{"class":133}," ranges ",[109,1955,1015],{"class":122},[109,1957,1958],{"class":133}," rng:\n",[109,1960,1961,1964,1966,1968,1971,1974,1977,1980],{"class":111,"line":264},[109,1962,1963],{"class":133},"            start ",[109,1965,342],{"class":122},[109,1967,802],{"class":126},[109,1969,1970],{"class":133},"(rng.removeprefix(",[109,1972,1973],{"class":307},"\"bytes=\"",[109,1975,1976],{"class":133},").rstrip(",[109,1978,1979],{"class":307},"\"-\"",[109,1981,1982],{"class":133},"))\n",[109,1984,1985,1988,1991,1994,1997],{"class":111,"line":269},[109,1986,1987],{"class":122},"            if",[109,1989,1990],{"class":133}," start ",[109,1992,1993],{"class":122},">=",[109,1995,1996],{"class":126}," len",[109,1998,1999],{"class":133},"(body):\n",[109,2001,2002,2005,2008,2011],{"class":111,"line":274},[109,2003,2004],{"class":122},"                return",[109,2006,2007],{"class":133}," httpx.Response(",[109,2009,2010],{"class":126},"416",[109,2012,448],{"class":133},[109,2014,2015,2018,2020],{"class":111,"line":292},[109,2016,2017],{"class":133},"            chunk ",[109,2019,342],{"class":122},[109,2021,2022],{"class":133}," body[start:]\n",[109,2024,2025,2027,2029,2032,2034,2037,2039,2042,2044,2046,2048,2050,2053,2055,2057,2059],{"class":111,"line":301},[109,2026,610],{"class":122},[109,2028,2007],{"class":133},[109,2030,2031],{"class":126},"206",[109,2033,470],{"class":133},[109,2035,2036],{"class":329},"content",[109,2038,342],{"class":122},[109,2040,2041],{"class":133},"chunk, ",[109,2043,566],{"class":329},[109,2045,342],{"class":122},[109,2047,534],{"class":133},[109,2049,784],{"class":307},[109,2051,2052],{"class":133},": ",[109,2054,378],{"class":126},[109,2056,249],{"class":133},[109,2058,910],{"class":126},[109,2060,2061],{"class":133},"(chunk))})\n",[109,2063,2064,2067,2069,2071,2073,2075,2077,2080,2082,2084,2086,2088,2090,2092,2094,2096],{"class":111,"line":320},[109,2065,2066],{"class":122},"        return",[109,2068,2007],{"class":133},[109,2070,1097],{"class":126},[109,2072,470],{"class":133},[109,2074,2036],{"class":329},[109,2076,342],{"class":122},[109,2078,2079],{"class":133},"body, ",[109,2081,566],{"class":329},[109,2083,342],{"class":122},[109,2085,534],{"class":133},[109,2087,784],{"class":307},[109,2089,2052],{"class":133},[109,2091,378],{"class":126},[109,2093,249],{"class":133},[109,2095,910],{"class":126},[109,2097,2098],{"class":133},"(body))})\n",[109,2100,2101,2103,2105,2108,2110],{"class":111,"line":326},[109,2102,295],{"class":122},[109,2104,1436],{"class":133},[109,2106,2107],{"class":329},"transport",[109,2109,342],{"class":122},[109,2111,2112],{"class":133},"httpx.MockTransport(handler))\n",[109,2114,2115],{"class":111,"line":351},[109,2116,141],{"emptyLinePlaceholder":140},[109,2118,2119],{"class":111,"line":357},[109,2120,141],{"emptyLinePlaceholder":140},[109,2122,2123,2125,2128],{"class":111,"line":362},[109,2124,277],{"class":122},[109,2126,2127],{"class":245}," test_full_download",[109,2129,2130],{"class":133},"(tmp_path):\n",[109,2132,2133,2135,2137,2140,2143,2146,2149,2152],{"class":111,"line":367},[109,2134,423],{"class":133},[109,2136,342],{"class":122},[109,2138,2139],{"class":133}," download(server(), ",[109,2141,2142],{"class":307},"\"https:\u002F\u002Ffiles.test\u002Fdata.bin\"",[109,2144,2145],{"class":133},", tmp_path ",[109,2147,2148],{"class":122},"\u002F",[109,2150,2151],{"class":307}," \"data.bin\"",[109,2153,348],{"class":133},[109,2155,2156,2159,2161,2163,2165,2167,2169,2172],{"class":111,"line":404},[109,2157,2158],{"class":329},"                    sha256",[109,2160,342],{"class":122},[109,2162,1874],{"class":126},[109,2164,470],{"class":133},[109,2166,624],{"class":329},[109,2168,342],{"class":122},[109,2170,2171],{"class":126},"False",[109,2173,448],{"class":133},[109,2175,2176,2179,2182,2184],{"class":111,"line":420},[109,2177,2178],{"class":122},"    assert",[109,2180,2181],{"class":133}," dest.read_bytes() ",[109,2183,589],{"class":122},[109,2185,2186],{"class":126}," BLOB\n",[109,2188,2189,2191,2193,2196,2198,2201],{"class":111,"line":431},[109,2190,2178],{"class":122},[109,2192,658],{"class":122},[109,2194,2195],{"class":133}," (tmp_path ",[109,2197,2148],{"class":122},[109,2199,2200],{"class":307}," \"data.bin.part\"",[109,2202,2203],{"class":133},").exists()\n",[109,2205,2206],{"class":111,"line":451},[109,2207,141],{"emptyLinePlaceholder":140},[109,2209,2210],{"class":111,"line":462},[109,2211,141],{"emptyLinePlaceholder":140},[109,2213,2214,2216,2219],{"class":111,"line":483},[109,2215,277],{"class":122},[109,2217,2218],{"class":245}," test_resume_appends_the_rest",[109,2220,2130],{"class":133},[109,2222,2223,2226,2228,2230,2233,2235,2238,2241],{"class":111,"line":506},[109,2224,2225],{"class":133},"    (tmp_path ",[109,2227,2148],{"class":122},[109,2229,2200],{"class":307},[109,2231,2232],{"class":133},").write_bytes(",[109,2234,1843],{"class":126},[109,2236,2237],{"class":133},"[:",[109,2239,2240],{"class":126},"100_000",[109,2242,2243],{"class":133},"])\n",[109,2245,2246,2248,2250,2252,2254,2256,2258,2260],{"class":111,"line":515},[109,2247,423],{"class":133},[109,2249,342],{"class":122},[109,2251,2139],{"class":133},[109,2253,2142],{"class":307},[109,2255,2145],{"class":133},[109,2257,2148],{"class":122},[109,2259,2151],{"class":307},[109,2261,348],{"class":133},[109,2263,2264,2266,2268,2270,2272,2274,2276,2278],{"class":111,"line":546},[109,2265,2158],{"class":329},[109,2267,342],{"class":122},[109,2269,1874],{"class":126},[109,2271,470],{"class":133},[109,2273,624],{"class":329},[109,2275,342],{"class":122},[109,2277,2171],{"class":126},[109,2279,448],{"class":133},[109,2281,2282,2284,2286,2288],{"class":111,"line":551},[109,2283,2178],{"class":122},[109,2285,2181],{"class":133},[109,2287,589],{"class":122},[109,2289,2186],{"class":126},[109,2291,2292],{"class":111,"line":580},[109,2293,141],{"emptyLinePlaceholder":140},[109,2295,2296],{"class":111,"line":601},[109,2297,141],{"emptyLinePlaceholder":140},[109,2299,2300,2302,2305],{"class":111,"line":607},[109,2301,277],{"class":122},[109,2303,2304],{"class":245}," test_server_without_ranges_restarts",[109,2306,2130],{"class":133},[109,2308,2309,2311,2313,2315,2317,2319,2322,2324,2327],{"class":111,"line":632},[109,2310,2225],{"class":133},[109,2312,2148],{"class":122},[109,2314,2200],{"class":307},[109,2316,2232],{"class":133},[109,2318,759],{"class":122},[109,2320,2321],{"class":307},"\"garbage\"",[109,2323,223],{"class":122},[109,2325,2326],{"class":126}," 10",[109,2328,448],{"class":133},[109,2330,2331,2333,2335,2338,2341,2343,2345,2347,2349,2351,2353,2355],{"class":111,"line":638},[109,2332,423],{"class":133},[109,2334,342],{"class":122},[109,2336,2337],{"class":133}," download(server(",[109,2339,2340],{"class":329},"ranges",[109,2342,342],{"class":122},[109,2344,2171],{"class":126},[109,2346,756],{"class":133},[109,2348,2142],{"class":307},[109,2350,2145],{"class":133},[109,2352,2148],{"class":122},[109,2354,2151],{"class":307},[109,2356,348],{"class":133},[109,2358,2359,2361,2363,2365,2367,2369,2371,2373],{"class":111,"line":653},[109,2360,2158],{"class":329},[109,2362,342],{"class":122},[109,2364,1874],{"class":126},[109,2366,470],{"class":133},[109,2368,624],{"class":329},[109,2370,342],{"class":122},[109,2372,2171],{"class":126},[109,2374,448],{"class":133},[109,2376,2377,2379,2381,2383],{"class":111,"line":664},[109,2378,2178],{"class":122},[109,2380,2181],{"class":133},[109,2382,589],{"class":122},[109,2384,2186],{"class":126},[109,2386,2387],{"class":111,"line":678},[109,2388,141],{"emptyLinePlaceholder":140},[109,2390,2391],{"class":111,"line":700},[109,2392,141],{"emptyLinePlaceholder":140},[109,2394,2395,2397,2400],{"class":111,"line":711},[109,2396,277],{"class":122},[109,2398,2399],{"class":245}," test_bad_checksum_leaves_no_file",[109,2401,2130],{"class":133},[109,2403,2404,2406,2409,2412,2414,2417],{"class":111,"line":731},[109,2405,554],{"class":122},[109,2407,2408],{"class":133}," pytest.raises(DownloadError, ",[109,2410,2411],{"class":329},"match",[109,2413,342],{"class":122},[109,2415,2416],{"class":307},"\"checksum\"",[109,2418,255],{"class":133},[109,2420,2421,2424,2426,2428,2430,2432],{"class":111,"line":767},[109,2422,2423],{"class":133},"        download(server(), ",[109,2425,2142],{"class":307},[109,2427,2145],{"class":133},[109,2429,2148],{"class":122},[109,2431,2151],{"class":307},[109,2433,348],{"class":133},[109,2435,2436,2439,2441,2444,2446,2448,2450,2452,2454,2456],{"class":111,"line":773},[109,2437,2438],{"class":329},"                 sha256",[109,2440,342],{"class":122},[109,2442,2443],{"class":307},"\"0\"",[109,2445,223],{"class":122},[109,2447,220],{"class":126},[109,2449,470],{"class":133},[109,2451,624],{"class":329},[109,2453,342],{"class":122},[109,2455,2171],{"class":126},[109,2457,448],{"class":133},[109,2459,2460,2462,2465,2468,2470],{"class":111,"line":789},[109,2461,2178],{"class":122},[109,2463,2464],{"class":126}," list",[109,2466,2467],{"class":133},"(tmp_path.iterdir()) ",[109,2469,589],{"class":122},[109,2471,2472],{"class":133}," []\n",[10,2474,2475,2476,2478,2479,2483],{},"The \"without ranges\" test is the one that catches the classic bug of appending a full ",[14,2477,1097],{}," response to a stale partial file. For wider coverage of network edge cases, the scripted-transport pattern from ",[23,2480,2482],{"href":2481},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls\u002F","retries and backoff for CLI HTTP calls"," works here too.",[29,2485,2487],{"id":2486},"conclusion","Conclusion",[10,2489,2490,2491,2493,2494,2496,2497,2499,2500,2502],{},"A dependable download streams in chunks, reports progress only where someone is watching, writes to a ",[14,2492,20],{}," file, verifies size and checksum, and renames into place as its last act. Resume by sending ",[14,2495,1786],{},", trust only a ",[14,2498,2031],{},", and recover from ",[14,2501,2010],{},". Each piece is a few lines; together they turn \"the download failed at 95%\" from a lost afternoon into a single rerun.",[29,2504,2506],{"id":2505},"frequently-asked-questions","Frequently asked questions",[2508,2509,2511],"h3",{"id":2510},"should-i-download-in-parallel-ranges-to-go-faster","Should I download in parallel ranges to go faster?",[10,2513,2514],{},"Splitting a file into several concurrent range requests can help on high-latency links, but it complicates resume and verification and many servers throttle per client anyway. Try a larger chunk size and HTTP\u002F2 first; reach for parallel ranges only after measuring.",[2508,2516,2518],{"id":2517},"how-do-i-download-many-files-at-once-with-one-progress-display","How do I download many files at once with one progress display?",[10,2520,2521,2522,2525,2526,2529,2530,2534],{},"Use one ",[14,2523,2524],{},"Progress"," instance with a task per file, plus an overall task, and run downloads in a small thread pool sharing one ",[14,2527,2528],{},"httpx.Client",". ",[23,2531,2533],{"href":2532},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools\u002F","Parallelising CLI work with thread pools"," covers the pool; Rich's progress is thread-safe.",[2508,2536,2538],{"id":2537},"where-should-downloaded-files-go-by-default","Where should downloaded files go by default?",[10,2540,2541,2542,2545,2546,27],{},"Files the user asked for go to the current directory or ",[14,2543,2544],{},"--out",". Files your tool downloads for its own use — indexes, models, plugins — belong in its cache directory; see ",[23,2547,2549],{"href":2548},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs\u002F","storing app data with platformdirs",[2508,2551,2553],{"id":2552},"what-if-there-is-no-published-checksum","What if there is no published checksum?",[10,2555,2556,2557,2559,2560,2563],{},"Size checking against ",[14,2558,57],{}," still catches truncation. For release artefacts you control, publish a ",[14,2561,2562],{},"SHA256SUMS"," file alongside them — it costs one CI step and gives every downloader a way to verify.",[29,2565,2567],{"id":2566},"related","Related",[34,2569,2570,2576,2581,2586,2591],{},[37,2571,2572,2573],{},"Up: ",[23,2574,2575],{"href":25},"Calling HTTP APIs from Python CLIs",[37,2577,2578],{},[23,2579,2580],{"href":2481},"Retries and backoff for CLI HTTP calls",[37,2582,2583],{},[23,2584,2585],{"href":75},"Writing files atomically in Python CLIs",[37,2587,2588],{},[23,2589,2590],{"href":1753},"Adding progress bars and spinners to Python CLIs",[37,2592,2593],{},[23,2594,2596],{"href":2595},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx\u002F","Building an API client CLI with httpx",[2598,2599,2600],"style",{},"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":105,"searchDepth":119,"depth":119,"links":2602},[2603,2604,2605,2606,2607,2608,2609,2610,2616],{"id":31,"depth":119,"text":32},{"id":68,"depth":119,"text":69},{"id":97,"depth":119,"text":98},{"id":1145,"depth":119,"text":1146},{"id":1720,"depth":119,"text":1721},{"id":1779,"depth":119,"text":1780},{"id":2486,"depth":119,"text":2487},{"id":2505,"depth":119,"text":2506,"children":2611},[2612,2613,2614,2615],{"id":2510,"depth":137,"text":2511},{"id":2517,"depth":137,"text":2518},{"id":2537,"depth":137,"text":2538},{"id":2552,"depth":137,"text":2553},{"id":2566,"depth":119,"text":2567},"2026-09-18","Stream large downloads in a Python CLI with httpx and Rich: progress bars, .part files, checksum verification, atomic rename and resuming with Range requests.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python",{"title":5,"description":2618},"cli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python\u002Findex",[2627,2628,46,2629,42],"http","downloads","progress","9m_Q9EJM-nD_dGteRE95f0gslMHzJrprRAKRKi6SZh8",[2632,2635,2638,2641,2644,2647,2650,2653,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,2788,2791,2794,2797,2800,2801,2803,2806,2809,2812,2815,2818,2821,2824,2827,2830,2833,2836,2839,2842,2845,2848,2851,2854,2857,2860,2863,2866,2869,2872,2875,2878,2881,2884,2887,2890,2893,2896,2899,2902,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],{"path":2633,"title":2634},"\u002Fabout","About Python CLI Toolcraft",{"path":2636,"title":2637},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2639,"title":2640},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2642,"title":2643},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2645,"title":2646},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2648,"title":2649},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2651,"title":2652},"\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":2654,"title":2655},"\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":2657,"title":2658},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2660,"title":2661},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2663,"title":2664},"\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":2666,"title":2667},"\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":2669,"title":2670},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2672,"title":2673},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2675,"title":2676},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2678,"title":2679},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2681,"title":2682},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2684,"title":2685},"\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":2687,"title":2688},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2690,"title":2691},"\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":2693,"title":2694},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2696,"title":2697},"\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":2699,"title":2700},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2702,"title":2703},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2705,"title":2706},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2708,"title":2709},"\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":2711,"title":2712},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2714,"title":2715},"\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":2717,"title":2718},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2720,"title":2721},"\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":2723,"title":2724},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2726,"title":2727},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2729,"title":2730},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2732,"title":2733},"\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":2735,"title":2736},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2738,"title":2739},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2741,"title":2742},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2744,"title":2745},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2747,"title":2748},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2750,"title":2751},"\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":2753,"title":2754},"\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":2756,"title":2757},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2759,"title":2760},"\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":2762,"title":2763},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2765,"title":2766},"\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":2768,"title":2769},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2771,"title":2772},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2774,"title":2775},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2777,"title":2778},"\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":2780,"title":2781},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2783,"title":2784},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2786,"title":2787},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2789,"title":2790},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2792,"title":2793},"\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":2795,"title":2796},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2798,"title":2799},"\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":2623,"title":5},{"path":2802,"title":2575},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis",{"path":2804,"title":2805},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2807,"title":2808},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2810,"title":2811},"\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":2813,"title":2814},"\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":2816,"title":2817},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2819,"title":2820},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2822,"title":2823},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2825,"title":2826},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2828,"title":2829},"\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":2831,"title":2832},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2834,"title":2835},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2837,"title":2838},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2840,"title":2841},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2843,"title":2844},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2846,"title":2847},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2849,"title":2850},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2852,"title":2853},"\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":2855,"title":2856},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2858,"title":2859},"\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":2861,"title":2862},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2864,"title":2865},"\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":2867,"title":2868},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2870,"title":2871},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2873,"title":2874},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2876,"title":2877},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2879,"title":2880},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2882,"title":2883},"\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":2885,"title":2886},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2888,"title":2889},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2891,"title":2892},"\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":2894,"title":2895},"\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":2897,"title":2898},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2900,"title":2901},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2148,"title":2903},"Python CLI Toolcraft",{"path":2905,"title":2906},"\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":2908,"title":2909},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2911,"title":2912},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2914,"title":2915},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2917,"title":2918},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2920,"title":2921},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2923,"title":2924},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2926,"title":2927},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2929,"title":2930},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2932,"title":2933},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2935,"title":2936},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2938,"title":2939},"\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":2941,"title":2942},"\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":2944,"title":2945},"\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":2947,"title":2948},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2950,"title":2951},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2953,"title":2954},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2956,"title":2957},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2959,"title":2960},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2962,"title":2963},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2965,"title":2966},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2968,"title":2969},"\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":2971,"title":2972},"\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":2974,"title":2975},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2977,"title":2978},"\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":2980,"title":2981},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2983,"title":2984},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2986,"title":2987},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2989,"title":2990},"\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":2992,"title":2993},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2995,"title":2996},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2998,"title":2999},"\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":3001,"title":3002},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3004,"title":3005},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3007,"title":3008},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3010,"title":3011},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3013,"title":3014},"\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":3016,"title":3017},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3019,"title":3020},"\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":3022,"title":3023},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3025,"title":3026},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3028,"title":3029},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3031,"title":3032},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3034,"title":3035},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3037,"title":3038},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3040,"title":3041},"\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":3043,"title":3044},"\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":3046,"title":3047},"\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":3049,"title":3050},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3052,"title":3053},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3055,"title":3056},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3058,"title":3059},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3061,"title":3062},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3064,"title":3065},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3067,"title":3068},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3070,"title":3071},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3073,"title":3074},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3076,"title":3077},"\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":3079,"title":3080},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3082,"title":3083},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3085,"title":3086},"\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":3088,"title":3089},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3091,"title":3092},"\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":3094,"title":3095},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3097,"title":3098},"\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":3100,"title":3101},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3103,"title":3104},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3106,"title":3107},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3109,"title":3110},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3112,"title":3113},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3115,"title":3116},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3118,"title":3119},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3121,"title":3122},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3124,"title":3125},"\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":3127,"title":3128},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3130,"title":3131},"\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":3133,"title":3134},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3136,"title":3137},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3139,"title":3140},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"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\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":3151,"title":3152},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3154,"title":3155},"\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":3157,"title":3158},"\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":3160,"title":3161},"\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":3163,"title":3164},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3166,"title":3167},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3169,"title":3170},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3172,"title":3173},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3175,"title":3176},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905048]