[{"data":1,"prerenderedAt":3805},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service\u002F":3,"content-directory":2961},{"id":4,"title":5,"body":6,"date":2946,"description":2947,"difficulty":2948,"draft":2949,"extension":2950,"meta":2951,"navigation":184,"path":2952,"seo":2953,"stem":2954,"tags":2955,"updated":2946,"__hash__":2960},"content\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service\u002Findex.md","Running a Python CLI as a systemd Service",{"type":7,"value":8,"toc":2923},"minimark",[9,67,72,86,90,94,127,131,136,530,560,564,1247,1270,1291,1295,1744,1798,1801,1805,1843,1847,1913,1916,1920,1930,2747,2756,2760,2793,2797,2807,2810,2814,2825,2836,2848,2852,2875,2879,2886,2890,2919],[10,11,12,13,17,18,21,22,25,26,30,31,34,35,39,40,43,44,47,48,51,52,55,56,61,62,66],"p",{},"Many CLIs grow an “agent” mode: ",[14,15,16],"code",{},"mytool agent"," syncs files every minute, ",[14,19,20],{},"mytool watch"," reacts to changes, ",[14,23,24],{},"mytool serve"," exposes a small API. Running it in a terminal tab works for a day; for real use it should start at login or boot, restart after crashes, log somewhere searchable and stop cleanly at shutdown. On Linux, ",[27,28,29],"strong",{},"systemd"," provides all of that, and a CLI can integrate with it with very little code. The integration has two halves: a ",[27,32,33],{},"unit file"," that tells systemd how to run the command, and a few lines in the program that tell systemd when the service is ",[36,37,38],"em",{},"ready",", what it is doing, and that it is still ",[36,41,42],{},"alive",". This guide writes both — a ",[14,45,46],{},"Type=notify"," unit with a watchdog, a dependency-free ",[14,49,50],{},"sd_notify"," implementation, a main loop that uses it, and a ",[14,53,54],{},"service install"," command for per-user services — and tests the protocol without systemd running the code. It belongs to the ",[57,58,60],"a",{"href":59},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002F","long-running and watch-mode CLIs topic","; scheduled one-shot jobs are covered in ",[57,63,65],{"href":64},"\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",".",[68,69,71],"h2",{"id":70},"prerequisites","Prerequisites",[73,74,75,79],"ul",{},[76,77,78],"li",{},"A Linux system with systemd (any current distribution); user services need a logged-in session or lingering enabled.",[76,80,81,82,66],{},"Graceful shutdown handling as in ",[57,83,85],{"href":84},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown\u002F","handling SIGTERM and graceful shutdown",[68,87,89],{"id":88},"how-systemd-and-the-service-talk","How systemd and the service talk",[91,92],"inline-diagram",{"name":93},"sysd-notify",[10,95,96,97,99,100,103,104,107,108,111,112,115,116,119,120,123,124,66],{},"With ",[14,98,46],{},", systemd starts the process and passes the path of a Unix datagram socket in the ",[14,101,102],{},"NOTIFY_SOCKET"," environment variable. The service sends short text messages to it: ",[14,105,106],{},"READY=1"," once start-up is complete (systemd considers the unit started only then, so units ordered after it wait correctly), ",[14,109,110],{},"STATUS=…"," with a human-readable line shown in ",[14,113,114],{},"systemctl status",", ",[14,117,118],{},"WATCHDOG=1"," periodically to prove it is not hung, and ",[14,121,122],{},"STOPPING=1"," when shutting down. Everything else — logging, restarts, stopping — uses mechanisms the program already has: stdout and stderr go to the journal, and stopping sends ",[14,125,126],{},"SIGTERM",[68,128,130],{"id":129},"the-recipe","The recipe",[132,133,135],"h3",{"id":134},"sd_notify-in-fifteen-lines","sd_notify in fifteen lines",[137,138,143],"pre",{"className":139,"code":140,"language":141,"meta":142,"style":142},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fsdnotify.py\n\"\"\"Minimal sd_notify: tell systemd we are ready, alive, reloading or stopping.\"\"\"\nfrom __future__ import annotations\n\nimport os\nimport socket\n\n\ndef notify(*fields: str) -> bool:\n    \"\"\"Send fields such as \"READY=1\" to $NOTIFY_SOCKET; a no-op outside systemd.\"\"\"\n    address = os.environ.get(\"NOTIFY_SOCKET\")\n    if not address:\n        return False\n    if address.startswith(\"@\"):                       # abstract namespace socket\n        address = \"\\0\" + address[1:]\n    with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC) as sock:\n        sock.connect(address)\n        sock.sendall(\"\\n\".join(fields).encode())\n    return True\n\n\ndef watchdog_interval() -> float | None:\n    \"\"\"Half of WatchdogSec, in seconds, if the watchdog is enabled for this process.\"\"\"\n    usec = os.environ.get(\"WATCHDOG_USEC\")\n    pid = os.environ.get(\"WATCHDOG_PID\")\n    if not usec or (pid and int(pid) != os.getpid()):\n        return None\n    return int(usec) \u002F 1_000_000 \u002F 2\n","python","",[14,144,145,154,161,179,186,195,203,208,213,244,250,268,280,289,306,335,371,377,393,402,407,412,433,439,454,469,500,508],{"__ignoreMap":142},[146,147,150],"span",{"class":148,"line":149},"line",1,[146,151,153],{"class":152},"sJ8bj","# src\u002Fmytool\u002Fsdnotify.py\n",[146,155,157],{"class":148,"line":156},2,[146,158,160],{"class":159},"sZZnC","\"\"\"Minimal sd_notify: tell systemd we are ready, alive, reloading or stopping.\"\"\"\n",[146,162,164,168,172,175],{"class":148,"line":163},3,[146,165,167],{"class":166},"szBVR","from",[146,169,171],{"class":170},"sj4cs"," __future__",[146,173,174],{"class":166}," import",[146,176,178],{"class":177},"sVt8B"," annotations\n",[146,180,182],{"class":148,"line":181},4,[146,183,185],{"emptyLinePlaceholder":184},true,"\n",[146,187,189,192],{"class":148,"line":188},5,[146,190,191],{"class":166},"import",[146,193,194],{"class":177}," os\n",[146,196,198,200],{"class":148,"line":197},6,[146,199,191],{"class":166},[146,201,202],{"class":177}," socket\n",[146,204,206],{"class":148,"line":205},7,[146,207,185],{"emptyLinePlaceholder":184},[146,209,211],{"class":148,"line":210},8,[146,212,185],{"emptyLinePlaceholder":184},[146,214,216,219,223,226,229,232,235,238,241],{"class":148,"line":215},9,[146,217,218],{"class":166},"def",[146,220,222],{"class":221},"sScJk"," notify",[146,224,225],{"class":177},"(",[146,227,228],{"class":166},"*",[146,230,231],{"class":177},"fields: ",[146,233,234],{"class":170},"str",[146,236,237],{"class":177},") -> ",[146,239,240],{"class":170},"bool",[146,242,243],{"class":177},":\n",[146,245,247],{"class":148,"line":246},10,[146,248,249],{"class":159},"    \"\"\"Send fields such as \"READY=1\" to $NOTIFY_SOCKET; a no-op outside systemd.\"\"\"\n",[146,251,253,256,259,262,265],{"class":148,"line":252},11,[146,254,255],{"class":177},"    address ",[146,257,258],{"class":166},"=",[146,260,261],{"class":177}," os.environ.get(",[146,263,264],{"class":159},"\"NOTIFY_SOCKET\"",[146,266,267],{"class":177},")\n",[146,269,271,274,277],{"class":148,"line":270},12,[146,272,273],{"class":166},"    if",[146,275,276],{"class":166}," not",[146,278,279],{"class":177}," address:\n",[146,281,283,286],{"class":148,"line":282},13,[146,284,285],{"class":166},"        return",[146,287,288],{"class":170}," False\n",[146,290,292,294,297,300,303],{"class":148,"line":291},14,[146,293,273],{"class":166},[146,295,296],{"class":177}," address.startswith(",[146,298,299],{"class":159},"\"@\"",[146,301,302],{"class":177},"):                       ",[146,304,305],{"class":152},"# abstract namespace socket\n",[146,307,309,312,314,317,320,323,326,329,332],{"class":148,"line":308},15,[146,310,311],{"class":177},"        address ",[146,313,258],{"class":166},[146,315,316],{"class":159}," \"",[146,318,319],{"class":170},"\\0",[146,321,322],{"class":159},"\"",[146,324,325],{"class":166}," +",[146,327,328],{"class":177}," address[",[146,330,331],{"class":170},"1",[146,333,334],{"class":177},":]\n",[146,336,338,341,344,347,350,353,356,359,362,365,368],{"class":148,"line":337},16,[146,339,340],{"class":166},"    with",[146,342,343],{"class":177}," socket.socket(socket.",[146,345,346],{"class":170},"AF_UNIX",[146,348,349],{"class":177},", socket.",[146,351,352],{"class":170},"SOCK_DGRAM",[146,354,355],{"class":166}," |",[146,357,358],{"class":177}," socket.",[146,360,361],{"class":170},"SOCK_CLOEXEC",[146,363,364],{"class":177},") ",[146,366,367],{"class":166},"as",[146,369,370],{"class":177}," sock:\n",[146,372,374],{"class":148,"line":373},17,[146,375,376],{"class":177},"        sock.connect(address)\n",[146,378,380,383,385,388,390],{"class":148,"line":379},18,[146,381,382],{"class":177},"        sock.sendall(",[146,384,322],{"class":159},[146,386,387],{"class":170},"\\n",[146,389,322],{"class":159},[146,391,392],{"class":177},".join(fields).encode())\n",[146,394,396,399],{"class":148,"line":395},19,[146,397,398],{"class":166},"    return",[146,400,401],{"class":170}," True\n",[146,403,405],{"class":148,"line":404},20,[146,406,185],{"emptyLinePlaceholder":184},[146,408,410],{"class":148,"line":409},21,[146,411,185],{"emptyLinePlaceholder":184},[146,413,415,417,420,423,426,428,431],{"class":148,"line":414},22,[146,416,218],{"class":166},[146,418,419],{"class":221}," watchdog_interval",[146,421,422],{"class":177},"() -> ",[146,424,425],{"class":170},"float",[146,427,355],{"class":166},[146,429,430],{"class":170}," None",[146,432,243],{"class":177},[146,434,436],{"class":148,"line":435},23,[146,437,438],{"class":159},"    \"\"\"Half of WatchdogSec, in seconds, if the watchdog is enabled for this process.\"\"\"\n",[146,440,442,445,447,449,452],{"class":148,"line":441},24,[146,443,444],{"class":177},"    usec ",[146,446,258],{"class":166},[146,448,261],{"class":177},[146,450,451],{"class":159},"\"WATCHDOG_USEC\"",[146,453,267],{"class":177},[146,455,457,460,462,464,467],{"class":148,"line":456},25,[146,458,459],{"class":177},"    pid ",[146,461,258],{"class":166},[146,463,261],{"class":177},[146,465,466],{"class":159},"\"WATCHDOG_PID\"",[146,468,267],{"class":177},[146,470,472,474,476,479,482,485,488,491,494,497],{"class":148,"line":471},26,[146,473,273],{"class":166},[146,475,276],{"class":166},[146,477,478],{"class":177}," usec ",[146,480,481],{"class":166},"or",[146,483,484],{"class":177}," (pid ",[146,486,487],{"class":166},"and",[146,489,490],{"class":170}," int",[146,492,493],{"class":177},"(pid) ",[146,495,496],{"class":166},"!=",[146,498,499],{"class":177}," os.getpid()):\n",[146,501,503,505],{"class":148,"line":502},27,[146,504,285],{"class":166},[146,506,507],{"class":170}," None\n",[146,509,511,513,515,518,521,524,527],{"class":148,"line":510},28,[146,512,398],{"class":166},[146,514,490],{"class":170},[146,516,517],{"class":177},"(usec) ",[146,519,520],{"class":166},"\u002F",[146,522,523],{"class":170}," 1_000_000",[146,525,526],{"class":166}," \u002F",[146,528,529],{"class":170}," 2\n",[10,531,532,533,536,537,540,541,543,544,547,548,551,552,555,556,559],{},"The protocol is simple enough that a dependency is not needed: connect a datagram socket to the given address and send newline-separated ",[14,534,535],{},"KEY=value"," fields. An address starting with ",[14,538,539],{},"@"," is in Linux’s abstract socket namespace, written with a leading NUL byte in Python. When ",[14,542,102],{}," is not set — running in a terminal, in tests, under another supervisor — ",[14,545,546],{},"notify"," does nothing, so the same code runs everywhere. ",[14,549,550],{},"watchdog_interval"," reads ",[14,553,554],{},"WATCHDOG_USEC",", which systemd sets from ",[14,557,558],{},"WatchdogSec=",", and returns half of it: pinging at half the timeout is the recommended margin.",[132,561,563],{"id":562},"the-service-loop","The service loop",[137,565,567],{"className":139,"code":566,"language":141,"meta":142,"style":142},"# src\u002Fmytool\u002Fservice.py\nfrom __future__ import annotations\n\nimport shutil\nimport signal\nimport sys\nimport threading\nimport time\nfrom pathlib import Path\n\nfrom mytool.sdnotify import notify, watchdog_interval\n\nUNIT = \"\"\"\\\n[Unit]\nDescription=mytool sync agent\nDocumentation=https:\u002F\u002Fexample.com\u002Fmytool\u002Fdocs\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=notify\nExecStart={exe} agent\nRestart=on-failure\nRestartSec=5\nWatchdogSec=30\nTimeoutStopSec=20\nEnvironment=PYTHONUNBUFFERED=1\nNoNewPrivileges=yes\nPrivateTmp=yes\n\n[Install]\nWantedBy=default.target\n\"\"\"\n\n\ndef render_unit(exe: str | None = None) -> str:\n    exe = exe or shutil.which(\"mytool\") or f\"{sys.executable} -m mytool\"\n    return UNIT.format(exe=exe)\n\n\ndef user_unit_path() -> Path:\n    return Path.home() \u002F \".config\u002Fsystemd\u002Fuser\u002Fmytool.service\"\n\n\ndef run_agent(work, *, interval: float = 5.0, stop: threading.Event | None = None) -> None:\n    \"\"\"Main loop: signal readiness, do work, pet the watchdog, stop cleanly on SIGTERM.\"\"\"\n    stop = stop or threading.Event()\n    if threading.current_thread() is threading.main_thread():\n        signal.signal(signal.SIGTERM, lambda *_: stop.set())\n    ping = watchdog_interval()\n    notify(\"READY=1\", \"STATUS=waiting for first run\")\n    last_ping = time.monotonic()\n    while not stop.is_set():\n        count = work()\n        notify(f\"STATUS=synced {count} files at {time.strftime('%H:%M:%S')}\")\n        # Wait in short slices so the watchdog keeps being fed during long intervals.\n        deadline = time.monotonic() + interval\n        while not stop.is_set() and time.monotonic() \u003C deadline:\n            if ping and time.monotonic() - last_ping >= ping:\n                notify(\"WATCHDOG=1\")\n                last_ping = time.monotonic()\n            stop.wait(min(1.0, max(0.0, deadline - time.monotonic())))\n    notify(\"STOPPING=1\")\n",[14,568,569,574,584,588,595,602,609,616,623,635,639,651,655,669,674,679,684,689,694,698,703,708,719,724,729,734,739,744,749,755,760,766,772,778,783,788,815,855,875,880,885,896,909,914,919,961,967,983,997,1016,1027,1043,1054,1065,1076,1115,1121,1138,1159,1184,1195,1205,1237],{"__ignoreMap":142},[146,570,571],{"class":148,"line":149},[146,572,573],{"class":152},"# src\u002Fmytool\u002Fservice.py\n",[146,575,576,578,580,582],{"class":148,"line":156},[146,577,167],{"class":166},[146,579,171],{"class":170},[146,581,174],{"class":166},[146,583,178],{"class":177},[146,585,586],{"class":148,"line":163},[146,587,185],{"emptyLinePlaceholder":184},[146,589,590,592],{"class":148,"line":181},[146,591,191],{"class":166},[146,593,594],{"class":177}," shutil\n",[146,596,597,599],{"class":148,"line":188},[146,598,191],{"class":166},[146,600,601],{"class":177}," signal\n",[146,603,604,606],{"class":148,"line":197},[146,605,191],{"class":166},[146,607,608],{"class":177}," sys\n",[146,610,611,613],{"class":148,"line":205},[146,612,191],{"class":166},[146,614,615],{"class":177}," threading\n",[146,617,618,620],{"class":148,"line":210},[146,619,191],{"class":166},[146,621,622],{"class":177}," time\n",[146,624,625,627,630,632],{"class":148,"line":215},[146,626,167],{"class":166},[146,628,629],{"class":177}," pathlib ",[146,631,191],{"class":166},[146,633,634],{"class":177}," Path\n",[146,636,637],{"class":148,"line":246},[146,638,185],{"emptyLinePlaceholder":184},[146,640,641,643,646,648],{"class":148,"line":252},[146,642,167],{"class":166},[146,644,645],{"class":177}," mytool.sdnotify ",[146,647,191],{"class":166},[146,649,650],{"class":177}," notify, watchdog_interval\n",[146,652,653],{"class":148,"line":270},[146,654,185],{"emptyLinePlaceholder":184},[146,656,657,660,663,666],{"class":148,"line":282},[146,658,659],{"class":170},"UNIT",[146,661,662],{"class":166}," =",[146,664,665],{"class":159}," \"\"\"",[146,667,668],{"class":170},"\\\n",[146,670,671],{"class":148,"line":291},[146,672,673],{"class":159},"[Unit]\n",[146,675,676],{"class":148,"line":308},[146,677,678],{"class":159},"Description=mytool sync agent\n",[146,680,681],{"class":148,"line":337},[146,682,683],{"class":159},"Documentation=https:\u002F\u002Fexample.com\u002Fmytool\u002Fdocs\n",[146,685,686],{"class":148,"line":373},[146,687,688],{"class":159},"After=network-online.target\n",[146,690,691],{"class":148,"line":379},[146,692,693],{"class":159},"Wants=network-online.target\n",[146,695,696],{"class":148,"line":395},[146,697,185],{"emptyLinePlaceholder":184},[146,699,700],{"class":148,"line":404},[146,701,702],{"class":159},"[Service]\n",[146,704,705],{"class":148,"line":409},[146,706,707],{"class":159},"Type=notify\n",[146,709,710,713,716],{"class":148,"line":414},[146,711,712],{"class":159},"ExecStart=",[146,714,715],{"class":170},"{exe}",[146,717,718],{"class":159}," agent\n",[146,720,721],{"class":148,"line":435},[146,722,723],{"class":159},"Restart=on-failure\n",[146,725,726],{"class":148,"line":441},[146,727,728],{"class":159},"RestartSec=5\n",[146,730,731],{"class":148,"line":456},[146,732,733],{"class":159},"WatchdogSec=30\n",[146,735,736],{"class":148,"line":471},[146,737,738],{"class":159},"TimeoutStopSec=20\n",[146,740,741],{"class":148,"line":502},[146,742,743],{"class":159},"Environment=PYTHONUNBUFFERED=1\n",[146,745,746],{"class":148,"line":510},[146,747,748],{"class":159},"NoNewPrivileges=yes\n",[146,750,752],{"class":148,"line":751},29,[146,753,754],{"class":159},"PrivateTmp=yes\n",[146,756,758],{"class":148,"line":757},30,[146,759,185],{"emptyLinePlaceholder":184},[146,761,763],{"class":148,"line":762},31,[146,764,765],{"class":159},"[Install]\n",[146,767,769],{"class":148,"line":768},32,[146,770,771],{"class":159},"WantedBy=default.target\n",[146,773,775],{"class":148,"line":774},33,[146,776,777],{"class":159},"\"\"\"\n",[146,779,781],{"class":148,"line":780},34,[146,782,185],{"emptyLinePlaceholder":184},[146,784,786],{"class":148,"line":785},35,[146,787,185],{"emptyLinePlaceholder":184},[146,789,791,793,796,799,801,803,805,807,809,811,813],{"class":148,"line":790},36,[146,792,218],{"class":166},[146,794,795],{"class":221}," render_unit",[146,797,798],{"class":177},"(exe: ",[146,800,234],{"class":170},[146,802,355],{"class":166},[146,804,430],{"class":170},[146,806,662],{"class":166},[146,808,430],{"class":170},[146,810,237],{"class":177},[146,812,234],{"class":170},[146,814,243],{"class":177},[146,816,818,821,823,826,828,831,834,836,838,841,843,846,849,852],{"class":148,"line":817},37,[146,819,820],{"class":177},"    exe ",[146,822,258],{"class":166},[146,824,825],{"class":177}," exe ",[146,827,481],{"class":166},[146,829,830],{"class":177}," shutil.which(",[146,832,833],{"class":159},"\"mytool\"",[146,835,364],{"class":177},[146,837,481],{"class":166},[146,839,840],{"class":166}," f",[146,842,322],{"class":159},[146,844,845],{"class":170},"{",[146,847,848],{"class":177},"sys.executable",[146,850,851],{"class":170},"}",[146,853,854],{"class":159}," -m mytool\"\n",[146,856,858,860,863,866,870,872],{"class":148,"line":857},38,[146,859,398],{"class":166},[146,861,862],{"class":170}," UNIT",[146,864,865],{"class":177},".format(",[146,867,869],{"class":868},"s4XuR","exe",[146,871,258],{"class":166},[146,873,874],{"class":177},"exe)\n",[146,876,878],{"class":148,"line":877},39,[146,879,185],{"emptyLinePlaceholder":184},[146,881,883],{"class":148,"line":882},40,[146,884,185],{"emptyLinePlaceholder":184},[146,886,888,890,893],{"class":148,"line":887},41,[146,889,218],{"class":166},[146,891,892],{"class":221}," user_unit_path",[146,894,895],{"class":177},"() -> Path:\n",[146,897,899,901,904,906],{"class":148,"line":898},42,[146,900,398],{"class":166},[146,902,903],{"class":177}," Path.home() ",[146,905,520],{"class":166},[146,907,908],{"class":159}," \".config\u002Fsystemd\u002Fuser\u002Fmytool.service\"\n",[146,910,912],{"class":148,"line":911},43,[146,913,185],{"emptyLinePlaceholder":184},[146,915,917],{"class":148,"line":916},44,[146,918,185],{"emptyLinePlaceholder":184},[146,920,922,924,927,930,932,935,937,939,942,945,948,950,952,954,956,959],{"class":148,"line":921},45,[146,923,218],{"class":166},[146,925,926],{"class":221}," run_agent",[146,928,929],{"class":177},"(work, ",[146,931,228],{"class":166},[146,933,934],{"class":177},", interval: ",[146,936,425],{"class":170},[146,938,662],{"class":166},[146,940,941],{"class":170}," 5.0",[146,943,944],{"class":177},", stop: threading.Event ",[146,946,947],{"class":166},"|",[146,949,430],{"class":170},[146,951,662],{"class":166},[146,953,430],{"class":170},[146,955,237],{"class":177},[146,957,958],{"class":170},"None",[146,960,243],{"class":177},[146,962,964],{"class":148,"line":963},46,[146,965,966],{"class":159},"    \"\"\"Main loop: signal readiness, do work, pet the watchdog, stop cleanly on SIGTERM.\"\"\"\n",[146,968,970,973,975,978,980],{"class":148,"line":969},47,[146,971,972],{"class":177},"    stop ",[146,974,258],{"class":166},[146,976,977],{"class":177}," stop ",[146,979,481],{"class":166},[146,981,982],{"class":177}," threading.Event()\n",[146,984,986,988,991,994],{"class":148,"line":985},48,[146,987,273],{"class":166},[146,989,990],{"class":177}," threading.current_thread() ",[146,992,993],{"class":166},"is",[146,995,996],{"class":177}," threading.main_thread():\n",[146,998,1000,1003,1005,1007,1010,1013],{"class":148,"line":999},49,[146,1001,1002],{"class":177},"        signal.signal(signal.",[146,1004,126],{"class":170},[146,1006,115],{"class":177},[146,1008,1009],{"class":166},"lambda",[146,1011,1012],{"class":166}," *",[146,1014,1015],{"class":177},"_: stop.set())\n",[146,1017,1019,1022,1024],{"class":148,"line":1018},50,[146,1020,1021],{"class":177},"    ping ",[146,1023,258],{"class":166},[146,1025,1026],{"class":177}," watchdog_interval()\n",[146,1028,1030,1033,1036,1038,1041],{"class":148,"line":1029},51,[146,1031,1032],{"class":177},"    notify(",[146,1034,1035],{"class":159},"\"READY=1\"",[146,1037,115],{"class":177},[146,1039,1040],{"class":159},"\"STATUS=waiting for first run\"",[146,1042,267],{"class":177},[146,1044,1046,1049,1051],{"class":148,"line":1045},52,[146,1047,1048],{"class":177},"    last_ping ",[146,1050,258],{"class":166},[146,1052,1053],{"class":177}," time.monotonic()\n",[146,1055,1057,1060,1062],{"class":148,"line":1056},53,[146,1058,1059],{"class":166},"    while",[146,1061,276],{"class":166},[146,1063,1064],{"class":177}," stop.is_set():\n",[146,1066,1068,1071,1073],{"class":148,"line":1067},54,[146,1069,1070],{"class":177},"        count ",[146,1072,258],{"class":166},[146,1074,1075],{"class":177}," work()\n",[146,1077,1079,1082,1085,1088,1090,1093,1095,1098,1100,1103,1106,1109,1111,1113],{"class":148,"line":1078},55,[146,1080,1081],{"class":177},"        notify(",[146,1083,1084],{"class":166},"f",[146,1086,1087],{"class":159},"\"STATUS=synced ",[146,1089,845],{"class":170},[146,1091,1092],{"class":177},"count",[146,1094,851],{"class":170},[146,1096,1097],{"class":159}," files at ",[146,1099,845],{"class":170},[146,1101,1102],{"class":177},"time.strftime(",[146,1104,1105],{"class":159},"'%H:%M:%S'",[146,1107,1108],{"class":177},")",[146,1110,851],{"class":170},[146,1112,322],{"class":159},[146,1114,267],{"class":177},[146,1116,1118],{"class":148,"line":1117},56,[146,1119,1120],{"class":152},"        # Wait in short slices so the watchdog keeps being fed during long intervals.\n",[146,1122,1124,1127,1129,1132,1135],{"class":148,"line":1123},57,[146,1125,1126],{"class":177},"        deadline ",[146,1128,258],{"class":166},[146,1130,1131],{"class":177}," time.monotonic() ",[146,1133,1134],{"class":166},"+",[146,1136,1137],{"class":177}," interval\n",[146,1139,1141,1144,1146,1149,1151,1153,1156],{"class":148,"line":1140},58,[146,1142,1143],{"class":166},"        while",[146,1145,276],{"class":166},[146,1147,1148],{"class":177}," stop.is_set() ",[146,1150,487],{"class":166},[146,1152,1131],{"class":177},[146,1154,1155],{"class":166},"\u003C",[146,1157,1158],{"class":177}," deadline:\n",[146,1160,1162,1165,1168,1170,1172,1175,1178,1181],{"class":148,"line":1161},59,[146,1163,1164],{"class":166},"            if",[146,1166,1167],{"class":177}," ping ",[146,1169,487],{"class":166},[146,1171,1131],{"class":177},[146,1173,1174],{"class":166},"-",[146,1176,1177],{"class":177}," last_ping ",[146,1179,1180],{"class":166},">=",[146,1182,1183],{"class":177}," ping:\n",[146,1185,1187,1190,1193],{"class":148,"line":1186},60,[146,1188,1189],{"class":177},"                notify(",[146,1191,1192],{"class":159},"\"WATCHDOG=1\"",[146,1194,267],{"class":177},[146,1196,1198,1201,1203],{"class":148,"line":1197},61,[146,1199,1200],{"class":177},"                last_ping ",[146,1202,258],{"class":166},[146,1204,1053],{"class":177},[146,1206,1208,1211,1214,1216,1219,1221,1224,1226,1229,1232,1234],{"class":148,"line":1207},62,[146,1209,1210],{"class":177},"            stop.wait(",[146,1212,1213],{"class":170},"min",[146,1215,225],{"class":177},[146,1217,1218],{"class":170},"1.0",[146,1220,115],{"class":177},[146,1222,1223],{"class":170},"max",[146,1225,225],{"class":177},[146,1227,1228],{"class":170},"0.0",[146,1230,1231],{"class":177},", deadline ",[146,1233,1174],{"class":166},[146,1235,1236],{"class":177}," time.monotonic())))\n",[146,1238,1240,1242,1245],{"class":148,"line":1239},63,[146,1241,1032],{"class":177},[146,1243,1244],{"class":159},"\"STOPPING=1\"",[146,1246,267],{"class":177},[10,1248,1249,1250,1252,1253,1256,1257,1259,1260,1262,1263,1266,1267,66],{},"The loop sends ",[14,1251,106],{}," only after setup (configuration loaded, connections established in a real agent), reports a short status after each iteration, and feeds the watchdog while waiting. The inner wait uses short slices so a long ",[14,1254,1255],{},"interval"," — an hourly sync — does not starve a 30-second watchdog. ",[14,1258,126],{}," sets an event, the loop finishes its current iteration and reports ",[14,1261,122],{},", and ",[14,1264,1265],{},"TimeoutStopSec=20"," tells systemd how long to wait before escalating to ",[14,1268,1269],{},"SIGKILL",[10,1271,1272,1273,1275,1276,1279,1280,1283,1284,1287,1288,1290],{},"The watchdog is the feature that justifies ",[14,1274,46],{}," for most CLIs. A process that has deadlocked on a network call or a stuck lock is still ",[36,1277,1278],{},"running",", so ",[14,1281,1282],{},"Restart=on-failure"," alone would never notice. With ",[14,1285,1286],{},"WatchdogSec=30",", systemd kills and restarts the service if no ",[14,1289,118],{}," arrives for 30 seconds. Ping from the place that proves real progress — the main loop, not a separate thread that keeps ticking while the work is stuck.",[132,1292,1294],{"id":1293},"the-unit-file-and-an-install-command","The unit file and an install command",[137,1296,1298],{"className":139,"code":1297,"language":141,"meta":142,"style":142},"# src\u002Fmytool\u002Fcli.py\nimport subprocess\nfrom typing import Annotated\n\nimport typer\n\nfrom mytool.service import render_unit, run_agent, user_unit_path\n\napp = typer.Typer()\nservice = typer.Typer(help=\"Run mytool as a background service (systemd).\")\napp.add_typer(service, name=\"service\")\n\n\n@app.command()\ndef agent(interval: float = 60.0) -> None:\n    \"\"\"Run the sync loop in the foreground (systemd starts this).\"\"\"\n    run_agent(lambda: 3, interval=interval)\n\n\n@service.command(\"unit\")\ndef print_unit() -> None:\n    \"\"\"Print the systemd unit file.\"\"\"\n    typer.echo(render_unit(), nl=False)\n\n\n@service.command()\ndef install(start: Annotated[bool, typer.Option(help=\"Enable and start it now.\")] = True) -> None:\n    \"\"\"Install a systemd user service for the current user.\"\"\"\n    path = user_unit_path()\n    path.parent.mkdir(parents=True, exist_ok=True)\n    path.write_text(render_unit(), encoding=\"utf-8\")\n    typer.echo(f\"wrote {path}\", err=True)\n    subprocess.run([\"systemctl\", \"--user\", \"daemon-reload\"], check=True)\n    if start:\n        subprocess.run([\"systemctl\", \"--user\", \"enable\", \"--now\", \"mytool.service\"], check=True)\n        typer.echo(\"started; follow logs with: journalctl --user -u mytool -f\", err=True)\n",[14,1299,1300,1305,1312,1324,1328,1335,1339,1351,1355,1365,1385,1400,1404,1408,1416,1439,1444,1466,1470,1474,1486,1499,1504,1519,1523,1527,1533,1569,1574,1584,1608,1623,1653,1683,1690,1726],{"__ignoreMap":142},[146,1301,1302],{"class":148,"line":149},[146,1303,1304],{"class":152},"# src\u002Fmytool\u002Fcli.py\n",[146,1306,1307,1309],{"class":148,"line":156},[146,1308,191],{"class":166},[146,1310,1311],{"class":177}," subprocess\n",[146,1313,1314,1316,1319,1321],{"class":148,"line":163},[146,1315,167],{"class":166},[146,1317,1318],{"class":177}," typing ",[146,1320,191],{"class":166},[146,1322,1323],{"class":177}," Annotated\n",[146,1325,1326],{"class":148,"line":181},[146,1327,185],{"emptyLinePlaceholder":184},[146,1329,1330,1332],{"class":148,"line":188},[146,1331,191],{"class":166},[146,1333,1334],{"class":177}," typer\n",[146,1336,1337],{"class":148,"line":197},[146,1338,185],{"emptyLinePlaceholder":184},[146,1340,1341,1343,1346,1348],{"class":148,"line":205},[146,1342,167],{"class":166},[146,1344,1345],{"class":177}," mytool.service ",[146,1347,191],{"class":166},[146,1349,1350],{"class":177}," render_unit, run_agent, user_unit_path\n",[146,1352,1353],{"class":148,"line":210},[146,1354,185],{"emptyLinePlaceholder":184},[146,1356,1357,1360,1362],{"class":148,"line":215},[146,1358,1359],{"class":177},"app ",[146,1361,258],{"class":166},[146,1363,1364],{"class":177}," typer.Typer()\n",[146,1366,1367,1370,1372,1375,1378,1380,1383],{"class":148,"line":246},[146,1368,1369],{"class":177},"service ",[146,1371,258],{"class":166},[146,1373,1374],{"class":177}," typer.Typer(",[146,1376,1377],{"class":868},"help",[146,1379,258],{"class":166},[146,1381,1382],{"class":159},"\"Run mytool as a background service (systemd).\"",[146,1384,267],{"class":177},[146,1386,1387,1390,1393,1395,1398],{"class":148,"line":252},[146,1388,1389],{"class":177},"app.add_typer(service, ",[146,1391,1392],{"class":868},"name",[146,1394,258],{"class":166},[146,1396,1397],{"class":159},"\"service\"",[146,1399,267],{"class":177},[146,1401,1402],{"class":148,"line":270},[146,1403,185],{"emptyLinePlaceholder":184},[146,1405,1406],{"class":148,"line":282},[146,1407,185],{"emptyLinePlaceholder":184},[146,1409,1410,1413],{"class":148,"line":291},[146,1411,1412],{"class":221},"@app.command",[146,1414,1415],{"class":177},"()\n",[146,1417,1418,1420,1423,1426,1428,1430,1433,1435,1437],{"class":148,"line":308},[146,1419,218],{"class":166},[146,1421,1422],{"class":221}," agent",[146,1424,1425],{"class":177},"(interval: ",[146,1427,425],{"class":170},[146,1429,662],{"class":166},[146,1431,1432],{"class":170}," 60.0",[146,1434,237],{"class":177},[146,1436,958],{"class":170},[146,1438,243],{"class":177},[146,1440,1441],{"class":148,"line":337},[146,1442,1443],{"class":159},"    \"\"\"Run the sync loop in the foreground (systemd starts this).\"\"\"\n",[146,1445,1446,1449,1451,1454,1457,1459,1461,1463],{"class":148,"line":373},[146,1447,1448],{"class":177},"    run_agent(",[146,1450,1009],{"class":166},[146,1452,1453],{"class":177},": ",[146,1455,1456],{"class":170},"3",[146,1458,115],{"class":177},[146,1460,1255],{"class":868},[146,1462,258],{"class":166},[146,1464,1465],{"class":177},"interval)\n",[146,1467,1468],{"class":148,"line":379},[146,1469,185],{"emptyLinePlaceholder":184},[146,1471,1472],{"class":148,"line":395},[146,1473,185],{"emptyLinePlaceholder":184},[146,1475,1476,1479,1481,1484],{"class":148,"line":404},[146,1477,1478],{"class":221},"@service.command",[146,1480,225],{"class":177},[146,1482,1483],{"class":159},"\"unit\"",[146,1485,267],{"class":177},[146,1487,1488,1490,1493,1495,1497],{"class":148,"line":409},[146,1489,218],{"class":166},[146,1491,1492],{"class":221}," print_unit",[146,1494,422],{"class":177},[146,1496,958],{"class":170},[146,1498,243],{"class":177},[146,1500,1501],{"class":148,"line":414},[146,1502,1503],{"class":159},"    \"\"\"Print the systemd unit file.\"\"\"\n",[146,1505,1506,1509,1512,1514,1517],{"class":148,"line":435},[146,1507,1508],{"class":177},"    typer.echo(render_unit(), ",[146,1510,1511],{"class":868},"nl",[146,1513,258],{"class":166},[146,1515,1516],{"class":170},"False",[146,1518,267],{"class":177},[146,1520,1521],{"class":148,"line":441},[146,1522,185],{"emptyLinePlaceholder":184},[146,1524,1525],{"class":148,"line":456},[146,1526,185],{"emptyLinePlaceholder":184},[146,1528,1529,1531],{"class":148,"line":471},[146,1530,1478],{"class":221},[146,1532,1415],{"class":177},[146,1534,1535,1537,1540,1543,1545,1548,1550,1552,1555,1558,1560,1563,1565,1567],{"class":148,"line":502},[146,1536,218],{"class":166},[146,1538,1539],{"class":221}," install",[146,1541,1542],{"class":177},"(start: Annotated[",[146,1544,240],{"class":170},[146,1546,1547],{"class":177},", typer.Option(",[146,1549,1377],{"class":868},[146,1551,258],{"class":166},[146,1553,1554],{"class":159},"\"Enable and start it now.\"",[146,1556,1557],{"class":177},")] ",[146,1559,258],{"class":166},[146,1561,1562],{"class":170}," True",[146,1564,237],{"class":177},[146,1566,958],{"class":170},[146,1568,243],{"class":177},[146,1570,1571],{"class":148,"line":510},[146,1572,1573],{"class":159},"    \"\"\"Install a systemd user service for the current user.\"\"\"\n",[146,1575,1576,1579,1581],{"class":148,"line":751},[146,1577,1578],{"class":177},"    path ",[146,1580,258],{"class":166},[146,1582,1583],{"class":177}," user_unit_path()\n",[146,1585,1586,1589,1592,1594,1597,1599,1602,1604,1606],{"class":148,"line":757},[146,1587,1588],{"class":177},"    path.parent.mkdir(",[146,1590,1591],{"class":868},"parents",[146,1593,258],{"class":166},[146,1595,1596],{"class":170},"True",[146,1598,115],{"class":177},[146,1600,1601],{"class":868},"exist_ok",[146,1603,258],{"class":166},[146,1605,1596],{"class":170},[146,1607,267],{"class":177},[146,1609,1610,1613,1616,1618,1621],{"class":148,"line":762},[146,1611,1612],{"class":177},"    path.write_text(render_unit(), ",[146,1614,1615],{"class":868},"encoding",[146,1617,258],{"class":166},[146,1619,1620],{"class":159},"\"utf-8\"",[146,1622,267],{"class":177},[146,1624,1625,1628,1630,1633,1635,1638,1640,1642,1644,1647,1649,1651],{"class":148,"line":768},[146,1626,1627],{"class":177},"    typer.echo(",[146,1629,1084],{"class":166},[146,1631,1632],{"class":159},"\"wrote ",[146,1634,845],{"class":170},[146,1636,1637],{"class":177},"path",[146,1639,851],{"class":170},[146,1641,322],{"class":159},[146,1643,115],{"class":177},[146,1645,1646],{"class":868},"err",[146,1648,258],{"class":166},[146,1650,1596],{"class":170},[146,1652,267],{"class":177},[146,1654,1655,1658,1661,1663,1666,1668,1671,1674,1677,1679,1681],{"class":148,"line":774},[146,1656,1657],{"class":177},"    subprocess.run([",[146,1659,1660],{"class":159},"\"systemctl\"",[146,1662,115],{"class":177},[146,1664,1665],{"class":159},"\"--user\"",[146,1667,115],{"class":177},[146,1669,1670],{"class":159},"\"daemon-reload\"",[146,1672,1673],{"class":177},"], ",[146,1675,1676],{"class":868},"check",[146,1678,258],{"class":166},[146,1680,1596],{"class":170},[146,1682,267],{"class":177},[146,1684,1685,1687],{"class":148,"line":780},[146,1686,273],{"class":166},[146,1688,1689],{"class":177}," start:\n",[146,1691,1692,1695,1697,1699,1701,1703,1706,1708,1711,1713,1716,1718,1720,1722,1724],{"class":148,"line":785},[146,1693,1694],{"class":177},"        subprocess.run([",[146,1696,1660],{"class":159},[146,1698,115],{"class":177},[146,1700,1665],{"class":159},[146,1702,115],{"class":177},[146,1704,1705],{"class":159},"\"enable\"",[146,1707,115],{"class":177},[146,1709,1710],{"class":159},"\"--now\"",[146,1712,115],{"class":177},[146,1714,1715],{"class":159},"\"mytool.service\"",[146,1717,1673],{"class":177},[146,1719,1676],{"class":868},[146,1721,258],{"class":166},[146,1723,1596],{"class":170},[146,1725,267],{"class":177},[146,1727,1728,1731,1734,1736,1738,1740,1742],{"class":148,"line":790},[146,1729,1730],{"class":177},"        typer.echo(",[146,1732,1733],{"class":159},"\"started; follow logs with: journalctl --user -u mytool -f\"",[146,1735,115],{"class":177},[146,1737,1646],{"class":868},[146,1739,258],{"class":166},[146,1741,1596],{"class":170},[146,1743,267],{"class":177},[10,1745,1746,1749,1750,1752,1753,1756,1757,1760,1761,520,1764,1767,1768,1770,1771,1774,1775,1778,1779,1782,1783,1786,1787,1790,1791,1794,1795,66],{},[14,1747,1748],{},"service unit"," prints the unit for inspection or for packagers; ",[14,1751,54],{}," writes it to ",[14,1754,1755],{},"~\u002F.config\u002Fsystemd\u002Fuser\u002F"," and enables it with ",[14,1758,1759],{},"systemctl --user",". The unit’s settings each have a job: ",[14,1762,1763],{},"After=",[14,1765,1766],{},"Wants=network-online.target"," delays start until the network is up; ",[14,1769,1282],{}," with ",[14,1772,1773],{},"RestartSec=5"," restarts after crashes without spinning; ",[14,1776,1777],{},"PYTHONUNBUFFERED=1"," makes log lines appear in the journal immediately rather than in 8 KB bursts; ",[14,1780,1781],{},"NoNewPrivileges"," and ",[14,1784,1785],{},"PrivateTmp"," are cheap hardening that rarely affect a CLI. ",[14,1788,1789],{},"ExecStart"," must be an absolute path, so the generator resolves the installed ",[14,1792,1793],{},"mytool"," executable — for a pipx or uv tool install that is the shim in ",[14,1796,1797],{},"~\u002F.local\u002Fbin",[91,1799],{"name":1800},"sysd-terminal",[68,1802,1804],{"id":1803},"user-services-or-system-services","User services or system services",[10,1806,1807,1808,1811,1812,1814,1815,1818,1819,1822,1823,1826,1827,1830,1831,1834,1835,1838,1839,1842],{},"A ",[27,1809,1810],{},"user service"," runs as the user, starts when they log in, and is managed with ",[14,1813,1759],{}," — no root needed, which suits personal agents such as sync tools. To keep it running when the user is logged out (on a server, for instance), an administrator enables lingering once: ",[14,1816,1817],{},"loginctl enable-linger username",". A ",[27,1820,1821],{},"system service"," in ",[14,1824,1825],{},"\u002Fetc\u002Fsystemd\u002Fsystem\u002F"," starts at boot and should run as a dedicated unprivileged user (",[14,1828,1829],{},"User=mytool",", or ",[14,1832,1833],{},"DynamicUser=yes"," for stateless services); packaging the unit with a ",[14,1836,1837],{},".deb"," or ",[14,1840,1841],{},".rpm"," is the usual way to ship one. The program code is identical for both.",[68,1844,1846],{"id":1845},"ux-considerations","UX considerations",[73,1848,1849,1863,1876,1882,1899],{},[76,1850,1851,1854,1855,1858,1859,66],{},[27,1852,1853],{},"Log to stdout\u002Fstderr, not files."," The journal captures both with timestamps and metadata; ",[14,1856,1857],{},"journalctl --user -u mytool -f"," follows them. Structured fields are covered in ",[57,1860,1862],{"href":1861},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog\u002F","sending CLI logs to journald and syslog",[76,1864,1865,1872,1873,1875],{},[27,1866,1867,1868,1871],{},"Keep ",[14,1869,1870],{},"STATUS="," short and current."," It is the line users see first in ",[14,1874,114],{},"; “synced 3 files at 14:02:11” beats “running”.",[76,1877,1878,1881],{},[27,1879,1880],{},"Print the next step."," After installing, tell users how to see logs and how to stop the service.",[76,1883,1884,1887,1888,1891,1892,1895,1896,66],{},[27,1885,1886],{},"Offer an uninstall."," ",[14,1889,1890],{},"service uninstall"," should ",[14,1893,1894],{},"disable --now",", remove the file and ",[14,1897,1898],{},"daemon-reload",[76,1900,1901,1904,1905,1908,1909,66],{},[27,1902,1903],{},"Support reload."," If the agent reads configuration, implement ",[14,1906,1907],{},"SIGHUP"," reloading as in ",[57,1910,1912],{"href":1911},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup\u002F","reloading config on SIGHUP",[91,1914],{"name":1915},"sysd-unit",[68,1917,1919],{"id":1918},"testing-the-behaviour","Testing the behaviour",[10,1921,1922,1923,1925,1926,1929],{},"The notify protocol is easy to test without systemd: bind a Unix datagram socket in a temporary directory, point ",[14,1924,102],{}," at it, run the loop and read what arrived. A real ",[14,1927,1928],{},"systemd-analyze verify"," checks the unit file where systemd is available:",[137,1931,1933],{"className":139,"code":1932,"language":141,"meta":142,"style":142},"# tests\u002Ftest_service.py\nimport os\nimport shutil\nimport socket\nimport subprocess\nimport threading\n\nimport pytest\n\nfrom mytool.sdnotify import notify, watchdog_interval\nfrom mytool.service import render_unit, run_agent\n\n\n@pytest.fixture\ndef notify_socket(tmp_path, monkeypatch):\n    path = str(tmp_path \u002F \"notify.sock\")\n    sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)\n    sock.bind(path)\n    sock.settimeout(2)\n    monkeypatch.setenv(\"NOTIFY_SOCKET\", path)\n    yield sock\n    sock.close()\n\n\ndef received(sock) -> list[str]:\n    messages = []\n    sock.setblocking(False)\n    while True:\n        try:\n            messages.append(sock.recv(4096).decode())\n        except BlockingIOError:\n            return messages\n\n\ndef test_notify_is_a_noop_outside_systemd(monkeypatch):\n    monkeypatch.delenv(\"NOTIFY_SOCKET\", raising=False)\n    assert notify(\"READY=1\") is False\n\n\ndef test_agent_reports_ready_status_and_stopping(notify_socket):\n    stop = threading.Event()\n    calls = []\n\n    def work():\n        calls.append(1)\n        stop.set()                                     # one iteration, then shut down\n        return 7\n\n    run_agent(work, interval=0.1, stop=stop)\n    messages = received(notify_socket)\n    assert messages[0].startswith(\"READY=1\\n\")\n    assert any(m.startswith(\"STATUS=synced 7 files\") for m in messages)\n    assert messages[-1] == \"STOPPING=1\"\n\n\ndef test_watchdog_is_fed_during_long_waits(notify_socket, monkeypatch):\n    monkeypatch.setenv(\"WATCHDOG_USEC\", \"400000\")      # WatchdogSec=0.4 → ping every 0.2 s\n    monkeypatch.setenv(\"WATCHDOG_PID\", str(os.getpid()))\n    assert watchdog_interval() == 0.2\n    stop = threading.Event()\n    threading.Timer(1.5, stop.set).start()\n    run_agent(lambda: 0, interval=10, stop=stop)\n    assert received(notify_socket).count(\"WATCHDOG=1\") >= 1\n\n\ndef test_watchdog_ignores_other_pids(monkeypatch):\n    monkeypatch.setenv(\"WATCHDOG_USEC\", \"1000000\")\n    monkeypatch.setenv(\"WATCHDOG_PID\", \"1\")\n    assert watchdog_interval() is None\n\n\n@pytest.mark.skipif(not shutil.which(\"systemd-analyze\"), reason=\"needs systemd\")\ndef test_unit_file_is_valid(tmp_path):\n    unit = tmp_path \u002F \"mytool.service\"\n    unit.write_text(render_unit(exe=\"\u002Fbin\u002Ftrue\"))\n    result = subprocess.run([\"systemd-analyze\", \"--user\", \"verify\", str(unit)],\n                            capture_output=True, text=True)\n    assert result.returncode == 0, result.stderr\n",[14,1934,1935,1940,1946,1952,1958,1964,1970,1974,1981,1985,1995,2006,2010,2014,2019,2029,2048,2065,2070,2080,2090,2098,2103,2107,2111,2126,2136,2145,2153,2160,2171,2181,2189,2193,2197,2207,2225,2241,2245,2249,2259,2267,2276,2280,2291,2300,2308,2315,2319,2341,2350,2372,2399,2418,2422,2426,2436,2453,2466,2478,2486,2497,2524,2540,2545,2550,2560,2574,2588,2599,2604,2609,2638,2649,2665,2681,2710,2731],{"__ignoreMap":142},[146,1936,1937],{"class":148,"line":149},[146,1938,1939],{"class":152},"# tests\u002Ftest_service.py\n",[146,1941,1942,1944],{"class":148,"line":156},[146,1943,191],{"class":166},[146,1945,194],{"class":177},[146,1947,1948,1950],{"class":148,"line":163},[146,1949,191],{"class":166},[146,1951,594],{"class":177},[146,1953,1954,1956],{"class":148,"line":181},[146,1955,191],{"class":166},[146,1957,202],{"class":177},[146,1959,1960,1962],{"class":148,"line":188},[146,1961,191],{"class":166},[146,1963,1311],{"class":177},[146,1965,1966,1968],{"class":148,"line":197},[146,1967,191],{"class":166},[146,1969,615],{"class":177},[146,1971,1972],{"class":148,"line":205},[146,1973,185],{"emptyLinePlaceholder":184},[146,1975,1976,1978],{"class":148,"line":210},[146,1977,191],{"class":166},[146,1979,1980],{"class":177}," pytest\n",[146,1982,1983],{"class":148,"line":215},[146,1984,185],{"emptyLinePlaceholder":184},[146,1986,1987,1989,1991,1993],{"class":148,"line":246},[146,1988,167],{"class":166},[146,1990,645],{"class":177},[146,1992,191],{"class":166},[146,1994,650],{"class":177},[146,1996,1997,1999,2001,2003],{"class":148,"line":252},[146,1998,167],{"class":166},[146,2000,1345],{"class":177},[146,2002,191],{"class":166},[146,2004,2005],{"class":177}," render_unit, run_agent\n",[146,2007,2008],{"class":148,"line":270},[146,2009,185],{"emptyLinePlaceholder":184},[146,2011,2012],{"class":148,"line":282},[146,2013,185],{"emptyLinePlaceholder":184},[146,2015,2016],{"class":148,"line":291},[146,2017,2018],{"class":221},"@pytest.fixture\n",[146,2020,2021,2023,2026],{"class":148,"line":308},[146,2022,218],{"class":166},[146,2024,2025],{"class":221}," notify_socket",[146,2027,2028],{"class":177},"(tmp_path, monkeypatch):\n",[146,2030,2031,2033,2035,2038,2041,2043,2046],{"class":148,"line":337},[146,2032,1578],{"class":177},[146,2034,258],{"class":166},[146,2036,2037],{"class":170}," str",[146,2039,2040],{"class":177},"(tmp_path ",[146,2042,520],{"class":166},[146,2044,2045],{"class":159}," \"notify.sock\"",[146,2047,267],{"class":177},[146,2049,2050,2053,2055,2057,2059,2061,2063],{"class":148,"line":373},[146,2051,2052],{"class":177},"    sock ",[146,2054,258],{"class":166},[146,2056,343],{"class":177},[146,2058,346],{"class":170},[146,2060,349],{"class":177},[146,2062,352],{"class":170},[146,2064,267],{"class":177},[146,2066,2067],{"class":148,"line":379},[146,2068,2069],{"class":177},"    sock.bind(path)\n",[146,2071,2072,2075,2078],{"class":148,"line":395},[146,2073,2074],{"class":177},"    sock.settimeout(",[146,2076,2077],{"class":170},"2",[146,2079,267],{"class":177},[146,2081,2082,2085,2087],{"class":148,"line":404},[146,2083,2084],{"class":177},"    monkeypatch.setenv(",[146,2086,264],{"class":159},[146,2088,2089],{"class":177},", path)\n",[146,2091,2092,2095],{"class":148,"line":409},[146,2093,2094],{"class":166},"    yield",[146,2096,2097],{"class":177}," sock\n",[146,2099,2100],{"class":148,"line":414},[146,2101,2102],{"class":177},"    sock.close()\n",[146,2104,2105],{"class":148,"line":435},[146,2106,185],{"emptyLinePlaceholder":184},[146,2108,2109],{"class":148,"line":441},[146,2110,185],{"emptyLinePlaceholder":184},[146,2112,2113,2115,2118,2121,2123],{"class":148,"line":456},[146,2114,218],{"class":166},[146,2116,2117],{"class":221}," received",[146,2119,2120],{"class":177},"(sock) -> list[",[146,2122,234],{"class":170},[146,2124,2125],{"class":177},"]:\n",[146,2127,2128,2131,2133],{"class":148,"line":471},[146,2129,2130],{"class":177},"    messages ",[146,2132,258],{"class":166},[146,2134,2135],{"class":177}," []\n",[146,2137,2138,2141,2143],{"class":148,"line":502},[146,2139,2140],{"class":177},"    sock.setblocking(",[146,2142,1516],{"class":170},[146,2144,267],{"class":177},[146,2146,2147,2149,2151],{"class":148,"line":510},[146,2148,1059],{"class":166},[146,2150,1562],{"class":170},[146,2152,243],{"class":177},[146,2154,2155,2158],{"class":148,"line":751},[146,2156,2157],{"class":166},"        try",[146,2159,243],{"class":177},[146,2161,2162,2165,2168],{"class":148,"line":757},[146,2163,2164],{"class":177},"            messages.append(sock.recv(",[146,2166,2167],{"class":170},"4096",[146,2169,2170],{"class":177},").decode())\n",[146,2172,2173,2176,2179],{"class":148,"line":762},[146,2174,2175],{"class":166},"        except",[146,2177,2178],{"class":170}," BlockingIOError",[146,2180,243],{"class":177},[146,2182,2183,2186],{"class":148,"line":768},[146,2184,2185],{"class":166},"            return",[146,2187,2188],{"class":177}," messages\n",[146,2190,2191],{"class":148,"line":774},[146,2192,185],{"emptyLinePlaceholder":184},[146,2194,2195],{"class":148,"line":780},[146,2196,185],{"emptyLinePlaceholder":184},[146,2198,2199,2201,2204],{"class":148,"line":785},[146,2200,218],{"class":166},[146,2202,2203],{"class":221}," test_notify_is_a_noop_outside_systemd",[146,2205,2206],{"class":177},"(monkeypatch):\n",[146,2208,2209,2212,2214,2216,2219,2221,2223],{"class":148,"line":790},[146,2210,2211],{"class":177},"    monkeypatch.delenv(",[146,2213,264],{"class":159},[146,2215,115],{"class":177},[146,2217,2218],{"class":868},"raising",[146,2220,258],{"class":166},[146,2222,1516],{"class":170},[146,2224,267],{"class":177},[146,2226,2227,2230,2233,2235,2237,2239],{"class":148,"line":817},[146,2228,2229],{"class":166},"    assert",[146,2231,2232],{"class":177}," notify(",[146,2234,1035],{"class":159},[146,2236,364],{"class":177},[146,2238,993],{"class":166},[146,2240,288],{"class":170},[146,2242,2243],{"class":148,"line":857},[146,2244,185],{"emptyLinePlaceholder":184},[146,2246,2247],{"class":148,"line":877},[146,2248,185],{"emptyLinePlaceholder":184},[146,2250,2251,2253,2256],{"class":148,"line":882},[146,2252,218],{"class":166},[146,2254,2255],{"class":221}," test_agent_reports_ready_status_and_stopping",[146,2257,2258],{"class":177},"(notify_socket):\n",[146,2260,2261,2263,2265],{"class":148,"line":887},[146,2262,972],{"class":177},[146,2264,258],{"class":166},[146,2266,982],{"class":177},[146,2268,2269,2272,2274],{"class":148,"line":898},[146,2270,2271],{"class":177},"    calls ",[146,2273,258],{"class":166},[146,2275,2135],{"class":177},[146,2277,2278],{"class":148,"line":911},[146,2279,185],{"emptyLinePlaceholder":184},[146,2281,2282,2285,2288],{"class":148,"line":916},[146,2283,2284],{"class":166},"    def",[146,2286,2287],{"class":221}," work",[146,2289,2290],{"class":177},"():\n",[146,2292,2293,2296,2298],{"class":148,"line":921},[146,2294,2295],{"class":177},"        calls.append(",[146,2297,331],{"class":170},[146,2299,267],{"class":177},[146,2301,2302,2305],{"class":148,"line":963},[146,2303,2304],{"class":177},"        stop.set()                                     ",[146,2306,2307],{"class":152},"# one iteration, then shut down\n",[146,2309,2310,2312],{"class":148,"line":969},[146,2311,285],{"class":166},[146,2313,2314],{"class":170}," 7\n",[146,2316,2317],{"class":148,"line":985},[146,2318,185],{"emptyLinePlaceholder":184},[146,2320,2321,2324,2326,2328,2331,2333,2336,2338],{"class":148,"line":999},[146,2322,2323],{"class":177},"    run_agent(work, ",[146,2325,1255],{"class":868},[146,2327,258],{"class":166},[146,2329,2330],{"class":170},"0.1",[146,2332,115],{"class":177},[146,2334,2335],{"class":868},"stop",[146,2337,258],{"class":166},[146,2339,2340],{"class":177},"stop)\n",[146,2342,2343,2345,2347],{"class":148,"line":1018},[146,2344,2130],{"class":177},[146,2346,258],{"class":166},[146,2348,2349],{"class":177}," received(notify_socket)\n",[146,2351,2352,2354,2357,2360,2363,2366,2368,2370],{"class":148,"line":1029},[146,2353,2229],{"class":166},[146,2355,2356],{"class":177}," messages[",[146,2358,2359],{"class":170},"0",[146,2361,2362],{"class":177},"].startswith(",[146,2364,2365],{"class":159},"\"READY=1",[146,2367,387],{"class":170},[146,2369,322],{"class":159},[146,2371,267],{"class":177},[146,2373,2374,2376,2379,2382,2385,2387,2390,2393,2396],{"class":148,"line":1045},[146,2375,2229],{"class":166},[146,2377,2378],{"class":170}," any",[146,2380,2381],{"class":177},"(m.startswith(",[146,2383,2384],{"class":159},"\"STATUS=synced 7 files\"",[146,2386,364],{"class":177},[146,2388,2389],{"class":166},"for",[146,2391,2392],{"class":177}," m ",[146,2394,2395],{"class":166},"in",[146,2397,2398],{"class":177}," messages)\n",[146,2400,2401,2403,2405,2407,2409,2412,2415],{"class":148,"line":1056},[146,2402,2229],{"class":166},[146,2404,2356],{"class":177},[146,2406,1174],{"class":166},[146,2408,331],{"class":170},[146,2410,2411],{"class":177},"] ",[146,2413,2414],{"class":166},"==",[146,2416,2417],{"class":159}," \"STOPPING=1\"\n",[146,2419,2420],{"class":148,"line":1067},[146,2421,185],{"emptyLinePlaceholder":184},[146,2423,2424],{"class":148,"line":1078},[146,2425,185],{"emptyLinePlaceholder":184},[146,2427,2428,2430,2433],{"class":148,"line":1117},[146,2429,218],{"class":166},[146,2431,2432],{"class":221}," test_watchdog_is_fed_during_long_waits",[146,2434,2435],{"class":177},"(notify_socket, monkeypatch):\n",[146,2437,2438,2440,2442,2444,2447,2450],{"class":148,"line":1123},[146,2439,2084],{"class":177},[146,2441,451],{"class":159},[146,2443,115],{"class":177},[146,2445,2446],{"class":159},"\"400000\"",[146,2448,2449],{"class":177},")      ",[146,2451,2452],{"class":152},"# WatchdogSec=0.4 → ping every 0.2 s\n",[146,2454,2455,2457,2459,2461,2463],{"class":148,"line":1140},[146,2456,2084],{"class":177},[146,2458,466],{"class":159},[146,2460,115],{"class":177},[146,2462,234],{"class":170},[146,2464,2465],{"class":177},"(os.getpid()))\n",[146,2467,2468,2470,2473,2475],{"class":148,"line":1161},[146,2469,2229],{"class":166},[146,2471,2472],{"class":177}," watchdog_interval() ",[146,2474,2414],{"class":166},[146,2476,2477],{"class":170}," 0.2\n",[146,2479,2480,2482,2484],{"class":148,"line":1186},[146,2481,972],{"class":177},[146,2483,258],{"class":166},[146,2485,982],{"class":177},[146,2487,2488,2491,2494],{"class":148,"line":1197},[146,2489,2490],{"class":177},"    threading.Timer(",[146,2492,2493],{"class":170},"1.5",[146,2495,2496],{"class":177},", stop.set).start()\n",[146,2498,2499,2501,2503,2505,2507,2509,2511,2513,2516,2518,2520,2522],{"class":148,"line":1207},[146,2500,1448],{"class":177},[146,2502,1009],{"class":166},[146,2504,1453],{"class":177},[146,2506,2359],{"class":170},[146,2508,115],{"class":177},[146,2510,1255],{"class":868},[146,2512,258],{"class":166},[146,2514,2515],{"class":170},"10",[146,2517,115],{"class":177},[146,2519,2335],{"class":868},[146,2521,258],{"class":166},[146,2523,2340],{"class":177},[146,2525,2526,2528,2531,2533,2535,2537],{"class":148,"line":1239},[146,2527,2229],{"class":166},[146,2529,2530],{"class":177}," received(notify_socket).count(",[146,2532,1192],{"class":159},[146,2534,364],{"class":177},[146,2536,1180],{"class":166},[146,2538,2539],{"class":170}," 1\n",[146,2541,2543],{"class":148,"line":2542},64,[146,2544,185],{"emptyLinePlaceholder":184},[146,2546,2548],{"class":148,"line":2547},65,[146,2549,185],{"emptyLinePlaceholder":184},[146,2551,2553,2555,2558],{"class":148,"line":2552},66,[146,2554,218],{"class":166},[146,2556,2557],{"class":221}," test_watchdog_ignores_other_pids",[146,2559,2206],{"class":177},[146,2561,2563,2565,2567,2569,2572],{"class":148,"line":2562},67,[146,2564,2084],{"class":177},[146,2566,451],{"class":159},[146,2568,115],{"class":177},[146,2570,2571],{"class":159},"\"1000000\"",[146,2573,267],{"class":177},[146,2575,2577,2579,2581,2583,2586],{"class":148,"line":2576},68,[146,2578,2084],{"class":177},[146,2580,466],{"class":159},[146,2582,115],{"class":177},[146,2584,2585],{"class":159},"\"1\"",[146,2587,267],{"class":177},[146,2589,2591,2593,2595,2597],{"class":148,"line":2590},69,[146,2592,2229],{"class":166},[146,2594,2472],{"class":177},[146,2596,993],{"class":166},[146,2598,507],{"class":170},[146,2600,2602],{"class":148,"line":2601},70,[146,2603,185],{"emptyLinePlaceholder":184},[146,2605,2607],{"class":148,"line":2606},71,[146,2608,185],{"emptyLinePlaceholder":184},[146,2610,2612,2615,2617,2620,2622,2625,2628,2631,2633,2636],{"class":148,"line":2611},72,[146,2613,2614],{"class":221},"@pytest.mark.skipif",[146,2616,225],{"class":177},[146,2618,2619],{"class":166},"not",[146,2621,830],{"class":177},[146,2623,2624],{"class":159},"\"systemd-analyze\"",[146,2626,2627],{"class":177},"), ",[146,2629,2630],{"class":868},"reason",[146,2632,258],{"class":166},[146,2634,2635],{"class":159},"\"needs systemd\"",[146,2637,267],{"class":177},[146,2639,2641,2643,2646],{"class":148,"line":2640},73,[146,2642,218],{"class":166},[146,2644,2645],{"class":221}," test_unit_file_is_valid",[146,2647,2648],{"class":177},"(tmp_path):\n",[146,2650,2652,2655,2657,2660,2662],{"class":148,"line":2651},74,[146,2653,2654],{"class":177},"    unit ",[146,2656,258],{"class":166},[146,2658,2659],{"class":177}," tmp_path ",[146,2661,520],{"class":166},[146,2663,2664],{"class":159}," \"mytool.service\"\n",[146,2666,2668,2671,2673,2675,2678],{"class":148,"line":2667},75,[146,2669,2670],{"class":177},"    unit.write_text(render_unit(",[146,2672,869],{"class":868},[146,2674,258],{"class":166},[146,2676,2677],{"class":159},"\"\u002Fbin\u002Ftrue\"",[146,2679,2680],{"class":177},"))\n",[146,2682,2684,2687,2689,2692,2694,2696,2698,2700,2703,2705,2707],{"class":148,"line":2683},76,[146,2685,2686],{"class":177},"    result ",[146,2688,258],{"class":166},[146,2690,2691],{"class":177}," subprocess.run([",[146,2693,2624],{"class":159},[146,2695,115],{"class":177},[146,2697,1665],{"class":159},[146,2699,115],{"class":177},[146,2701,2702],{"class":159},"\"verify\"",[146,2704,115],{"class":177},[146,2706,234],{"class":170},[146,2708,2709],{"class":177},"(unit)],\n",[146,2711,2713,2716,2718,2720,2722,2725,2727,2729],{"class":148,"line":2712},77,[146,2714,2715],{"class":868},"                            capture_output",[146,2717,258],{"class":166},[146,2719,1596],{"class":170},[146,2721,115],{"class":177},[146,2723,2724],{"class":868},"text",[146,2726,258],{"class":166},[146,2728,1596],{"class":170},[146,2730,267],{"class":177},[146,2732,2734,2736,2739,2741,2744],{"class":148,"line":2733},78,[146,2735,2229],{"class":166},[146,2737,2738],{"class":177}," result.returncode ",[146,2740,2414],{"class":166},[146,2742,2743],{"class":170}," 0",[146,2745,2746],{"class":177},", result.stderr\n",[10,2748,2749,2750,2752,2753,2755],{},"The watchdog test sets a 0.4-second watchdog and a 10-second work interval, then stops the loop after 1.5 seconds; at least one ",[14,2751,118],{}," must have arrived, which proves long waits do not starve the watchdog. ",[14,2754,1928],{}," catches typos in directive names and invalid values — the kind of error that otherwise appears only as a cryptic failure when a user installs the service.",[68,2757,2759],{"id":2758},"conclusion","Conclusion",[10,2761,2762,2763,2765,2766,2768,2769,2771,2772,2774,2775,1262,2778,2780,2781,2783,2784,2786,2787,2789,2790,2792],{},"A long-running CLI becomes a reliable service with a small unit file and a few notifications. Use ",[14,2764,46],{},"; send ",[14,2767,106],{}," after setup, a short ",[14,2770,1870],{}," after each unit of work, ",[14,2773,118],{}," from the main loop at half of ",[14,2776,2777],{},"WatchdogSec",[14,2779,122],{}," on ",[14,2782,126],{},"; implement ",[14,2785,50],{}," with a datagram socket and make it a no-op elsewhere; generate the unit with an absolute ",[14,2788,1789],{},", restart and hardening options; install user services with ",[14,2791,1759],{},"; log to stdout; and test the protocol against a temporary socket.",[68,2794,2796],{"id":2795},"frequently-asked-questions","Frequently asked questions",[132,2798,2800,2801,1838,2803,2806],{"id":2799},"do-i-need-the-systemd-or-sdnotify-python-packages","Do I need the ",[14,2802,29],{},[14,2804,2805],{},"sdnotify"," Python packages?",[10,2808,2809],{},"No. The protocol is a single datagram per message, and the fifteen-line function above covers it. The packages are convenient, but every dependency is something to install on servers.",[132,2811,2813],{"id":2812},"what-if-my-cli-also-runs-on-macos-or-windows","What if my CLI also runs on macOS or Windows?",[10,2815,2816,2818,2819,2821,2822,2824],{},[14,2817,546],{}," is a no-op without ",[14,2820,102],{},", so the agent runs anywhere. Service installation differs: launchd property lists on macOS, a scheduled task or a service wrapper on Windows. Keep ",[14,2823,54],{}," Linux-only and say so in its help.",[132,2826,2828,2829,2831,2832,2835],{"id":2827},"why-typenotify-rather-than-typesimple","Why ",[14,2830,46],{}," rather than ",[14,2833,2834],{},"Type=simple","?",[10,2837,96,2838,2840,2841,2843,2844,2847],{},[14,2839,2834],{},", systemd considers the service started the moment the process is forked, even if it then fails while loading configuration. ",[14,2842,546],{}," reports readiness precisely and enables the watchdog and status line. ",[14,2845,2846],{},"Type=exec"," is a middle ground that at least waits for the program to start executing.",[132,2849,2851],{"id":2850},"how-do-i-pass-secrets-to-the-service","How do I pass secrets to the service?",[10,2853,2854,2855,2858,2859,2862,2863,2866,2867,2870,2871,66],{},"Not in the unit file, which is world-readable for system units. Use ",[14,2856,2857],{},"EnvironmentFile="," with a ",[14,2860,2861],{},"0600"," file, or systemd credentials (",[14,2864,2865],{},"LoadCredential=","), which appear as files under ",[14,2868,2869],{},"$CREDENTIALS_DIRECTORY","; see ",[57,2872,2874],{"href":2873},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002F","reading secrets from env and files",[132,2876,2878],{"id":2877},"can-the-service-restart-itself-after-an-upgrade","Can the service restart itself after an upgrade?",[10,2880,2881,2882,2885],{},"The upgrade process should run ",[14,2883,2884],{},"systemctl --user restart mytool"," after replacing the files; a running Python process keeps using the modules it already imported, so a restart is needed for new code to take effect.",[68,2887,2889],{"id":2888},"related","Related",[73,2891,2892,2898,2903,2909,2914],{},[76,2893,2894,2895],{},"Up: ",[57,2896,2897],{"href":59},"Long-running and watch-mode CLIs",[76,2899,2900],{},[57,2901,2902],{"href":84},"Handling SIGTERM and graceful shutdown",[76,2904,2905],{},[57,2906,2908],{"href":2907},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis\u002F","Health checks and heartbeats for long-running CLIs",[76,2910,2911],{},[57,2912,2913],{"href":64},"Running a CLI on a schedule with cron and systemd",[76,2915,2916],{},[57,2917,2918],{"href":1911},"Reloading config on SIGHUP",[2920,2921,2922],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":142,"searchDepth":156,"depth":156,"links":2924},[2925,2926,2927,2932,2933,2934,2935,2936,2945],{"id":70,"depth":156,"text":71},{"id":88,"depth":156,"text":89},{"id":129,"depth":156,"text":130,"children":2928},[2929,2930,2931],{"id":134,"depth":163,"text":135},{"id":562,"depth":163,"text":563},{"id":1293,"depth":163,"text":1294},{"id":1803,"depth":156,"text":1804},{"id":1845,"depth":156,"text":1846},{"id":1918,"depth":156,"text":1919},{"id":2758,"depth":156,"text":2759},{"id":2795,"depth":156,"text":2796,"children":2937},[2938,2940,2941,2943,2944],{"id":2799,"depth":163,"text":2939},"Do I need the systemd or sdnotify Python packages?",{"id":2812,"depth":163,"text":2813},{"id":2827,"depth":163,"text":2942},"Why Type=notify rather than Type=simple?",{"id":2850,"depth":163,"text":2851},{"id":2877,"depth":163,"text":2878},{"id":2888,"depth":156,"text":2889},"2026-10-02","Turn a long-running Python CLI into a systemd service: a Type=notify unit, sd_notify readiness and status, a watchdog, user services, an install command, tests.","advanced",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service",{"title":5,"description":2947},"cli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-as-a-systemd-service\u002Findex",[29,2956,2957,2958,2959],"services","linux","watchdog","long-running","U1G67KU7mCDSVW3MiWCImP5wAJwDcpel-FnQotpUwh0",[2962,2965,2968,2971,2974,2977,2980,2983,2986,2989,2992,2995,2998,3001,3004,3007,3010,3013,3016,3019,3022,3025,3028,3031,3034,3037,3040,3043,3046,3049,3052,3055,3058,3061,3064,3067,3070,3073,3076,3079,3082,3085,3088,3091,3094,3097,3100,3103,3106,3109,3112,3115,3118,3121,3124,3127,3130,3133,3136,3139,3142,3145,3148,3151,3154,3157,3160,3163,3166,3169,3172,3175,3178,3181,3184,3187,3190,3193,3196,3199,3202,3205,3208,3211,3214,3217,3220,3223,3226,3229,3232,3235,3238,3241,3244,3247,3250,3253,3256,3259,3262,3265,3268,3271,3274,3277,3280,3283,3286,3289,3292,3295,3298,3301,3304,3307,3310,3311,3314,3317,3320,3323,3326,3329,3332,3335,3338,3341,3344,3347,3350,3353,3356,3359,3362,3365,3368,3371,3374,3377,3379,3382,3385,3388,3391,3394,3397,3400,3403,3406,3409,3412,3415,3418,3421,3424,3427,3430,3433,3436,3439,3442,3445,3448,3451,3454,3457,3460,3463,3466,3469,3472,3475,3478,3481,3484,3487,3490,3493,3496,3499,3502,3505,3508,3511,3514,3517,3520,3523,3526,3529,3532,3535,3538,3541,3544,3547,3550,3553,3556,3559,3562,3565,3568,3571,3574,3577,3580,3583,3586,3589,3592,3595,3598,3601,3604,3607,3610,3613,3616,3619,3622,3625,3628,3631,3634,3637,3640,3643,3646,3649,3652,3655,3658,3661,3664,3667,3670,3673,3676,3679,3682,3685,3688,3691,3694,3697,3700,3703,3706,3709,3712,3715,3718,3721,3724,3727,3730,3733,3736,3739,3742,3745,3748,3751,3754,3757,3760,3763,3766,3769,3772,3775,3778,3781,3784,3787,3790,3793,3796,3799,3802],{"path":2963,"title":2964},"\u002Fabout","About Python CLI Toolcraft",{"path":2966,"title":2967},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2969,"title":2970},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2972,"title":2973},"\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":2975,"title":2976},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2978,"title":2979},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2981,"title":2982},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":2984,"title":2985},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2987,"title":2988},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":2990,"title":2991},"\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":2993,"title":2994},"\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":2996,"title":2997},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2999,"title":3000},"\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":3002,"title":3003},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":3005,"title":3006},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":3008,"title":3009},"\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":3011,"title":3012},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":3014,"title":3015},"\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":3017,"title":3018},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":3020,"title":3021},"\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":3023,"title":3024},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":3026,"title":3027},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":3029,"title":3030},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":3032,"title":3033},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":3035,"title":3036},"\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":3038,"title":3039},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":3041,"title":3042},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":3044,"title":3045},"\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":3047,"title":3048},"\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":3050,"title":3051},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":3053,"title":3054},"\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":3056,"title":3057},"\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":3059,"title":3060},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":3062,"title":3063},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":3065,"title":3066},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":3068,"title":3069},"\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":3071,"title":3072},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":3074,"title":3075},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":3077,"title":3078},"\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":3080,"title":3081},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":3083,"title":3084},"\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":3086,"title":3087},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":3089,"title":3090},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":3092,"title":3093},"\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":3095,"title":3096},"\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":3098,"title":3099},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":3101,"title":3102},"\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":3104,"title":3105},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":3107,"title":3108},"\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":3110,"title":3111},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":3113,"title":3114},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":3116,"title":3117},"\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":3119,"title":3120},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":3122,"title":3123},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":3125,"title":3126},"\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":3128,"title":3129},"\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":3131,"title":3132},"\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":3134,"title":3135},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":3137,"title":3138},"\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":3140,"title":3141},"\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":3143,"title":3144},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":3146,"title":3147},"\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":3149,"title":3150},"\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":3152,"title":3153},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":3155,"title":3156},"\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":3158,"title":3159},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":3161,"title":3162},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":3164,"title":3165},"\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":3167,"title":3168},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3170,"title":3171},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3173,"title":3174},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":3176,"title":3177},"\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":3179,"title":3180},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3182,"title":3183},"\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":3185,"title":3186},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3188,"title":3189},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3191,"title":3192},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3194,"title":3195},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3197,"title":3198},"\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":3200,"title":3201},"\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":3203,"title":3204},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":3206,"title":3207},"\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":3209,"title":3210},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":3212,"title":3213},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":3215,"title":3216},"\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":3218,"title":3219},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":3221,"title":3222},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":3224,"title":3225},"\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":3227,"title":3228},"\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":3230,"title":3231},"\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":3233,"title":3234},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":3236,"title":3237},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":3239,"title":3240},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":3242,"title":3243},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":3245,"title":3246},"\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":3248,"title":3249},"\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":3251,"title":3252},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":3254,"title":3255},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":3257,"title":3258},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":3260,"title":3261},"\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":3263,"title":3264},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":3266,"title":3267},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":3269,"title":3270},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":3272,"title":3273},"\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":3275,"title":3276},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":3278,"title":3279},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3281,"title":3282},"\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":3284,"title":3285},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":3287,"title":3288},"\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":3290,"title":3291},"\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":3293,"title":3294},"\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":3296,"title":3297},"\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":3299,"title":3300},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3302,"title":3303},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":3305,"title":3306},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3308,"title":3309},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":2952,"title":5},{"path":3312,"title":3313},"\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":3315,"title":3316},"\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":3318,"title":3319},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3321,"title":3322},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3324,"title":3325},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3327,"title":3328},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3330,"title":3331},"\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":3333,"title":3334},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":3336,"title":3337},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3339,"title":3340},"\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":3342,"title":3343},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3345,"title":3346},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3348,"title":3349},"\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":3351,"title":3352},"\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":3354,"title":3355},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":3357,"title":3358},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3360,"title":3361},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3363,"title":3364},"\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":3366,"title":3367},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":3369,"title":3370},"\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":3372,"title":3373},"\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":3375,"title":3376},"\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":520,"title":3378},"Python CLI Toolcraft",{"path":3380,"title":3381},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":3383,"title":3384},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":3386,"title":3387},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":3389,"title":3390},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":3392,"title":3393},"\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":3395,"title":3396},"\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":3398,"title":3399},"\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":3401,"title":3402},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":3404,"title":3405},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3407,"title":3408},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3410,"title":3411},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3413,"title":3414},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3416,"title":3417},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3419,"title":3420},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3422,"title":3423},"\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":3425,"title":3426},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3428,"title":3429},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3431,"title":3432},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3434,"title":3435},"\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":3437,"title":3438},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3440,"title":3441},"\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":3443,"title":3444},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":3446,"title":3447},"\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":3449,"title":3450},"\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":3452,"title":3453},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3455,"title":3456},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3458,"title":3459},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":3461,"title":3462},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3464,"title":3465},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3467,"title":3468},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3470,"title":3471},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3473,"title":3474},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":3476,"title":3477},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":3479,"title":3480},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":3482,"title":3483},"\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":3485,"title":3486},"\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":3488,"title":3489},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":3491,"title":3492},"\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":3494,"title":3495},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":3497,"title":3498},"\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":3500,"title":3501},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":3503,"title":3504},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":3506,"title":3507},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":3509,"title":3510},"\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":3512,"title":3513},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":3515,"title":3516},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":3518,"title":3519},"\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":3521,"title":3522},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":3524,"title":3525},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":3527,"title":3528},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":3530,"title":3531},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":3533,"title":3534},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":3536,"title":3537},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":3539,"title":3540},"\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":3542,"title":3543},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":3545,"title":3546},"\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":3548,"title":3549},"\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":3551,"title":3552},"\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":3554,"title":3555},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":3557,"title":3558},"\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":3560,"title":3561},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":3563,"title":3564},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":3566,"title":3567},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":3569,"title":3570},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":3572,"title":3573},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":3575,"title":3576},"\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":3578,"title":3579},"\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":3581,"title":3582},"\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":3584,"title":3585},"\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":3587,"title":3588},"\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":3590,"title":3591},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":3593,"title":3594},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":3596,"title":3597},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":3599,"title":3600},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":3602,"title":3603},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":3605,"title":3606},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":3608,"title":3609},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":3611,"title":3612},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":3614,"title":3615},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":3617,"title":3618},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":3620,"title":3621},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":3623,"title":3624},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":3626,"title":3627},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":3629,"title":3630},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":3632,"title":3633},"\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":3635,"title":3636},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":3638,"title":3639},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":3641,"title":3642},"\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":3644,"title":3645},"\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":3647,"title":3648},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":3650,"title":3651},"\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":3653,"title":3654},"\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":3656,"title":3657},"\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":3659,"title":3660},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":3662,"title":3663},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":3665,"title":3666},"\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":3668,"title":3669},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":3671,"title":3672},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":3674,"title":3675},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":3677,"title":3678},"\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":3680,"title":3681},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":3683,"title":3684},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":3686,"title":3687},"\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":3689,"title":3690},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":3692,"title":3693},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":3695,"title":3696},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":3698,"title":3699},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":3701,"title":3702},"\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":3704,"title":3705},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":3707,"title":3708},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":3710,"title":3711},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":3713,"title":3714},"\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":3716,"title":3717},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":3719,"title":3720},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":3722,"title":3723},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":3725,"title":3726},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3728,"title":3729},"\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":3731,"title":3732},"\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":3734,"title":3735},"\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":3737,"title":3738},"\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":3740,"title":3741},"\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":3743,"title":3744},"\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":3746,"title":3747},"\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":3749,"title":3750},"\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":3752,"title":3753},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":3755,"title":3756},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":3758,"title":3759},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":3761,"title":3762},"\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":3764,"title":3765},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3767,"title":3768},"\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":3770,"title":3771},"\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":3773,"title":3774},"\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":3776,"title":3777},"\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":3779,"title":3780},"\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":3782,"title":3783},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3785,"title":3786},"\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":3788,"title":3789},"\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":3791,"title":3792},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3794,"title":3795},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":3797,"title":3798},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3800,"title":3801},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3803,"title":3804},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967540078]