[{"data":1,"prerenderedAt":3528},["ShallowReactive",2],{"page-\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files\u002F":3,"content-directory":2684},{"id":4,"title":5,"body":6,"date":2668,"description":2669,"difficulty":2670,"draft":2671,"extension":2672,"meta":2673,"navigation":196,"path":2674,"seo":2675,"stem":2676,"tags":2677,"updated":2668,"__hash__":2683},"content\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files\u002Findex.md","Exporting CLI Results to Files from a Python CLI",{"type":7,"value":8,"toc":2650},"minimark",[9,32,37,59,63,67,151,155,1002,1013,1016,1762,1769,1772,1776,1779,1908,1918,1925,1929,1994,1997,2001,2007,2532,2535,2539,2548,2552,2561,2575,2579,2589,2593,2600,2607,2613,2617,2646],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"\"Just redirect stdout\" is the right answer for most CLI output, and it stops being right in a handful of common situations. On Windows PowerShell 5, ",[14,15,16],"code",{},"mytool report > out.csv"," re-encodes the text as UTF-16, and the CSV opens as garbage. A command that shows a progress bar while exporting cannot also put the data on stdout without mixing the two. A nightly job that writes a report should never leave a half-written file when it is killed. And users simply expect ",[14,19,20],{},"mytool export --to report.xlsx","-style ergonomics from a data tool. This guide adds an ",[14,23,24],{},"--output-file"," option that handles all of these, built on the record-and-renderer layer from the ",[27,28,30],"a",{"href":29},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002F","output formats topic",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,44,51],"ul",{},[41,42,43],"li",{},"Python 3.10+ and Typer or Click.",[41,45,46,47,31],{},"Renderers that turn records into text for each format, as in ",[27,48,50],{"href":49},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv\u002F","adding a format flag",[41,52,53,54,58],{},"The atomic write helper from ",[27,55,57],{"href":56},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis\u002F","writing files atomically in Python CLIs",", or the compact version below.",[33,60,62],{"id":61},"the-rules-an-export-option-should-follow","The rules an export option should follow",[64,65],"inline-diagram",{"name":66},"ofmt-export-rules",[68,69,70,83,111,125,139,145],"ol",{},[41,71,72,79,80,82],{},[73,74,75,78],"strong",{},[14,76,77],{},"-"," means stdout."," Scripts can then pass a variable that is sometimes a path and sometimes ",[14,81,77],{},", with one code path.",[41,84,85,88,89,92,93,96,97,96,100,96,103,106,107,110],{},[73,86,87],{},"The extension picks the format"," when ",[14,90,91],{},"--output\u002F-o"," is not given: ",[14,94,95],{},".csv",", ",[14,98,99],{},".tsv",[14,101,102],{},".json",[14,104,105],{},".jsonl","\u002F",[14,108,109],{},".ndjson",". An explicit format always wins, and an unknown extension without one is an error rather than a guess.",[41,112,113,120,121,124],{},[73,114,115,116,119],{},"A trailing ",[14,117,118],{},".gz"," compresses",", so ",[14,122,123],{},"report.jsonl.gz"," is JSON Lines inside gzip.",[41,126,127,130,131,134,135,31],{},[73,128,129],{},"Existing files are not overwritten silently."," Refuse unless ",[14,132,133],{},"--force"," is given — the same principle as in ",[27,136,138],{"href":137},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands\u002F","adding dry run and confirmation to destructive commands",[41,140,141,144],{},[73,142,143],{},"Writes are atomic."," The file appears complete or not at all.",[41,146,147,150],{},[73,148,149],{},"Feedback goes to stderr."," \"Wrote 1,204 rows to report.csv\" helps the human and never pollutes a pipe.",[33,152,154],{"id":153},"the-recipe","The recipe",[156,157,162],"pre",{"className":158,"code":159,"language":160,"meta":161,"style":161},"language-python shiki shiki-themes github-light github-dark","# src\u002Ffleet\u002Fexport.py\nfrom __future__ import annotations\n\nimport gzip\nimport os\nimport sys\nimport tempfile\nfrom pathlib import Path\n\nFORMAT_BY_SUFFIX = {\".csv\": \"csv\", \".tsv\": \"tsv\", \".json\": \"json\",\n                    \".jsonl\": \"jsonl\", \".ndjson\": \"jsonl\"}\n\n\nclass ExportError(Exception):\n    pass\n\n\ndef infer_format(path: Path, explicit: str | None) -> str:\n    if explicit:\n        return explicit\n    suffixes = [s.lower() for s in path.suffixes]\n    if suffixes and suffixes[-1] == \".gz\":\n        suffixes.pop()\n    fmt = FORMAT_BY_SUFFIX.get(suffixes[-1] if suffixes else \"\")\n    if fmt is None:\n        known = \", \".join(sorted(FORMAT_BY_SUFFIX))\n        raise ExportError(f\"cannot tell the format of {path.name!r}; use -o or one of {known}\")\n    return fmt\n\n\ndef write_output(text: str, target: str, *, force: bool = False,\n                 encoding: str = \"utf-8\") -> Path | None:\n    \"\"\"Write rendered text to stdout ('-') or atomically to a file. Returns the path.\"\"\"\n    if target == \"-\":\n        sys.stdout.write(text)\n        sys.stdout.flush()\n        return None\n    path = Path(target).expanduser()\n    if path.is_dir():\n        raise ExportError(f\"{path} is a directory\")\n    if path.exists() and not force:\n        raise ExportError(f\"{path} already exists; pass --force to overwrite\")\n    path.parent.mkdir(parents=True, exist_ok=True)\n    data = text.encode(encoding)\n    if path.suffix.lower() == \".gz\":\n        data = gzip.compress(data, mtime=0)      # mtime=0 keeps output reproducible\n    fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f\".{path.name}.\", suffix=\".tmp\")\n    try:\n        with os.fdopen(fd, \"wb\") as fh:\n            fh.write(data)\n            fh.flush()\n            os.fsync(fh.fileno())\n        os.replace(tmp, path)\n    except BaseException:\n        Path(tmp).unlink(missing_ok=True)\n        raise\n    return path\n","python","",[14,163,164,173,191,198,207,215,223,231,244,249,294,317,322,327,346,352,357,362,391,400,409,433,463,469,503,518,542,584,593,598,603,639,662,668,683,689,695,703,714,722,745,761,783,809,820,834,859,909,917,938,944,950,956,962,973,988,994],{"__ignoreMap":161},[165,166,169],"span",{"class":167,"line":168},"line",1,[165,170,172],{"class":171},"sJ8bj","# src\u002Ffleet\u002Fexport.py\n",[165,174,176,180,184,187],{"class":167,"line":175},2,[165,177,179],{"class":178},"szBVR","from",[165,181,183],{"class":182},"sj4cs"," __future__",[165,185,186],{"class":178}," import",[165,188,190],{"class":189},"sVt8B"," annotations\n",[165,192,194],{"class":167,"line":193},3,[165,195,197],{"emptyLinePlaceholder":196},true,"\n",[165,199,201,204],{"class":167,"line":200},4,[165,202,203],{"class":178},"import",[165,205,206],{"class":189}," gzip\n",[165,208,210,212],{"class":167,"line":209},5,[165,211,203],{"class":178},[165,213,214],{"class":189}," os\n",[165,216,218,220],{"class":167,"line":217},6,[165,219,203],{"class":178},[165,221,222],{"class":189}," sys\n",[165,224,226,228],{"class":167,"line":225},7,[165,227,203],{"class":178},[165,229,230],{"class":189}," tempfile\n",[165,232,234,236,239,241],{"class":167,"line":233},8,[165,235,179],{"class":178},[165,237,238],{"class":189}," pathlib ",[165,240,203],{"class":178},[165,242,243],{"class":189}," Path\n",[165,245,247],{"class":167,"line":246},9,[165,248,197],{"emptyLinePlaceholder":196},[165,250,252,255,258,261,265,268,271,273,276,278,281,283,286,288,291],{"class":167,"line":251},10,[165,253,254],{"class":182},"FORMAT_BY_SUFFIX",[165,256,257],{"class":178}," =",[165,259,260],{"class":189}," {",[165,262,264],{"class":263},"sZZnC","\".csv\"",[165,266,267],{"class":189},": ",[165,269,270],{"class":263},"\"csv\"",[165,272,96],{"class":189},[165,274,275],{"class":263},"\".tsv\"",[165,277,267],{"class":189},[165,279,280],{"class":263},"\"tsv\"",[165,282,96],{"class":189},[165,284,285],{"class":263},"\".json\"",[165,287,267],{"class":189},[165,289,290],{"class":263},"\"json\"",[165,292,293],{"class":189},",\n",[165,295,297,300,302,305,307,310,312,314],{"class":167,"line":296},11,[165,298,299],{"class":263},"                    \".jsonl\"",[165,301,267],{"class":189},[165,303,304],{"class":263},"\"jsonl\"",[165,306,96],{"class":189},[165,308,309],{"class":263},"\".ndjson\"",[165,311,267],{"class":189},[165,313,304],{"class":263},[165,315,316],{"class":189},"}\n",[165,318,320],{"class":167,"line":319},12,[165,321,197],{"emptyLinePlaceholder":196},[165,323,325],{"class":167,"line":324},13,[165,326,197],{"emptyLinePlaceholder":196},[165,328,330,333,337,340,343],{"class":167,"line":329},14,[165,331,332],{"class":178},"class",[165,334,336],{"class":335},"sScJk"," ExportError",[165,338,339],{"class":189},"(",[165,341,342],{"class":182},"Exception",[165,344,345],{"class":189},"):\n",[165,347,349],{"class":167,"line":348},15,[165,350,351],{"class":178},"    pass\n",[165,353,355],{"class":167,"line":354},16,[165,356,197],{"emptyLinePlaceholder":196},[165,358,360],{"class":167,"line":359},17,[165,361,197],{"emptyLinePlaceholder":196},[165,363,365,368,371,374,377,380,383,386,388],{"class":167,"line":364},18,[165,366,367],{"class":178},"def",[165,369,370],{"class":335}," infer_format",[165,372,373],{"class":189},"(path: Path, explicit: ",[165,375,376],{"class":182},"str",[165,378,379],{"class":178}," |",[165,381,382],{"class":182}," None",[165,384,385],{"class":189},") -> ",[165,387,376],{"class":182},[165,389,390],{"class":189},":\n",[165,392,394,397],{"class":167,"line":393},19,[165,395,396],{"class":178},"    if",[165,398,399],{"class":189}," explicit:\n",[165,401,403,406],{"class":167,"line":402},20,[165,404,405],{"class":178},"        return",[165,407,408],{"class":189}," explicit\n",[165,410,412,415,418,421,424,427,430],{"class":167,"line":411},21,[165,413,414],{"class":189},"    suffixes ",[165,416,417],{"class":178},"=",[165,419,420],{"class":189}," [s.lower() ",[165,422,423],{"class":178},"for",[165,425,426],{"class":189}," s ",[165,428,429],{"class":178},"in",[165,431,432],{"class":189}," path.suffixes]\n",[165,434,436,438,441,444,447,449,452,455,458,461],{"class":167,"line":435},22,[165,437,396],{"class":178},[165,439,440],{"class":189}," suffixes ",[165,442,443],{"class":178},"and",[165,445,446],{"class":189}," suffixes[",[165,448,77],{"class":178},[165,450,451],{"class":182},"1",[165,453,454],{"class":189},"] ",[165,456,457],{"class":178},"==",[165,459,460],{"class":263}," \".gz\"",[165,462,390],{"class":189},[165,464,466],{"class":167,"line":465},23,[165,467,468],{"class":189},"        suffixes.pop()\n",[165,470,472,475,477,480,483,485,487,489,492,494,497,500],{"class":167,"line":471},24,[165,473,474],{"class":189},"    fmt ",[165,476,417],{"class":178},[165,478,479],{"class":182}," FORMAT_BY_SUFFIX",[165,481,482],{"class":189},".get(suffixes[",[165,484,77],{"class":178},[165,486,451],{"class":182},[165,488,454],{"class":189},[165,490,491],{"class":178},"if",[165,493,440],{"class":189},[165,495,496],{"class":178},"else",[165,498,499],{"class":263}," \"\"",[165,501,502],{"class":189},")\n",[165,504,506,508,511,514,516],{"class":167,"line":505},25,[165,507,396],{"class":178},[165,509,510],{"class":189}," fmt ",[165,512,513],{"class":178},"is",[165,515,382],{"class":182},[165,517,390],{"class":189},[165,519,521,524,526,529,532,535,537,539],{"class":167,"line":520},26,[165,522,523],{"class":189},"        known ",[165,525,417],{"class":178},[165,527,528],{"class":263}," \", \"",[165,530,531],{"class":189},".join(",[165,533,534],{"class":182},"sorted",[165,536,339],{"class":189},[165,538,254],{"class":182},[165,540,541],{"class":189},"))\n",[165,543,545,548,551,554,557,560,563,566,569,572,574,577,579,582],{"class":167,"line":544},27,[165,546,547],{"class":178},"        raise",[165,549,550],{"class":189}," ExportError(",[165,552,553],{"class":178},"f",[165,555,556],{"class":263},"\"cannot tell the format of ",[165,558,559],{"class":182},"{",[165,561,562],{"class":189},"path.name",[165,564,565],{"class":178},"!r",[165,567,568],{"class":182},"}",[165,570,571],{"class":263},"; use -o or one of ",[165,573,559],{"class":182},[165,575,576],{"class":189},"known",[165,578,568],{"class":182},[165,580,581],{"class":263},"\"",[165,583,502],{"class":189},[165,585,587,590],{"class":167,"line":586},28,[165,588,589],{"class":178},"    return",[165,591,592],{"class":189}," fmt\n",[165,594,596],{"class":167,"line":595},29,[165,597,197],{"emptyLinePlaceholder":196},[165,599,601],{"class":167,"line":600},30,[165,602,197],{"emptyLinePlaceholder":196},[165,604,606,608,611,614,616,619,621,623,626,629,632,634,637],{"class":167,"line":605},31,[165,607,367],{"class":178},[165,609,610],{"class":335}," write_output",[165,612,613],{"class":189},"(text: ",[165,615,376],{"class":182},[165,617,618],{"class":189},", target: ",[165,620,376],{"class":182},[165,622,96],{"class":189},[165,624,625],{"class":178},"*",[165,627,628],{"class":189},", force: ",[165,630,631],{"class":182},"bool",[165,633,257],{"class":178},[165,635,636],{"class":182}," False",[165,638,293],{"class":189},[165,640,642,645,647,649,652,655,658,660],{"class":167,"line":641},32,[165,643,644],{"class":189},"                 encoding: ",[165,646,376],{"class":182},[165,648,257],{"class":178},[165,650,651],{"class":263}," \"utf-8\"",[165,653,654],{"class":189},") -> Path ",[165,656,657],{"class":178},"|",[165,659,382],{"class":182},[165,661,390],{"class":189},[165,663,665],{"class":167,"line":664},33,[165,666,667],{"class":263},"    \"\"\"Write rendered text to stdout ('-') or atomically to a file. Returns the path.\"\"\"\n",[165,669,671,673,676,678,681],{"class":167,"line":670},34,[165,672,396],{"class":178},[165,674,675],{"class":189}," target ",[165,677,457],{"class":178},[165,679,680],{"class":263}," \"-\"",[165,682,390],{"class":189},[165,684,686],{"class":167,"line":685},35,[165,687,688],{"class":189},"        sys.stdout.write(text)\n",[165,690,692],{"class":167,"line":691},36,[165,693,694],{"class":189},"        sys.stdout.flush()\n",[165,696,698,700],{"class":167,"line":697},37,[165,699,405],{"class":178},[165,701,702],{"class":182}," None\n",[165,704,706,709,711],{"class":167,"line":705},38,[165,707,708],{"class":189},"    path ",[165,710,417],{"class":178},[165,712,713],{"class":189}," Path(target).expanduser()\n",[165,715,717,719],{"class":167,"line":716},39,[165,718,396],{"class":178},[165,720,721],{"class":189}," path.is_dir():\n",[165,723,725,727,729,731,733,735,738,740,743],{"class":167,"line":724},40,[165,726,547],{"class":178},[165,728,550],{"class":189},[165,730,553],{"class":178},[165,732,581],{"class":263},[165,734,559],{"class":182},[165,736,737],{"class":189},"path",[165,739,568],{"class":182},[165,741,742],{"class":263}," is a directory\"",[165,744,502],{"class":189},[165,746,748,750,753,755,758],{"class":167,"line":747},41,[165,749,396],{"class":178},[165,751,752],{"class":189}," path.exists() ",[165,754,443],{"class":178},[165,756,757],{"class":178}," not",[165,759,760],{"class":189}," force:\n",[165,762,764,766,768,770,772,774,776,778,781],{"class":167,"line":763},42,[165,765,547],{"class":178},[165,767,550],{"class":189},[165,769,553],{"class":178},[165,771,581],{"class":263},[165,773,559],{"class":182},[165,775,737],{"class":189},[165,777,568],{"class":182},[165,779,780],{"class":263}," already exists; pass --force to overwrite\"",[165,782,502],{"class":189},[165,784,786,789,793,795,798,800,803,805,807],{"class":167,"line":785},43,[165,787,788],{"class":189},"    path.parent.mkdir(",[165,790,792],{"class":791},"s4XuR","parents",[165,794,417],{"class":178},[165,796,797],{"class":182},"True",[165,799,96],{"class":189},[165,801,802],{"class":791},"exist_ok",[165,804,417],{"class":178},[165,806,797],{"class":182},[165,808,502],{"class":189},[165,810,812,815,817],{"class":167,"line":811},44,[165,813,814],{"class":189},"    data ",[165,816,417],{"class":178},[165,818,819],{"class":189}," text.encode(encoding)\n",[165,821,823,825,828,830,832],{"class":167,"line":822},45,[165,824,396],{"class":178},[165,826,827],{"class":189}," path.suffix.lower() ",[165,829,457],{"class":178},[165,831,460],{"class":263},[165,833,390],{"class":189},[165,835,837,840,842,845,848,850,853,856],{"class":167,"line":836},46,[165,838,839],{"class":189},"        data ",[165,841,417],{"class":178},[165,843,844],{"class":189}," gzip.compress(data, ",[165,846,847],{"class":791},"mtime",[165,849,417],{"class":178},[165,851,852],{"class":182},"0",[165,854,855],{"class":189},")      ",[165,857,858],{"class":171},"# mtime=0 keeps output reproducible\n",[165,860,862,865,867,870,873,875,878,881,883,885,888,890,892,894,897,899,902,904,907],{"class":167,"line":861},47,[165,863,864],{"class":189},"    fd, tmp ",[165,866,417],{"class":178},[165,868,869],{"class":189}," tempfile.mkstemp(",[165,871,872],{"class":791},"dir",[165,874,417],{"class":178},[165,876,877],{"class":189},"path.parent, ",[165,879,880],{"class":791},"prefix",[165,882,417],{"class":178},[165,884,553],{"class":178},[165,886,887],{"class":263},"\".",[165,889,559],{"class":182},[165,891,562],{"class":189},[165,893,568],{"class":182},[165,895,896],{"class":263},".\"",[165,898,96],{"class":189},[165,900,901],{"class":791},"suffix",[165,903,417],{"class":178},[165,905,906],{"class":263},"\".tmp\"",[165,908,502],{"class":189},[165,910,912,915],{"class":167,"line":911},48,[165,913,914],{"class":178},"    try",[165,916,390],{"class":189},[165,918,920,923,926,929,932,935],{"class":167,"line":919},49,[165,921,922],{"class":178},"        with",[165,924,925],{"class":189}," os.fdopen(fd, ",[165,927,928],{"class":263},"\"wb\"",[165,930,931],{"class":189},") ",[165,933,934],{"class":178},"as",[165,936,937],{"class":189}," fh:\n",[165,939,941],{"class":167,"line":940},50,[165,942,943],{"class":189},"            fh.write(data)\n",[165,945,947],{"class":167,"line":946},51,[165,948,949],{"class":189},"            fh.flush()\n",[165,951,953],{"class":167,"line":952},52,[165,954,955],{"class":189},"            os.fsync(fh.fileno())\n",[165,957,959],{"class":167,"line":958},53,[165,960,961],{"class":189},"        os.replace(tmp, path)\n",[165,963,965,968,971],{"class":167,"line":964},54,[165,966,967],{"class":178},"    except",[165,969,970],{"class":182}," BaseException",[165,972,390],{"class":189},[165,974,976,979,982,984,986],{"class":167,"line":975},55,[165,977,978],{"class":189},"        Path(tmp).unlink(",[165,980,981],{"class":791},"missing_ok",[165,983,417],{"class":178},[165,985,797],{"class":182},[165,987,502],{"class":189},[165,989,991],{"class":167,"line":990},56,[165,992,993],{"class":178},"        raise\n",[165,995,997,999],{"class":167,"line":996},57,[165,998,589],{"class":178},[165,1000,1001],{"class":189}," path\n",[10,1003,1004,1005,1008,1009,31],{},"Writing bytes rather than text sidesteps two traps at once: there is no newline translation on Windows, and the encoding is explicit. Passing ",[14,1006,1007],{},"encoding=\"utf-8-sig\""," for an Excel-bound CSV is then a one-word change, as discussed in ",[27,1010,1012],{"href":1011},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly\u002F","writing CSV and TSV output correctly",[10,1014,1015],{},"The command layer resolves the format, renders, writes, and reports:",[156,1017,1019],{"className":158,"code":1018,"language":160,"meta":161,"style":161},"# src\u002Ffleet\u002Fcli.py\nimport json\nfrom pathlib import Path\nfrom typing import Annotated, Optional\n\nimport typer\n\nfrom fleet.export import ExportError, infer_format, write_output\n\napp = typer.Typer()\nROWS = [{\"name\": \"web-1\", \"cpu\": 0.42}, {\"name\": \"web-2\", \"cpu\": 0.91}]\n\n\ndef render(rows, fmt: str) -> str:\n    if fmt == \"json\":\n        return json.dumps(rows, indent=2) + \"\\n\"\n    if fmt == \"jsonl\":\n        return \"\".join(json.dumps(r) + \"\\n\" for r in rows)\n    sep = \",\" if fmt == \"csv\" else \"\\t\"\n    cols = list(rows[0]) if rows else []\n    return \"\\n\".join([sep.join(cols)] + [sep.join(str(r[c]) for c in cols) for r in rows]) + \"\\n\"\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Manage the fleet.\"\"\"\n\n\n@app.command()\ndef export(\n    output_file: Annotated[str, typer.Option(\"--output-file\", \"-O\",\n        help=\"Where to write: a path, or '-' for stdout.\")] = \"-\",\n    output: Annotated[Optional[str], typer.Option(\"--output\", \"-o\",\n        help=\"Format; inferred from the file extension when omitted.\")] = None,\n    force: Annotated[bool, typer.Option(\"--force\", help=\"Overwrite an existing file.\")] = False,\n) -> None:\n    \"\"\"Export servers.\"\"\"\n    try:\n        fmt = output or (\"jsonl\" if output_file == \"-\" else None)\n        fmt = infer_format(Path(output_file), fmt)\n        path = write_output(render(ROWS, fmt), output_file, force=force)\n    except ExportError as exc:\n        typer.echo(f\"error: {exc}\", err=True)\n        raise typer.Exit(1)\n    if path is not None:\n        typer.echo(f\"wrote {len(ROWS)} rows to {path} ({fmt})\", err=True)\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1020,1021,1026,1033,1043,1055,1059,1066,1070,1082,1086,1096,1146,1150,1154,1172,1185,1214,1227,1255,1285,1313,1363,1367,1371,1379,1394,1399,1403,1407,1414,1424,1444,1463,1483,1500,1530,1538,1543,1549,1582,1591,1614,1626,1656,1667,1682,1734,1738,1742,1757],{"__ignoreMap":161},[165,1022,1023],{"class":167,"line":168},[165,1024,1025],{"class":171},"# src\u002Ffleet\u002Fcli.py\n",[165,1027,1028,1030],{"class":167,"line":175},[165,1029,203],{"class":178},[165,1031,1032],{"class":189}," json\n",[165,1034,1035,1037,1039,1041],{"class":167,"line":193},[165,1036,179],{"class":178},[165,1038,238],{"class":189},[165,1040,203],{"class":178},[165,1042,243],{"class":189},[165,1044,1045,1047,1050,1052],{"class":167,"line":200},[165,1046,179],{"class":178},[165,1048,1049],{"class":189}," typing ",[165,1051,203],{"class":178},[165,1053,1054],{"class":189}," Annotated, Optional\n",[165,1056,1057],{"class":167,"line":209},[165,1058,197],{"emptyLinePlaceholder":196},[165,1060,1061,1063],{"class":167,"line":217},[165,1062,203],{"class":178},[165,1064,1065],{"class":189}," typer\n",[165,1067,1068],{"class":167,"line":225},[165,1069,197],{"emptyLinePlaceholder":196},[165,1071,1072,1074,1077,1079],{"class":167,"line":233},[165,1073,179],{"class":178},[165,1075,1076],{"class":189}," fleet.export ",[165,1078,203],{"class":178},[165,1080,1081],{"class":189}," ExportError, infer_format, write_output\n",[165,1083,1084],{"class":167,"line":246},[165,1085,197],{"emptyLinePlaceholder":196},[165,1087,1088,1091,1093],{"class":167,"line":251},[165,1089,1090],{"class":189},"app ",[165,1092,417],{"class":178},[165,1094,1095],{"class":189}," typer.Typer()\n",[165,1097,1098,1101,1103,1106,1109,1111,1114,1116,1119,1121,1124,1127,1129,1131,1134,1136,1138,1140,1143],{"class":167,"line":296},[165,1099,1100],{"class":182},"ROWS",[165,1102,257],{"class":178},[165,1104,1105],{"class":189}," [{",[165,1107,1108],{"class":263},"\"name\"",[165,1110,267],{"class":189},[165,1112,1113],{"class":263},"\"web-1\"",[165,1115,96],{"class":189},[165,1117,1118],{"class":263},"\"cpu\"",[165,1120,267],{"class":189},[165,1122,1123],{"class":182},"0.42",[165,1125,1126],{"class":189},"}, {",[165,1128,1108],{"class":263},[165,1130,267],{"class":189},[165,1132,1133],{"class":263},"\"web-2\"",[165,1135,96],{"class":189},[165,1137,1118],{"class":263},[165,1139,267],{"class":189},[165,1141,1142],{"class":182},"0.91",[165,1144,1145],{"class":189},"}]\n",[165,1147,1148],{"class":167,"line":319},[165,1149,197],{"emptyLinePlaceholder":196},[165,1151,1152],{"class":167,"line":324},[165,1153,197],{"emptyLinePlaceholder":196},[165,1155,1156,1158,1161,1164,1166,1168,1170],{"class":167,"line":329},[165,1157,367],{"class":178},[165,1159,1160],{"class":335}," render",[165,1162,1163],{"class":189},"(rows, fmt: ",[165,1165,376],{"class":182},[165,1167,385],{"class":189},[165,1169,376],{"class":182},[165,1171,390],{"class":189},[165,1173,1174,1176,1178,1180,1183],{"class":167,"line":348},[165,1175,396],{"class":178},[165,1177,510],{"class":189},[165,1179,457],{"class":178},[165,1181,1182],{"class":263}," \"json\"",[165,1184,390],{"class":189},[165,1186,1187,1189,1192,1195,1197,1200,1202,1205,1208,1211],{"class":167,"line":354},[165,1188,405],{"class":178},[165,1190,1191],{"class":189}," json.dumps(rows, ",[165,1193,1194],{"class":791},"indent",[165,1196,417],{"class":178},[165,1198,1199],{"class":182},"2",[165,1201,931],{"class":189},[165,1203,1204],{"class":178},"+",[165,1206,1207],{"class":263}," \"",[165,1209,1210],{"class":182},"\\n",[165,1212,1213],{"class":263},"\"\n",[165,1215,1216,1218,1220,1222,1225],{"class":167,"line":359},[165,1217,396],{"class":178},[165,1219,510],{"class":189},[165,1221,457],{"class":178},[165,1223,1224],{"class":263}," \"jsonl\"",[165,1226,390],{"class":189},[165,1228,1229,1231,1233,1236,1238,1240,1242,1244,1247,1250,1252],{"class":167,"line":364},[165,1230,405],{"class":178},[165,1232,499],{"class":263},[165,1234,1235],{"class":189},".join(json.dumps(r) ",[165,1237,1204],{"class":178},[165,1239,1207],{"class":263},[165,1241,1210],{"class":182},[165,1243,581],{"class":263},[165,1245,1246],{"class":178}," for",[165,1248,1249],{"class":189}," r ",[165,1251,429],{"class":178},[165,1253,1254],{"class":189}," rows)\n",[165,1256,1257,1260,1262,1265,1268,1270,1272,1275,1278,1280,1283],{"class":167,"line":393},[165,1258,1259],{"class":189},"    sep ",[165,1261,417],{"class":178},[165,1263,1264],{"class":263}," \",\"",[165,1266,1267],{"class":178}," if",[165,1269,510],{"class":189},[165,1271,457],{"class":178},[165,1273,1274],{"class":263}," \"csv\"",[165,1276,1277],{"class":178}," else",[165,1279,1207],{"class":263},[165,1281,1282],{"class":182},"\\t",[165,1284,1213],{"class":263},[165,1286,1287,1290,1292,1295,1298,1300,1303,1305,1308,1310],{"class":167,"line":402},[165,1288,1289],{"class":189},"    cols ",[165,1291,417],{"class":178},[165,1293,1294],{"class":182}," list",[165,1296,1297],{"class":189},"(rows[",[165,1299,852],{"class":182},[165,1301,1302],{"class":189},"]) ",[165,1304,491],{"class":178},[165,1306,1307],{"class":189}," rows ",[165,1309,496],{"class":178},[165,1311,1312],{"class":189}," []\n",[165,1314,1315,1317,1319,1321,1323,1326,1328,1331,1333,1336,1338,1341,1343,1346,1348,1350,1352,1355,1357,1359,1361],{"class":167,"line":411},[165,1316,589],{"class":178},[165,1318,1207],{"class":263},[165,1320,1210],{"class":182},[165,1322,581],{"class":263},[165,1324,1325],{"class":189},".join([sep.join(cols)] ",[165,1327,1204],{"class":178},[165,1329,1330],{"class":189}," [sep.join(",[165,1332,376],{"class":182},[165,1334,1335],{"class":189},"(r[c]) ",[165,1337,423],{"class":178},[165,1339,1340],{"class":189}," c ",[165,1342,429],{"class":178},[165,1344,1345],{"class":189}," cols) ",[165,1347,423],{"class":178},[165,1349,1249],{"class":189},[165,1351,429],{"class":178},[165,1353,1354],{"class":189}," rows]) ",[165,1356,1204],{"class":178},[165,1358,1207],{"class":263},[165,1360,1210],{"class":182},[165,1362,1213],{"class":263},[165,1364,1365],{"class":167,"line":435},[165,1366,197],{"emptyLinePlaceholder":196},[165,1368,1369],{"class":167,"line":465},[165,1370,197],{"emptyLinePlaceholder":196},[165,1372,1373,1376],{"class":167,"line":471},[165,1374,1375],{"class":335},"@app.callback",[165,1377,1378],{"class":189},"()\n",[165,1380,1381,1383,1386,1389,1392],{"class":167,"line":505},[165,1382,367],{"class":178},[165,1384,1385],{"class":335}," main",[165,1387,1388],{"class":189},"() -> ",[165,1390,1391],{"class":182},"None",[165,1393,390],{"class":189},[165,1395,1396],{"class":167,"line":520},[165,1397,1398],{"class":263},"    \"\"\"Manage the fleet.\"\"\"\n",[165,1400,1401],{"class":167,"line":544},[165,1402,197],{"emptyLinePlaceholder":196},[165,1404,1405],{"class":167,"line":586},[165,1406,197],{"emptyLinePlaceholder":196},[165,1408,1409,1412],{"class":167,"line":595},[165,1410,1411],{"class":335},"@app.command",[165,1413,1378],{"class":189},[165,1415,1416,1418,1421],{"class":167,"line":600},[165,1417,367],{"class":178},[165,1419,1420],{"class":335}," export",[165,1422,1423],{"class":189},"(\n",[165,1425,1426,1429,1431,1434,1437,1439,1442],{"class":167,"line":605},[165,1427,1428],{"class":189},"    output_file: Annotated[",[165,1430,376],{"class":182},[165,1432,1433],{"class":189},", typer.Option(",[165,1435,1436],{"class":263},"\"--output-file\"",[165,1438,96],{"class":189},[165,1440,1441],{"class":263},"\"-O\"",[165,1443,293],{"class":189},[165,1445,1446,1449,1451,1454,1457,1459,1461],{"class":167,"line":641},[165,1447,1448],{"class":791},"        help",[165,1450,417],{"class":178},[165,1452,1453],{"class":263},"\"Where to write: a path, or '-' for stdout.\"",[165,1455,1456],{"class":189},")] ",[165,1458,417],{"class":178},[165,1460,680],{"class":263},[165,1462,293],{"class":189},[165,1464,1465,1468,1470,1473,1476,1478,1481],{"class":167,"line":664},[165,1466,1467],{"class":189},"    output: Annotated[Optional[",[165,1469,376],{"class":182},[165,1471,1472],{"class":189},"], typer.Option(",[165,1474,1475],{"class":263},"\"--output\"",[165,1477,96],{"class":189},[165,1479,1480],{"class":263},"\"-o\"",[165,1482,293],{"class":189},[165,1484,1485,1487,1489,1492,1494,1496,1498],{"class":167,"line":670},[165,1486,1448],{"class":791},[165,1488,417],{"class":178},[165,1490,1491],{"class":263},"\"Format; inferred from the file extension when omitted.\"",[165,1493,1456],{"class":189},[165,1495,417],{"class":178},[165,1497,382],{"class":182},[165,1499,293],{"class":189},[165,1501,1502,1505,1507,1509,1512,1514,1517,1519,1522,1524,1526,1528],{"class":167,"line":685},[165,1503,1504],{"class":189},"    force: Annotated[",[165,1506,631],{"class":182},[165,1508,1433],{"class":189},[165,1510,1511],{"class":263},"\"--force\"",[165,1513,96],{"class":189},[165,1515,1516],{"class":791},"help",[165,1518,417],{"class":178},[165,1520,1521],{"class":263},"\"Overwrite an existing file.\"",[165,1523,1456],{"class":189},[165,1525,417],{"class":178},[165,1527,636],{"class":182},[165,1529,293],{"class":189},[165,1531,1532,1534,1536],{"class":167,"line":691},[165,1533,385],{"class":189},[165,1535,1391],{"class":182},[165,1537,390],{"class":189},[165,1539,1540],{"class":167,"line":697},[165,1541,1542],{"class":263},"    \"\"\"Export servers.\"\"\"\n",[165,1544,1545,1547],{"class":167,"line":705},[165,1546,914],{"class":178},[165,1548,390],{"class":189},[165,1550,1551,1554,1556,1559,1562,1565,1567,1569,1572,1574,1576,1578,1580],{"class":167,"line":716},[165,1552,1553],{"class":189},"        fmt ",[165,1555,417],{"class":178},[165,1557,1558],{"class":189}," output ",[165,1560,1561],{"class":178},"or",[165,1563,1564],{"class":189}," (",[165,1566,304],{"class":263},[165,1568,1267],{"class":178},[165,1570,1571],{"class":189}," output_file ",[165,1573,457],{"class":178},[165,1575,680],{"class":263},[165,1577,1277],{"class":178},[165,1579,382],{"class":182},[165,1581,502],{"class":189},[165,1583,1584,1586,1588],{"class":167,"line":724},[165,1585,1553],{"class":189},[165,1587,417],{"class":178},[165,1589,1590],{"class":189}," infer_format(Path(output_file), fmt)\n",[165,1592,1593,1596,1598,1601,1603,1606,1609,1611],{"class":167,"line":747},[165,1594,1595],{"class":189},"        path ",[165,1597,417],{"class":178},[165,1599,1600],{"class":189}," write_output(render(",[165,1602,1100],{"class":182},[165,1604,1605],{"class":189},", fmt), output_file, ",[165,1607,1608],{"class":791},"force",[165,1610,417],{"class":178},[165,1612,1613],{"class":189},"force)\n",[165,1615,1616,1618,1621,1623],{"class":167,"line":763},[165,1617,967],{"class":178},[165,1619,1620],{"class":189}," ExportError ",[165,1622,934],{"class":178},[165,1624,1625],{"class":189}," exc:\n",[165,1627,1628,1631,1633,1636,1638,1641,1643,1645,1647,1650,1652,1654],{"class":167,"line":785},[165,1629,1630],{"class":189},"        typer.echo(",[165,1632,553],{"class":178},[165,1634,1635],{"class":263},"\"error: ",[165,1637,559],{"class":182},[165,1639,1640],{"class":189},"exc",[165,1642,568],{"class":182},[165,1644,581],{"class":263},[165,1646,96],{"class":189},[165,1648,1649],{"class":791},"err",[165,1651,417],{"class":178},[165,1653,797],{"class":182},[165,1655,502],{"class":189},[165,1657,1658,1660,1663,1665],{"class":167,"line":811},[165,1659,547],{"class":178},[165,1661,1662],{"class":189}," typer.Exit(",[165,1664,451],{"class":182},[165,1666,502],{"class":189},[165,1668,1669,1671,1674,1676,1678,1680],{"class":167,"line":822},[165,1670,396],{"class":178},[165,1672,1673],{"class":189}," path ",[165,1675,513],{"class":178},[165,1677,757],{"class":178},[165,1679,382],{"class":182},[165,1681,390],{"class":189},[165,1683,1684,1686,1688,1691,1694,1696,1698,1701,1703,1706,1708,1710,1712,1714,1716,1719,1721,1724,1726,1728,1730,1732],{"class":167,"line":836},[165,1685,1630],{"class":189},[165,1687,553],{"class":178},[165,1689,1690],{"class":263},"\"wrote ",[165,1692,1693],{"class":182},"{len",[165,1695,339],{"class":189},[165,1697,1100],{"class":182},[165,1699,1700],{"class":189},")",[165,1702,568],{"class":182},[165,1704,1705],{"class":263}," rows to ",[165,1707,559],{"class":182},[165,1709,737],{"class":189},[165,1711,568],{"class":182},[165,1713,1564],{"class":263},[165,1715,559],{"class":182},[165,1717,1718],{"class":189},"fmt",[165,1720,568],{"class":182},[165,1722,1723],{"class":263},")\"",[165,1725,96],{"class":189},[165,1727,1649],{"class":791},[165,1729,417],{"class":178},[165,1731,797],{"class":182},[165,1733,502],{"class":189},[165,1735,1736],{"class":167,"line":861},[165,1737,197],{"emptyLinePlaceholder":196},[165,1739,1740],{"class":167,"line":911},[165,1741,197],{"emptyLinePlaceholder":196},[165,1743,1744,1746,1749,1752,1755],{"class":167,"line":919},[165,1745,491],{"class":178},[165,1747,1748],{"class":182}," __name__",[165,1750,1751],{"class":178}," ==",[165,1753,1754],{"class":263}," \"__main__\"",[165,1756,390],{"class":189},[165,1758,1759],{"class":167,"line":940},[165,1760,1761],{"class":189},"    app()\n",[10,1763,1764,1765,1768],{},"(The tiny ",[14,1766,1767],{},"render"," stands in for the shared renderers; use the real ones in your tool.)",[64,1770],{"name":1771},"ofmt-export-terminal",[33,1773,1775],{"id":1774},"default-file-names-and-timestamps","Default file names and timestamps",[10,1777,1778],{},"Scheduled exports usually want a fresh, sortable file name on every run. Rather than making every cron job assemble one in shell, accept a small set of placeholders in the path and expand them before writing:",[156,1780,1782],{"className":158,"code":1781,"language":160,"meta":161,"style":161},"from datetime import datetime, timezone\n\n\ndef expand_target(target: str, *, now: datetime | None = None) -> str:\n    \"\"\"Expand {date} and {time} placeholders in an output path.\"\"\"\n    now = now or datetime.now(timezone.utc)\n    return target.format(date=now.strftime(\"%Y-%m-%d\"), time=now.strftime(\"%H%M%S\"))\n\n\n# fleet export -O 'exports\u002Fservers-{date}.csv'  ->  exports\u002Fservers-2026-10-02.csv\n",[14,1783,1784,1796,1800,1804,1837,1842,1857,1895,1899,1903],{"__ignoreMap":161},[165,1785,1786,1788,1791,1793],{"class":167,"line":168},[165,1787,179],{"class":178},[165,1789,1790],{"class":189}," datetime ",[165,1792,203],{"class":178},[165,1794,1795],{"class":189}," datetime, timezone\n",[165,1797,1798],{"class":167,"line":175},[165,1799,197],{"emptyLinePlaceholder":196},[165,1801,1802],{"class":167,"line":193},[165,1803,197],{"emptyLinePlaceholder":196},[165,1805,1806,1808,1811,1814,1816,1818,1820,1823,1825,1827,1829,1831,1833,1835],{"class":167,"line":200},[165,1807,367],{"class":178},[165,1809,1810],{"class":335}," expand_target",[165,1812,1813],{"class":189},"(target: ",[165,1815,376],{"class":182},[165,1817,96],{"class":189},[165,1819,625],{"class":178},[165,1821,1822],{"class":189},", now: datetime ",[165,1824,657],{"class":178},[165,1826,382],{"class":182},[165,1828,257],{"class":178},[165,1830,382],{"class":182},[165,1832,385],{"class":189},[165,1834,376],{"class":182},[165,1836,390],{"class":189},[165,1838,1839],{"class":167,"line":209},[165,1840,1841],{"class":263},"    \"\"\"Expand {date} and {time} placeholders in an output path.\"\"\"\n",[165,1843,1844,1847,1849,1852,1854],{"class":167,"line":217},[165,1845,1846],{"class":189},"    now ",[165,1848,417],{"class":178},[165,1850,1851],{"class":189}," now ",[165,1853,1561],{"class":178},[165,1855,1856],{"class":189}," datetime.now(timezone.utc)\n",[165,1858,1859,1861,1864,1867,1869,1872,1875,1878,1880,1883,1886,1888,1890,1893],{"class":167,"line":225},[165,1860,589],{"class":178},[165,1862,1863],{"class":189}," target.format(",[165,1865,1866],{"class":791},"date",[165,1868,417],{"class":178},[165,1870,1871],{"class":189},"now.strftime(",[165,1873,1874],{"class":263},"\"%Y-%m-",[165,1876,1877],{"class":182},"%d",[165,1879,581],{"class":263},[165,1881,1882],{"class":189},"), ",[165,1884,1885],{"class":791},"time",[165,1887,417],{"class":178},[165,1889,1871],{"class":189},[165,1891,1892],{"class":263},"\"%H%M%S\"",[165,1894,541],{"class":189},[165,1896,1897],{"class":167,"line":233},[165,1898,197],{"emptyLinePlaceholder":196},[165,1900,1901],{"class":167,"line":246},[165,1902,197],{"emptyLinePlaceholder":196},[165,1904,1905],{"class":167,"line":251},[165,1906,1907],{"class":171},"# fleet export -O 'exports\u002Fservers-{date}.csv'  ->  exports\u002Fservers-2026-10-02.csv\n",[10,1909,1910,1911,1914,1915,1917],{},"ISO dates sort correctly as plain strings, so ",[14,1912,1913],{},"ls exports\u002F"," lists runs in order, and UTC avoids two runs an hour apart producing the same name on the night clocks change. Keep the placeholder set tiny and documented; it is a convenience, not a template language. If two runs could share a name, combine the date with the time, or let ",[14,1916,133],{}," decide whether the second run replaces the first.",[10,1919,1920,1921,1924],{},"For exports that run unattended, also consider what happens to old files. A ",[14,1922,1923],{},"--keep N"," option that deletes all but the newest N matching files is simple to add and saves someone writing a cleanup script — but make it opt-in, and only ever delete files that match the pattern your tool created.",[33,1926,1928],{"id":1927},"ux-considerations","UX considerations",[38,1930,1931,1951,1957,1970,1980],{},[41,1932,1933,1936,1937,1940,1941,106,1944,1946,1947,1950],{},[73,1934,1935],{},"Pick an unambiguous name."," Many tools use ",[14,1938,1939],{},"--output"," for the format and ",[14,1942,1943],{},"-O",[14,1945,24],{}," for the path; curl uses ",[14,1948,1949],{},"-o"," for the path. Whatever you choose, never let one flag mean both things in different commands.",[41,1952,1953,1956],{},[73,1954,1955],{},"Confirm writes on stderr with a count and a path."," It is the only feedback a user gets, and the absolute path removes doubt about which directory it landed in.",[41,1958,1959,1962,1963,1965,1966,31],{},[73,1960,1961],{},"Show progress only for real files."," When the target is ",[14,1964,77],{},", stdout is the data stream; keep progress bars on stderr and switch them off when stderr is not a terminal, as in ",[27,1967,1969],{"href":1968},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis\u002F","adding progress bars and spinners",[41,1971,1972,1975,1976,1979],{},[73,1973,1974],{},"Do not create surprise directories deep in the tree."," Creating a missing parent is convenient; creating ",[14,1977,1978],{},"~\u002Freprots\u002F2026\u002F"," because of a typo is not. Some tools only create one level, or ask first in interactive mode.",[41,1981,1982,1985,1986,1989,1990,1993],{},[73,1983,1984],{},"Respect permissions."," A file that may contain sensitive data should be created with ",[14,1987,1988],{},"0o600"," — ",[14,1991,1992],{},"tempfile.mkstemp"," already does that, and the rename keeps it.",[64,1995],{"name":1996},"ofmt-export-flag-names",[33,1998,2000],{"id":1999},"testing-the-behaviour","Testing the behaviour",[10,2002,2003,2006],{},[14,2004,2005],{},"tmp_path"," makes file exports easy to test. Cover inference, refusal, force, compression, and the stdout path:",[156,2008,2010],{"className":158,"code":2009,"language":160,"meta":161,"style":161},"# tests\u002Ftest_export.py\nimport gzip\nimport json\n\nimport pytest\nfrom typer.testing import CliRunner\n\nfrom fleet.cli import app\nfrom fleet.export import ExportError, infer_format\n\nrunner = CliRunner()\n\n\n@pytest.mark.parametrize(\"name,fmt\", [(\"a.csv\", \"csv\"), (\"a.JSONL\", \"jsonl\"),\n                                      (\"a.ndjson.gz\", \"jsonl\"), (\"a.tar.json\", \"json\")])\ndef test_infer_format(tmp_path, name, fmt):\n    assert infer_format(tmp_path \u002F name, None) == fmt\n\n\ndef test_unknown_extension_needs_explicit_format(tmp_path):\n    with pytest.raises(ExportError, match=\"cannot tell the format\"):\n        infer_format(tmp_path \u002F \"report.txt\", None)\n\n\ndef test_refuses_to_overwrite_without_force(tmp_path):\n    target = tmp_path \u002F \"out.json\"\n    target.write_text(\"keep me\")\n    result = runner.invoke(app, [\"export\", \"-O\", str(target)])\n    assert result.exit_code == 1\n    assert target.read_text() == \"keep me\"\n    assert runner.invoke(app, [\"export\", \"-O\", str(target), \"--force\"]).exit_code == 0\n    assert json.loads(target.read_text())[0][\"name\"] == \"web-1\"\n\n\ndef test_gzip_and_message_on_stderr(tmp_path):\n    target = tmp_path \u002F \"out.jsonl.gz\"\n    result = runner.invoke(app, [\"export\", \"-O\", str(target)])\n    assert result.stdout == \"\"\n    assert \"wrote 2 rows\" in result.stderr\n    assert gzip.decompress(target.read_bytes()).decode().count(\"\\n\") == 2\n\n\ndef test_dash_means_stdout():\n    result = runner.invoke(app, [\"export\", \"-O\", \"-\"])\n    assert result.stdout.splitlines()[0] == '{\"name\": \"web-1\", \"cpu\": 0.42}'\n",[14,2011,2012,2017,2023,2029,2033,2040,2052,2056,2068,2079,2083,2093,2097,2101,2134,2158,2168,2189,2193,2197,2207,2225,2241,2245,2249,2258,2273,2283,2307,2319,2331,2360,2381,2385,2389,2398,2411,2431,2443,2456,2476,2480,2484,2494,2516],{"__ignoreMap":161},[165,2013,2014],{"class":167,"line":168},[165,2015,2016],{"class":171},"# tests\u002Ftest_export.py\n",[165,2018,2019,2021],{"class":167,"line":175},[165,2020,203],{"class":178},[165,2022,206],{"class":189},[165,2024,2025,2027],{"class":167,"line":193},[165,2026,203],{"class":178},[165,2028,1032],{"class":189},[165,2030,2031],{"class":167,"line":200},[165,2032,197],{"emptyLinePlaceholder":196},[165,2034,2035,2037],{"class":167,"line":209},[165,2036,203],{"class":178},[165,2038,2039],{"class":189}," pytest\n",[165,2041,2042,2044,2047,2049],{"class":167,"line":217},[165,2043,179],{"class":178},[165,2045,2046],{"class":189}," typer.testing ",[165,2048,203],{"class":178},[165,2050,2051],{"class":189}," CliRunner\n",[165,2053,2054],{"class":167,"line":225},[165,2055,197],{"emptyLinePlaceholder":196},[165,2057,2058,2060,2063,2065],{"class":167,"line":233},[165,2059,179],{"class":178},[165,2061,2062],{"class":189}," fleet.cli ",[165,2064,203],{"class":178},[165,2066,2067],{"class":189}," app\n",[165,2069,2070,2072,2074,2076],{"class":167,"line":246},[165,2071,179],{"class":178},[165,2073,1076],{"class":189},[165,2075,203],{"class":178},[165,2077,2078],{"class":189}," ExportError, infer_format\n",[165,2080,2081],{"class":167,"line":251},[165,2082,197],{"emptyLinePlaceholder":196},[165,2084,2085,2088,2090],{"class":167,"line":296},[165,2086,2087],{"class":189},"runner ",[165,2089,417],{"class":178},[165,2091,2092],{"class":189}," CliRunner()\n",[165,2094,2095],{"class":167,"line":319},[165,2096,197],{"emptyLinePlaceholder":196},[165,2098,2099],{"class":167,"line":324},[165,2100,197],{"emptyLinePlaceholder":196},[165,2102,2103,2106,2108,2111,2114,2117,2119,2121,2124,2127,2129,2131],{"class":167,"line":329},[165,2104,2105],{"class":335},"@pytest.mark.parametrize",[165,2107,339],{"class":189},[165,2109,2110],{"class":263},"\"name,fmt\"",[165,2112,2113],{"class":189},", [(",[165,2115,2116],{"class":263},"\"a.csv\"",[165,2118,96],{"class":189},[165,2120,270],{"class":263},[165,2122,2123],{"class":189},"), (",[165,2125,2126],{"class":263},"\"a.JSONL\"",[165,2128,96],{"class":189},[165,2130,304],{"class":263},[165,2132,2133],{"class":189},"),\n",[165,2135,2136,2139,2142,2144,2146,2148,2151,2153,2155],{"class":167,"line":348},[165,2137,2138],{"class":189},"                                      (",[165,2140,2141],{"class":263},"\"a.ndjson.gz\"",[165,2143,96],{"class":189},[165,2145,304],{"class":263},[165,2147,2123],{"class":189},[165,2149,2150],{"class":263},"\"a.tar.json\"",[165,2152,96],{"class":189},[165,2154,290],{"class":263},[165,2156,2157],{"class":189},")])\n",[165,2159,2160,2162,2165],{"class":167,"line":354},[165,2161,367],{"class":178},[165,2163,2164],{"class":335}," test_infer_format",[165,2166,2167],{"class":189},"(tmp_path, name, fmt):\n",[165,2169,2170,2173,2176,2178,2181,2183,2185,2187],{"class":167,"line":359},[165,2171,2172],{"class":178},"    assert",[165,2174,2175],{"class":189}," infer_format(tmp_path ",[165,2177,106],{"class":178},[165,2179,2180],{"class":189}," name, ",[165,2182,1391],{"class":182},[165,2184,931],{"class":189},[165,2186,457],{"class":178},[165,2188,592],{"class":189},[165,2190,2191],{"class":167,"line":364},[165,2192,197],{"emptyLinePlaceholder":196},[165,2194,2195],{"class":167,"line":393},[165,2196,197],{"emptyLinePlaceholder":196},[165,2198,2199,2201,2204],{"class":167,"line":402},[165,2200,367],{"class":178},[165,2202,2203],{"class":335}," test_unknown_extension_needs_explicit_format",[165,2205,2206],{"class":189},"(tmp_path):\n",[165,2208,2209,2212,2215,2218,2220,2223],{"class":167,"line":411},[165,2210,2211],{"class":178},"    with",[165,2213,2214],{"class":189}," pytest.raises(ExportError, ",[165,2216,2217],{"class":791},"match",[165,2219,417],{"class":178},[165,2221,2222],{"class":263},"\"cannot tell the format\"",[165,2224,345],{"class":189},[165,2226,2227,2230,2232,2235,2237,2239],{"class":167,"line":435},[165,2228,2229],{"class":189},"        infer_format(tmp_path ",[165,2231,106],{"class":178},[165,2233,2234],{"class":263}," \"report.txt\"",[165,2236,96],{"class":189},[165,2238,1391],{"class":182},[165,2240,502],{"class":189},[165,2242,2243],{"class":167,"line":465},[165,2244,197],{"emptyLinePlaceholder":196},[165,2246,2247],{"class":167,"line":471},[165,2248,197],{"emptyLinePlaceholder":196},[165,2250,2251,2253,2256],{"class":167,"line":505},[165,2252,367],{"class":178},[165,2254,2255],{"class":335}," test_refuses_to_overwrite_without_force",[165,2257,2206],{"class":189},[165,2259,2260,2263,2265,2268,2270],{"class":167,"line":520},[165,2261,2262],{"class":189},"    target ",[165,2264,417],{"class":178},[165,2266,2267],{"class":189}," tmp_path ",[165,2269,106],{"class":178},[165,2271,2272],{"class":263}," \"out.json\"\n",[165,2274,2275,2278,2281],{"class":167,"line":544},[165,2276,2277],{"class":189},"    target.write_text(",[165,2279,2280],{"class":263},"\"keep me\"",[165,2282,502],{"class":189},[165,2284,2285,2288,2290,2293,2296,2298,2300,2302,2304],{"class":167,"line":586},[165,2286,2287],{"class":189},"    result ",[165,2289,417],{"class":178},[165,2291,2292],{"class":189}," runner.invoke(app, [",[165,2294,2295],{"class":263},"\"export\"",[165,2297,96],{"class":189},[165,2299,1441],{"class":263},[165,2301,96],{"class":189},[165,2303,376],{"class":182},[165,2305,2306],{"class":189},"(target)])\n",[165,2308,2309,2311,2314,2316],{"class":167,"line":595},[165,2310,2172],{"class":178},[165,2312,2313],{"class":189}," result.exit_code ",[165,2315,457],{"class":178},[165,2317,2318],{"class":182}," 1\n",[165,2320,2321,2323,2326,2328],{"class":167,"line":600},[165,2322,2172],{"class":178},[165,2324,2325],{"class":189}," target.read_text() ",[165,2327,457],{"class":178},[165,2329,2330],{"class":263}," \"keep me\"\n",[165,2332,2333,2335,2337,2339,2341,2343,2345,2347,2350,2352,2355,2357],{"class":167,"line":605},[165,2334,2172],{"class":178},[165,2336,2292],{"class":189},[165,2338,2295],{"class":263},[165,2340,96],{"class":189},[165,2342,1441],{"class":263},[165,2344,96],{"class":189},[165,2346,376],{"class":182},[165,2348,2349],{"class":189},"(target), ",[165,2351,1511],{"class":263},[165,2353,2354],{"class":189},"]).exit_code ",[165,2356,457],{"class":178},[165,2358,2359],{"class":182}," 0\n",[165,2361,2362,2364,2367,2369,2372,2374,2376,2378],{"class":167,"line":641},[165,2363,2172],{"class":178},[165,2365,2366],{"class":189}," json.loads(target.read_text())[",[165,2368,852],{"class":182},[165,2370,2371],{"class":189},"][",[165,2373,1108],{"class":263},[165,2375,454],{"class":189},[165,2377,457],{"class":178},[165,2379,2380],{"class":263}," \"web-1\"\n",[165,2382,2383],{"class":167,"line":664},[165,2384,197],{"emptyLinePlaceholder":196},[165,2386,2387],{"class":167,"line":670},[165,2388,197],{"emptyLinePlaceholder":196},[165,2390,2391,2393,2396],{"class":167,"line":685},[165,2392,367],{"class":178},[165,2394,2395],{"class":335}," test_gzip_and_message_on_stderr",[165,2397,2206],{"class":189},[165,2399,2400,2402,2404,2406,2408],{"class":167,"line":691},[165,2401,2262],{"class":189},[165,2403,417],{"class":178},[165,2405,2267],{"class":189},[165,2407,106],{"class":178},[165,2409,2410],{"class":263}," \"out.jsonl.gz\"\n",[165,2412,2413,2415,2417,2419,2421,2423,2425,2427,2429],{"class":167,"line":697},[165,2414,2287],{"class":189},[165,2416,417],{"class":178},[165,2418,2292],{"class":189},[165,2420,2295],{"class":263},[165,2422,96],{"class":189},[165,2424,1441],{"class":263},[165,2426,96],{"class":189},[165,2428,376],{"class":182},[165,2430,2306],{"class":189},[165,2432,2433,2435,2438,2440],{"class":167,"line":705},[165,2434,2172],{"class":178},[165,2436,2437],{"class":189}," result.stdout ",[165,2439,457],{"class":178},[165,2441,2442],{"class":263}," \"\"\n",[165,2444,2445,2447,2450,2453],{"class":167,"line":716},[165,2446,2172],{"class":178},[165,2448,2449],{"class":263}," \"wrote 2 rows\"",[165,2451,2452],{"class":178}," in",[165,2454,2455],{"class":189}," result.stderr\n",[165,2457,2458,2460,2463,2465,2467,2469,2471,2473],{"class":167,"line":724},[165,2459,2172],{"class":178},[165,2461,2462],{"class":189}," gzip.decompress(target.read_bytes()).decode().count(",[165,2464,581],{"class":263},[165,2466,1210],{"class":182},[165,2468,581],{"class":263},[165,2470,931],{"class":189},[165,2472,457],{"class":178},[165,2474,2475],{"class":182}," 2\n",[165,2477,2478],{"class":167,"line":747},[165,2479,197],{"emptyLinePlaceholder":196},[165,2481,2482],{"class":167,"line":763},[165,2483,197],{"emptyLinePlaceholder":196},[165,2485,2486,2488,2491],{"class":167,"line":785},[165,2487,367],{"class":178},[165,2489,2490],{"class":335}," test_dash_means_stdout",[165,2492,2493],{"class":189},"():\n",[165,2495,2496,2498,2500,2502,2504,2506,2508,2510,2513],{"class":167,"line":811},[165,2497,2287],{"class":189},[165,2499,417],{"class":178},[165,2501,2292],{"class":189},[165,2503,2295],{"class":263},[165,2505,96],{"class":189},[165,2507,1441],{"class":263},[165,2509,96],{"class":189},[165,2511,2512],{"class":263},"\"-\"",[165,2514,2515],{"class":189},"])\n",[165,2517,2518,2520,2523,2525,2527,2529],{"class":167,"line":822},[165,2519,2172],{"class":178},[165,2521,2522],{"class":189}," result.stdout.splitlines()[",[165,2524,852],{"class":182},[165,2526,454],{"class":189},[165,2528,457],{"class":178},[165,2530,2531],{"class":263}," '{\"name\": \"web-1\", \"cpu\": 0.42}'\n",[10,2533,2534],{},"Note the assertion that stdout is empty when writing to a file: it is the regression test for the most common export bug, a status message that ends up inside the data stream.",[33,2536,2538],{"id":2537},"conclusion","Conclusion",[10,2540,2541,2542,2544,2545,2547],{},"An export option is a few dozen lines once records and renderers are separated: infer the format from the extension, treat ",[14,2543,77],{}," as stdout, refuse to clobber, write atomically, compress on ",[14,2546,118],{},", and report on stderr. Those rules make file output as safe to script as stdout and much kinder to Windows and spreadsheet users.",[33,2549,2551],{"id":2550},"frequently-asked-questions","Frequently asked questions",[2553,2554,2556,2557,2560],"h3",{"id":2555},"should-i-support-writing-excel-xlsx-files","Should I support writing Excel ",[14,2558,2559],{},".xlsx"," files?",[10,2562,2563,2564,2567,2568,2571,2572,2574],{},"If your users live in Excel, yes — it avoids the type-guessing problems of CSV because cells can be typed. Use ",[14,2565,2566],{},"openpyxl"," as an optional extra (",[14,2569,2570],{},"pip install mytool[excel]",") and import it only when the extension is ",[14,2573,2559],{},", so the base install stays light.",[2553,2576,2578],{"id":2577},"how-do-i-stream-a-huge-export-without-building-the-whole-string","How do I stream a huge export without building the whole string?",[10,2580,2581,2582,2584,2585,2588],{},"Change the writer to accept an iterator of text chunks and write them into the temporary file as they arrive, then rename at the end. For ",[14,2583,118],{},", wrap the temporary file in ",[14,2586,2587],{},"gzip.GzipFile(fileobj=fh, mode=\"wb\", mtime=0)",". Atomicity is unchanged because the rename still happens last.",[2553,2590,2592],{"id":2591},"what-if-the-user-passes-a-directory","What if the user passes a directory?",[10,2594,2595,2596,2599],{},"Either refuse, as above, or treat it as \"write a default file name inside it\" (",[14,2597,2598],{},"exports\u002Fservers-2026-10-02.csv","). The second is friendly but surprising when someone meant a file; if you do it, print the resulting path.",[2553,2601,2603,2604,2606],{"id":2602},"should-force-also-create-missing-parent-directories","Should ",[14,2605,133],{}," also create missing parent directories?",[10,2608,2609,2610,2612],{},"Keep them separate. ",[14,2611,133],{}," answers \"may I overwrite?\"; creating parents is a convenience you can either always do or never do. Tying them together makes the flag mean two things.",[33,2614,2616],{"id":2615},"related","Related",[38,2618,2619,2625,2630,2635,2640],{},[41,2620,2621,2622],{},"Up: ",[27,2623,2624],{"href":29},"Output formats for data-heavy Python CLIs",[41,2626,2627],{},[27,2628,2629],{"href":1011},"Writing CSV and TSV output correctly",[41,2631,2632],{},[27,2633,2634],{"href":49},"Adding a format flag for table, JSON and CSV",[41,2636,2637],{},[27,2638,2639],{"href":56},"Writing files atomically in Python CLIs",[41,2641,2642],{},[27,2643,2645],{"href":2644},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows\u002F","Fixing Unicode and encoding errors on Windows",[2647,2648,2649],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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":161,"searchDepth":175,"depth":175,"links":2651},[2652,2653,2654,2655,2656,2657,2658,2659,2667],{"id":35,"depth":175,"text":36},{"id":61,"depth":175,"text":62},{"id":153,"depth":175,"text":154},{"id":1774,"depth":175,"text":1775},{"id":1927,"depth":175,"text":1928},{"id":1999,"depth":175,"text":2000},{"id":2537,"depth":175,"text":2538},{"id":2550,"depth":175,"text":2551,"children":2660},[2661,2663,2664,2665],{"id":2555,"depth":193,"text":2662},"Should I support writing Excel .xlsx files?",{"id":2577,"depth":193,"text":2578},{"id":2591,"depth":193,"text":2592},{"id":2602,"depth":193,"text":2666},"Should --force also create missing parent directories?",{"id":2615,"depth":175,"text":2616},"2026-10-02","Add an --output-file option that infers the format from the extension, treats - as stdout, refuses to clobber, writes atomically and compresses .gz files.","intermediate",false,"md",{},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files",{"title":5,"description":2669},"advanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fexporting-cli-results-to-files\u002Findex",[2678,2679,2680,2681,2682],"output","files","export","csv","atomic-writes","9dpv9hvpMHIX0wSa9kvycWliYRGK8QrSdFV4DA5-SyA",[2685,2688,2691,2694,2697,2700,2703,2706,2709,2712,2715,2718,2721,2724,2727,2730,2733,2736,2739,2742,2745,2748,2751,2754,2757,2760,2763,2766,2769,2772,2775,2778,2781,2784,2787,2790,2793,2796,2799,2802,2805,2808,2811,2814,2817,2820,2823,2826,2829,2832,2835,2838,2841,2844,2847,2850,2853,2854,2857,2860,2863,2866,2869,2872,2875,2878,2881,2884,2887,2890,2893,2896,2899,2902,2905,2908,2911,2914,2917,2920,2923,2926,2929,2932,2935,2938,2941,2944,2947,2950,2953,2956,2959,2962,2965,2968,2971,2974,2977,2980,2983,2986,2989,2992,2995,2998,3001,3004,3007,3010,3013,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3102,3105,3108,3111,3114,3117,3120,3123,3126,3129,3132,3135,3138,3141,3144,3147,3150,3153,3156,3159,3162,3165,3168,3171,3174,3177,3180,3183,3186,3189,3192,3195,3198,3201,3204,3207,3210,3213,3216,3219,3222,3225,3228,3231,3234,3237,3240,3243,3246,3249,3252,3255,3258,3261,3264,3267,3270,3273,3276,3279,3282,3285,3288,3291,3294,3297,3300,3303,3306,3309,3312,3315,3318,3321,3324,3327,3330,3333,3336,3339,3342,3345,3348,3351,3354,3357,3360,3363,3366,3369,3372,3375,3378,3381,3384,3387,3390,3393,3396,3399,3402,3405,3408,3411,3414,3417,3420,3423,3426,3429,3432,3435,3438,3441,3444,3447,3450,3453,3456,3459,3462,3465,3468,3471,3474,3477,3480,3483,3486,3489,3492,3495,3498,3501,3504,3507,3510,3513,3516,3519,3522,3525],{"path":2686,"title":2687},"\u002Fabout","About Python CLI Toolcraft",{"path":2689,"title":2690},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2692,"title":2693},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2695,"title":2696},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dates-and-durations-in-cli-arguments","Validating Dates and Durations in Python CLI Arguments",{"path":2698,"title":2699},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2701,"title":2702},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2704,"title":2705},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2707,"title":2708},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2710,"title":2711},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2713,"title":2714},"\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":2716,"title":2717},"\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":2719,"title":2720},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2722,"title":2723},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Frunning-background-work-in-textual-with-workers","Running Background Work in Textual with Workers",{"path":2725,"title":2726},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":2728,"title":2729},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2731,"title":2732},"\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":2734,"title":2735},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":2737,"title":2738},"\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":2740,"title":2741},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2743,"title":2744},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Frich-formatted-help-with-rich-click","Rich-Formatted Help for Click CLIs with rich-click",{"path":2746,"title":2747},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2749,"title":2750},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2752,"title":2753},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2755,"title":2756},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2758,"title":2759},"\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":2761,"title":2762},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":2764,"title":2765},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2767,"title":2768},"\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":2770,"title":2771},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fsupporting-dumb-terminals-and-screen-readers","Supporting Dumb Terminals and Screen Readers in a Python CLI",{"path":2773,"title":2774},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2776,"title":2777},"\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":2779,"title":2780},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdid-you-mean-suggestions-for-mistyped-input","Did You Mean…? Suggestions for Mistyped CLI Input",{"path":2782,"title":2783},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2785,"title":2786},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2788,"title":2789},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2791,"title":2792},"\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":2794,"title":2795},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":2797,"title":2798},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2800,"title":2801},"\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":2803,"title":2804},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2806,"title":2807},"\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":2809,"title":2810},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2812,"title":2813},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2815,"title":2816},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fvalidating-config-files-with-json-schema","Validating Config Files with JSON Schema in a Python CLI",{"path":2818,"title":2819},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fwriting-a-config-init-and-edit-command","Writing a Config Init and Edit Command for a Python CLI",{"path":2821,"title":2822},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2824,"title":2825},"\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":2827,"title":2828},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2830,"title":2831},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-tree-views-with-rich","Building Tree Views with Rich in a Python CLI",{"path":2833,"title":2834},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2836,"title":2837},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2839,"title":2840},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-markdown-and-syntax-highlighting-with-rich","Rendering Markdown and Syntax Highlighting with Rich",{"path":2842,"title":2843},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2845,"title":2846},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2848,"title":2849},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fadding-a-format-flag-for-table-json-and-csv","Adding a Format Flag for Table, JSON and CSV Output",{"path":2851,"title":2852},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fcustom-output-templates-with-a-format-string","Custom Output Templates with a Format String in Python CLIs",{"path":2674,"title":5},{"path":2855,"title":2856},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":2858,"title":2859},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fselecting-fields-and-columns-from-cli-output","Selecting Fields and Columns from Python CLI Output",{"path":2861,"title":2862},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002Fwriting-csv-and-tsv-output-correctly","Writing CSV and TSV Output Correctly from a Python CLI",{"path":2864,"title":2865},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":2867,"title":2868},"\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":2870,"title":2871},"\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":2873,"title":2874},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2876,"title":2877},"\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":2879,"title":2880},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":2882,"title":2883},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2885,"title":2886},"\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":2888,"title":2889},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2891,"title":2892},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2894,"title":2895},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":2897,"title":2898},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fseparating-logs-from-program-output","Separating Logs from Program Output in a Python CLI",{"path":2900,"title":2901},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2903,"title":2904},"\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":2906,"title":2907},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2909,"title":2910},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2912,"title":2913},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2915,"title":2916},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2918,"title":2919},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fnull-delimited-input-and-xargs-compatibility","Null-Delimited Input and xargs Compatibility in Python CLIs",{"path":2921,"title":2922},"\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":2924,"title":2925},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2927,"title":2928},"\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":2930,"title":2931},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2933,"title":2934},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2936,"title":2937},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fmocking-http-in-cli-tests-with-respx","Mocking HTTP in Python CLI Tests with respx",{"path":2939,"title":2940},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2942,"title":2943},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2945,"title":2946},"\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":2948,"title":2949},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fuploading-files-with-multipart-and-progress","Uploading Files with Multipart and Progress in a Python CLI",{"path":2951,"title":2952},"\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":2954,"title":2955},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2957,"title":2958},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2960,"title":2961},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2963,"title":2964},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2966,"title":2967},"\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":2969,"title":2970},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fshowing-progress-for-concurrent-tasks","Showing Progress for Concurrent Tasks in a Python CLI",{"path":2972,"title":2973},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":2975,"title":2976},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2978,"title":2979},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2981,"title":2982},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fhandling-file-permissions-and-umask-in-clis","Handling File Permissions and umask in Python CLIs",{"path":2984,"title":2985},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2987,"title":2988},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2990,"title":2991},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2993,"title":2994},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwalking-directory-trees-with-ignore-rules","Walking Directory Trees with Ignore Rules in a Python CLI",{"path":2996,"title":2997},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2999,"title":3000},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3002,"title":3003},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fcaching-http-responses-on-disk-in-a-cli","Caching HTTP Responses on Disk in a Python CLI",{"path":3005,"title":3006},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":3008,"title":3009},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema","Migrating a CLI’s SQLite Schema Between Releases",{"path":3011,"title":3012},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history","Recording and Querying CLI Run History in SQLite",{"path":3014,"title":3015},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite","Storing CLI State in SQLite with a Small Repository Class",{"path":3017,"title":3018},"\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":3020,"title":3021},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3023,"title":3024},"\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":3026,"title":3027},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3029,"title":3030},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":3032,"title":3033},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service","Running a Python CLI as a systemd Service",{"path":3035,"title":3036},"\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":3038,"title":3039},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":3041,"title":3042},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3044,"title":3045},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3047,"title":3048},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3050,"title":3051},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3053,"title":3054},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Flaunching-the-users-editor-from-a-cli","Launching the User’s Editor from a Python CLI",{"path":3056,"title":3057},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":3059,"title":3060},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3062,"title":3063},"\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":3065,"title":3066},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3068,"title":3069},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3071,"title":3072},"\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":3074,"title":3075},"\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":3077,"title":3078},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":3080,"title":3081},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3083,"title":3084},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3086,"title":3087},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fchecking-pypi-for-a-newer-version","Checking PyPI for a Newer Version of Your Python CLI",{"path":3089,"title":3090},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":3092,"title":3093},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fopt-in-usage-telemetry-for-python-clis","Opt-In Usage Telemetry for Python CLIs Done Responsibly",{"path":3095,"title":3096},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fself-upgrading-a-cli-installed-with-pipx-or-uv","Self-Upgrading a Python CLI Installed with pipx or uv",{"path":3098,"title":3099},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis\u002Fshowing-non-blocking-update-notices","Showing Non-Blocking Update Notices in a Python CLI",{"path":106,"title":3101},"Python CLI Toolcraft",{"path":3103,"title":3104},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":3106,"title":3107},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":3109,"title":3110},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":3112,"title":3113},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":3115,"title":3116},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fusage-string-driven-clis-with-docopt-ng","Usage-String Driven Python CLIs with docopt-ng",{"path":3118,"title":3119},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Favoiding-import-time-side-effects","Avoiding Import-Time Side Effects in a Python CLI",{"path":3121,"title":3122},"\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":3124,"title":3125},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":3127,"title":3128},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3130,"title":3131},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3133,"title":3134},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3136,"title":3137},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3139,"title":3140},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3142,"title":3143},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3145,"title":3146},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargument-groups-and-help-formatting-in-argparse","Argument Groups and Help Formatting in argparse",{"path":3148,"title":3149},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3151,"title":3152},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3154,"title":3155},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3157,"title":3158},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Freading-arguments-from-files-with-fromfile-prefix-chars","Reading Arguments from Files with argparse’s fromfile_prefix_chars",{"path":3160,"title":3161},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3163,"title":3164},"\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":3166,"title":3167},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":3169,"title":3170},"\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":3172,"title":3173},"\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":3175,"title":3176},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3178,"title":3179},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3181,"title":3182},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":3184,"title":3185},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3187,"title":3188},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3190,"title":3191},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3193,"title":3194},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3196,"title":3197},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":3199,"title":3200},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":3202,"title":3203},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3205,"title":3206},"\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":3208,"title":3209},"\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":3211,"title":3212},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3214,"title":3215},"\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":3217,"title":3218},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3220,"title":3221},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Foffering-a-python-api-alongside-your-cli","Offering a Python API Alongside Your CLI",{"path":3223,"title":3224},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":3226,"title":3227},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3229,"title":3230},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3232,"title":3233},"\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":3235,"title":3236},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3238,"title":3239},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3241,"title":3242},"\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":3244,"title":3245},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3247,"title":3248},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3250,"title":3251},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3253,"title":3254},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":3256,"title":3257},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3259,"title":3260},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":3262,"title":3263},"\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":3265,"title":3266},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3268,"title":3269},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fchoices-and-enums-in-typer-and-click","Choices and Enums in Typer and Click Options",{"path":3271,"title":3272},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fclick-option-callbacks-and-eager-options","Click Option Callbacks and Eager Options Explained",{"path":3274,"title":3275},"\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":3277,"title":3278},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3280,"title":3281},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Frich-markup-and-help-panels-in-typer","Rich Markup and Help Panels in Typer",{"path":3283,"title":3284},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3286,"title":3287},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3289,"title":3290},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3292,"title":3293},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3295,"title":3296},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3298,"title":3299},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-a-cli-docker-image-from-ci","Publishing a Python CLI as a Docker Image from CI",{"path":3301,"title":3302},"\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":3304,"title":3305},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Frunning-cli-tests-on-windows-and-macos-runners","Running Python CLI Tests on Windows and macOS Runners",{"path":3307,"title":3308},"\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":3310,"title":3311},"\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":3313,"title":3314},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3316,"title":3317},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3319,"title":3320},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3322,"title":3323},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3325,"title":3326},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3328,"title":3329},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3331,"title":3332},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3334,"title":3335},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3337,"title":3338},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3340,"title":3341},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3343,"title":3344},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3346,"title":3347},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3349,"title":3350},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3352,"title":3353},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3355,"title":3356},"\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":3358,"title":3359},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3361,"title":3362},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3364,"title":3365},"\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":3367,"title":3368},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ffinding-unused-code-and-dependencies-with-vulture-and-deptry","Finding Unused Code and Dependencies with vulture and deptry",{"path":3370,"title":3371},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3373,"title":3374},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-pyright-in-strict-mode-on-a-cli","Running Pyright in Strict Mode on a Python CLI",{"path":3376,"title":3377},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Frunning-ruff-and-mypy-in-ci-with-annotations","Running Ruff and mypy in CI with Inline Annotations",{"path":3379,"title":3380},"\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":3382,"title":3383},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3385,"title":3386},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3388,"title":3389},"\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":3391,"title":3392},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3394,"title":3395},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3397,"title":3398},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3400,"title":3401},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fshipping-pre-releases-and-release-candidates","Shipping Pre-Releases and Release Candidates of a Python CLI",{"path":3403,"title":3404},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3406,"title":3407},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3409,"title":3410},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fchoosing-a-build-backend-for-a-python-cli","Choosing a Build Backend for a Python CLI",{"path":3412,"title":3413},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3415,"title":3416},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3418,"title":3419},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3421,"title":3422},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3424,"title":3425},"\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":3427,"title":3428},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3430,"title":3431},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3433,"title":3434},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3436,"title":3437},"\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":3439,"title":3440},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3442,"title":3443},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3445,"title":3446},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3448,"title":3449},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3451,"title":3452},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fkeeping-hook-versions-current-with-autoupdate","Keeping pre-commit Hook Versions Current with autoupdate",{"path":3454,"title":3455},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Frunning-pre-commit-in-ci","Running pre-commit in CI for a Python CLI Repository",{"path":3457,"title":3458},"\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":3460,"title":3461},"\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":3463,"title":3464},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fspeeding-up-slow-pre-commit-hooks","Speeding Up Slow pre-commit Hooks in a CLI Repository",{"path":3466,"title":3467},"\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":3469,"title":3470},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fauditing-dependencies-with-pip-audit","Auditing a Python CLI’s Dependencies with pip-audit",{"path":3472,"title":3473},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fgenerating-an-sbom-for-a-python-cli","Generating an SBOM for a Python CLI Release",{"path":3475,"title":3476},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3478,"title":3479},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3481,"title":3482},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3484,"title":3485},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fbuilding-and-publishing-a-cli-with-uv","Building and Publishing a Python CLI with uv",{"path":3487,"title":3488},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3490,"title":3491},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Flocking-and-syncing-cli-dependencies-with-uv","Locking and Syncing a Python CLI’s Dependencies with uv",{"path":3493,"title":3494},"\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":3496,"title":3497},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fusing-private-package-indexes-with-uv","Using Private Package Indexes with uv for Internal CLIs",{"path":3499,"title":3500},"\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":3502,"title":3503},"\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":3505,"title":3506},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3508,"title":3509},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fdebugging-wrong-python-and-wrong-venv-problems","Debugging Wrong-Python and Wrong-Venv Problems in CLIs",{"path":3511,"title":3512},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fexternally-managed-environments-and-pep-668","PEP 668 and Python CLIs: the externally-managed-environment Error",{"path":3514,"title":3515},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3517,"title":3518},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3520,"title":3521},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3523,"title":3524},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3526,"title":3527},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967537213]