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