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