[{"data":1,"prerenderedAt":3037},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls\u002F":3,"content-directory":2490},{"id":4,"title":5,"body":6,"date":2476,"description":2477,"difficulty":2478,"draft":2479,"extension":2480,"meta":2481,"navigation":251,"path":2482,"seo":2483,"stem":2484,"tags":2485,"updated":2476,"__hash__":2489},"content\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls\u002Findex.md","Retries and Backoff for CLI HTTP Calls",{"type":7,"value":8,"toc":2459},"minimark",[9,32,37,72,76,88,121,129,133,162,166,177,180,183,196,199,203,210,1246,1253,1442,1448,1452,1504,1508,1519,2340,2350,2354,2360,2364,2376,2388,2392,2395,2399,2407,2411,2421,2425,2455],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"Your deploy script calls the release API, gets a ",[14,15,16],"code",{},"502 Bad Gateway"," because a load balancer was rotating an instance, and aborts — leaving a half-finished release and a user who reruns it by hand thirty seconds later, when it works. A short, polite retry would have hidden the blip completely. But retries have sharp edges: retrying a request that the server already processed can charge a customer twice, retrying in a tight loop turns a struggling service into a failed one, and retrying a ",[14,19,20],{},"400 Bad Request"," just wastes a minute before showing the same error. This guide builds a small retry layer for an ",[14,23,24],{},"httpx","-based CLI that retries the right failures, waits the right amount, respects the server, and is fully testable without real sleeps. It is part of the ",[27,28,30],"a",{"href":29},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002F","HTTP APIs topic",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,46,53],"ul",{},[41,42,43,44,31],"li",{},"Python 3.10+ and ",[14,45,24],{},[41,47,48,49,31],{},"An API client module along the lines of ",[27,50,52],{"href":51},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx\u002F","building an API client CLI with httpx",[41,54,55,56,59,60,63,64,67,68,71],{},"Awareness of HTTP method semantics: ",[14,57,58],{},"GET",", ",[14,61,62],{},"PUT"," and ",[14,65,66],{},"DELETE"," are idempotent; ",[14,69,70],{},"POST"," generally is not.",[33,73,75],{"id":74},"what-to-retry-and-when-it-is-safe","What to retry, and when it is safe",[10,77,78,79,83,84,87],{},"A retry policy answers two questions: ",[80,81,82],"strong",{},"is this failure transient",", and ",[80,85,86],{},"is repeating the request harmless","?",[10,89,90,91,94,95,98,99,59,102,63,105,108,109,112,113,116,117,120],{},"Transient failures are those where trying again later has a real chance of succeeding: connection failures, timeouts, ",[14,92,93],{},"429 Too Many Requests",", and the ",[14,96,97],{},"5xx"," family — especially ",[14,100,101],{},"502",[14,103,104],{},"503",[14,106,107],{},"504",", which usually mean a gateway could not reach a healthy backend. Everything in the ",[14,110,111],{},"4xx"," range other than ",[14,114,115],{},"429"," (and occasionally ",[14,118,119],{},"408",") is permanent: the request itself is wrong and will be just as wrong in five seconds.",[10,122,123,124,128],{},"Harmlessness depends on the method and on ",[125,126,127],"em",{},"where"," the failure happened:",[130,131],"inline-diagram",{"name":132},"http-retry-matrix",[10,134,135,136,139,140,143,144,59,146,63,148,150,151,153,154,157,158,161],{},"A failed ",[80,137,138],{},"connect"," is always safe to retry: nothing reached the server. A ",[80,141,142],{},"read timeout"," is the dangerous case — the request was sent, and the server may have acted on it before the response was lost. For ",[14,145,58],{},[14,147,62],{},[14,149,66],{}," that does not matter, since repeating them produces the same end state. For ",[14,152,70],{},", retry only if the API supports an ",[80,155,156],{},"idempotency key",": a unique header (commonly ",[14,159,160],{},"Idempotency-Key",") that lets the server recognise the repeat and return the original result instead of acting twice.",[33,163,165],{"id":164},"how-long-to-wait","How long to wait",[10,167,168,169,172,173,176],{},"Retrying immediately rarely helps — the condition that caused the failure is still there — and when many clients do it at once they amplify the overload. The standard answer is ",[80,170,171],{},"exponential backoff",": wait a base delay, doubling on each attempt, up to a cap. Then add ",[80,174,175],{},"jitter",", a random component that spreads clients out so they do not all retry in the same instant.",[130,178],{"name":179},"http-backoff-bars",[10,181,182],{},"With a base of 0.5 seconds and a cap of 8, the first retry waits up to half a second and the fifth up to eight. \"Full jitter\" — choosing uniformly between zero and the computed delay — is simple and performs well in practice. For an interactive CLI, keep the total budget modest: a person will tolerate fifteen seconds of \"retrying...\" far better than two minutes.",[10,184,185,186,188,189,191,192,195],{},"Servers often know better than any formula. A ",[14,187,115],{}," or ",[14,190,104],{}," may carry a ",[14,193,194],{},"Retry-After"," header, either in seconds or as an HTTP date. Honour it when present, capped at a maximum so a misconfigured server cannot put your tool to sleep for an hour.",[130,197],{"name":198},"http-retry-after",[33,200,202],{"id":201},"the-recipe","The recipe",[10,204,205,206,209],{},"The wrapper below sits in your API module and wraps ",[14,207,208],{},"client.send()",". Sleep and randomness are injectable, which is what makes it testable without waiting.",[211,212,217],"pre",{"className":213,"code":214,"language":215,"meta":216,"style":216},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fretry.py\nfrom __future__ import annotations\n\nimport random\nimport time\nfrom collections.abc import Callable\nfrom dataclasses import dataclass, field\nfrom email.utils import parsedate_to_datetime\nfrom datetime import datetime, timezone\n\nimport httpx\n\nRETRY_STATUSES = frozenset({429, 502, 503, 504})\nIDEMPOTENT = frozenset({\"GET\", \"HEAD\", \"OPTIONS\", \"PUT\", \"DELETE\"})\n\n\n@dataclass\nclass RetryPolicy:\n    attempts: int = 5                 # total tries, including the first\n    base: float = 0.5\n    cap: float = 8.0\n    max_retry_after: float = 60.0\n    sleep: Callable[[float], None] = time.sleep\n    rand: Callable[[], float] = random.random\n    on_retry: Callable[[int, float, str], None] = field(default=lambda n, d, why: None)\n\n    def backoff(self, retry_number: int) -> float:\n        ceiling = min(self.cap, self.base * 2 ** (retry_number - 1))\n        return ceiling * self.rand()          # full jitter\n\n\ndef retry_after_seconds(response: httpx.Response) -> float | None:\n    value = response.headers.get(\"Retry-After\")\n    if value is None:\n        return None\n    if value.strip().isdigit():\n        return float(value)\n    try:\n        when = parsedate_to_datetime(value)\n    except (TypeError, ValueError):\n        return None\n    return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())\n\n\ndef send_with_retries(client: httpx.Client, request: httpx.Request,\n                      policy: RetryPolicy | None = None) -> httpx.Response:\n    policy = policy or RetryPolicy()\n    can_repeat = request.method in IDEMPOTENT or \"Idempotency-Key\" in request.headers\n    for attempt in range(1, policy.attempts + 1):\n        last = attempt == policy.attempts\n        try:\n            response = client.send(request)\n        except httpx.ConnectError:\n            if last:\n                raise\n            why = \"connection failed\"          # nothing was sent: always safe\n        except (httpx.ReadTimeout, httpx.RemoteProtocolError):\n            if last or not can_repeat:\n                raise\n            why = \"no response\"\n        else:\n            if response.status_code not in RETRY_STATUSES or last:\n                return response\n            if response.status_code != 429 and not can_repeat:\n                return response\n            why = f\"HTTP {response.status_code}\"\n            server_delay = retry_after_seconds(response)\n            response.close()\n            if server_delay is not None:\n                delay = min(server_delay, policy.max_retry_after)\n                policy.on_retry(attempt, delay, why)\n                policy.sleep(delay)\n                continue\n        delay = policy.backoff(attempt)\n        policy.on_retry(attempt, delay, why)\n        policy.sleep(delay)\n    raise AssertionError(\"unreachable\")\n","python","",[14,218,219,228,246,253,262,270,283,296,309,322,327,335,340,372,410,415,420,427,439,456,470,483,496,519,534,577,582,603,649,669,674,679,701,717,733,741,749,760,768,779,799,806,828,833,838,849,867,884,913,942,958,966,977,986,995,1001,1015,1023,1039,1044,1054,1062,1082,1091,1111,1118,1143,1154,1160,1176,1189,1195,1201,1207,1218,1224,1230],{"__ignoreMap":216},[220,221,224],"span",{"class":222,"line":223},"line",1,[220,225,227],{"class":226},"sJ8bj","# src\u002Fmytool\u002Fretry.py\n",[220,229,231,235,239,242],{"class":222,"line":230},2,[220,232,234],{"class":233},"szBVR","from",[220,236,238],{"class":237},"sj4cs"," __future__",[220,240,241],{"class":233}," import",[220,243,245],{"class":244},"sVt8B"," annotations\n",[220,247,249],{"class":222,"line":248},3,[220,250,252],{"emptyLinePlaceholder":251},true,"\n",[220,254,256,259],{"class":222,"line":255},4,[220,257,258],{"class":233},"import",[220,260,261],{"class":244}," random\n",[220,263,265,267],{"class":222,"line":264},5,[220,266,258],{"class":233},[220,268,269],{"class":244}," time\n",[220,271,273,275,278,280],{"class":222,"line":272},6,[220,274,234],{"class":233},[220,276,277],{"class":244}," collections.abc ",[220,279,258],{"class":233},[220,281,282],{"class":244}," Callable\n",[220,284,286,288,291,293],{"class":222,"line":285},7,[220,287,234],{"class":233},[220,289,290],{"class":244}," dataclasses ",[220,292,258],{"class":233},[220,294,295],{"class":244}," dataclass, field\n",[220,297,299,301,304,306],{"class":222,"line":298},8,[220,300,234],{"class":233},[220,302,303],{"class":244}," email.utils ",[220,305,258],{"class":233},[220,307,308],{"class":244}," parsedate_to_datetime\n",[220,310,312,314,317,319],{"class":222,"line":311},9,[220,313,234],{"class":233},[220,315,316],{"class":244}," datetime ",[220,318,258],{"class":233},[220,320,321],{"class":244}," datetime, timezone\n",[220,323,325],{"class":222,"line":324},10,[220,326,252],{"emptyLinePlaceholder":251},[220,328,330,332],{"class":222,"line":329},11,[220,331,258],{"class":233},[220,333,334],{"class":244}," httpx\n",[220,336,338],{"class":222,"line":337},12,[220,339,252],{"emptyLinePlaceholder":251},[220,341,343,346,349,352,355,357,359,361,363,365,367,369],{"class":222,"line":342},13,[220,344,345],{"class":237},"RETRY_STATUSES",[220,347,348],{"class":233}," =",[220,350,351],{"class":237}," frozenset",[220,353,354],{"class":244},"({",[220,356,115],{"class":237},[220,358,59],{"class":244},[220,360,101],{"class":237},[220,362,59],{"class":244},[220,364,104],{"class":237},[220,366,59],{"class":244},[220,368,107],{"class":237},[220,370,371],{"class":244},"})\n",[220,373,375,378,380,382,384,388,390,393,395,398,400,403,405,408],{"class":222,"line":374},14,[220,376,377],{"class":237},"IDEMPOTENT",[220,379,348],{"class":233},[220,381,351],{"class":237},[220,383,354],{"class":244},[220,385,387],{"class":386},"sZZnC","\"GET\"",[220,389,59],{"class":244},[220,391,392],{"class":386},"\"HEAD\"",[220,394,59],{"class":244},[220,396,397],{"class":386},"\"OPTIONS\"",[220,399,59],{"class":244},[220,401,402],{"class":386},"\"PUT\"",[220,404,59],{"class":244},[220,406,407],{"class":386},"\"DELETE\"",[220,409,371],{"class":244},[220,411,413],{"class":222,"line":412},15,[220,414,252],{"emptyLinePlaceholder":251},[220,416,418],{"class":222,"line":417},16,[220,419,252],{"emptyLinePlaceholder":251},[220,421,423],{"class":222,"line":422},17,[220,424,426],{"class":425},"sScJk","@dataclass\n",[220,428,430,433,436],{"class":222,"line":429},18,[220,431,432],{"class":233},"class",[220,434,435],{"class":425}," RetryPolicy",[220,437,438],{"class":244},":\n",[220,440,442,445,448,450,453],{"class":222,"line":441},19,[220,443,444],{"class":244},"    attempts: ",[220,446,447],{"class":237},"int",[220,449,348],{"class":233},[220,451,452],{"class":237}," 5",[220,454,455],{"class":226},"                 # total tries, including the first\n",[220,457,459,462,465,467],{"class":222,"line":458},20,[220,460,461],{"class":244},"    base: ",[220,463,464],{"class":237},"float",[220,466,348],{"class":233},[220,468,469],{"class":237}," 0.5\n",[220,471,473,476,478,480],{"class":222,"line":472},21,[220,474,475],{"class":244},"    cap: ",[220,477,464],{"class":237},[220,479,348],{"class":233},[220,481,482],{"class":237}," 8.0\n",[220,484,486,489,491,493],{"class":222,"line":485},22,[220,487,488],{"class":244},"    max_retry_after: ",[220,490,464],{"class":237},[220,492,348],{"class":233},[220,494,495],{"class":237}," 60.0\n",[220,497,499,502,504,507,510,513,516],{"class":222,"line":498},23,[220,500,501],{"class":244},"    sleep: Callable[[",[220,503,464],{"class":237},[220,505,506],{"class":244},"], ",[220,508,509],{"class":237},"None",[220,511,512],{"class":244},"] ",[220,514,515],{"class":233},"=",[220,517,518],{"class":244}," time.sleep\n",[220,520,522,525,527,529,531],{"class":222,"line":521},24,[220,523,524],{"class":244},"    rand: Callable[[], ",[220,526,464],{"class":237},[220,528,512],{"class":244},[220,530,515],{"class":233},[220,532,533],{"class":244}," random.random\n",[220,535,537,540,542,544,546,548,551,553,555,557,559,562,566,569,572,574],{"class":222,"line":536},25,[220,538,539],{"class":244},"    on_retry: Callable[[",[220,541,447],{"class":237},[220,543,59],{"class":244},[220,545,464],{"class":237},[220,547,59],{"class":244},[220,549,550],{"class":237},"str",[220,552,506],{"class":244},[220,554,509],{"class":237},[220,556,512],{"class":244},[220,558,515],{"class":233},[220,560,561],{"class":244}," field(",[220,563,565],{"class":564},"s4XuR","default",[220,567,568],{"class":233},"=lambda",[220,570,571],{"class":244}," n, d, why: ",[220,573,509],{"class":237},[220,575,576],{"class":244},")\n",[220,578,580],{"class":222,"line":579},26,[220,581,252],{"emptyLinePlaceholder":251},[220,583,585,588,591,594,596,599,601],{"class":222,"line":584},27,[220,586,587],{"class":233},"    def",[220,589,590],{"class":425}," backoff",[220,592,593],{"class":244},"(self, retry_number: ",[220,595,447],{"class":237},[220,597,598],{"class":244},") -> ",[220,600,464],{"class":237},[220,602,438],{"class":244},[220,604,606,609,611,614,617,620,623,625,628,631,634,637,640,643,646],{"class":222,"line":605},28,[220,607,608],{"class":244},"        ceiling ",[220,610,515],{"class":233},[220,612,613],{"class":237}," min",[220,615,616],{"class":244},"(",[220,618,619],{"class":237},"self",[220,621,622],{"class":244},".cap, ",[220,624,619],{"class":237},[220,626,627],{"class":244},".base ",[220,629,630],{"class":233},"*",[220,632,633],{"class":237}," 2",[220,635,636],{"class":233}," **",[220,638,639],{"class":244}," (retry_number ",[220,641,642],{"class":233},"-",[220,644,645],{"class":237}," 1",[220,647,648],{"class":244},"))\n",[220,650,652,655,658,660,663,666],{"class":222,"line":651},29,[220,653,654],{"class":233},"        return",[220,656,657],{"class":244}," ceiling ",[220,659,630],{"class":233},[220,661,662],{"class":237}," self",[220,664,665],{"class":244},".rand()          ",[220,667,668],{"class":226},"# full jitter\n",[220,670,672],{"class":222,"line":671},30,[220,673,252],{"emptyLinePlaceholder":251},[220,675,677],{"class":222,"line":676},31,[220,678,252],{"emptyLinePlaceholder":251},[220,680,682,685,688,691,693,696,699],{"class":222,"line":681},32,[220,683,684],{"class":233},"def",[220,686,687],{"class":425}," retry_after_seconds",[220,689,690],{"class":244},"(response: httpx.Response) -> ",[220,692,464],{"class":237},[220,694,695],{"class":233}," |",[220,697,698],{"class":237}," None",[220,700,438],{"class":244},[220,702,704,707,709,712,715],{"class":222,"line":703},33,[220,705,706],{"class":244},"    value ",[220,708,515],{"class":233},[220,710,711],{"class":244}," response.headers.get(",[220,713,714],{"class":386},"\"Retry-After\"",[220,716,576],{"class":244},[220,718,720,723,726,729,731],{"class":222,"line":719},34,[220,721,722],{"class":233},"    if",[220,724,725],{"class":244}," value ",[220,727,728],{"class":233},"is",[220,730,698],{"class":237},[220,732,438],{"class":244},[220,734,736,738],{"class":222,"line":735},35,[220,737,654],{"class":233},[220,739,740],{"class":237}," None\n",[220,742,744,746],{"class":222,"line":743},36,[220,745,722],{"class":233},[220,747,748],{"class":244}," value.strip().isdigit():\n",[220,750,752,754,757],{"class":222,"line":751},37,[220,753,654],{"class":233},[220,755,756],{"class":237}," float",[220,758,759],{"class":244},"(value)\n",[220,761,763,766],{"class":222,"line":762},38,[220,764,765],{"class":233},"    try",[220,767,438],{"class":244},[220,769,771,774,776],{"class":222,"line":770},39,[220,772,773],{"class":244},"        when ",[220,775,515],{"class":233},[220,777,778],{"class":244}," parsedate_to_datetime(value)\n",[220,780,782,785,788,791,793,796],{"class":222,"line":781},40,[220,783,784],{"class":233},"    except",[220,786,787],{"class":244}," (",[220,789,790],{"class":237},"TypeError",[220,792,59],{"class":244},[220,794,795],{"class":237},"ValueError",[220,797,798],{"class":244},"):\n",[220,800,802,804],{"class":222,"line":801},41,[220,803,654],{"class":233},[220,805,740],{"class":237},[220,807,809,812,815,817,820,823,825],{"class":222,"line":808},42,[220,810,811],{"class":233},"    return",[220,813,814],{"class":237}," max",[220,816,616],{"class":244},[220,818,819],{"class":237},"0.0",[220,821,822],{"class":244},", (when ",[220,824,642],{"class":233},[220,826,827],{"class":244}," datetime.now(timezone.utc)).total_seconds())\n",[220,829,831],{"class":222,"line":830},43,[220,832,252],{"emptyLinePlaceholder":251},[220,834,836],{"class":222,"line":835},44,[220,837,252],{"emptyLinePlaceholder":251},[220,839,841,843,846],{"class":222,"line":840},45,[220,842,684],{"class":233},[220,844,845],{"class":425}," send_with_retries",[220,847,848],{"class":244},"(client: httpx.Client, request: httpx.Request,\n",[220,850,852,855,858,860,862,864],{"class":222,"line":851},46,[220,853,854],{"class":244},"                      policy: RetryPolicy ",[220,856,857],{"class":233},"|",[220,859,698],{"class":237},[220,861,348],{"class":233},[220,863,698],{"class":237},[220,865,866],{"class":244},") -> httpx.Response:\n",[220,868,870,873,875,878,881],{"class":222,"line":869},47,[220,871,872],{"class":244},"    policy ",[220,874,515],{"class":233},[220,876,877],{"class":244}," policy ",[220,879,880],{"class":233},"or",[220,882,883],{"class":244}," RetryPolicy()\n",[220,885,887,890,892,895,898,901,904,907,910],{"class":222,"line":886},48,[220,888,889],{"class":244},"    can_repeat ",[220,891,515],{"class":233},[220,893,894],{"class":244}," request.method ",[220,896,897],{"class":233},"in",[220,899,900],{"class":237}," IDEMPOTENT",[220,902,903],{"class":233}," or",[220,905,906],{"class":386}," \"Idempotency-Key\"",[220,908,909],{"class":233}," in",[220,911,912],{"class":244}," request.headers\n",[220,914,916,919,922,924,927,929,932,935,938,940],{"class":222,"line":915},49,[220,917,918],{"class":233},"    for",[220,920,921],{"class":244}," attempt ",[220,923,897],{"class":233},[220,925,926],{"class":237}," range",[220,928,616],{"class":244},[220,930,931],{"class":237},"1",[220,933,934],{"class":244},", policy.attempts ",[220,936,937],{"class":233},"+",[220,939,645],{"class":237},[220,941,798],{"class":244},[220,943,945,948,950,952,955],{"class":222,"line":944},50,[220,946,947],{"class":244},"        last ",[220,949,515],{"class":233},[220,951,921],{"class":244},[220,953,954],{"class":233},"==",[220,956,957],{"class":244}," policy.attempts\n",[220,959,961,964],{"class":222,"line":960},51,[220,962,963],{"class":233},"        try",[220,965,438],{"class":244},[220,967,969,972,974],{"class":222,"line":968},52,[220,970,971],{"class":244},"            response ",[220,973,515],{"class":233},[220,975,976],{"class":244}," client.send(request)\n",[220,978,980,983],{"class":222,"line":979},53,[220,981,982],{"class":233},"        except",[220,984,985],{"class":244}," httpx.ConnectError:\n",[220,987,989,992],{"class":222,"line":988},54,[220,990,991],{"class":233},"            if",[220,993,994],{"class":244}," last:\n",[220,996,998],{"class":222,"line":997},55,[220,999,1000],{"class":233},"                raise\n",[220,1002,1004,1007,1009,1012],{"class":222,"line":1003},56,[220,1005,1006],{"class":244},"            why ",[220,1008,515],{"class":233},[220,1010,1011],{"class":386}," \"connection failed\"",[220,1013,1014],{"class":226},"          # nothing was sent: always safe\n",[220,1016,1018,1020],{"class":222,"line":1017},57,[220,1019,982],{"class":233},[220,1021,1022],{"class":244}," (httpx.ReadTimeout, httpx.RemoteProtocolError):\n",[220,1024,1026,1028,1031,1033,1036],{"class":222,"line":1025},58,[220,1027,991],{"class":233},[220,1029,1030],{"class":244}," last ",[220,1032,880],{"class":233},[220,1034,1035],{"class":233}," not",[220,1037,1038],{"class":244}," can_repeat:\n",[220,1040,1042],{"class":222,"line":1041},59,[220,1043,1000],{"class":233},[220,1045,1047,1049,1051],{"class":222,"line":1046},60,[220,1048,1006],{"class":244},[220,1050,515],{"class":233},[220,1052,1053],{"class":386}," \"no response\"\n",[220,1055,1057,1060],{"class":222,"line":1056},61,[220,1058,1059],{"class":233},"        else",[220,1061,438],{"class":244},[220,1063,1065,1067,1070,1073,1075,1078,1080],{"class":222,"line":1064},62,[220,1066,991],{"class":233},[220,1068,1069],{"class":244}," response.status_code ",[220,1071,1072],{"class":233},"not",[220,1074,909],{"class":233},[220,1076,1077],{"class":237}," RETRY_STATUSES",[220,1079,903],{"class":233},[220,1081,994],{"class":244},[220,1083,1085,1088],{"class":222,"line":1084},63,[220,1086,1087],{"class":233},"                return",[220,1089,1090],{"class":244}," response\n",[220,1092,1094,1096,1098,1101,1104,1107,1109],{"class":222,"line":1093},64,[220,1095,991],{"class":233},[220,1097,1069],{"class":244},[220,1099,1100],{"class":233},"!=",[220,1102,1103],{"class":237}," 429",[220,1105,1106],{"class":233}," and",[220,1108,1035],{"class":233},[220,1110,1038],{"class":244},[220,1112,1114,1116],{"class":222,"line":1113},65,[220,1115,1087],{"class":233},[220,1117,1090],{"class":244},[220,1119,1121,1123,1125,1128,1131,1134,1137,1140],{"class":222,"line":1120},66,[220,1122,1006],{"class":244},[220,1124,515],{"class":233},[220,1126,1127],{"class":233}," f",[220,1129,1130],{"class":386},"\"HTTP ",[220,1132,1133],{"class":237},"{",[220,1135,1136],{"class":244},"response.status_code",[220,1138,1139],{"class":237},"}",[220,1141,1142],{"class":386},"\"\n",[220,1144,1146,1149,1151],{"class":222,"line":1145},67,[220,1147,1148],{"class":244},"            server_delay ",[220,1150,515],{"class":233},[220,1152,1153],{"class":244}," retry_after_seconds(response)\n",[220,1155,1157],{"class":222,"line":1156},68,[220,1158,1159],{"class":244},"            response.close()\n",[220,1161,1163,1165,1168,1170,1172,1174],{"class":222,"line":1162},69,[220,1164,991],{"class":233},[220,1166,1167],{"class":244}," server_delay ",[220,1169,728],{"class":233},[220,1171,1035],{"class":233},[220,1173,698],{"class":237},[220,1175,438],{"class":244},[220,1177,1179,1182,1184,1186],{"class":222,"line":1178},70,[220,1180,1181],{"class":244},"                delay ",[220,1183,515],{"class":233},[220,1185,613],{"class":237},[220,1187,1188],{"class":244},"(server_delay, policy.max_retry_after)\n",[220,1190,1192],{"class":222,"line":1191},71,[220,1193,1194],{"class":244},"                policy.on_retry(attempt, delay, why)\n",[220,1196,1198],{"class":222,"line":1197},72,[220,1199,1200],{"class":244},"                policy.sleep(delay)\n",[220,1202,1204],{"class":222,"line":1203},73,[220,1205,1206],{"class":233},"                continue\n",[220,1208,1210,1213,1215],{"class":222,"line":1209},74,[220,1211,1212],{"class":244},"        delay ",[220,1214,515],{"class":233},[220,1216,1217],{"class":244}," policy.backoff(attempt)\n",[220,1219,1221],{"class":222,"line":1220},75,[220,1222,1223],{"class":244},"        policy.on_retry(attempt, delay, why)\n",[220,1225,1227],{"class":222,"line":1226},76,[220,1228,1229],{"class":244},"        policy.sleep(delay)\n",[220,1231,1233,1236,1239,1241,1244],{"class":222,"line":1232},77,[220,1234,1235],{"class":233},"    raise",[220,1237,1238],{"class":237}," AssertionError",[220,1240,616],{"class":244},[220,1242,1243],{"class":386},"\"unreachable\"",[220,1245,576],{"class":244},[10,1247,1248,1249,1252],{},"Using it from the API layer is a one-line change per request: build the request, then send it through the wrapper. The ",[14,1250,1251],{},"on_retry"," hook is how the command layer tells the user what is happening:",[211,1254,1256],{"className":213,"code":1255,"language":215,"meta":216,"style":216},"import typer\n\nfrom mytool.retry import RetryPolicy, send_with_retries\n\n\ndef announce(n: int, delay: float, why: str) -> None:\n    typer.echo(f\"  {why}; retrying in {delay:.1f}s (attempt {n + 1})\", err=True)\n\n\ndef get_json(client, path: str):\n    request = client.build_request(\"GET\", path)\n    response = send_with_retries(client, request, RetryPolicy(on_retry=announce))\n    response.raise_for_status()\n    return response.json()\n",[14,1257,1258,1265,1269,1281,1285,1289,1317,1376,1380,1384,1398,1413,1430,1435],{"__ignoreMap":216},[220,1259,1260,1262],{"class":222,"line":223},[220,1261,258],{"class":233},[220,1263,1264],{"class":244}," typer\n",[220,1266,1267],{"class":222,"line":230},[220,1268,252],{"emptyLinePlaceholder":251},[220,1270,1271,1273,1276,1278],{"class":222,"line":248},[220,1272,234],{"class":233},[220,1274,1275],{"class":244}," mytool.retry ",[220,1277,258],{"class":233},[220,1279,1280],{"class":244}," RetryPolicy, send_with_retries\n",[220,1282,1283],{"class":222,"line":255},[220,1284,252],{"emptyLinePlaceholder":251},[220,1286,1287],{"class":222,"line":264},[220,1288,252],{"emptyLinePlaceholder":251},[220,1290,1291,1293,1296,1299,1301,1304,1306,1309,1311,1313,1315],{"class":222,"line":272},[220,1292,684],{"class":233},[220,1294,1295],{"class":425}," announce",[220,1297,1298],{"class":244},"(n: ",[220,1300,447],{"class":237},[220,1302,1303],{"class":244},", delay: ",[220,1305,464],{"class":237},[220,1307,1308],{"class":244},", why: ",[220,1310,550],{"class":237},[220,1312,598],{"class":244},[220,1314,509],{"class":237},[220,1316,438],{"class":244},[220,1318,1319,1322,1325,1328,1330,1333,1335,1338,1340,1343,1346,1348,1351,1353,1356,1358,1361,1364,1366,1369,1371,1374],{"class":222,"line":285},[220,1320,1321],{"class":244},"    typer.echo(",[220,1323,1324],{"class":233},"f",[220,1326,1327],{"class":386},"\"  ",[220,1329,1133],{"class":237},[220,1331,1332],{"class":244},"why",[220,1334,1139],{"class":237},[220,1336,1337],{"class":386},"; retrying in ",[220,1339,1133],{"class":237},[220,1341,1342],{"class":244},"delay",[220,1344,1345],{"class":233},":.1f",[220,1347,1139],{"class":237},[220,1349,1350],{"class":386},"s (attempt ",[220,1352,1133],{"class":237},[220,1354,1355],{"class":244},"n ",[220,1357,937],{"class":233},[220,1359,1360],{"class":237}," 1}",[220,1362,1363],{"class":386},")\"",[220,1365,59],{"class":244},[220,1367,1368],{"class":564},"err",[220,1370,515],{"class":233},[220,1372,1373],{"class":237},"True",[220,1375,576],{"class":244},[220,1377,1378],{"class":222,"line":298},[220,1379,252],{"emptyLinePlaceholder":251},[220,1381,1382],{"class":222,"line":311},[220,1383,252],{"emptyLinePlaceholder":251},[220,1385,1386,1388,1391,1394,1396],{"class":222,"line":324},[220,1387,684],{"class":233},[220,1389,1390],{"class":425}," get_json",[220,1392,1393],{"class":244},"(client, path: ",[220,1395,550],{"class":237},[220,1397,798],{"class":244},[220,1399,1400,1403,1405,1408,1410],{"class":222,"line":329},[220,1401,1402],{"class":244},"    request ",[220,1404,515],{"class":233},[220,1406,1407],{"class":244}," client.build_request(",[220,1409,387],{"class":386},[220,1411,1412],{"class":244},", path)\n",[220,1414,1415,1418,1420,1423,1425,1427],{"class":222,"line":337},[220,1416,1417],{"class":244},"    response ",[220,1419,515],{"class":233},[220,1421,1422],{"class":244}," send_with_retries(client, request, RetryPolicy(",[220,1424,1251],{"class":564},[220,1426,515],{"class":233},[220,1428,1429],{"class":244},"announce))\n",[220,1431,1432],{"class":222,"line":342},[220,1433,1434],{"class":244},"    response.raise_for_status()\n",[220,1436,1437,1439],{"class":222,"line":374},[220,1438,811],{"class":233},[220,1440,1441],{"class":244}," response.json()\n",[10,1443,1444,1447],{},[14,1445,1446],{},"httpx.HTTPTransport(retries=N)"," is still worth setting: it retries failed connection attempts at the transport level, below this wrapper. The two layers are complementary — the transport handles \"could not connect\", the wrapper handles everything that needs a policy decision.",[33,1449,1451],{"id":1450},"ux-considerations","UX considerations",[38,1453,1454,1460,1474,1480,1486],{},[41,1455,1456,1459],{},[80,1457,1458],{},"Say that you are retrying."," One stderr line per retry — the reason, the delay, the attempt — turns a mysterious pause into visible resilience. Silence for fifteen seconds reads as a hang.",[41,1461,1462,1465,1466,1469,1470,1473],{},[80,1463,1464],{},"Expose the budget."," A ",[14,1467,1468],{},"--retries N"," option (with ",[14,1471,1472],{},"0"," to disable) helps in CI, where a fast failure may be preferable, and in flaky networks, where users want more patience.",[41,1475,1476,1479],{},[80,1477,1478],{},"Report the final failure with its history."," \"the service returned 503 after 5 attempts over 14s\" tells the user that retrying by hand right now is pointless.",[41,1481,1482,1485],{},[80,1483,1484],{},"Never retry user errors."," A typo in a project name producing a 404 should fail on the first attempt, instantly.",[41,1487,1488,1491,1492,1495,1496,1499,1500,31],{},[80,1489,1490],{},"Respect Ctrl+C during waits."," ",[14,1493,1494],{},"time.sleep"," is interruptible by ",[14,1497,1498],{},"KeyboardInterrupt",", so a user can always abandon a retry loop; make sure your top-level handler exits cleanly, as in ",[27,1501,1503],{"href":1502},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly\u002F","handling KeyboardInterrupt cleanly",[33,1505,1507],{"id":1506},"testing-the-behaviour","Testing the behaviour",[10,1509,1510,1511,1514,1515,1518],{},"Inject a fake sleep that records delays and a fixed ",[14,1512,1513],{},"rand",", then drive responses from a ",[14,1516,1517],{},"MockTransport"," that replays a scripted sequence. The whole suite runs instantly:",[211,1520,1522],{"className":213,"code":1521,"language":215,"meta":216,"style":216},"# tests\u002Ftest_retry.py\nimport httpx\nimport pytest\n\nfrom mytool.retry import RetryPolicy, send_with_retries\n\n\ndef scripted(*outcomes):\n    \"\"\"A transport that returns (or raises) each outcome in turn.\"\"\"\n    calls = []\n\n    def handler(request):\n        calls.append(request)\n        outcome = outcomes[len(calls) - 1]\n        if isinstance(outcome, Exception):\n            raise outcome\n        return outcome\n\n    return httpx.Client(transport=httpx.MockTransport(handler), base_url=\"https:\u002F\u002Fapi.test\"), calls\n\n\ndef policy(slept):\n    return RetryPolicy(sleep=slept.append, rand=lambda: 1.0)\n\n\ndef test_retries_503_then_succeeds():\n    client, calls = scripted(httpx.Response(503), httpx.Response(503), httpx.Response(200))\n    slept = []\n    r = send_with_retries(client, client.build_request(\"GET\", \"\u002Fx\"), policy(slept))\n    assert r.status_code == 200\n    assert len(calls) == 3\n    assert slept == [0.5, 1.0]\n\n\ndef test_does_not_retry_404():\n    client, calls = scripted(httpx.Response(404))\n    r = send_with_retries(client, client.build_request(\"GET\", \"\u002Fx\"), policy([]))\n    assert r.status_code == 404 and len(calls) == 1\n\n\ndef test_honours_retry_after():\n    client, _ = scripted(httpx.Response(429, headers={\"Retry-After\": \"3\"}), httpx.Response(200))\n    slept = []\n    send_with_retries(client, client.build_request(\"GET\", \"\u002Fx\"), policy(slept))\n    assert slept == [3.0]\n\n\ndef test_post_is_not_retried_on_503_without_key():\n    client, calls = scripted(httpx.Response(503), httpx.Response(200))\n    r = send_with_retries(client, client.build_request(\"POST\", \"\u002Fx\"), policy([]))\n    assert r.status_code == 503 and len(calls) == 1\n\n\ndef test_post_with_idempotency_key_is_retried():\n    client, calls = scripted(httpx.Response(503), httpx.Response(201))\n    req = client.build_request(\"POST\", \"\u002Fx\", headers={\"Idempotency-Key\": \"abc\"})\n    assert send_with_retries(client, req, policy([])).status_code == 201\n\n\ndef test_gives_up_after_budget():\n    client, calls = scripted(*[httpx.ConnectError(\"down\")] * 5)\n    with pytest.raises(httpx.ConnectError):\n        send_with_retries(client, client.build_request(\"GET\", \"\u002Fx\"), policy([]))\n    assert len(calls) == 5\n\n\ndef test_backoff_is_capped():\n    p = RetryPolicy(rand=lambda: 1.0)\n    assert [p.backoff(n) for n in range(1, 8)] == [0.5, 1, 2, 4, 8, 8, 8]\n",[14,1523,1524,1529,1535,1542,1546,1556,1560,1564,1578,1583,1593,1597,1607,1612,1635,1651,1659,1665,1669,1695,1699,1703,1713,1740,1744,1748,1758,1782,1791,1811,1824,1838,1859,1863,1867,1876,1889,1906,1928,1932,1936,1945,1979,1987,2000,2015,2019,2023,2032,2048,2065,2086,2090,2094,2103,2120,2153,2165,2169,2173,2182,2208,2216,2229,2242,2246,2250,2259,2278],{"__ignoreMap":216},[220,1525,1526],{"class":222,"line":223},[220,1527,1528],{"class":226},"# tests\u002Ftest_retry.py\n",[220,1530,1531,1533],{"class":222,"line":230},[220,1532,258],{"class":233},[220,1534,334],{"class":244},[220,1536,1537,1539],{"class":222,"line":248},[220,1538,258],{"class":233},[220,1540,1541],{"class":244}," pytest\n",[220,1543,1544],{"class":222,"line":255},[220,1545,252],{"emptyLinePlaceholder":251},[220,1547,1548,1550,1552,1554],{"class":222,"line":264},[220,1549,234],{"class":233},[220,1551,1275],{"class":244},[220,1553,258],{"class":233},[220,1555,1280],{"class":244},[220,1557,1558],{"class":222,"line":272},[220,1559,252],{"emptyLinePlaceholder":251},[220,1561,1562],{"class":222,"line":285},[220,1563,252],{"emptyLinePlaceholder":251},[220,1565,1566,1568,1571,1573,1575],{"class":222,"line":298},[220,1567,684],{"class":233},[220,1569,1570],{"class":425}," scripted",[220,1572,616],{"class":244},[220,1574,630],{"class":233},[220,1576,1577],{"class":244},"outcomes):\n",[220,1579,1580],{"class":222,"line":311},[220,1581,1582],{"class":386},"    \"\"\"A transport that returns (or raises) each outcome in turn.\"\"\"\n",[220,1584,1585,1588,1590],{"class":222,"line":324},[220,1586,1587],{"class":244},"    calls ",[220,1589,515],{"class":233},[220,1591,1592],{"class":244}," []\n",[220,1594,1595],{"class":222,"line":329},[220,1596,252],{"emptyLinePlaceholder":251},[220,1598,1599,1601,1604],{"class":222,"line":337},[220,1600,587],{"class":233},[220,1602,1603],{"class":425}," handler",[220,1605,1606],{"class":244},"(request):\n",[220,1608,1609],{"class":222,"line":342},[220,1610,1611],{"class":244},"        calls.append(request)\n",[220,1613,1614,1617,1619,1622,1625,1628,1630,1632],{"class":222,"line":374},[220,1615,1616],{"class":244},"        outcome ",[220,1618,515],{"class":233},[220,1620,1621],{"class":244}," outcomes[",[220,1623,1624],{"class":237},"len",[220,1626,1627],{"class":244},"(calls) ",[220,1629,642],{"class":233},[220,1631,645],{"class":237},[220,1633,1634],{"class":244},"]\n",[220,1636,1637,1640,1643,1646,1649],{"class":222,"line":412},[220,1638,1639],{"class":233},"        if",[220,1641,1642],{"class":237}," isinstance",[220,1644,1645],{"class":244},"(outcome, ",[220,1647,1648],{"class":237},"Exception",[220,1650,798],{"class":244},[220,1652,1653,1656],{"class":222,"line":417},[220,1654,1655],{"class":233},"            raise",[220,1657,1658],{"class":244}," outcome\n",[220,1660,1661,1663],{"class":222,"line":422},[220,1662,654],{"class":233},[220,1664,1658],{"class":244},[220,1666,1667],{"class":222,"line":429},[220,1668,252],{"emptyLinePlaceholder":251},[220,1670,1671,1673,1676,1679,1681,1684,1687,1689,1692],{"class":222,"line":441},[220,1672,811],{"class":233},[220,1674,1675],{"class":244}," httpx.Client(",[220,1677,1678],{"class":564},"transport",[220,1680,515],{"class":233},[220,1682,1683],{"class":244},"httpx.MockTransport(handler), ",[220,1685,1686],{"class":564},"base_url",[220,1688,515],{"class":233},[220,1690,1691],{"class":386},"\"https:\u002F\u002Fapi.test\"",[220,1693,1694],{"class":244},"), calls\n",[220,1696,1697],{"class":222,"line":458},[220,1698,252],{"emptyLinePlaceholder":251},[220,1700,1701],{"class":222,"line":472},[220,1702,252],{"emptyLinePlaceholder":251},[220,1704,1705,1707,1710],{"class":222,"line":485},[220,1706,684],{"class":233},[220,1708,1709],{"class":425}," policy",[220,1711,1712],{"class":244},"(slept):\n",[220,1714,1715,1717,1720,1723,1725,1728,1730,1732,1735,1738],{"class":222,"line":498},[220,1716,811],{"class":233},[220,1718,1719],{"class":244}," RetryPolicy(",[220,1721,1722],{"class":564},"sleep",[220,1724,515],{"class":233},[220,1726,1727],{"class":244},"slept.append, ",[220,1729,1513],{"class":564},[220,1731,568],{"class":233},[220,1733,1734],{"class":244},": ",[220,1736,1737],{"class":237},"1.0",[220,1739,576],{"class":244},[220,1741,1742],{"class":222,"line":521},[220,1743,252],{"emptyLinePlaceholder":251},[220,1745,1746],{"class":222,"line":536},[220,1747,252],{"emptyLinePlaceholder":251},[220,1749,1750,1752,1755],{"class":222,"line":579},[220,1751,684],{"class":233},[220,1753,1754],{"class":425}," test_retries_503_then_succeeds",[220,1756,1757],{"class":244},"():\n",[220,1759,1760,1763,1765,1768,1770,1773,1775,1777,1780],{"class":222,"line":584},[220,1761,1762],{"class":244},"    client, calls ",[220,1764,515],{"class":233},[220,1766,1767],{"class":244}," scripted(httpx.Response(",[220,1769,104],{"class":237},[220,1771,1772],{"class":244},"), httpx.Response(",[220,1774,104],{"class":237},[220,1776,1772],{"class":244},[220,1778,1779],{"class":237},"200",[220,1781,648],{"class":244},[220,1783,1784,1787,1789],{"class":222,"line":605},[220,1785,1786],{"class":244},"    slept ",[220,1788,515],{"class":233},[220,1790,1592],{"class":244},[220,1792,1793,1796,1798,1801,1803,1805,1808],{"class":222,"line":651},[220,1794,1795],{"class":244},"    r ",[220,1797,515],{"class":233},[220,1799,1800],{"class":244}," send_with_retries(client, client.build_request(",[220,1802,387],{"class":386},[220,1804,59],{"class":244},[220,1806,1807],{"class":386},"\"\u002Fx\"",[220,1809,1810],{"class":244},"), policy(slept))\n",[220,1812,1813,1816,1819,1821],{"class":222,"line":671},[220,1814,1815],{"class":233},"    assert",[220,1817,1818],{"class":244}," r.status_code ",[220,1820,954],{"class":233},[220,1822,1823],{"class":237}," 200\n",[220,1825,1826,1828,1831,1833,1835],{"class":222,"line":676},[220,1827,1815],{"class":233},[220,1829,1830],{"class":237}," len",[220,1832,1627],{"class":244},[220,1834,954],{"class":233},[220,1836,1837],{"class":237}," 3\n",[220,1839,1840,1842,1845,1847,1850,1853,1855,1857],{"class":222,"line":681},[220,1841,1815],{"class":233},[220,1843,1844],{"class":244}," slept ",[220,1846,954],{"class":233},[220,1848,1849],{"class":244}," [",[220,1851,1852],{"class":237},"0.5",[220,1854,59],{"class":244},[220,1856,1737],{"class":237},[220,1858,1634],{"class":244},[220,1860,1861],{"class":222,"line":703},[220,1862,252],{"emptyLinePlaceholder":251},[220,1864,1865],{"class":222,"line":719},[220,1866,252],{"emptyLinePlaceholder":251},[220,1868,1869,1871,1874],{"class":222,"line":735},[220,1870,684],{"class":233},[220,1872,1873],{"class":425}," test_does_not_retry_404",[220,1875,1757],{"class":244},[220,1877,1878,1880,1882,1884,1887],{"class":222,"line":743},[220,1879,1762],{"class":244},[220,1881,515],{"class":233},[220,1883,1767],{"class":244},[220,1885,1886],{"class":237},"404",[220,1888,648],{"class":244},[220,1890,1891,1893,1895,1897,1899,1901,1903],{"class":222,"line":751},[220,1892,1795],{"class":244},[220,1894,515],{"class":233},[220,1896,1800],{"class":244},[220,1898,387],{"class":386},[220,1900,59],{"class":244},[220,1902,1807],{"class":386},[220,1904,1905],{"class":244},"), policy([]))\n",[220,1907,1908,1910,1912,1914,1917,1919,1921,1923,1925],{"class":222,"line":762},[220,1909,1815],{"class":233},[220,1911,1818],{"class":244},[220,1913,954],{"class":233},[220,1915,1916],{"class":237}," 404",[220,1918,1106],{"class":233},[220,1920,1830],{"class":237},[220,1922,1627],{"class":244},[220,1924,954],{"class":233},[220,1926,1927],{"class":237}," 1\n",[220,1929,1930],{"class":222,"line":770},[220,1931,252],{"emptyLinePlaceholder":251},[220,1933,1934],{"class":222,"line":781},[220,1935,252],{"emptyLinePlaceholder":251},[220,1937,1938,1940,1943],{"class":222,"line":801},[220,1939,684],{"class":233},[220,1941,1942],{"class":425}," test_honours_retry_after",[220,1944,1757],{"class":244},[220,1946,1947,1950,1952,1954,1956,1958,1961,1963,1965,1967,1969,1972,1975,1977],{"class":222,"line":808},[220,1948,1949],{"class":244},"    client, _ ",[220,1951,515],{"class":233},[220,1953,1767],{"class":244},[220,1955,115],{"class":237},[220,1957,59],{"class":244},[220,1959,1960],{"class":564},"headers",[220,1962,515],{"class":233},[220,1964,1133],{"class":244},[220,1966,714],{"class":386},[220,1968,1734],{"class":244},[220,1970,1971],{"class":386},"\"3\"",[220,1973,1974],{"class":244},"}), httpx.Response(",[220,1976,1779],{"class":237},[220,1978,648],{"class":244},[220,1980,1981,1983,1985],{"class":222,"line":830},[220,1982,1786],{"class":244},[220,1984,515],{"class":233},[220,1986,1592],{"class":244},[220,1988,1989,1992,1994,1996,1998],{"class":222,"line":835},[220,1990,1991],{"class":244},"    send_with_retries(client, client.build_request(",[220,1993,387],{"class":386},[220,1995,59],{"class":244},[220,1997,1807],{"class":386},[220,1999,1810],{"class":244},[220,2001,2002,2004,2006,2008,2010,2013],{"class":222,"line":840},[220,2003,1815],{"class":233},[220,2005,1844],{"class":244},[220,2007,954],{"class":233},[220,2009,1849],{"class":244},[220,2011,2012],{"class":237},"3.0",[220,2014,1634],{"class":244},[220,2016,2017],{"class":222,"line":851},[220,2018,252],{"emptyLinePlaceholder":251},[220,2020,2021],{"class":222,"line":869},[220,2022,252],{"emptyLinePlaceholder":251},[220,2024,2025,2027,2030],{"class":222,"line":886},[220,2026,684],{"class":233},[220,2028,2029],{"class":425}," test_post_is_not_retried_on_503_without_key",[220,2031,1757],{"class":244},[220,2033,2034,2036,2038,2040,2042,2044,2046],{"class":222,"line":915},[220,2035,1762],{"class":244},[220,2037,515],{"class":233},[220,2039,1767],{"class":244},[220,2041,104],{"class":237},[220,2043,1772],{"class":244},[220,2045,1779],{"class":237},[220,2047,648],{"class":244},[220,2049,2050,2052,2054,2056,2059,2061,2063],{"class":222,"line":944},[220,2051,1795],{"class":244},[220,2053,515],{"class":233},[220,2055,1800],{"class":244},[220,2057,2058],{"class":386},"\"POST\"",[220,2060,59],{"class":244},[220,2062,1807],{"class":386},[220,2064,1905],{"class":244},[220,2066,2067,2069,2071,2073,2076,2078,2080,2082,2084],{"class":222,"line":960},[220,2068,1815],{"class":233},[220,2070,1818],{"class":244},[220,2072,954],{"class":233},[220,2074,2075],{"class":237}," 503",[220,2077,1106],{"class":233},[220,2079,1830],{"class":237},[220,2081,1627],{"class":244},[220,2083,954],{"class":233},[220,2085,1927],{"class":237},[220,2087,2088],{"class":222,"line":968},[220,2089,252],{"emptyLinePlaceholder":251},[220,2091,2092],{"class":222,"line":979},[220,2093,252],{"emptyLinePlaceholder":251},[220,2095,2096,2098,2101],{"class":222,"line":988},[220,2097,684],{"class":233},[220,2099,2100],{"class":425}," test_post_with_idempotency_key_is_retried",[220,2102,1757],{"class":244},[220,2104,2105,2107,2109,2111,2113,2115,2118],{"class":222,"line":997},[220,2106,1762],{"class":244},[220,2108,515],{"class":233},[220,2110,1767],{"class":244},[220,2112,104],{"class":237},[220,2114,1772],{"class":244},[220,2116,2117],{"class":237},"201",[220,2119,648],{"class":244},[220,2121,2122,2125,2127,2129,2131,2133,2135,2137,2139,2141,2143,2146,2148,2151],{"class":222,"line":1003},[220,2123,2124],{"class":244},"    req ",[220,2126,515],{"class":233},[220,2128,1407],{"class":244},[220,2130,2058],{"class":386},[220,2132,59],{"class":244},[220,2134,1807],{"class":386},[220,2136,59],{"class":244},[220,2138,1960],{"class":564},[220,2140,515],{"class":233},[220,2142,1133],{"class":244},[220,2144,2145],{"class":386},"\"Idempotency-Key\"",[220,2147,1734],{"class":244},[220,2149,2150],{"class":386},"\"abc\"",[220,2152,371],{"class":244},[220,2154,2155,2157,2160,2162],{"class":222,"line":1017},[220,2156,1815],{"class":233},[220,2158,2159],{"class":244}," send_with_retries(client, req, policy([])).status_code ",[220,2161,954],{"class":233},[220,2163,2164],{"class":237}," 201\n",[220,2166,2167],{"class":222,"line":1025},[220,2168,252],{"emptyLinePlaceholder":251},[220,2170,2171],{"class":222,"line":1041},[220,2172,252],{"emptyLinePlaceholder":251},[220,2174,2175,2177,2180],{"class":222,"line":1046},[220,2176,684],{"class":233},[220,2178,2179],{"class":425}," test_gives_up_after_budget",[220,2181,1757],{"class":244},[220,2183,2184,2186,2188,2191,2193,2196,2199,2202,2204,2206],{"class":222,"line":1056},[220,2185,1762],{"class":244},[220,2187,515],{"class":233},[220,2189,2190],{"class":244}," scripted(",[220,2192,630],{"class":233},[220,2194,2195],{"class":244},"[httpx.ConnectError(",[220,2197,2198],{"class":386},"\"down\"",[220,2200,2201],{"class":244},")] ",[220,2203,630],{"class":233},[220,2205,452],{"class":237},[220,2207,576],{"class":244},[220,2209,2210,2213],{"class":222,"line":1064},[220,2211,2212],{"class":233},"    with",[220,2214,2215],{"class":244}," pytest.raises(httpx.ConnectError):\n",[220,2217,2218,2221,2223,2225,2227],{"class":222,"line":1084},[220,2219,2220],{"class":244},"        send_with_retries(client, client.build_request(",[220,2222,387],{"class":386},[220,2224,59],{"class":244},[220,2226,1807],{"class":386},[220,2228,1905],{"class":244},[220,2230,2231,2233,2235,2237,2239],{"class":222,"line":1093},[220,2232,1815],{"class":233},[220,2234,1830],{"class":237},[220,2236,1627],{"class":244},[220,2238,954],{"class":233},[220,2240,2241],{"class":237}," 5\n",[220,2243,2244],{"class":222,"line":1113},[220,2245,252],{"emptyLinePlaceholder":251},[220,2247,2248],{"class":222,"line":1120},[220,2249,252],{"emptyLinePlaceholder":251},[220,2251,2252,2254,2257],{"class":222,"line":1145},[220,2253,684],{"class":233},[220,2255,2256],{"class":425}," test_backoff_is_capped",[220,2258,1757],{"class":244},[220,2260,2261,2264,2266,2268,2270,2272,2274,2276],{"class":222,"line":1156},[220,2262,2263],{"class":244},"    p ",[220,2265,515],{"class":233},[220,2267,1719],{"class":244},[220,2269,1513],{"class":564},[220,2271,568],{"class":233},[220,2273,1734],{"class":244},[220,2275,1737],{"class":237},[220,2277,576],{"class":244},[220,2279,2280,2282,2285,2288,2291,2293,2295,2297,2299,2301,2304,2306,2308,2310,2312,2314,2316,2318,2321,2323,2326,2328,2330,2332,2334,2336,2338],{"class":222,"line":1162},[220,2281,1815],{"class":233},[220,2283,2284],{"class":244}," [p.backoff(n) ",[220,2286,2287],{"class":233},"for",[220,2289,2290],{"class":244}," n ",[220,2292,897],{"class":233},[220,2294,926],{"class":237},[220,2296,616],{"class":244},[220,2298,931],{"class":237},[220,2300,59],{"class":244},[220,2302,2303],{"class":237},"8",[220,2305,2201],{"class":244},[220,2307,954],{"class":233},[220,2309,1849],{"class":244},[220,2311,1852],{"class":237},[220,2313,59],{"class":244},[220,2315,931],{"class":237},[220,2317,59],{"class":244},[220,2319,2320],{"class":237},"2",[220,2322,59],{"class":244},[220,2324,2325],{"class":237},"4",[220,2327,59],{"class":244},[220,2329,2303],{"class":237},[220,2331,59],{"class":244},[220,2333,2303],{"class":237},[220,2335,59],{"class":244},[220,2337,2303],{"class":237},[220,2339,1634],{"class":244},[10,2341,2342,2343,2345,2346,31],{},"With ",[14,2344,1513],{}," fixed at 1.0 the jitter disappears and delays are exact, which makes the backoff schedule itself a tested contract. The same scripted-transport idea extends to testing whole commands, as in ",[27,2347,2349],{"href":2348},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests\u002F","mocking filesystem and network in CLI tests",[33,2351,2353],{"id":2352},"conclusion","Conclusion",[10,2355,2356,2357,2359],{},"Good retries are narrow and polite: only transient failures, only requests that are safe to repeat, capped exponential backoff with jitter, the server's ",[14,2358,194],{}," when it gives one, and a visible message each time. Keep the policy in one small function with injectable sleep and randomness, and it becomes one of the best-tested parts of your CLI rather than an untested loop nobody dares touch.",[33,2361,2363],{"id":2362},"frequently-asked-questions","Frequently asked questions",[2365,2366,2368,2369,188,2372,2375],"h3",{"id":2367},"should-i-use-tenacity-or-stamina-instead-of-writing-this","Should I use ",[14,2370,2371],{},"tenacity",[14,2373,2374],{},"stamina"," instead of writing this?",[10,2377,2378,2379,2381,2382,2384,2385,2387],{},"Both are good. ",[14,2380,2371],{}," is flexible and widely used; ",[14,2383,2374],{}," wraps it with sensible defaults and test helpers. A library earns its place if you retry many different kinds of operation. For HTTP alone, the forty lines above encode HTTP-specific rules — idempotency, ",[14,2386,194],{}," — that you would configure into a library anyway.",[2365,2389,2391],{"id":2390},"how-many-attempts-are-appropriate-for-a-cli","How many attempts are appropriate for a CLI?",[10,2393,2394],{},"Three to five total attempts with a cap of eight to ten seconds per wait covers most blips while keeping the worst case under half a minute. Batch jobs running unattended can afford more; interactive commands should prefer failing clearly.",[2365,2396,2398],{"id":2397},"what-about-retrying-inside-a-paginated-listing","What about retrying inside a paginated listing?",[10,2400,2401,2402,2406],{},"Retry each page request individually, so a failure on page 40 does not restart from page 1. The generator pattern in ",[27,2403,2405],{"href":2404},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli\u002F","paginating API results in a CLI"," makes that natural.",[2365,2408,2410],{"id":2409},"can-retries-make-rate-limiting-worse","Can retries make rate limiting worse?",[10,2412,2413,2414,2416,2417,31],{},"They can if they ignore ",[14,2415,194],{}," or retry without backoff. When you also run requests concurrently, add a client-side rate limiter so you do not provoke the 429s in the first place; see ",[27,2418,2420],{"href":2419},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis\u002F","rate-limiting concurrent requests in CLIs",[33,2422,2424],{"id":2423},"related","Related",[38,2426,2427,2433,2438,2444,2449],{},[41,2428,2429,2430],{},"Up: ",[27,2431,2432],{"href":29},"Calling HTTP APIs from Python CLIs",[41,2434,2435],{},[27,2436,2437],{"href":51},"Building an API client CLI with httpx",[41,2439,2440],{},[27,2441,2443],{"href":2442},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python\u002F","Downloading files with progress in Python",[41,2445,2446],{},[27,2447,2448],{"href":2419},"Rate-limiting concurrent requests in CLIs",[41,2450,2451],{},[27,2452,2454],{"href":2453},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","Friendly error messages and tracebacks",[2456,2457,2458],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":216,"searchDepth":230,"depth":230,"links":2460},[2461,2462,2463,2464,2465,2466,2467,2468,2475],{"id":35,"depth":230,"text":36},{"id":74,"depth":230,"text":75},{"id":164,"depth":230,"text":165},{"id":201,"depth":230,"text":202},{"id":1450,"depth":230,"text":1451},{"id":1506,"depth":230,"text":1507},{"id":2352,"depth":230,"text":2353},{"id":2362,"depth":230,"text":2363,"children":2469},[2470,2472,2473,2474],{"id":2367,"depth":248,"text":2471},"Should I use tenacity or stamina instead of writing this?",{"id":2390,"depth":248,"text":2391},{"id":2397,"depth":248,"text":2398},{"id":2409,"depth":248,"text":2410},{"id":2423,"depth":230,"text":2424},"2026-09-18","Retry failed HTTP requests from a Python CLI safely: which failures to retry, capped exponential backoff with jitter, Retry-After, idempotency and tests.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls",{"title":5,"description":2477},"cli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls\u002Findex",[2486,2487,24,2488],"http","retries","reliability","WhbzL1FbvAmztdjb6Z-FvnEsQ2fZh7CKouQeHuMG-VE",[2491,2494,2497,2500,2503,2506,2509,2512,2515,2518,2521,2524,2527,2530,2533,2536,2539,2542,2545,2548,2551,2554,2557,2560,2563,2566,2569,2572,2575,2578,2581,2584,2587,2590,2593,2596,2599,2602,2605,2608,2611,2614,2617,2620,2623,2626,2629,2632,2635,2638,2641,2644,2647,2650,2653,2656,2659,2662,2664,2667,2670,2671,2674,2677,2680,2683,2686,2689,2692,2695,2698,2701,2704,2707,2710,2713,2716,2719,2722,2725,2728,2731,2734,2737,2740,2743,2746,2749,2752,2755,2758,2761,2764,2767,2770,2773,2776,2779,2782,2785,2788,2791,2794,2797,2800,2803,2806,2809,2812,2815,2818,2821,2824,2827,2830,2833,2836,2839,2842,2845,2848,2851,2854,2857,2860,2863,2866,2869,2872,2875,2878,2881,2884,2887,2890,2893,2896,2899,2902,2905,2908,2911,2914,2917,2920,2923,2926,2929,2932,2935,2938,2941,2944,2947,2950,2953,2956,2959,2962,2965,2968,2971,2974,2977,2980,2983,2986,2989,2992,2995,2998,3001,3004,3007,3010,3013,3016,3019,3022,3025,3028,3031,3034],{"path":2492,"title":2493},"\u002Fabout","About Python CLI Toolcraft",{"path":2495,"title":2496},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2498,"title":2499},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2501,"title":2502},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2504,"title":2505},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2507,"title":2508},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2510,"title":2511},"\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":2513,"title":2514},"\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":2516,"title":2517},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2519,"title":2520},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2522,"title":2523},"\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":2525,"title":2526},"\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":2528,"title":2529},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2531,"title":2532},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2534,"title":2535},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2537,"title":2538},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2540,"title":2541},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2543,"title":2544},"\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":2546,"title":2547},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2549,"title":2550},"\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":2552,"title":2553},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2555,"title":2556},"\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":2558,"title":2559},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2561,"title":2562},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2564,"title":2565},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2567,"title":2568},"\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":2570,"title":2571},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2573,"title":2574},"\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":2576,"title":2577},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2579,"title":2580},"\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":2582,"title":2583},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2585,"title":2586},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2588,"title":2589},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2591,"title":2592},"\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":2594,"title":2595},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2597,"title":2598},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2600,"title":2601},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2603,"title":2604},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2606,"title":2607},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2609,"title":2610},"\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":2612,"title":2613},"\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":2615,"title":2616},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2618,"title":2619},"\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":2621,"title":2622},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2624,"title":2625},"\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":2627,"title":2628},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2630,"title":2631},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2633,"title":2634},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2636,"title":2637},"\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":2639,"title":2640},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2642,"title":2643},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2645,"title":2646},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2648,"title":2649},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2651,"title":2652},"\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":2654,"title":2655},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2657,"title":2658},"\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":2660,"title":2661},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2663,"title":2432},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis",{"path":2665,"title":2666},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2668,"title":2669},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2482,"title":5},{"path":2672,"title":2673},"\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":2675,"title":2676},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2678,"title":2679},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2681,"title":2682},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2684,"title":2685},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2687,"title":2688},"\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":2690,"title":2691},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2693,"title":2694},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2696,"title":2697},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2699,"title":2700},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2702,"title":2703},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2705,"title":2706},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2708,"title":2709},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2711,"title":2712},"\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":2714,"title":2715},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2717,"title":2718},"\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":2720,"title":2721},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2723,"title":2724},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2726,"title":2727},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2729,"title":2730},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2732,"title":2733},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2735,"title":2736},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2738,"title":2739},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2741,"title":2742},"\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":2744,"title":2745},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2747,"title":2748},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2750,"title":2751},"\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":2753,"title":2754},"\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":2756,"title":2757},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2759,"title":2760},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2762,"title":2763},"\u002F","Python CLI Toolcraft",{"path":2765,"title":2766},"\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":2768,"title":2769},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2771,"title":2772},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2774,"title":2775},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2777,"title":2778},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2780,"title":2781},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2783,"title":2784},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2786,"title":2787},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2789,"title":2790},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2792,"title":2793},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2795,"title":2796},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2798,"title":2799},"\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":2801,"title":2802},"\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":2804,"title":2805},"\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":2807,"title":2808},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2810,"title":2811},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2813,"title":2814},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2816,"title":2817},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2819,"title":2820},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2822,"title":2823},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2825,"title":2826},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2828,"title":2829},"\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":2831,"title":2832},"\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":2834,"title":2835},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2837,"title":2838},"\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":2840,"title":2841},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2843,"title":2844},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2846,"title":2847},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2849,"title":2850},"\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":2852,"title":2853},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2855,"title":2856},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2858,"title":2859},"\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":2861,"title":2862},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2864,"title":2865},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2867,"title":2868},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2870,"title":2871},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2873,"title":2874},"\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":2876,"title":2877},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2879,"title":2880},"\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":2882,"title":2883},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2885,"title":2886},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2888,"title":2889},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2891,"title":2892},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2894,"title":2895},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2897,"title":2898},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2900,"title":2901},"\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":2903,"title":2904},"\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":2906,"title":2907},"\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":2909,"title":2910},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2912,"title":2913},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2915,"title":2916},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2918,"title":2919},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2921,"title":2922},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2924,"title":2925},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2927,"title":2928},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2930,"title":2931},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2933,"title":2934},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2936,"title":2937},"\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":2939,"title":2940},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2942,"title":2943},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2945,"title":2946},"\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":2948,"title":2949},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2951,"title":2952},"\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":2954,"title":2955},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2957,"title":2958},"\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":2960,"title":2961},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2963,"title":2964},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2966,"title":2967},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2969,"title":2970},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2972,"title":2973},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2975,"title":2976},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2978,"title":2979},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2981,"title":2982},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2984,"title":2985},"\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":2987,"title":2988},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2990,"title":2991},"\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":2993,"title":2994},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2996,"title":2997},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2999,"title":3000},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":3002,"title":3003},"\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":3005,"title":3006},"\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":3008,"title":3009},"\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":3011,"title":3012},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":3014,"title":3015},"\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":3017,"title":3018},"\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":3020,"title":3021},"\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":3023,"title":3024},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":3026,"title":3027},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":3029,"title":3030},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":3032,"title":3033},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":3035,"title":3036},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905048]