[{"data":1,"prerenderedAt":4328},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history\u002F":3,"content-directory":3484},{"id":4,"title":5,"body":6,"date":3468,"description":3469,"difficulty":3470,"draft":3471,"extension":3472,"meta":3473,"navigation":155,"path":3474,"seo":3475,"stem":3476,"tags":3477,"updated":3468,"__hash__":3483},"content\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history\u002Findex.md","Recording and Querying CLI Run History in SQLite",{"type":7,"value":8,"toc":3449},"minimark",[9,32,37,59,63,71,75,110,114,1562,1585,1590,1593,1885,1899,1902,1906,2568,2582,2585,2589,2652,2655,2659,2666,3335,3342,3346,3357,3361,3365,3368,3372,3379,3391,3403,3407,3410,3414,3445],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"\"What did that deploy on Tuesday actually do?\" \"When did the sync last succeed?\" \"Which of yesterday's runs failed, and why?\" Shell history answers none of these — it records what was typed, not what happened, and it is per-terminal and easily lost. A CLI that changes things — deploys, syncs, migrations, bulk edits — benefits enormously from its own run history: one row per invocation with the command, the outcome, the duration and a short summary written by the command itself. Users get ",[14,15,16],"code",{},"mytool history"," and ",[14,19,20],{},"mytool last","; support gets a way to ask \"send me the output of ",[14,23,24],{},"mytool history --failed --since 7d","\"; and you get a record that survives terminal sessions and reboots. This guide builds it on SQLite, with privacy-conscious recording, flexible queries, export and retention. It belongs to the ",[27,28,30],"a",{"href":29},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002F","SQLite state topic",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,52],"ul",{},[41,42,43,44,47,48,31],"li",{},"Python 3.10+, Typer, ",[14,45,46],{},"platformdirs","; the SQLite practices from ",[27,49,51],{"href":50},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fstoring-cli-state-in-sqlite\u002F","storing CLI state in SQLite",[41,53,54,55,31],{},"An entry-point function you control, as in ",[27,56,58],{"href":57},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points\u002F","best practices for Python CLI entry points",[33,60,62],{"id":61},"what-to-record-and-what-not-to","What to record — and what not to",[10,64,65,66,70],{},"A history row should let someone reconstruct ",[67,68,69],"em",{},"what happened"," without becoming a second copy of everything the user typed. That rules out storing the raw command line: arguments routinely contain tokens, passwords passed by careless scripts, customer names and file paths the user may not want kept.",[72,73],"inline-diagram",{"name":74},"hist-record-fields",[10,76,77,78,82,83,86,87,90,91,94,95,98,99,98,102,105,106,109],{},"Record the ",[79,80,81],"strong",{},"command path"," (",[14,84,85],{},"deploy",", ",[14,88,89],{},"db migrate","), the ",[79,92,93],{},"start time"," in UTC, the ",[79,96,97],{},"duration",", the ",[79,100,101],{},"exit code",[79,103,104],{},"tool version",", and a ",[79,107,108],{},"summary"," that the command writes deliberately — \"deployed api 2.3.1 to production (4 hosts)\". The summary is the key design choice: the command knows which details are meaningful and safe, so it writes them explicitly, and nothing else is captured implicitly.",[33,111,113],{"id":112},"the-recipe","The recipe",[115,116,121],"pre",{"className":117,"code":118,"language":119,"meta":120,"style":120},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fhistory.py\nfrom __future__ import annotations\n\nimport os\nimport re\nimport sqlite3\nimport time\nfrom contextvars import ContextVar\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\n\nfrom platformdirs import user_state_path\n\nSCHEMA = [\n    \"\"\"CREATE TABLE runs (\n           id INTEGER PRIMARY KEY,\n           command TEXT NOT NULL,\n           started_at TEXT NOT NULL,\n           duration_ms INTEGER,\n           exit_code INTEGER,\n           version TEXT NOT NULL,\n           summary TEXT\n       )\"\"\",\n    \"CREATE INDEX runs_started ON runs (started_at)\",\n]\n_current: ContextVar[int | None] = ContextVar(\"current_run\", default=None)\n\n\ndef db_path() -> Path:\n    return Path(os.environ.get(\"MYTOOL_HISTORY_DB\") or user_state_path(\"mytool\") \u002F \"history.db\")\n\n\ndef connect(path: Path | None = None) -> sqlite3.Connection:\n    path = path or db_path()\n    path.parent.mkdir(parents=True, exist_ok=True)\n    conn = sqlite3.connect(path, timeout=5.0)\n    conn.row_factory = sqlite3.Row\n    conn.execute(\"PRAGMA journal_mode = WAL\")\n    version = conn.execute(\"PRAGMA user_version\").fetchone()[0]\n    for number, sql in enumerate(SCHEMA[version:], start=version + 1):\n        conn.execute(\"BEGIN IMMEDIATE\")\n        try:\n            conn.execute(sql)\n            conn.execute(f\"PRAGMA user_version = {number}\")\n            conn.execute(\"COMMIT\")\n        except BaseException:\n            conn.execute(\"ROLLBACK\")\n            raise\n    return conn\n\n\ndef start(conn: sqlite3.Connection, command: str, version: str) -> int:\n    with conn:\n        cur = conn.execute(\n            \"INSERT INTO runs (command, started_at, version) VALUES (?, ?, ?)\",\n            (command, datetime.now(timezone.utc).isoformat(timespec=\"seconds\"), version),\n        )\n    _current.set(cur.lastrowid)\n    return cur.lastrowid\n\n\ndef note(conn: sqlite3.Connection, summary: str) -> None:\n    \"\"\"Called by commands to describe what they did.\"\"\"\n    run_id = _current.get()\n    if run_id is not None:\n        with conn:\n            conn.execute(\"UPDATE runs SET summary = ? WHERE id = ?\", (summary[:500], run_id))\n\n\ndef finish(conn: sqlite3.Connection, run_id: int, exit_code: int, started: float) -> None:\n    with conn:\n        conn.execute(\"UPDATE runs SET exit_code = ?, duration_ms = ? WHERE id = ?\",\n                     (exit_code, int((time.monotonic() - started) * 1000), run_id))\n\n\n_SPAN = re.compile(r\"^(\\d+)([mhdw])$\")\n_UNITS = {\"m\": \"minutes\", \"h\": \"hours\", \"d\": \"days\", \"w\": \"weeks\"}\n\n\ndef parse_since(text: str, *, now: datetime | None = None) -> datetime:\n    \"\"\"'30m', '12h', '7d', '2w' or an ISO date -> an aware datetime.\"\"\"\n    now = now or datetime.now(timezone.utc)\n    if m := _SPAN.match(text.strip()):\n        return now - timedelta(**{_UNITS[m.group(2)]: int(m.group(1))})\n    value = datetime.fromisoformat(text)\n    return value if value.tzinfo else value.replace(tzinfo=timezone.utc)\n\n\ndef query(conn: sqlite3.Connection, *, since: datetime | None = None, failed: bool = False,\n          command: str | None = None, limit: int = 20) -> list[dict]:\n    sql = [\"SELECT * FROM runs WHERE 1=1\"]\n    params: list = []\n    if since:\n        sql.append(\"AND started_at >= ?\")\n        params.append(since.isoformat(timespec=\"seconds\"))\n    if failed:\n        sql.append(\"AND (exit_code IS NULL OR exit_code != 0)\")\n    if command:\n        sql.append(\"AND command = ?\")\n        params.append(command)\n    sql.append(\"ORDER BY id DESC LIMIT ?\")\n    params.append(limit)\n    return [dict(r) for r in conn.execute(\" \".join(sql), params)]\n\n\ndef prune(conn: sqlite3.Connection, *, keep_days: int = 90) -> int:\n    cutoff = datetime.now(timezone.utc) - timedelta(days=keep_days)\n    with conn:\n        return conn.execute(\"DELETE FROM runs WHERE started_at \u003C ?\",\n                            (cutoff.isoformat(timespec=\"seconds\"),)).rowcount\n","python","",[14,122,123,132,150,157,166,174,182,190,203,216,229,234,247,252,264,271,277,283,289,295,301,307,313,322,330,336,377,382,387,400,434,439,444,467,483,508,529,540,551,573,613,624,633,639,665,675,686,696,702,710,715,720,746,755,766,774,791,797,803,811,816,821,840,846,857,876,884,901,906,911,941,948,958,984,989,994,1022,1075,1080,1085,1116,1122,1138,1155,1195,1206,1234,1239,1244,1281,1316,1332,1346,1354,1365,1380,1388,1398,1406,1416,1422,1433,1439,1467,1472,1477,1505,1528,1535,1547],{"__ignoreMap":120},[124,125,128],"span",{"class":126,"line":127},"line",1,[124,129,131],{"class":130},"sJ8bj","# src\u002Fmytool\u002Fhistory.py\n",[124,133,135,139,143,146],{"class":126,"line":134},2,[124,136,138],{"class":137},"szBVR","from",[124,140,142],{"class":141},"sj4cs"," __future__",[124,144,145],{"class":137}," import",[124,147,149],{"class":148},"sVt8B"," annotations\n",[124,151,153],{"class":126,"line":152},3,[124,154,156],{"emptyLinePlaceholder":155},true,"\n",[124,158,160,163],{"class":126,"line":159},4,[124,161,162],{"class":137},"import",[124,164,165],{"class":148}," os\n",[124,167,169,171],{"class":126,"line":168},5,[124,170,162],{"class":137},[124,172,173],{"class":148}," re\n",[124,175,177,179],{"class":126,"line":176},6,[124,178,162],{"class":137},[124,180,181],{"class":148}," sqlite3\n",[124,183,185,187],{"class":126,"line":184},7,[124,186,162],{"class":137},[124,188,189],{"class":148}," time\n",[124,191,193,195,198,200],{"class":126,"line":192},8,[124,194,138],{"class":137},[124,196,197],{"class":148}," contextvars ",[124,199,162],{"class":137},[124,201,202],{"class":148}," ContextVar\n",[124,204,206,208,211,213],{"class":126,"line":205},9,[124,207,138],{"class":137},[124,209,210],{"class":148}," datetime ",[124,212,162],{"class":137},[124,214,215],{"class":148}," datetime, timedelta, timezone\n",[124,217,219,221,224,226],{"class":126,"line":218},10,[124,220,138],{"class":137},[124,222,223],{"class":148}," pathlib ",[124,225,162],{"class":137},[124,227,228],{"class":148}," Path\n",[124,230,232],{"class":126,"line":231},11,[124,233,156],{"emptyLinePlaceholder":155},[124,235,237,239,242,244],{"class":126,"line":236},12,[124,238,138],{"class":137},[124,240,241],{"class":148}," platformdirs ",[124,243,162],{"class":137},[124,245,246],{"class":148}," user_state_path\n",[124,248,250],{"class":126,"line":249},13,[124,251,156],{"emptyLinePlaceholder":155},[124,253,255,258,261],{"class":126,"line":254},14,[124,256,257],{"class":141},"SCHEMA",[124,259,260],{"class":137}," =",[124,262,263],{"class":148}," [\n",[124,265,267],{"class":126,"line":266},15,[124,268,270],{"class":269},"sZZnC","    \"\"\"CREATE TABLE runs (\n",[124,272,274],{"class":126,"line":273},16,[124,275,276],{"class":269},"           id INTEGER PRIMARY KEY,\n",[124,278,280],{"class":126,"line":279},17,[124,281,282],{"class":269},"           command TEXT NOT NULL,\n",[124,284,286],{"class":126,"line":285},18,[124,287,288],{"class":269},"           started_at TEXT NOT NULL,\n",[124,290,292],{"class":126,"line":291},19,[124,293,294],{"class":269},"           duration_ms INTEGER,\n",[124,296,298],{"class":126,"line":297},20,[124,299,300],{"class":269},"           exit_code INTEGER,\n",[124,302,304],{"class":126,"line":303},21,[124,305,306],{"class":269},"           version TEXT NOT NULL,\n",[124,308,310],{"class":126,"line":309},22,[124,311,312],{"class":269},"           summary TEXT\n",[124,314,316,319],{"class":126,"line":315},23,[124,317,318],{"class":269},"       )\"\"\"",[124,320,321],{"class":148},",\n",[124,323,325,328],{"class":126,"line":324},24,[124,326,327],{"class":269},"    \"CREATE INDEX runs_started ON runs (started_at)\"",[124,329,321],{"class":148},[124,331,333],{"class":126,"line":332},25,[124,334,335],{"class":148},"]\n",[124,337,339,342,345,348,351,354,357,360,363,365,369,371,374],{"class":126,"line":338},26,[124,340,341],{"class":148},"_current: ContextVar[",[124,343,344],{"class":141},"int",[124,346,347],{"class":137}," |",[124,349,350],{"class":141}," None",[124,352,353],{"class":148},"] ",[124,355,356],{"class":137},"=",[124,358,359],{"class":148}," ContextVar(",[124,361,362],{"class":269},"\"current_run\"",[124,364,86],{"class":148},[124,366,368],{"class":367},"s4XuR","default",[124,370,356],{"class":137},[124,372,373],{"class":141},"None",[124,375,376],{"class":148},")\n",[124,378,380],{"class":126,"line":379},27,[124,381,156],{"emptyLinePlaceholder":155},[124,383,385],{"class":126,"line":384},28,[124,386,156],{"emptyLinePlaceholder":155},[124,388,390,393,397],{"class":126,"line":389},29,[124,391,392],{"class":137},"def",[124,394,396],{"class":395},"sScJk"," db_path",[124,398,399],{"class":148},"() -> Path:\n",[124,401,403,406,409,412,415,418,421,424,426,429,432],{"class":126,"line":402},30,[124,404,405],{"class":137},"    return",[124,407,408],{"class":148}," Path(os.environ.get(",[124,410,411],{"class":269},"\"MYTOOL_HISTORY_DB\"",[124,413,414],{"class":148},") ",[124,416,417],{"class":137},"or",[124,419,420],{"class":148}," user_state_path(",[124,422,423],{"class":269},"\"mytool\"",[124,425,414],{"class":148},[124,427,428],{"class":137},"\u002F",[124,430,431],{"class":269}," \"history.db\"",[124,433,376],{"class":148},[124,435,437],{"class":126,"line":436},31,[124,438,156],{"emptyLinePlaceholder":155},[124,440,442],{"class":126,"line":441},32,[124,443,156],{"emptyLinePlaceholder":155},[124,445,447,449,452,455,458,460,462,464],{"class":126,"line":446},33,[124,448,392],{"class":137},[124,450,451],{"class":395}," connect",[124,453,454],{"class":148},"(path: Path ",[124,456,457],{"class":137},"|",[124,459,350],{"class":141},[124,461,260],{"class":137},[124,463,350],{"class":141},[124,465,466],{"class":148},") -> sqlite3.Connection:\n",[124,468,470,473,475,478,480],{"class":126,"line":469},34,[124,471,472],{"class":148},"    path ",[124,474,356],{"class":137},[124,476,477],{"class":148}," path ",[124,479,417],{"class":137},[124,481,482],{"class":148}," db_path()\n",[124,484,486,489,492,494,497,499,502,504,506],{"class":126,"line":485},35,[124,487,488],{"class":148},"    path.parent.mkdir(",[124,490,491],{"class":367},"parents",[124,493,356],{"class":137},[124,495,496],{"class":141},"True",[124,498,86],{"class":148},[124,500,501],{"class":367},"exist_ok",[124,503,356],{"class":137},[124,505,496],{"class":141},[124,507,376],{"class":148},[124,509,511,514,516,519,522,524,527],{"class":126,"line":510},36,[124,512,513],{"class":148},"    conn ",[124,515,356],{"class":137},[124,517,518],{"class":148}," sqlite3.connect(path, ",[124,520,521],{"class":367},"timeout",[124,523,356],{"class":137},[124,525,526],{"class":141},"5.0",[124,528,376],{"class":148},[124,530,532,535,537],{"class":126,"line":531},37,[124,533,534],{"class":148},"    conn.row_factory ",[124,536,356],{"class":137},[124,538,539],{"class":148}," sqlite3.Row\n",[124,541,543,546,549],{"class":126,"line":542},38,[124,544,545],{"class":148},"    conn.execute(",[124,547,548],{"class":269},"\"PRAGMA journal_mode = WAL\"",[124,550,376],{"class":148},[124,552,554,557,559,562,565,568,571],{"class":126,"line":553},39,[124,555,556],{"class":148},"    version ",[124,558,356],{"class":137},[124,560,561],{"class":148}," conn.execute(",[124,563,564],{"class":269},"\"PRAGMA user_version\"",[124,566,567],{"class":148},").fetchone()[",[124,569,570],{"class":141},"0",[124,572,335],{"class":148},[124,574,576,579,582,585,588,591,593,596,599,601,604,607,610],{"class":126,"line":575},40,[124,577,578],{"class":137},"    for",[124,580,581],{"class":148}," number, sql ",[124,583,584],{"class":137},"in",[124,586,587],{"class":141}," enumerate",[124,589,590],{"class":148},"(",[124,592,257],{"class":141},[124,594,595],{"class":148},"[version:], ",[124,597,598],{"class":367},"start",[124,600,356],{"class":137},[124,602,603],{"class":148},"version ",[124,605,606],{"class":137},"+",[124,608,609],{"class":141}," 1",[124,611,612],{"class":148},"):\n",[124,614,616,619,622],{"class":126,"line":615},41,[124,617,618],{"class":148},"        conn.execute(",[124,620,621],{"class":269},"\"BEGIN IMMEDIATE\"",[124,623,376],{"class":148},[124,625,627,630],{"class":126,"line":626},42,[124,628,629],{"class":137},"        try",[124,631,632],{"class":148},":\n",[124,634,636],{"class":126,"line":635},43,[124,637,638],{"class":148},"            conn.execute(sql)\n",[124,640,642,645,648,651,654,657,660,663],{"class":126,"line":641},44,[124,643,644],{"class":148},"            conn.execute(",[124,646,647],{"class":137},"f",[124,649,650],{"class":269},"\"PRAGMA user_version = ",[124,652,653],{"class":141},"{",[124,655,656],{"class":148},"number",[124,658,659],{"class":141},"}",[124,661,662],{"class":269},"\"",[124,664,376],{"class":148},[124,666,668,670,673],{"class":126,"line":667},45,[124,669,644],{"class":148},[124,671,672],{"class":269},"\"COMMIT\"",[124,674,376],{"class":148},[124,676,678,681,684],{"class":126,"line":677},46,[124,679,680],{"class":137},"        except",[124,682,683],{"class":141}," BaseException",[124,685,632],{"class":148},[124,687,689,691,694],{"class":126,"line":688},47,[124,690,644],{"class":148},[124,692,693],{"class":269},"\"ROLLBACK\"",[124,695,376],{"class":148},[124,697,699],{"class":126,"line":698},48,[124,700,701],{"class":137},"            raise\n",[124,703,705,707],{"class":126,"line":704},49,[124,706,405],{"class":137},[124,708,709],{"class":148}," conn\n",[124,711,713],{"class":126,"line":712},50,[124,714,156],{"emptyLinePlaceholder":155},[124,716,718],{"class":126,"line":717},51,[124,719,156],{"emptyLinePlaceholder":155},[124,721,723,725,728,731,734,737,739,742,744],{"class":126,"line":722},52,[124,724,392],{"class":137},[124,726,727],{"class":395}," start",[124,729,730],{"class":148},"(conn: sqlite3.Connection, command: ",[124,732,733],{"class":141},"str",[124,735,736],{"class":148},", version: ",[124,738,733],{"class":141},[124,740,741],{"class":148},") -> ",[124,743,344],{"class":141},[124,745,632],{"class":148},[124,747,749,752],{"class":126,"line":748},53,[124,750,751],{"class":137},"    with",[124,753,754],{"class":148}," conn:\n",[124,756,758,761,763],{"class":126,"line":757},54,[124,759,760],{"class":148},"        cur ",[124,762,356],{"class":137},[124,764,765],{"class":148}," conn.execute(\n",[124,767,769,772],{"class":126,"line":768},55,[124,770,771],{"class":269},"            \"INSERT INTO runs (command, started_at, version) VALUES (?, ?, ?)\"",[124,773,321],{"class":148},[124,775,777,780,783,785,788],{"class":126,"line":776},56,[124,778,779],{"class":148},"            (command, datetime.now(timezone.utc).isoformat(",[124,781,782],{"class":367},"timespec",[124,784,356],{"class":137},[124,786,787],{"class":269},"\"seconds\"",[124,789,790],{"class":148},"), version),\n",[124,792,794],{"class":126,"line":793},57,[124,795,796],{"class":148},"        )\n",[124,798,800],{"class":126,"line":799},58,[124,801,802],{"class":148},"    _current.set(cur.lastrowid)\n",[124,804,806,808],{"class":126,"line":805},59,[124,807,405],{"class":137},[124,809,810],{"class":148}," cur.lastrowid\n",[124,812,814],{"class":126,"line":813},60,[124,815,156],{"emptyLinePlaceholder":155},[124,817,819],{"class":126,"line":818},61,[124,820,156],{"emptyLinePlaceholder":155},[124,822,824,826,829,832,834,836,838],{"class":126,"line":823},62,[124,825,392],{"class":137},[124,827,828],{"class":395}," note",[124,830,831],{"class":148},"(conn: sqlite3.Connection, summary: ",[124,833,733],{"class":141},[124,835,741],{"class":148},[124,837,373],{"class":141},[124,839,632],{"class":148},[124,841,843],{"class":126,"line":842},63,[124,844,845],{"class":269},"    \"\"\"Called by commands to describe what they did.\"\"\"\n",[124,847,849,852,854],{"class":126,"line":848},64,[124,850,851],{"class":148},"    run_id ",[124,853,356],{"class":137},[124,855,856],{"class":148}," _current.get()\n",[124,858,860,863,866,869,872,874],{"class":126,"line":859},65,[124,861,862],{"class":137},"    if",[124,864,865],{"class":148}," run_id ",[124,867,868],{"class":137},"is",[124,870,871],{"class":137}," not",[124,873,350],{"class":141},[124,875,632],{"class":148},[124,877,879,882],{"class":126,"line":878},66,[124,880,881],{"class":137},"        with",[124,883,754],{"class":148},[124,885,887,889,892,895,898],{"class":126,"line":886},67,[124,888,644],{"class":148},[124,890,891],{"class":269},"\"UPDATE runs SET summary = ? WHERE id = ?\"",[124,893,894],{"class":148},", (summary[:",[124,896,897],{"class":141},"500",[124,899,900],{"class":148},"], run_id))\n",[124,902,904],{"class":126,"line":903},68,[124,905,156],{"emptyLinePlaceholder":155},[124,907,909],{"class":126,"line":908},69,[124,910,156],{"emptyLinePlaceholder":155},[124,912,914,916,919,922,924,927,929,932,935,937,939],{"class":126,"line":913},70,[124,915,392],{"class":137},[124,917,918],{"class":395}," finish",[124,920,921],{"class":148},"(conn: sqlite3.Connection, run_id: ",[124,923,344],{"class":141},[124,925,926],{"class":148},", exit_code: ",[124,928,344],{"class":141},[124,930,931],{"class":148},", started: ",[124,933,934],{"class":141},"float",[124,936,741],{"class":148},[124,938,373],{"class":141},[124,940,632],{"class":148},[124,942,944,946],{"class":126,"line":943},71,[124,945,751],{"class":137},[124,947,754],{"class":148},[124,949,951,953,956],{"class":126,"line":950},72,[124,952,618],{"class":148},[124,954,955],{"class":269},"\"UPDATE runs SET exit_code = ?, duration_ms = ? WHERE id = ?\"",[124,957,321],{"class":148},[124,959,961,964,966,969,972,975,978,981],{"class":126,"line":960},73,[124,962,963],{"class":148},"                     (exit_code, ",[124,965,344],{"class":141},[124,967,968],{"class":148},"((time.monotonic() ",[124,970,971],{"class":137},"-",[124,973,974],{"class":148}," started) ",[124,976,977],{"class":137},"*",[124,979,980],{"class":141}," 1000",[124,982,983],{"class":148},"), run_id))\n",[124,985,987],{"class":126,"line":986},74,[124,988,156],{"emptyLinePlaceholder":155},[124,990,992],{"class":126,"line":991},75,[124,993,156],{"emptyLinePlaceholder":155},[124,995,997,1000,1002,1005,1008,1010,1013,1015,1018,1020],{"class":126,"line":996},76,[124,998,999],{"class":141},"_SPAN",[124,1001,260],{"class":137},[124,1003,1004],{"class":148}," re.compile(",[124,1006,1007],{"class":137},"r",[124,1009,662],{"class":269},[124,1011,1012],{"class":141},"^(\\d",[124,1014,606],{"class":137},[124,1016,1017],{"class":141},")([mhdw])$",[124,1019,662],{"class":269},[124,1021,376],{"class":148},[124,1023,1025,1028,1030,1033,1036,1039,1042,1044,1047,1049,1052,1054,1057,1059,1062,1064,1067,1069,1072],{"class":126,"line":1024},77,[124,1026,1027],{"class":141},"_UNITS",[124,1029,260],{"class":137},[124,1031,1032],{"class":148}," {",[124,1034,1035],{"class":269},"\"m\"",[124,1037,1038],{"class":148},": ",[124,1040,1041],{"class":269},"\"minutes\"",[124,1043,86],{"class":148},[124,1045,1046],{"class":269},"\"h\"",[124,1048,1038],{"class":148},[124,1050,1051],{"class":269},"\"hours\"",[124,1053,86],{"class":148},[124,1055,1056],{"class":269},"\"d\"",[124,1058,1038],{"class":148},[124,1060,1061],{"class":269},"\"days\"",[124,1063,86],{"class":148},[124,1065,1066],{"class":269},"\"w\"",[124,1068,1038],{"class":148},[124,1070,1071],{"class":269},"\"weeks\"",[124,1073,1074],{"class":148},"}\n",[124,1076,1078],{"class":126,"line":1077},78,[124,1079,156],{"emptyLinePlaceholder":155},[124,1081,1083],{"class":126,"line":1082},79,[124,1084,156],{"emptyLinePlaceholder":155},[124,1086,1088,1090,1093,1096,1098,1100,1102,1105,1107,1109,1111,1113],{"class":126,"line":1087},80,[124,1089,392],{"class":137},[124,1091,1092],{"class":395}," parse_since",[124,1094,1095],{"class":148},"(text: ",[124,1097,733],{"class":141},[124,1099,86],{"class":148},[124,1101,977],{"class":137},[124,1103,1104],{"class":148},", now: datetime ",[124,1106,457],{"class":137},[124,1108,350],{"class":141},[124,1110,260],{"class":137},[124,1112,350],{"class":141},[124,1114,1115],{"class":148},") -> datetime:\n",[124,1117,1119],{"class":126,"line":1118},81,[124,1120,1121],{"class":269},"    \"\"\"'30m', '12h', '7d', '2w' or an ISO date -> an aware datetime.\"\"\"\n",[124,1123,1125,1128,1130,1133,1135],{"class":126,"line":1124},82,[124,1126,1127],{"class":148},"    now ",[124,1129,356],{"class":137},[124,1131,1132],{"class":148}," now ",[124,1134,417],{"class":137},[124,1136,1137],{"class":148}," datetime.now(timezone.utc)\n",[124,1139,1141,1143,1146,1149,1152],{"class":126,"line":1140},83,[124,1142,862],{"class":137},[124,1144,1145],{"class":148}," m ",[124,1147,1148],{"class":137},":=",[124,1150,1151],{"class":141}," _SPAN",[124,1153,1154],{"class":148},".match(text.strip()):\n",[124,1156,1158,1161,1163,1165,1168,1171,1173,1175,1178,1181,1184,1186,1189,1192],{"class":126,"line":1157},84,[124,1159,1160],{"class":137},"        return",[124,1162,1132],{"class":148},[124,1164,971],{"class":137},[124,1166,1167],{"class":148}," timedelta(",[124,1169,1170],{"class":137},"**",[124,1172,653],{"class":148},[124,1174,1027],{"class":141},[124,1176,1177],{"class":148},"[m.group(",[124,1179,1180],{"class":141},"2",[124,1182,1183],{"class":148},")]: ",[124,1185,344],{"class":141},[124,1187,1188],{"class":148},"(m.group(",[124,1190,1191],{"class":141},"1",[124,1193,1194],{"class":148},"))})\n",[124,1196,1198,1201,1203],{"class":126,"line":1197},85,[124,1199,1200],{"class":148},"    value ",[124,1202,356],{"class":137},[124,1204,1205],{"class":148}," datetime.fromisoformat(text)\n",[124,1207,1209,1211,1214,1217,1220,1223,1226,1229,1231],{"class":126,"line":1208},86,[124,1210,405],{"class":137},[124,1212,1213],{"class":148}," value ",[124,1215,1216],{"class":137},"if",[124,1218,1219],{"class":148}," value.tzinfo ",[124,1221,1222],{"class":137},"else",[124,1224,1225],{"class":148}," value.replace(",[124,1227,1228],{"class":367},"tzinfo",[124,1230,356],{"class":137},[124,1232,1233],{"class":148},"timezone.utc)\n",[124,1235,1237],{"class":126,"line":1236},87,[124,1238,156],{"emptyLinePlaceholder":155},[124,1240,1242],{"class":126,"line":1241},88,[124,1243,156],{"emptyLinePlaceholder":155},[124,1245,1247,1249,1252,1255,1257,1260,1262,1264,1266,1268,1271,1274,1276,1279],{"class":126,"line":1246},89,[124,1248,392],{"class":137},[124,1250,1251],{"class":395}," query",[124,1253,1254],{"class":148},"(conn: sqlite3.Connection, ",[124,1256,977],{"class":137},[124,1258,1259],{"class":148},", since: datetime ",[124,1261,457],{"class":137},[124,1263,350],{"class":141},[124,1265,260],{"class":137},[124,1267,350],{"class":141},[124,1269,1270],{"class":148},", failed: ",[124,1272,1273],{"class":141},"bool",[124,1275,260],{"class":137},[124,1277,1278],{"class":141}," False",[124,1280,321],{"class":148},[124,1282,1284,1287,1289,1291,1293,1295,1297,1300,1302,1304,1307,1310,1313],{"class":126,"line":1283},90,[124,1285,1286],{"class":148},"          command: ",[124,1288,733],{"class":141},[124,1290,347],{"class":137},[124,1292,350],{"class":141},[124,1294,260],{"class":137},[124,1296,350],{"class":141},[124,1298,1299],{"class":148},", limit: ",[124,1301,344],{"class":141},[124,1303,260],{"class":137},[124,1305,1306],{"class":141}," 20",[124,1308,1309],{"class":148},") -> list[",[124,1311,1312],{"class":141},"dict",[124,1314,1315],{"class":148},"]:\n",[124,1317,1319,1322,1324,1327,1330],{"class":126,"line":1318},91,[124,1320,1321],{"class":148},"    sql ",[124,1323,356],{"class":137},[124,1325,1326],{"class":148}," [",[124,1328,1329],{"class":269},"\"SELECT * FROM runs WHERE 1=1\"",[124,1331,335],{"class":148},[124,1333,1335,1338,1341,1343],{"class":126,"line":1334},92,[124,1336,1337],{"class":148},"    params: ",[124,1339,1340],{"class":141},"list",[124,1342,260],{"class":137},[124,1344,1345],{"class":148}," []\n",[124,1347,1349,1351],{"class":126,"line":1348},93,[124,1350,862],{"class":137},[124,1352,1353],{"class":148}," since:\n",[124,1355,1357,1360,1363],{"class":126,"line":1356},94,[124,1358,1359],{"class":148},"        sql.append(",[124,1361,1362],{"class":269},"\"AND started_at >= ?\"",[124,1364,376],{"class":148},[124,1366,1368,1371,1373,1375,1377],{"class":126,"line":1367},95,[124,1369,1370],{"class":148},"        params.append(since.isoformat(",[124,1372,782],{"class":367},[124,1374,356],{"class":137},[124,1376,787],{"class":269},[124,1378,1379],{"class":148},"))\n",[124,1381,1383,1385],{"class":126,"line":1382},96,[124,1384,862],{"class":137},[124,1386,1387],{"class":148}," failed:\n",[124,1389,1391,1393,1396],{"class":126,"line":1390},97,[124,1392,1359],{"class":148},[124,1394,1395],{"class":269},"\"AND (exit_code IS NULL OR exit_code != 0)\"",[124,1397,376],{"class":148},[124,1399,1401,1403],{"class":126,"line":1400},98,[124,1402,862],{"class":137},[124,1404,1405],{"class":148}," command:\n",[124,1407,1409,1411,1414],{"class":126,"line":1408},99,[124,1410,1359],{"class":148},[124,1412,1413],{"class":269},"\"AND command = ?\"",[124,1415,376],{"class":148},[124,1417,1419],{"class":126,"line":1418},100,[124,1420,1421],{"class":148},"        params.append(command)\n",[124,1423,1425,1428,1431],{"class":126,"line":1424},101,[124,1426,1427],{"class":148},"    sql.append(",[124,1429,1430],{"class":269},"\"ORDER BY id DESC LIMIT ?\"",[124,1432,376],{"class":148},[124,1434,1436],{"class":126,"line":1435},102,[124,1437,1438],{"class":148},"    params.append(limit)\n",[124,1440,1442,1444,1446,1448,1451,1454,1457,1459,1461,1464],{"class":126,"line":1441},103,[124,1443,405],{"class":137},[124,1445,1326],{"class":148},[124,1447,1312],{"class":141},[124,1449,1450],{"class":148},"(r) ",[124,1452,1453],{"class":137},"for",[124,1455,1456],{"class":148}," r ",[124,1458,584],{"class":137},[124,1460,561],{"class":148},[124,1462,1463],{"class":269},"\" \"",[124,1465,1466],{"class":148},".join(sql), params)]\n",[124,1468,1470],{"class":126,"line":1469},104,[124,1471,156],{"emptyLinePlaceholder":155},[124,1473,1475],{"class":126,"line":1474},105,[124,1476,156],{"emptyLinePlaceholder":155},[124,1478,1480,1482,1485,1487,1489,1492,1494,1496,1499,1501,1503],{"class":126,"line":1479},106,[124,1481,392],{"class":137},[124,1483,1484],{"class":395}," prune",[124,1486,1254],{"class":148},[124,1488,977],{"class":137},[124,1490,1491],{"class":148},", keep_days: ",[124,1493,344],{"class":141},[124,1495,260],{"class":137},[124,1497,1498],{"class":141}," 90",[124,1500,741],{"class":148},[124,1502,344],{"class":141},[124,1504,632],{"class":148},[124,1506,1508,1511,1513,1516,1518,1520,1523,1525],{"class":126,"line":1507},107,[124,1509,1510],{"class":148},"    cutoff ",[124,1512,356],{"class":137},[124,1514,1515],{"class":148}," datetime.now(timezone.utc) ",[124,1517,971],{"class":137},[124,1519,1167],{"class":148},[124,1521,1522],{"class":367},"days",[124,1524,356],{"class":137},[124,1526,1527],{"class":148},"keep_days)\n",[124,1529,1531,1533],{"class":126,"line":1530},108,[124,1532,751],{"class":137},[124,1534,754],{"class":148},[124,1536,1538,1540,1542,1545],{"class":126,"line":1537},109,[124,1539,1160],{"class":137},[124,1541,561],{"class":148},[124,1543,1544],{"class":269},"\"DELETE FROM runs WHERE started_at \u003C ?\"",[124,1546,321],{"class":148},[124,1548,1550,1553,1555,1557,1559],{"class":126,"line":1549},110,[124,1551,1552],{"class":148},"                            (cutoff.isoformat(",[124,1554,782],{"class":367},[124,1556,356],{"class":137},[124,1558,787],{"class":269},[124,1560,1561],{"class":148},"),)).rowcount\n",[10,1563,1564,1565,1568,1569,1572,1573,1576,1577,1580,1581,1584],{},"Storing timestamps as ISO 8601 strings in UTC with a fixed precision means they sort and compare correctly as text, so ",[14,1566,1567],{},"started_at >= ?"," works with the index and needs no date functions. Filters are built from fixed SQL fragments with ",[14,1570,1571],{},"?"," parameters, never by formatting user input into the query. A row whose ",[14,1574,1575],{},"exit_code"," is still ",[14,1578,1579],{},"NULL"," was interrupted — killed, or the machine lost power — and the ",[14,1582,1583],{},"--failed"," filter counts it as a failure, which is what a user investigating problems wants to see.",[1586,1587,1589],"h3",{"id":1588},"recording-every-run","Recording every run",[10,1591,1592],{},"The entry point wraps the whole application, so every run is recorded, including usage errors and crashes:",[115,1594,1596],{"className":117,"code":1595,"language":119,"meta":120,"style":120},"# src\u002Fmytool\u002Fmain.py\nimport sys\nimport time\nfrom importlib.metadata import version\n\nfrom mytool import history\nfrom mytool.cli import app\n\n\ndef main() -> None:\n    conn = history.connect()\n    command = next((a for a in sys.argv[1:] if not a.startswith(\"-\")), \"(none)\")\n    started = time.monotonic()\n    run_id = history.start(conn, command, version(\"mytool\"))\n    code = 1\n    try:\n        app()\n    except SystemExit as exc:\n        code = exc.code if isinstance(exc.code, int) else (0 if exc.code is None else 1)\n        raise\n    except KeyboardInterrupt:\n        code = 130\n        raise\n    finally:\n        history.finish(conn, run_id, code, started)\n        history.prune(conn)\n        conn.close()\n",[14,1597,1598,1603,1610,1616,1628,1632,1644,1656,1660,1664,1678,1687,1733,1743,1756,1766,1773,1778,1792,1836,1841,1850,1859,1863,1870,1875,1880],{"__ignoreMap":120},[124,1599,1600],{"class":126,"line":127},[124,1601,1602],{"class":130},"# src\u002Fmytool\u002Fmain.py\n",[124,1604,1605,1607],{"class":126,"line":134},[124,1606,162],{"class":137},[124,1608,1609],{"class":148}," sys\n",[124,1611,1612,1614],{"class":126,"line":152},[124,1613,162],{"class":137},[124,1615,189],{"class":148},[124,1617,1618,1620,1623,1625],{"class":126,"line":159},[124,1619,138],{"class":137},[124,1621,1622],{"class":148}," importlib.metadata ",[124,1624,162],{"class":137},[124,1626,1627],{"class":148}," version\n",[124,1629,1630],{"class":126,"line":168},[124,1631,156],{"emptyLinePlaceholder":155},[124,1633,1634,1636,1639,1641],{"class":126,"line":176},[124,1635,138],{"class":137},[124,1637,1638],{"class":148}," mytool ",[124,1640,162],{"class":137},[124,1642,1643],{"class":148}," history\n",[124,1645,1646,1648,1651,1653],{"class":126,"line":184},[124,1647,138],{"class":137},[124,1649,1650],{"class":148}," mytool.cli ",[124,1652,162],{"class":137},[124,1654,1655],{"class":148}," app\n",[124,1657,1658],{"class":126,"line":192},[124,1659,156],{"emptyLinePlaceholder":155},[124,1661,1662],{"class":126,"line":205},[124,1663,156],{"emptyLinePlaceholder":155},[124,1665,1666,1668,1671,1674,1676],{"class":126,"line":218},[124,1667,392],{"class":137},[124,1669,1670],{"class":395}," main",[124,1672,1673],{"class":148},"() -> ",[124,1675,373],{"class":141},[124,1677,632],{"class":148},[124,1679,1680,1682,1684],{"class":126,"line":231},[124,1681,513],{"class":148},[124,1683,356],{"class":137},[124,1685,1686],{"class":148}," history.connect()\n",[124,1688,1689,1692,1694,1697,1700,1702,1705,1707,1710,1712,1715,1717,1719,1722,1725,1728,1731],{"class":126,"line":236},[124,1690,1691],{"class":148},"    command ",[124,1693,356],{"class":137},[124,1695,1696],{"class":141}," next",[124,1698,1699],{"class":148},"((a ",[124,1701,1453],{"class":137},[124,1703,1704],{"class":148}," a ",[124,1706,584],{"class":137},[124,1708,1709],{"class":148}," sys.argv[",[124,1711,1191],{"class":141},[124,1713,1714],{"class":148},":] ",[124,1716,1216],{"class":137},[124,1718,871],{"class":137},[124,1720,1721],{"class":148}," a.startswith(",[124,1723,1724],{"class":269},"\"-\"",[124,1726,1727],{"class":148},")), ",[124,1729,1730],{"class":269},"\"(none)\"",[124,1732,376],{"class":148},[124,1734,1735,1738,1740],{"class":126,"line":249},[124,1736,1737],{"class":148},"    started ",[124,1739,356],{"class":137},[124,1741,1742],{"class":148}," time.monotonic()\n",[124,1744,1745,1747,1749,1752,1754],{"class":126,"line":254},[124,1746,851],{"class":148},[124,1748,356],{"class":137},[124,1750,1751],{"class":148}," history.start(conn, command, version(",[124,1753,423],{"class":269},[124,1755,1379],{"class":148},[124,1757,1758,1761,1763],{"class":126,"line":266},[124,1759,1760],{"class":148},"    code ",[124,1762,356],{"class":137},[124,1764,1765],{"class":141}," 1\n",[124,1767,1768,1771],{"class":126,"line":273},[124,1769,1770],{"class":137},"    try",[124,1772,632],{"class":148},[124,1774,1775],{"class":126,"line":279},[124,1776,1777],{"class":148},"        app()\n",[124,1779,1780,1783,1786,1789],{"class":126,"line":285},[124,1781,1782],{"class":137},"    except",[124,1784,1785],{"class":141}," SystemExit",[124,1787,1788],{"class":137}," as",[124,1790,1791],{"class":148}," exc:\n",[124,1793,1794,1797,1799,1802,1804,1807,1810,1812,1814,1816,1818,1820,1823,1825,1827,1829,1832,1834],{"class":126,"line":291},[124,1795,1796],{"class":148},"        code ",[124,1798,356],{"class":137},[124,1800,1801],{"class":148}," exc.code ",[124,1803,1216],{"class":137},[124,1805,1806],{"class":141}," isinstance",[124,1808,1809],{"class":148},"(exc.code, ",[124,1811,344],{"class":141},[124,1813,414],{"class":148},[124,1815,1222],{"class":137},[124,1817,82],{"class":148},[124,1819,570],{"class":141},[124,1821,1822],{"class":137}," if",[124,1824,1801],{"class":148},[124,1826,868],{"class":137},[124,1828,350],{"class":141},[124,1830,1831],{"class":137}," else",[124,1833,609],{"class":141},[124,1835,376],{"class":148},[124,1837,1838],{"class":126,"line":297},[124,1839,1840],{"class":137},"        raise\n",[124,1842,1843,1845,1848],{"class":126,"line":303},[124,1844,1782],{"class":137},[124,1846,1847],{"class":141}," KeyboardInterrupt",[124,1849,632],{"class":148},[124,1851,1852,1854,1856],{"class":126,"line":309},[124,1853,1796],{"class":148},[124,1855,356],{"class":137},[124,1857,1858],{"class":141}," 130\n",[124,1860,1861],{"class":126,"line":315},[124,1862,1840],{"class":137},[124,1864,1865,1868],{"class":126,"line":324},[124,1866,1867],{"class":137},"    finally",[124,1869,632],{"class":148},[124,1871,1872],{"class":126,"line":332},[124,1873,1874],{"class":148},"        history.finish(conn, run_id, code, started)\n",[124,1876,1877],{"class":126,"line":338},[124,1878,1879],{"class":148},"        history.prune(conn)\n",[124,1881,1882],{"class":126,"line":379},[124,1883,1884],{"class":148},"        conn.close()\n",[10,1886,1887,1888,1891,1892,1895,1896,31],{},"Typer and Click always end with ",[14,1889,1890],{},"SystemExit"," in their default mode, carrying the exit code — 0 for success, 2 for usage errors, whatever a command passed to ",[14,1893,1894],{},"typer.Exit",". An unexpected exception skips that branch and is recorded as 1. Recording only the first non-option word as the command keeps arguments out of the database; commands that matter add a summary with ",[14,1897,1898],{},"history.note()",[72,1900],{"name":1901},"hist-run-lifecycle",[1586,1903,1905],{"id":1904},"the-history-commands","The history commands",[115,1907,1909],{"className":117,"code":1908,"language":119,"meta":120,"style":120},"# src\u002Fmytool\u002Fcli.py\nimport json\nfrom typing import Optional\n\nimport typer\n\nfrom mytool import history\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"My tool.\"\"\"\n\n\n@app.command()\ndef deploy(service: str, env: str = \"staging\") -> None:\n    \"\"\"Deploy a service.\"\"\"\n    conn = history.connect()\n    history.note(conn, f\"deployed {service} to {env}\")\n    typer.echo(f\"deployed {service} to {env}\")\n\n\n@app.command(\"history\")\ndef show_history(\n    since: Optional[str] = typer.Option(None, help=\"e.g. 30m, 12h, 7d, 2w or 2026-09-01\"),\n    failed: bool = typer.Option(False, \"--failed\", help=\"Only failed or interrupted runs.\"),\n    command: Optional[str] = typer.Option(None, help=\"Only runs of this command.\"),\n    limit: int = typer.Option(20, min=1),\n    as_json: bool = typer.Option(False, \"--json\", help=\"JSON Lines output.\"),\n) -> None:\n    \"\"\"Show previous runs, newest first.\"\"\"\n    try:\n        when = history.parse_since(since) if since else None\n    except ValueError:\n        raise typer.BadParameter(f\"cannot understand {since!r}\", param_hint=\"--since\")\n    rows = history.query(history.connect(), since=when, failed=failed,\n                         command=command, limit=limit)\n    for r in rows:\n        if as_json:\n            typer.echo(json.dumps(r))\n        else:\n            status = \"ok\" if r[\"exit_code\"] == 0 else f\"exit {r['exit_code'] or '?'}\"\n            typer.echo(f\"{r['started_at']}  {r['command']:\u003C10} {status:\u003C8} {r['summary'] or ''}\")\n",[14,1910,1911,1916,1923,1935,1939,1946,1950,1960,1964,1974,1978,1982,1990,2002,2007,2011,2015,2022,2050,2055,2063,2094,2121,2125,2129,2140,2150,2179,2209,2235,2260,2289,2297,2302,2308,2328,2337,2374,2399,2417,2428,2436,2441,2448,2502],{"__ignoreMap":120},[124,1912,1913],{"class":126,"line":127},[124,1914,1915],{"class":130},"# src\u002Fmytool\u002Fcli.py\n",[124,1917,1918,1920],{"class":126,"line":134},[124,1919,162],{"class":137},[124,1921,1922],{"class":148}," json\n",[124,1924,1925,1927,1930,1932],{"class":126,"line":152},[124,1926,138],{"class":137},[124,1928,1929],{"class":148}," typing ",[124,1931,162],{"class":137},[124,1933,1934],{"class":148}," Optional\n",[124,1936,1937],{"class":126,"line":159},[124,1938,156],{"emptyLinePlaceholder":155},[124,1940,1941,1943],{"class":126,"line":168},[124,1942,162],{"class":137},[124,1944,1945],{"class":148}," typer\n",[124,1947,1948],{"class":126,"line":176},[124,1949,156],{"emptyLinePlaceholder":155},[124,1951,1952,1954,1956,1958],{"class":126,"line":184},[124,1953,138],{"class":137},[124,1955,1638],{"class":148},[124,1957,162],{"class":137},[124,1959,1643],{"class":148},[124,1961,1962],{"class":126,"line":192},[124,1963,156],{"emptyLinePlaceholder":155},[124,1965,1966,1969,1971],{"class":126,"line":205},[124,1967,1968],{"class":148},"app ",[124,1970,356],{"class":137},[124,1972,1973],{"class":148}," typer.Typer()\n",[124,1975,1976],{"class":126,"line":218},[124,1977,156],{"emptyLinePlaceholder":155},[124,1979,1980],{"class":126,"line":231},[124,1981,156],{"emptyLinePlaceholder":155},[124,1983,1984,1987],{"class":126,"line":236},[124,1985,1986],{"class":395},"@app.callback",[124,1988,1989],{"class":148},"()\n",[124,1991,1992,1994,1996,1998,2000],{"class":126,"line":249},[124,1993,392],{"class":137},[124,1995,1670],{"class":395},[124,1997,1673],{"class":148},[124,1999,373],{"class":141},[124,2001,632],{"class":148},[124,2003,2004],{"class":126,"line":254},[124,2005,2006],{"class":269},"    \"\"\"My tool.\"\"\"\n",[124,2008,2009],{"class":126,"line":266},[124,2010,156],{"emptyLinePlaceholder":155},[124,2012,2013],{"class":126,"line":273},[124,2014,156],{"emptyLinePlaceholder":155},[124,2016,2017,2020],{"class":126,"line":279},[124,2018,2019],{"class":395},"@app.command",[124,2021,1989],{"class":148},[124,2023,2024,2026,2029,2032,2034,2037,2039,2041,2044,2046,2048],{"class":126,"line":285},[124,2025,392],{"class":137},[124,2027,2028],{"class":395}," deploy",[124,2030,2031],{"class":148},"(service: ",[124,2033,733],{"class":141},[124,2035,2036],{"class":148},", env: ",[124,2038,733],{"class":141},[124,2040,260],{"class":137},[124,2042,2043],{"class":269}," \"staging\"",[124,2045,741],{"class":148},[124,2047,373],{"class":141},[124,2049,632],{"class":148},[124,2051,2052],{"class":126,"line":291},[124,2053,2054],{"class":269},"    \"\"\"Deploy a service.\"\"\"\n",[124,2056,2057,2059,2061],{"class":126,"line":297},[124,2058,513],{"class":148},[124,2060,356],{"class":137},[124,2062,1686],{"class":148},[124,2064,2065,2068,2070,2073,2075,2078,2080,2083,2085,2088,2090,2092],{"class":126,"line":303},[124,2066,2067],{"class":148},"    history.note(conn, ",[124,2069,647],{"class":137},[124,2071,2072],{"class":269},"\"deployed ",[124,2074,653],{"class":141},[124,2076,2077],{"class":148},"service",[124,2079,659],{"class":141},[124,2081,2082],{"class":269}," to ",[124,2084,653],{"class":141},[124,2086,2087],{"class":148},"env",[124,2089,659],{"class":141},[124,2091,662],{"class":269},[124,2093,376],{"class":148},[124,2095,2096,2099,2101,2103,2105,2107,2109,2111,2113,2115,2117,2119],{"class":126,"line":309},[124,2097,2098],{"class":148},"    typer.echo(",[124,2100,647],{"class":137},[124,2102,2072],{"class":269},[124,2104,653],{"class":141},[124,2106,2077],{"class":148},[124,2108,659],{"class":141},[124,2110,2082],{"class":269},[124,2112,653],{"class":141},[124,2114,2087],{"class":148},[124,2116,659],{"class":141},[124,2118,662],{"class":269},[124,2120,376],{"class":148},[124,2122,2123],{"class":126,"line":315},[124,2124,156],{"emptyLinePlaceholder":155},[124,2126,2127],{"class":126,"line":324},[124,2128,156],{"emptyLinePlaceholder":155},[124,2130,2131,2133,2135,2138],{"class":126,"line":332},[124,2132,2019],{"class":395},[124,2134,590],{"class":148},[124,2136,2137],{"class":269},"\"history\"",[124,2139,376],{"class":148},[124,2141,2142,2144,2147],{"class":126,"line":338},[124,2143,392],{"class":137},[124,2145,2146],{"class":395}," show_history",[124,2148,2149],{"class":148},"(\n",[124,2151,2152,2155,2157,2159,2161,2164,2166,2168,2171,2173,2176],{"class":126,"line":379},[124,2153,2154],{"class":148},"    since: Optional[",[124,2156,733],{"class":141},[124,2158,353],{"class":148},[124,2160,356],{"class":137},[124,2162,2163],{"class":148}," typer.Option(",[124,2165,373],{"class":141},[124,2167,86],{"class":148},[124,2169,2170],{"class":367},"help",[124,2172,356],{"class":137},[124,2174,2175],{"class":269},"\"e.g. 30m, 12h, 7d, 2w or 2026-09-01\"",[124,2177,2178],{"class":148},"),\n",[124,2180,2181,2184,2186,2188,2190,2193,2195,2198,2200,2202,2204,2207],{"class":126,"line":384},[124,2182,2183],{"class":148},"    failed: ",[124,2185,1273],{"class":141},[124,2187,260],{"class":137},[124,2189,2163],{"class":148},[124,2191,2192],{"class":141},"False",[124,2194,86],{"class":148},[124,2196,2197],{"class":269},"\"--failed\"",[124,2199,86],{"class":148},[124,2201,2170],{"class":367},[124,2203,356],{"class":137},[124,2205,2206],{"class":269},"\"Only failed or interrupted runs.\"",[124,2208,2178],{"class":148},[124,2210,2211,2214,2216,2218,2220,2222,2224,2226,2228,2230,2233],{"class":126,"line":389},[124,2212,2213],{"class":148},"    command: Optional[",[124,2215,733],{"class":141},[124,2217,353],{"class":148},[124,2219,356],{"class":137},[124,2221,2163],{"class":148},[124,2223,373],{"class":141},[124,2225,86],{"class":148},[124,2227,2170],{"class":367},[124,2229,356],{"class":137},[124,2231,2232],{"class":269},"\"Only runs of this command.\"",[124,2234,2178],{"class":148},[124,2236,2237,2240,2242,2244,2246,2249,2251,2254,2256,2258],{"class":126,"line":402},[124,2238,2239],{"class":148},"    limit: ",[124,2241,344],{"class":141},[124,2243,260],{"class":137},[124,2245,2163],{"class":148},[124,2247,2248],{"class":141},"20",[124,2250,86],{"class":148},[124,2252,2253],{"class":367},"min",[124,2255,356],{"class":137},[124,2257,1191],{"class":141},[124,2259,2178],{"class":148},[124,2261,2262,2265,2267,2269,2271,2273,2275,2278,2280,2282,2284,2287],{"class":126,"line":436},[124,2263,2264],{"class":148},"    as_json: ",[124,2266,1273],{"class":141},[124,2268,260],{"class":137},[124,2270,2163],{"class":148},[124,2272,2192],{"class":141},[124,2274,86],{"class":148},[124,2276,2277],{"class":269},"\"--json\"",[124,2279,86],{"class":148},[124,2281,2170],{"class":367},[124,2283,356],{"class":137},[124,2285,2286],{"class":269},"\"JSON Lines output.\"",[124,2288,2178],{"class":148},[124,2290,2291,2293,2295],{"class":126,"line":441},[124,2292,741],{"class":148},[124,2294,373],{"class":141},[124,2296,632],{"class":148},[124,2298,2299],{"class":126,"line":446},[124,2300,2301],{"class":269},"    \"\"\"Show previous runs, newest first.\"\"\"\n",[124,2303,2304,2306],{"class":126,"line":469},[124,2305,1770],{"class":137},[124,2307,632],{"class":148},[124,2309,2310,2313,2315,2318,2320,2323,2325],{"class":126,"line":485},[124,2311,2312],{"class":148},"        when ",[124,2314,356],{"class":137},[124,2316,2317],{"class":148}," history.parse_since(since) ",[124,2319,1216],{"class":137},[124,2321,2322],{"class":148}," since ",[124,2324,1222],{"class":137},[124,2326,2327],{"class":141}," None\n",[124,2329,2330,2332,2335],{"class":126,"line":510},[124,2331,1782],{"class":137},[124,2333,2334],{"class":141}," ValueError",[124,2336,632],{"class":148},[124,2338,2339,2342,2345,2347,2350,2352,2355,2358,2360,2362,2364,2367,2369,2372],{"class":126,"line":531},[124,2340,2341],{"class":137},"        raise",[124,2343,2344],{"class":148}," typer.BadParameter(",[124,2346,647],{"class":137},[124,2348,2349],{"class":269},"\"cannot understand ",[124,2351,653],{"class":141},[124,2353,2354],{"class":148},"since",[124,2356,2357],{"class":137},"!r",[124,2359,659],{"class":141},[124,2361,662],{"class":269},[124,2363,86],{"class":148},[124,2365,2366],{"class":367},"param_hint",[124,2368,356],{"class":137},[124,2370,2371],{"class":269},"\"--since\"",[124,2373,376],{"class":148},[124,2375,2376,2379,2381,2384,2386,2388,2391,2394,2396],{"class":126,"line":542},[124,2377,2378],{"class":148},"    rows ",[124,2380,356],{"class":137},[124,2382,2383],{"class":148}," history.query(history.connect(), ",[124,2385,2354],{"class":367},[124,2387,356],{"class":137},[124,2389,2390],{"class":148},"when, ",[124,2392,2393],{"class":367},"failed",[124,2395,356],{"class":137},[124,2397,2398],{"class":148},"failed,\n",[124,2400,2401,2404,2406,2409,2412,2414],{"class":126,"line":553},[124,2402,2403],{"class":367},"                         command",[124,2405,356],{"class":137},[124,2407,2408],{"class":148},"command, ",[124,2410,2411],{"class":367},"limit",[124,2413,356],{"class":137},[124,2415,2416],{"class":148},"limit)\n",[124,2418,2419,2421,2423,2425],{"class":126,"line":575},[124,2420,578],{"class":137},[124,2422,1456],{"class":148},[124,2424,584],{"class":137},[124,2426,2427],{"class":148}," rows:\n",[124,2429,2430,2433],{"class":126,"line":615},[124,2431,2432],{"class":137},"        if",[124,2434,2435],{"class":148}," as_json:\n",[124,2437,2438],{"class":126,"line":626},[124,2439,2440],{"class":148},"            typer.echo(json.dumps(r))\n",[124,2442,2443,2446],{"class":126,"line":635},[124,2444,2445],{"class":137},"        else",[124,2447,632],{"class":148},[124,2449,2450,2453,2455,2458,2460,2463,2466,2468,2471,2474,2476,2479,2482,2484,2487,2490,2492,2494,2497,2499],{"class":126,"line":641},[124,2451,2452],{"class":148},"            status ",[124,2454,356],{"class":137},[124,2456,2457],{"class":269}," \"ok\"",[124,2459,1822],{"class":137},[124,2461,2462],{"class":148}," r[",[124,2464,2465],{"class":269},"\"exit_code\"",[124,2467,353],{"class":148},[124,2469,2470],{"class":137},"==",[124,2472,2473],{"class":141}," 0",[124,2475,1831],{"class":137},[124,2477,2478],{"class":137}," f",[124,2480,2481],{"class":269},"\"exit ",[124,2483,653],{"class":141},[124,2485,2486],{"class":148},"r[",[124,2488,2489],{"class":269},"'exit_code'",[124,2491,353],{"class":148},[124,2493,417],{"class":137},[124,2495,2496],{"class":269}," '?'",[124,2498,659],{"class":141},[124,2500,2501],{"class":269},"\"\n",[124,2503,2504,2507,2509,2511,2513,2515,2518,2521,2523,2526,2528,2531,2533,2536,2538,2540,2543,2546,2548,2550,2552,2555,2557,2559,2562,2564,2566],{"class":126,"line":667},[124,2505,2506],{"class":148},"            typer.echo(",[124,2508,647],{"class":137},[124,2510,662],{"class":269},[124,2512,653],{"class":141},[124,2514,2486],{"class":148},[124,2516,2517],{"class":269},"'started_at'",[124,2519,2520],{"class":148},"]",[124,2522,659],{"class":141},[124,2524,2525],{"class":141},"  {",[124,2527,2486],{"class":148},[124,2529,2530],{"class":269},"'command'",[124,2532,2520],{"class":148},[124,2534,2535],{"class":137},":\u003C10",[124,2537,659],{"class":141},[124,2539,1032],{"class":141},[124,2541,2542],{"class":148},"status",[124,2544,2545],{"class":137},":\u003C8",[124,2547,659],{"class":141},[124,2549,1032],{"class":141},[124,2551,2486],{"class":148},[124,2553,2554],{"class":269},"'summary'",[124,2556,353],{"class":148},[124,2558,417],{"class":137},[124,2560,2561],{"class":269}," ''",[124,2563,659],{"class":141},[124,2565,662],{"class":269},[124,2567,376],{"class":148},[10,2569,2570,2573,2574,2577,2578,31],{},[14,2571,2572],{},"--json"," emits JSON Lines so the history composes with ",[14,2575,2576],{},"jq"," and with the export patterns from ",[27,2579,2581],{"href":2580},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis\u002F","the output formats topic",[72,2583],{"name":2584},"hist-terminal",[33,2586,2588],{"id":2587},"ux-considerations","UX considerations",[38,2590,2591,2601,2611,2617,2633,2639],{},[41,2592,2593,2596,2597,2600],{},[79,2594,2595],{},"Summaries are the product."," A history of bare command names is barely useful. Make ",[14,2598,2599],{},"note()"," part of every command that changes something, and write summaries a colleague could read.",[41,2602,2603,2606,2607,31],{},[79,2604,2605],{},"Never store secrets."," Recording only the command name plus deliberate summaries is the simplest way to guarantee it; if you must store arguments, run them through the same redaction as ",[27,2608,2610],{"href":2609},"\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",[41,2612,2613,2616],{},[79,2614,2615],{},"Retention by default."," Ninety days, configurable, pruned automatically. Unbounded history is a slow leak of disk and of data users may not want kept.",[41,2618,2619,2622,2623,2626,2627,17,2630,31],{},[79,2620,2621],{},"Make it easy to turn off."," Some users and environments must not keep records at all; honour ",[14,2624,2625],{},"MYTOOL_NO_HISTORY=1"," by skipping ",[14,2628,2629],{},"start()",[14,2631,2632],{},"finish()",[41,2634,2635,2638],{},[79,2636,2637],{},"Human time formats in the table, ISO in JSON."," The table can say \"2 hours ago\"; JSON keeps the precise timestamp.",[41,2640,2641,2644,2645,428,2648,2651],{},[79,2642,2643],{},"History must never break a command."," Wrap recording in ",[14,2646,2647],{},"try",[14,2649,2650],{},"except sqlite3.Error"," in production and continue without it, logging a warning at debug level.",[72,2653],{"name":2654},"hist-query-matrix",[33,2656,2658],{"id":2657},"testing-the-behaviour","Testing the behaviour",[10,2660,2661,2662,2665],{},"Point the database at ",[14,2663,2664],{},"tmp_path"," with the environment variable and drive the real entry point:",[115,2667,2669],{"className":117,"code":2668,"language":119,"meta":120,"style":120},"# tests\u002Ftest_history.py\nimport json\nimport sys\nfrom datetime import datetime, timezone\n\nimport pytest\n\nfrom mytool import history\nfrom mytool.main import main\n\n\n@pytest.fixture(autouse=True)\ndef isolated_db(tmp_path, monkeypatch):\n    monkeypatch.setenv(\"MYTOOL_HISTORY_DB\", str(tmp_path \u002F \"h.db\"))\n    monkeypatch.setattr(\"mytool.main.version\", lambda name: \"1.0.0\")\n\n\ndef run(monkeypatch, *args):\n    monkeypatch.setattr(sys, \"argv\", [\"mytool\", *args])\n    with pytest.raises(SystemExit) as exc:\n        main()\n    return exc.value.code\n\n\ndef test_success_and_summary_are_recorded(monkeypatch):\n    assert run(monkeypatch, \"deploy\", \"api\", \"--env\", \"prod\") == 0\n    [row] = history.query(history.connect())\n    assert (row[\"command\"], row[\"exit_code\"]) == (\"deploy\", 0)\n    assert row[\"summary\"] == \"deployed api to prod\"\n    assert \"prod\" not in row[\"command\"]                  # arguments are not stored\n\n\ndef test_usage_errors_are_failures(monkeypatch):\n    assert run(monkeypatch, \"deploy\") == 2\n    [row] = history.query(history.connect(), failed=True)\n    assert row[\"exit_code\"] == 2\n\n\ndef test_history_json_output(monkeypatch, capsys):\n    run(monkeypatch, \"deploy\", \"web\")\n    capsys.readouterr()\n    run(monkeypatch, \"history\", \"--json\", \"--command\", \"deploy\")\n    line = capsys.readouterr().out.splitlines()[0]\n    assert json.loads(line)[\"summary\"] == \"deployed web to staging\"\n\n\n@pytest.mark.parametrize(\"text,hours\", [(\"30m\", 0.5), (\"12h\", 12), (\"7d\", 168), (\"2w\", 336)])\ndef test_parse_since_spans(text, hours):\n    now = datetime(2026, 10, 2, tzinfo=timezone.utc)\n    assert (now - history.parse_since(text, now=now)).total_seconds() == hours * 3600\n\n\ndef test_bad_since_is_a_usage_error(monkeypatch):\n    assert run(monkeypatch, \"history\", \"--since\", \"last tuesday\") == 2\n",[14,2670,2671,2676,2682,2688,2699,2703,2710,2714,2724,2736,2740,2744,2760,2770,2791,2812,2816,2820,2835,2855,2871,2876,2883,2887,2891,2901,2934,2944,2974,2991,3013,3017,3021,3030,3045,3061,3075,3079,3083,3093,3107,3112,3133,3147,3163,3167,3171,3226,3236,3265,3295,3299,3303,3312],{"__ignoreMap":120},[124,2672,2673],{"class":126,"line":127},[124,2674,2675],{"class":130},"# tests\u002Ftest_history.py\n",[124,2677,2678,2680],{"class":126,"line":134},[124,2679,162],{"class":137},[124,2681,1922],{"class":148},[124,2683,2684,2686],{"class":126,"line":152},[124,2685,162],{"class":137},[124,2687,1609],{"class":148},[124,2689,2690,2692,2694,2696],{"class":126,"line":159},[124,2691,138],{"class":137},[124,2693,210],{"class":148},[124,2695,162],{"class":137},[124,2697,2698],{"class":148}," datetime, timezone\n",[124,2700,2701],{"class":126,"line":168},[124,2702,156],{"emptyLinePlaceholder":155},[124,2704,2705,2707],{"class":126,"line":176},[124,2706,162],{"class":137},[124,2708,2709],{"class":148}," pytest\n",[124,2711,2712],{"class":126,"line":184},[124,2713,156],{"emptyLinePlaceholder":155},[124,2715,2716,2718,2720,2722],{"class":126,"line":192},[124,2717,138],{"class":137},[124,2719,1638],{"class":148},[124,2721,162],{"class":137},[124,2723,1643],{"class":148},[124,2725,2726,2728,2731,2733],{"class":126,"line":205},[124,2727,138],{"class":137},[124,2729,2730],{"class":148}," mytool.main ",[124,2732,162],{"class":137},[124,2734,2735],{"class":148}," main\n",[124,2737,2738],{"class":126,"line":218},[124,2739,156],{"emptyLinePlaceholder":155},[124,2741,2742],{"class":126,"line":231},[124,2743,156],{"emptyLinePlaceholder":155},[124,2745,2746,2749,2751,2754,2756,2758],{"class":126,"line":236},[124,2747,2748],{"class":395},"@pytest.fixture",[124,2750,590],{"class":148},[124,2752,2753],{"class":367},"autouse",[124,2755,356],{"class":137},[124,2757,496],{"class":141},[124,2759,376],{"class":148},[124,2761,2762,2764,2767],{"class":126,"line":249},[124,2763,392],{"class":137},[124,2765,2766],{"class":395}," isolated_db",[124,2768,2769],{"class":148},"(tmp_path, monkeypatch):\n",[124,2771,2772,2775,2777,2779,2781,2784,2786,2789],{"class":126,"line":254},[124,2773,2774],{"class":148},"    monkeypatch.setenv(",[124,2776,411],{"class":269},[124,2778,86],{"class":148},[124,2780,733],{"class":141},[124,2782,2783],{"class":148},"(tmp_path ",[124,2785,428],{"class":137},[124,2787,2788],{"class":269}," \"h.db\"",[124,2790,1379],{"class":148},[124,2792,2793,2796,2799,2801,2804,2807,2810],{"class":126,"line":266},[124,2794,2795],{"class":148},"    monkeypatch.setattr(",[124,2797,2798],{"class":269},"\"mytool.main.version\"",[124,2800,86],{"class":148},[124,2802,2803],{"class":137},"lambda",[124,2805,2806],{"class":148}," name: ",[124,2808,2809],{"class":269},"\"1.0.0\"",[124,2811,376],{"class":148},[124,2813,2814],{"class":126,"line":273},[124,2815,156],{"emptyLinePlaceholder":155},[124,2817,2818],{"class":126,"line":279},[124,2819,156],{"emptyLinePlaceholder":155},[124,2821,2822,2824,2827,2830,2832],{"class":126,"line":285},[124,2823,392],{"class":137},[124,2825,2826],{"class":395}," run",[124,2828,2829],{"class":148},"(monkeypatch, ",[124,2831,977],{"class":137},[124,2833,2834],{"class":148},"args):\n",[124,2836,2837,2840,2843,2846,2848,2850,2852],{"class":126,"line":291},[124,2838,2839],{"class":148},"    monkeypatch.setattr(sys, ",[124,2841,2842],{"class":269},"\"argv\"",[124,2844,2845],{"class":148},", [",[124,2847,423],{"class":269},[124,2849,86],{"class":148},[124,2851,977],{"class":137},[124,2853,2854],{"class":148},"args])\n",[124,2856,2857,2859,2862,2864,2866,2869],{"class":126,"line":297},[124,2858,751],{"class":137},[124,2860,2861],{"class":148}," pytest.raises(",[124,2863,1890],{"class":141},[124,2865,414],{"class":148},[124,2867,2868],{"class":137},"as",[124,2870,1791],{"class":148},[124,2872,2873],{"class":126,"line":303},[124,2874,2875],{"class":148},"        main()\n",[124,2877,2878,2880],{"class":126,"line":309},[124,2879,405],{"class":137},[124,2881,2882],{"class":148}," exc.value.code\n",[124,2884,2885],{"class":126,"line":315},[124,2886,156],{"emptyLinePlaceholder":155},[124,2888,2889],{"class":126,"line":324},[124,2890,156],{"emptyLinePlaceholder":155},[124,2892,2893,2895,2898],{"class":126,"line":332},[124,2894,392],{"class":137},[124,2896,2897],{"class":395}," test_success_and_summary_are_recorded",[124,2899,2900],{"class":148},"(monkeypatch):\n",[124,2902,2903,2906,2909,2912,2914,2917,2919,2922,2924,2927,2929,2931],{"class":126,"line":338},[124,2904,2905],{"class":137},"    assert",[124,2907,2908],{"class":148}," run(monkeypatch, ",[124,2910,2911],{"class":269},"\"deploy\"",[124,2913,86],{"class":148},[124,2915,2916],{"class":269},"\"api\"",[124,2918,86],{"class":148},[124,2920,2921],{"class":269},"\"--env\"",[124,2923,86],{"class":148},[124,2925,2926],{"class":269},"\"prod\"",[124,2928,414],{"class":148},[124,2930,2470],{"class":137},[124,2932,2933],{"class":141}," 0\n",[124,2935,2936,2939,2941],{"class":126,"line":379},[124,2937,2938],{"class":148},"    [row] ",[124,2940,356],{"class":137},[124,2942,2943],{"class":148}," history.query(history.connect())\n",[124,2945,2946,2948,2951,2954,2957,2959,2962,2964,2966,2968,2970,2972],{"class":126,"line":384},[124,2947,2905],{"class":137},[124,2949,2950],{"class":148}," (row[",[124,2952,2953],{"class":269},"\"command\"",[124,2955,2956],{"class":148},"], row[",[124,2958,2465],{"class":269},[124,2960,2961],{"class":148},"]) ",[124,2963,2470],{"class":137},[124,2965,82],{"class":148},[124,2967,2911],{"class":269},[124,2969,86],{"class":148},[124,2971,570],{"class":141},[124,2973,376],{"class":148},[124,2975,2976,2978,2981,2984,2986,2988],{"class":126,"line":389},[124,2977,2905],{"class":137},[124,2979,2980],{"class":148}," row[",[124,2982,2983],{"class":269},"\"summary\"",[124,2985,353],{"class":148},[124,2987,2470],{"class":137},[124,2989,2990],{"class":269}," \"deployed api to prod\"\n",[124,2992,2993,2995,2998,3000,3003,3005,3007,3010],{"class":126,"line":402},[124,2994,2905],{"class":137},[124,2996,2997],{"class":269}," \"prod\"",[124,2999,871],{"class":137},[124,3001,3002],{"class":137}," in",[124,3004,2980],{"class":148},[124,3006,2953],{"class":269},[124,3008,3009],{"class":148},"]                  ",[124,3011,3012],{"class":130},"# arguments are not stored\n",[124,3014,3015],{"class":126,"line":436},[124,3016,156],{"emptyLinePlaceholder":155},[124,3018,3019],{"class":126,"line":441},[124,3020,156],{"emptyLinePlaceholder":155},[124,3022,3023,3025,3028],{"class":126,"line":446},[124,3024,392],{"class":137},[124,3026,3027],{"class":395}," test_usage_errors_are_failures",[124,3029,2900],{"class":148},[124,3031,3032,3034,3036,3038,3040,3042],{"class":126,"line":469},[124,3033,2905],{"class":137},[124,3035,2908],{"class":148},[124,3037,2911],{"class":269},[124,3039,414],{"class":148},[124,3041,2470],{"class":137},[124,3043,3044],{"class":141}," 2\n",[124,3046,3047,3049,3051,3053,3055,3057,3059],{"class":126,"line":485},[124,3048,2938],{"class":148},[124,3050,356],{"class":137},[124,3052,2383],{"class":148},[124,3054,2393],{"class":367},[124,3056,356],{"class":137},[124,3058,496],{"class":141},[124,3060,376],{"class":148},[124,3062,3063,3065,3067,3069,3071,3073],{"class":126,"line":510},[124,3064,2905],{"class":137},[124,3066,2980],{"class":148},[124,3068,2465],{"class":269},[124,3070,353],{"class":148},[124,3072,2470],{"class":137},[124,3074,3044],{"class":141},[124,3076,3077],{"class":126,"line":531},[124,3078,156],{"emptyLinePlaceholder":155},[124,3080,3081],{"class":126,"line":542},[124,3082,156],{"emptyLinePlaceholder":155},[124,3084,3085,3087,3090],{"class":126,"line":553},[124,3086,392],{"class":137},[124,3088,3089],{"class":395}," test_history_json_output",[124,3091,3092],{"class":148},"(monkeypatch, capsys):\n",[124,3094,3095,3098,3100,3102,3105],{"class":126,"line":575},[124,3096,3097],{"class":148},"    run(monkeypatch, ",[124,3099,2911],{"class":269},[124,3101,86],{"class":148},[124,3103,3104],{"class":269},"\"web\"",[124,3106,376],{"class":148},[124,3108,3109],{"class":126,"line":615},[124,3110,3111],{"class":148},"    capsys.readouterr()\n",[124,3113,3114,3116,3118,3120,3122,3124,3127,3129,3131],{"class":126,"line":626},[124,3115,3097],{"class":148},[124,3117,2137],{"class":269},[124,3119,86],{"class":148},[124,3121,2277],{"class":269},[124,3123,86],{"class":148},[124,3125,3126],{"class":269},"\"--command\"",[124,3128,86],{"class":148},[124,3130,2911],{"class":269},[124,3132,376],{"class":148},[124,3134,3135,3138,3140,3143,3145],{"class":126,"line":635},[124,3136,3137],{"class":148},"    line ",[124,3139,356],{"class":137},[124,3141,3142],{"class":148}," capsys.readouterr().out.splitlines()[",[124,3144,570],{"class":141},[124,3146,335],{"class":148},[124,3148,3149,3151,3154,3156,3158,3160],{"class":126,"line":641},[124,3150,2905],{"class":137},[124,3152,3153],{"class":148}," json.loads(line)[",[124,3155,2983],{"class":269},[124,3157,353],{"class":148},[124,3159,2470],{"class":137},[124,3161,3162],{"class":269}," \"deployed web to staging\"\n",[124,3164,3165],{"class":126,"line":667},[124,3166,156],{"emptyLinePlaceholder":155},[124,3168,3169],{"class":126,"line":677},[124,3170,156],{"emptyLinePlaceholder":155},[124,3172,3173,3176,3178,3181,3184,3187,3189,3192,3195,3198,3200,3203,3205,3208,3210,3213,3215,3218,3220,3223],{"class":126,"line":688},[124,3174,3175],{"class":395},"@pytest.mark.parametrize",[124,3177,590],{"class":148},[124,3179,3180],{"class":269},"\"text,hours\"",[124,3182,3183],{"class":148},", [(",[124,3185,3186],{"class":269},"\"30m\"",[124,3188,86],{"class":148},[124,3190,3191],{"class":141},"0.5",[124,3193,3194],{"class":148},"), (",[124,3196,3197],{"class":269},"\"12h\"",[124,3199,86],{"class":148},[124,3201,3202],{"class":141},"12",[124,3204,3194],{"class":148},[124,3206,3207],{"class":269},"\"7d\"",[124,3209,86],{"class":148},[124,3211,3212],{"class":141},"168",[124,3214,3194],{"class":148},[124,3216,3217],{"class":269},"\"2w\"",[124,3219,86],{"class":148},[124,3221,3222],{"class":141},"336",[124,3224,3225],{"class":148},")])\n",[124,3227,3228,3230,3233],{"class":126,"line":698},[124,3229,392],{"class":137},[124,3231,3232],{"class":395}," test_parse_since_spans",[124,3234,3235],{"class":148},"(text, hours):\n",[124,3237,3238,3240,3242,3245,3248,3250,3253,3255,3257,3259,3261,3263],{"class":126,"line":704},[124,3239,1127],{"class":148},[124,3241,356],{"class":137},[124,3243,3244],{"class":148}," datetime(",[124,3246,3247],{"class":141},"2026",[124,3249,86],{"class":148},[124,3251,3252],{"class":141},"10",[124,3254,86],{"class":148},[124,3256,1180],{"class":141},[124,3258,86],{"class":148},[124,3260,1228],{"class":367},[124,3262,356],{"class":137},[124,3264,1233],{"class":148},[124,3266,3267,3269,3272,3274,3277,3280,3282,3285,3287,3290,3292],{"class":126,"line":712},[124,3268,2905],{"class":137},[124,3270,3271],{"class":148}," (now ",[124,3273,971],{"class":137},[124,3275,3276],{"class":148}," history.parse_since(text, ",[124,3278,3279],{"class":367},"now",[124,3281,356],{"class":137},[124,3283,3284],{"class":148},"now)).total_seconds() ",[124,3286,2470],{"class":137},[124,3288,3289],{"class":148}," hours ",[124,3291,977],{"class":137},[124,3293,3294],{"class":141}," 3600\n",[124,3296,3297],{"class":126,"line":717},[124,3298,156],{"emptyLinePlaceholder":155},[124,3300,3301],{"class":126,"line":722},[124,3302,156],{"emptyLinePlaceholder":155},[124,3304,3305,3307,3310],{"class":126,"line":748},[124,3306,392],{"class":137},[124,3308,3309],{"class":395}," test_bad_since_is_a_usage_error",[124,3311,2900],{"class":148},[124,3313,3314,3316,3318,3320,3322,3324,3326,3329,3331,3333],{"class":126,"line":757},[124,3315,2905],{"class":137},[124,3317,2908],{"class":148},[124,3319,2137],{"class":269},[124,3321,86],{"class":148},[124,3323,2371],{"class":269},[124,3325,86],{"class":148},[124,3327,3328],{"class":269},"\"last tuesday\"",[124,3330,414],{"class":148},[124,3332,2470],{"class":137},[124,3334,3044],{"class":141},[10,3336,3337,3338,3341],{},"Running the real ",[14,3339,3340],{},"main()"," means each test exercises the wrapper, the exit-code capture and the database together — the places where a history feature usually breaks.",[33,3343,3345],{"id":3344},"conclusion","Conclusion",[10,3347,3348,3349,86,3352,17,3354,3356],{},"A run history turns a CLI's past into something users can query: which commands ran, when, how they ended and what they did. Record a row from the entry point for every run, let commands add deliberate summaries instead of storing arguments, store UTC ISO timestamps so queries are simple, offer ",[14,3350,3351],{},"--since",[14,3353,1583],{},[14,3355,2572],{},", and prune automatically. The result is a small feature that answers big questions.",[33,3358,3360],{"id":3359},"frequently-asked-questions","Frequently asked questions",[1586,3362,3364],{"id":3363},"should-history-be-shared-across-a-team","Should history be shared across a team?",[10,3366,3367],{},"The local history is per user and per machine by design. For shared visibility, have commands also emit an event to your team's logging or audit system — the summary you write for history is exactly the right payload. Keep the local database as the user's own record.",[1586,3369,3371],{"id":3370},"how-do-i-record-output-not-just-a-summary","How do I record output, not just a summary?",[10,3373,3374,3375,31],{},"Storing full output makes the database grow quickly and risks keeping sensitive data. A middle ground is to write the full output to a per-run log file in the state directory and store its path in the history row, pruned on the same schedule; see ",[27,3376,3378],{"href":3377},"\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",[1586,3380,3382,3383,3386,3387,3390],{"id":3381},"can-i-add-a-last-or-redo-command","Can I add a ",[14,3384,3385],{},"last"," or ",[14,3388,3389],{},"redo"," command?",[10,3392,3393,3395,3396,3399,3400,3402],{},[14,3394,3385],{}," is a thin wrapper over ",[14,3397,3398],{},"query(limit=1)",". ",[14,3401,3389],{}," requires storing the full arguments, which conflicts with the privacy rule above; if you add it, store arguments only for commands that opt in, after redaction.",[1586,3404,3406],{"id":3405},"what-happens-when-two-runs-record-at-the-same-time","What happens when two runs record at the same time?",[10,3408,3409],{},"Each run inserts and updates its own row in short transactions, so concurrent runs simply interleave. WAL mode and the busy timeout handle the locking; no extra coordination is needed.",[33,3411,3413],{"id":3412},"related","Related",[38,3415,3416,3422,3427,3433,3439],{},[41,3417,3418,3419],{},"Up: ",[27,3420,3421],{"href":29},"Local state and SQLite in Python CLIs",[41,3423,3424],{},[27,3425,3426],{"href":50},"Storing CLI state in SQLite",[41,3428,3429],{},[27,3430,3432],{"href":3431},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Fmigrating-a-cli-sqlite-schema\u002F","Migrating a CLI's SQLite schema",[41,3434,3435],{},[27,3436,3438],{"href":3437},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools\u002F","Choosing exit codes for CLI tools",[41,3440,3441],{},[27,3442,3444],{"href":3443},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs\u002F","Adding trace IDs and context to CLI logs",[3446,3447,3448],"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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":120,"searchDepth":134,"depth":134,"links":3450},[3451,3452,3453,3457,3458,3459,3460,3467],{"id":35,"depth":134,"text":36},{"id":61,"depth":134,"text":62},{"id":112,"depth":134,"text":113,"children":3454},[3455,3456],{"id":1588,"depth":152,"text":1589},{"id":1904,"depth":152,"text":1905},{"id":2587,"depth":134,"text":2588},{"id":2657,"depth":134,"text":2658},{"id":3344,"depth":134,"text":3345},{"id":3359,"depth":134,"text":3360,"children":3461},[3462,3463,3464,3466],{"id":3363,"depth":152,"text":3364},{"id":3370,"depth":152,"text":3371},{"id":3381,"depth":152,"text":3465},"Can I add a last or redo command?",{"id":3405,"depth":152,"text":3406},{"id":3412,"depth":134,"text":3413},"2026-10-02","Give a Python CLI a history command: record each run’s command, outcome and duration in SQLite, attach summaries, query by date and status, export, and prune.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history",{"title":5,"description":3469},"cli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis\u002Frecording-and-querying-cli-run-history\u002Findex",[3478,3479,3480,3481,3482],"sqlite","history","auditing","ux","state","TG5E3umYUABFnJI3GZTvZ-OcbMyEhPYydRrNmixcUTU",[3485,3488,3491,3494,3497,3500,3503,3506,3509,3512,3515,3518,3521,3524,3527,3530,3533,3536,3539,3542,3545,3548,3551,3554,3557,3560,3563,3566,3569,3572,3575,3578,3581,3584,3587,3590,3593,3596,3599,3602,3605,3608,3611,3614,3617,3620,3623,3626,3629,3632,3635,3638,3641,3644,3647,3650,3653,3656,3659,3662,3665,3668,3671,3674,3677,3680,3683,3686,3689,3692,3695,3698,3701,3704,3707,3710,3713,3716,3719,3722,3725,3728,3731,3734,3737,3740,3743,3746,3749,3752,3755,3758,3761,3764,3767,3770,3773,3776,3779,3782,3785,3788,3791,3794,3797,3800,3803,3806,3809,3812,3813,3816,3819,3822,3825,3828,3831,3834,3837,3840,3843,3846,3849,3852,3855,3858,3861,3864,3867,3870,3873,3876,3879,3882,3885,3888,3891,3894,3897,3900,3902,3905,3908,3911,3914,3917,3920,3923,3926,3929,3932,3935,3938,3941,3944,3947,3950,3953,3956,3959,3962,3965,3968,3971,3974,3977,3980,3983,3986,3989,3992,3995,3998,4001,4004,4007,4010,4013,4016,4019,4022,4025,4028,4031,4034,4037,4040,4043,4046,4049,4052,4055,4058,4061,4064,4067,4070,4073,4076,4079,4082,4085,4088,4091,4094,4097,4100,4103,4106,4109,4112,4115,4118,4121,4124,4127,4130,4133,4136,4139,4142,4145,4148,4151,4154,4157,4160,4163,4166,4169,4172,4175,4178,4181,4184,4187,4190,4193,4196,4199,4202,4205,4208,4211,4214,4217,4220,4223,4226,4229,4232,4235,4238,4241,4244,4247,4250,4253,4256,4259,4262,4265,4268,4271,4274,4277,4280,4283,4286,4289,4292,4295,4298,4301,4304,4307,4310,4313,4316,4319,4322,4325],{"path":3486,"title":3487},"\u002Fabout","About Python CLI Toolcraft",{"path":3489,"title":3490},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":3492,"title":3493},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":3495,"title":3496},"\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":3498,"title":3499},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":3501,"title":3502},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":3504,"title":3505},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-urls-hosts-and-ports","Validating URLs, Hosts and Ports in Python CLI Arguments",{"path":3507,"title":3508},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":3510,"title":3511},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbrowsing-records-with-a-textual-datatable","Browsing Records with a Textual DataTable Picker",{"path":3513,"title":3514},"\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":3516,"title":3517},"\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":3519,"title":3520},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":3522,"title":3523},"\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":3525,"title":3526},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fstyling-textual-apps-with-tcss","Styling Textual Apps with TCSS",{"path":3528,"title":3529},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":3531,"title":3532},"\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":3534,"title":3535},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fdocumenting-environment-variables-in-help","Documenting Environment Variables in CLI Help",{"path":3537,"title":3538},"\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":3540,"title":3541},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":3543,"title":3544},"\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":3546,"title":3547},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":3549,"title":3550},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":3552,"title":3553},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":3555,"title":3556},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":3558,"title":3559},"\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":3561,"title":3562},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fhandling-ansi-escape-codes-on-windows-consoles","Handling ANSI Escape Codes on Windows Consoles",{"path":3564,"title":3565},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":3567,"title":3568},"\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":3570,"title":3571},"\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":3573,"title":3574},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":3576,"title":3577},"\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":3579,"title":3580},"\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":3582,"title":3583},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":3585,"title":3586},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":3588,"title":3589},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":3591,"title":3592},"\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":3594,"title":3595},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fwriting-crash-reports-users-can-send","Writing Crash Reports Users Can Send",{"path":3597,"title":3598},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":3600,"title":3601},"\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":3603,"title":3604},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":3606,"title":3607},"\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":3609,"title":3610},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":3612,"title":3613},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":3615,"title":3616},"\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":3618,"title":3619},"\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":3621,"title":3622},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":3624,"title":3625},"\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":3627,"title":3628},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":3630,"title":3631},"\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":3633,"title":3634},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":3636,"title":3637},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":3639,"title":3640},"\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":3642,"title":3643},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":3645,"title":3646},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":3648,"title":3649},"\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":3651,"title":3652},"\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":3654,"title":3655},"\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":3657,"title":3658},"\u002Fadvanced-input-parsing-user-experience\u002Foutput-formats-for-data-clis","Output Formats for Data-Heavy Python CLIs",{"path":3660,"title":3661},"\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":3663,"title":3664},"\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":3666,"title":3667},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fargcomplete-for-argparse-clis","Tab Completion for argparse CLIs with argcomplete",{"path":3669,"title":3670},"\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":3672,"title":3673},"\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":3675,"title":3676},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":3678,"title":3679},"\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":3681,"title":3682},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fshipping-completion-scripts-with-packages","Shipping Shell Completion Scripts with a Python CLI",{"path":3684,"title":3685},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":3687,"title":3688},"\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":3690,"title":3691},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":3693,"title":3694},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":3696,"title":3697},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Flogging-with-structlog-in-a-cli","Logging with structlog in a Python CLI",{"path":3699,"title":3700},"\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":3702,"title":3703},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":3705,"title":3706},"\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":3708,"title":3709},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":3711,"title":3712},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":3714,"title":3715},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":3717,"title":3718},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":3720,"title":3721},"\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":3723,"title":3724},"\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":3726,"title":3727},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":3729,"title":3730},"\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":3732,"title":3733},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":3735,"title":3736},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":3738,"title":3739},"\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":3741,"title":3742},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":3744,"title":3745},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":3747,"title":3748},"\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":3750,"title":3751},"\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":3753,"title":3754},"\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":3756,"title":3757},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":3759,"title":3760},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":3762,"title":3763},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":3765,"title":3766},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":3768,"title":3769},"\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":3771,"title":3772},"\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":3774,"title":3775},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fstructured-concurrency-with-taskgroups-in-clis","Structured Concurrency with TaskGroups in Python CLIs",{"path":3777,"title":3778},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":3780,"title":3781},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":3783,"title":3784},"\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":3786,"title":3787},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":3789,"title":3790},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":3792,"title":3793},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":3795,"title":3796},"\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":3798,"title":3799},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":3801,"title":3802},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":3804,"title":3805},"\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":3807,"title":3808},"\u002Fcli-runtime-systems-integration\u002Flocal-state-and-sqlite-in-python-clis","Local State and SQLite in Python CLIs",{"path":3810,"title":3811},"\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":3474,"title":5},{"path":3814,"title":3815},"\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":3817,"title":3818},"\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":3820,"title":3821},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":3823,"title":3824},"\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":3826,"title":3827},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":3829,"title":3830},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Freloading-config-on-sighup","Reloading Configuration on SIGHUP in a Python CLI",{"path":3832,"title":3833},"\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":3835,"title":3836},"\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":3838,"title":3839},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fsending-cli-logs-to-journald-and-syslog","Sending Python CLI Logs to journald and syslog",{"path":3841,"title":3842},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":3844,"title":3845},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":3847,"title":3848},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":3850,"title":3851},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":3853,"title":3854},"\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":3856,"title":3857},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fpiping-between-subprocesses-in-python","Piping Between Subprocesses in Python Without a Shell",{"path":3859,"title":3860},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":3862,"title":3863},"\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":3865,"title":3866},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":3868,"title":3869},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":3871,"title":3872},"\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":3874,"title":3875},"\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":3877,"title":3878},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Frefreshing-expired-tokens-automatically","Refreshing Expired Tokens Automatically in a Python CLI",{"path":3880,"title":3881},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":3883,"title":3884},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":3886,"title":3887},"\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":3889,"title":3890},"\u002Fcli-runtime-systems-integration\u002Fupdate-checks-and-self-updating-clis","Update Checks, Upgrade Commands and Telemetry for Python CLIs",{"path":3892,"title":3893},"\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":3895,"title":3896},"\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":3898,"title":3899},"\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":428,"title":3901},"Python CLI Toolcraft",{"path":3903,"title":3904},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cleo","Building a Python CLI with Cleo Command Classes",{"path":3906,"title":3907},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fbuilding-a-cli-with-cyclopts","Building a Type-Hinted Python CLI with Cyclopts",{"path":3909,"title":3910},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks","Beyond Click and Typer: Alternative Python CLI Frameworks",{"path":3912,"title":3913},"\u002Fmodern-python-cli-frameworks-architecture\u002Falternative-python-cli-frameworks\u002Fquick-clis-from-functions-with-python-fire","Quick CLIs from Functions with Python Fire",{"path":3915,"title":3916},"\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":3918,"title":3919},"\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":3921,"title":3922},"\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":3924,"title":3925},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fguarding-startup-with-import-tests","Guarding CLI Startup with Import Tests",{"path":3927,"title":3928},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":3930,"title":3931},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":3933,"title":3934},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":3936,"title":3937},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":3939,"title":3940},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":3942,"title":3943},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":3945,"title":3946},"\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":3948,"title":3949},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":3951,"title":3952},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":3954,"title":3955},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":3957,"title":3958},"\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":3960,"title":3961},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":3963,"title":3964},"\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":3966,"title":3967},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fdesigning-idempotent-commands","Designing Idempotent Commands in a Python CLI",{"path":3969,"title":3970},"\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":3972,"title":3973},"\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":3975,"title":3976},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":3978,"title":3979},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":3981,"title":3982},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fpositional-arguments-vs-options","Positional Arguments vs Options: Designing a CLI Signature",{"path":3984,"title":3985},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":3987,"title":3988},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":3990,"title":3991},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":3993,"title":3994},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":3996,"title":3997},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fisolating-plugin-failures","Isolating Plugin Failures in an Extensible Python CLI",{"path":3999,"title":4000},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Ftesting-plugins-against-the-host-cli","Testing Plugins Against the Host CLI",{"path":4002,"title":4003},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":4005,"title":4006},"\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":4008,"title":4009},"\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":4011,"title":4012},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":4014,"title":4015},"\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":4017,"title":4018},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":4020,"title":4021},"\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":4023,"title":4024},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fregistering-commands-from-modules-automatically","Registering CLI Commands from Modules Automatically",{"path":4026,"title":4027},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":4029,"title":4030},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":4032,"title":4033},"\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":4035,"title":4036},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":4038,"title":4039},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":4041,"title":4042},"\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":4044,"title":4045},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":4047,"title":4048},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":4050,"title":4051},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":4053,"title":4054},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-commands-that-call-subprocesses","Testing Python CLI Commands That Call Subprocesses",{"path":4056,"title":4057},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":4059,"title":4060},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftranscript-tests-for-cli-commands","Transcript Tests for Python CLI Commands",{"path":4062,"title":4063},"\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":4065,"title":4066},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":4068,"title":4069},"\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":4071,"title":4072},"\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":4074,"title":4075},"\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":4077,"title":4078},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":4080,"title":4081},"\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":4083,"title":4084},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":4086,"title":4087},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":4089,"title":4090},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":4092,"title":4093},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":4095,"title":4096},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":4098,"title":4099},"\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":4101,"title":4102},"\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":4104,"title":4105},"\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":4107,"title":4108},"\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":4110,"title":4111},"\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":4113,"title":4114},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":4116,"title":4117},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":4119,"title":4120},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":4122,"title":4123},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":4125,"title":4126},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftemplate-variables-and-conditional-files","Template Variables and Conditional Files in CLI Templates",{"path":4128,"title":4129},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Ftesting-a-project-template-with-pytest","Testing a CLI Project Template with pytest",{"path":4131,"title":4132},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fupdating-generated-projects-with-copier-update","Updating Generated CLI Projects with copier update",{"path":4134,"title":4135},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":4137,"title":4138},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":4140,"title":4141},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fcode-signing-and-notarizing-cli-binaries","Code Signing and Notarizing Python CLI Binaries",{"path":4143,"title":4144},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":4146,"title":4147},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":4149,"title":4150},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":4152,"title":4153},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Freducing-pyinstaller-binary-size","Reducing the Size of a PyInstaller CLI Binary",{"path":4155,"title":4156},"\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":4158,"title":4159},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":4161,"title":4162},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":4164,"title":4165},"\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":4167,"title":4168},"\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":4170,"title":4171},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":4173,"title":4174},"\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":4176,"title":4177},"\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":4179,"title":4180},"\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":4182,"title":4183},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":4185,"title":4186},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fbumping-versions-consistently-across-a-cli-project","Bumping Versions Consistently Across a Python CLI Project",{"path":4188,"title":4189},"\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":4191,"title":4192},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":4194,"title":4195},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":4197,"title":4198},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":4200,"title":4201},"\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":4203,"title":4204},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":4206,"title":4207},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":4209,"title":4210},"\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":4212,"title":4213},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":4215,"title":4216},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":4218,"title":4219},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Foptional-dependencies-and-extras-for-clis","Optional Dependencies and Extras for Python CLIs",{"path":4221,"title":4222},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":4224,"title":4225},"\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":4227,"title":4228},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fdynamic-versioning-with-poetry-plugins","Dynamic Versioning for a Poetry CLI from Git Tags",{"path":4230,"title":4231},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":4233,"title":4234},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmanaging-poetry-lock-files-for-cli-tools","Managing Poetry Lock Files for CLI Tools",{"path":4236,"title":4237},"\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":4239,"title":4240},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":4242,"title":4243},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":4245,"title":4246},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpublishing-a-cli-with-poetry","Building and Publishing a Python CLI with Poetry",{"path":4248,"title":4249},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":4251,"title":4252},"\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":4254,"title":4255},"\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":4257,"title":4258},"\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":4260,"title":4261},"\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":4263,"title":4264},"\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":4266,"title":4267},"\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":4269,"title":4270},"\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":4272,"title":4273},"\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":4275,"title":4276},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain","Securing the Supply Chain of a Python CLI",{"path":4278,"title":4279},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpinning-dependencies-with-hashes","Pinning a Python CLI’s Dependencies with Hashes",{"path":4281,"title":4282},"\u002Fproject-setup-dependency-management\u002Fsecuring-a-python-cli-supply-chain\u002Fpublishing-attestations-and-verifying-releases","Publishing Attestations and Verifying Python CLI Releases",{"path":4284,"title":4285},"\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":4287,"title":4288},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":4290,"title":4291},"\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":4293,"title":4294},"\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":4296,"title":4297},"\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":4299,"title":4300},"\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":4302,"title":4303},"\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":4305,"title":4306},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":4308,"title":4309},"\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":4311,"title":4312},"\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":4314,"title":4315},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":4317,"title":4318},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fisolating-cli-tools-from-project-dependencies","Isolating CLI Tools from Your Project’s Dependencies",{"path":4320,"title":4321},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":4323,"title":4324},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":4326,"title":4327},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1790967539853]