77import os
88import sys
99import time
10- from typing import Any , Iterable , List , Optional
10+ from typing import TYPE_CHECKING , Any , Dict , Iterable , List , Optional
1111
1212import requests
1313from packaging import version
2323)
2424from .helpers import handle_url_overrides
2525
26+ if TYPE_CHECKING :
27+ from linodecli .cli import CLI
2628
27- def get_all_pages (ctx , operation : OpenAPIOperation , args : List [str ]):
29+
30+ def get_all_pages (
31+ ctx : "CLI" , operation : OpenAPIOperation , args : List [str ]
32+ ) -> Dict [str , Any ]:
2833 """
29- Receive all pages of a resource from multiple
30- API responses then merge into one page.
34+ Retrieves all pages of a resource from multiple API responses
35+ and merges them into a single page.
36+
37+ :param ctx: The main CLI object that maintains API request state.
38+ :param operation: The OpenAPI operation to be executed.
39+ :param args: A list of arguments passed to the API request.
3140
32- :param ctx: The main CLI object
41+ :return: A dictionary containing the merged results from all pages.
3342 """
3443
3544 ctx .page_size = 500
@@ -38,6 +47,7 @@ def get_all_pages(ctx, operation: OpenAPIOperation, args: List[str]):
3847
3948 total_pages = result .get ("pages" )
4049
50+ # If multiple pages exist, generate results for all additional pages
4151 if total_pages and total_pages > 1 :
4252 pages_needed = range (2 , total_pages + 1 )
4353
@@ -51,19 +61,25 @@ def get_all_pages(ctx, operation: OpenAPIOperation, args: List[str]):
5161
5262
5363def do_request (
54- ctx ,
55- operation ,
56- args ,
57- filter_header = None ,
58- skip_error_handling = False ,
64+ ctx : "CLI" ,
65+ operation : OpenAPIOperation ,
66+ args : List [ str ] ,
67+ filter_header : Optional [ dict ] = None ,
68+ skip_error_handling : bool = False ,
5969) -> (
6070 Response
6171): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
6272 """
63- Makes a request to an operation's URL and returns the resulting JSON, or
64- prints and error if a non-200 comes back
73+ Makes an HTTP request to an API operation's URL and returns the resulting response.
74+ Optionally retries the request if specified, handles errors, and supports debugging.
75+
76+ :param ctx: The main CLI object that maintains API request state.
77+ :param operation: The OpenAPI operation to be executed.
78+ :param args: A list of arguments passed to the API request.
79+ :param filter_header: Optional filter header to be included in the request (default: None).
80+ :param skip_error_handling: Whether to skip error handling (default: False).
6581
66- :param ctx : The main CLI object
82+ :return : The `Response` object returned from the HTTP request.
6783 """
6884 # TODO: Revisit using pre-built calls from OpenAPI
6985 method = getattr (requests , operation .method )
@@ -95,31 +111,45 @@ def do_request(
95111 if ctx .debug_request :
96112 _print_response_debug_info (result )
97113
114+ # Retry the request if necessary
98115 while _check_retry (result ) and not ctx .no_retry and ctx .retry_count < 3 :
99116 time .sleep (_get_retry_after (result .headers ))
100117 ctx .retry_count += 1
101118 result = method (url , headers = headers , data = body , verify = API_CA_PATH )
102119
103120 _attempt_warn_old_version (ctx , result )
104121
122+ # If the response is an error and we're not skipping error handling, raise an error
105123 if not 199 < result .status_code < 399 and not skip_error_handling :
106124 _handle_error (ctx , result )
107125
108126 return result
109127
110128
111- def _merge_results_data (results : Iterable [dict ]):
112- """Merge multiple json response into one"""
129+ def _merge_results_data (results : Iterable [dict ]) -> Optional [Dict [str , Any ]]:
130+ """
131+ Merges multiple JSON responses into one, combining their 'data' fields
132+ and setting 'pages' and 'page' to 1 if they exist.
133+
134+ :param results: An iterable of dictionaries containing JSON response data.
135+
136+ :return: A merged dictionary containing the combined data or None if no results are provided.
137+ """
113138
114139 iterator = iter (results )
115140 merged_result = next (iterator , None )
141+
142+ # If there are no results to merge, return None
116143 if not merged_result :
117144 return None
118145
146+ # Set 'pages' and 'page' to 1 if they exist in the first result
119147 if "pages" in merged_result :
120148 merged_result ["pages" ] = 1
121149 if "page" in merged_result :
122150 merged_result ["page" ] = 1
151+
152+ # Merge the 'data' fields by combining the 'data' from all results
123153 if "data" in merged_result :
124154 merged_result ["data" ] += list (
125155 itertools .chain .from_iterable (r ["data" ] for r in iterator )
@@ -128,22 +158,43 @@ def _merge_results_data(results: Iterable[dict]):
128158
129159
130160def _generate_all_pages_results (
131- ctx ,
161+ ctx : "CLI" ,
132162 operation : OpenAPIOperation ,
133163 args : List [str ],
134164 pages_needed : Iterable [int ],
135- ):
165+ ) -> Iterable [ dict ] :
136166 """
137- :param ctx: The main CLI object
167+ Generates results from multiple pages by iterating through the specified page numbers
168+ and yielding the JSON response for each page.e.
169+
170+ :param ctx: The main CLI object that maintains API request state.
171+ :param operation: The OpenAPI operation to be executed.
172+ :param args: A list of arguments passed to the API request.
173+ :param pages_needed: An iterable of page numbers to request.
174+
175+ :yield: The JSON response (as a dictionary) for each requested page.
138176 """
139177 for p in pages_needed :
140178 ctx .page = p
141179 yield do_request (ctx , operation , args ).json ()
142180
143181
144182def _build_filter_header (
145- operation , parsed_args , filter_header = None
183+ operation : OpenAPIOperation ,
184+ parsed_args : Any ,
185+ filter_header : Optional [dict ] = None ,
146186) -> Optional [str ]:
187+ """
188+ Builds a filter header for a request based on the parsed
189+ arguments. This is used for GET requests to filter results according
190+ to the specified arguments. If no filter is provided, returns None.
191+
192+ :param operation: The OpenAPI operation to be executed.
193+ :param parsed_args: The parsed arguments from the CLI or request
194+ :param filter_header: Optional filter header to be included in the request (default: None).
195+
196+ :return: A JSON string representing the filter header, or None if no filters are applied.
197+ """
147198 if operation .method != "get" :
148199 # Non-GET operations don't support filters
149200 return None
@@ -188,7 +239,19 @@ def _build_filter_header(
188239 return json .dumps (result ) if len (result ) > 0 else None
189240
190241
191- def _build_request_url (ctx , operation , parsed_args ) -> str :
242+ def _build_request_url (
243+ ctx : "CLI" , operation : OpenAPIOperation , parsed_args : Any
244+ ) -> str :
245+ """
246+ Constructs the full request URL for an API operation,
247+ incorporating user-defined API host and scheme overrides.
248+
249+ :param ctx: The main CLI object that maintains API request state.
250+ :param operation: The OpenAPI operation to be executed.
251+ :param parsed_args: The parsed arguments from the CLI or request.
252+
253+ :return: The fully constructed request URL as a string.
254+ """
192255 url_base = handle_url_overrides (
193256 operation .url_base ,
194257 host = ctx .config .get_value ("api_host" ),
@@ -206,6 +269,7 @@ def _build_request_url(ctx, operation, parsed_args) -> str:
206269 ** vars (parsed_args ),
207270 )
208271
272+ # Append pagination parameters for GET requests
209273 if operation .method == "get" :
210274 result += f"?page={ ctx .page } &page_size={ ctx .page_size } "
211275
@@ -214,10 +278,15 @@ def _build_request_url(ctx, operation, parsed_args) -> str:
214278
215279def _traverse_request_body (o : Any ) -> Any :
216280 """
217- This function traverses is intended to be called immediately before
218- request body serialization and contains special handling for dropping
219- keys with null values and translating ExplicitNullValue instances into
220- serializable null values.
281+ Traverses a request body before serialization, handling special cases:
282+ - Drops keys with `None` values (implicit null values).
283+ - Converts `ExplicitEmptyListValue` instances to empty lists.
284+ - Converts `ExplicitNullValue` instances to `None`.
285+ - Recursively processes nested dictionaries and lists.
286+
287+ :param o: The request body object to process.
288+
289+ :return: A modified version of `o` with appropriate transformations applied.
221290 """
222291 if isinstance (o , dict ):
223292 result = {}
@@ -253,7 +322,18 @@ def _traverse_request_body(o: Any) -> Any:
253322 return o
254323
255324
256- def _build_request_body (ctx , operation , parsed_args ) -> Optional [str ]:
325+ def _build_request_body (
326+ ctx : "CLI" , operation : OpenAPIOperation , parsed_args : Any
327+ ) -> Optional [str ]:
328+ """
329+ Builds the request body for API calls, handling default values and nested structures.
330+
331+ :param ctx: The main CLI object that maintains API request state.
332+ :param operation: The OpenAPI operation to be executed.
333+ :param parsed_args: The parsed arguments from the CLI or request.
334+
335+ :return: A JSON string representing the request body, or None if not applicable.
336+ """
257337 if operation .method == "get" :
258338 # Get operations don't have a body
259339 return None
@@ -266,7 +346,7 @@ def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
266346
267347 expanded_json = {}
268348
269- # expand paths
349+ # Expand dotted keys into nested dictionaries
270350 for k , v in vars (parsed_args ).items ():
271351 if v is None or k in param_names :
272352 continue
@@ -282,9 +362,17 @@ def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
282362 return json .dumps (_traverse_request_body (expanded_json ))
283363
284364
285- def _print_request_debug_info (method , url , headers , body ):
365+ def _print_request_debug_info (
366+ method : Any , url : str , headers : Dict [str , Any ], body : Optional [str ]
367+ ) -> None :
286368 """
287- Prints debug info for an HTTP request
369+ Prints debug info for an HTTP request.
370+
371+ :param method: An object with a `__name__` attribute representing
372+ the HTTP method (e.g., "get", "post").
373+ :param url: The full request URL.
374+ :param headers: A dictionary of request headers.
375+ :param body: The request body as a string, or None if no body exists.
288376 """
289377 print (f"> { method .__name__ .upper ()} { url } " , file = sys .stderr )
290378 for k , v in headers .items ():
@@ -297,9 +385,11 @@ def _print_request_debug_info(method, url, headers, body):
297385 print ("> " , file = sys .stderr )
298386
299387
300- def _print_response_debug_info (response ) :
388+ def _print_response_debug_info (response : Any ) -> None :
301389 """
302- Prints debug info for a response from requests
390+ Prints debug info for a response from requests.
391+
392+ :param response: The response object returned by a `requests` call.
303393 """
304394 # these come back as ints, convert to HTTP version
305395 http_version = response .raw .version / 10
@@ -316,7 +406,14 @@ def _print_response_debug_info(response):
316406 print ("< " , file = sys .stderr )
317407
318408
319- def _attempt_warn_old_version (ctx , result ):
409+ def _attempt_warn_old_version (ctx : "CLI" , result : Any ) -> None :
410+ """
411+ Checks if the API version is newer than the CLI version and
412+ warns the user if an upgrade is available.
413+
414+ :param ctx: The main CLI object that maintains API request state.
415+ :param result: The HTTP response object from the API request.
416+ """
320417 if ctx .suppress_warnings :
321418 return
322419
@@ -398,9 +495,13 @@ def _attempt_warn_old_version(ctx, result):
398495 )
399496
400497
401- def _handle_error (ctx , response ) :
498+ def _handle_error (ctx : "CLI" , response : Any ) -> None :
402499 """
403- Given an error message, properly displays the error to the user and exits.
500+ Handles API error responses by displaying a formatted error message
501+ and exiting with the appropriate error code.
502+
503+ :param ctx: The main CLI object that maintains API request state.
504+ :param response: The HTTP response object from the API request.
404505 """
405506 print (f"Request failed: { response .status_code } " , file = sys .stderr )
406507
@@ -422,7 +523,9 @@ def _handle_error(ctx, response):
422523
423524def _check_retry (response ):
424525 """
425- Check for valid retry scenario, returns true if retry is valid
526+ Check for valid retry scenario, returns true if retry is valid.
527+
528+ :param response: The HTTP response object from the API request.
426529 """
427530 if response .status_code in (408 , 429 ):
428531 # request timed out or rate limit exceeded
@@ -436,6 +539,14 @@ def _check_retry(response):
436539 )
437540
438541
439- def _get_retry_after (headers ):
542+ def _get_retry_after (headers : Dict [str , str ]) -> int :
543+ """
544+ Extracts the "Retry-After" value from the response headers and returns it
545+ as an integer representing the number of seconds to wait before retrying.
546+
547+ :param headers: The HTTP response headers as a dictionary.
548+
549+ :return: The number of seconds to wait before retrying, or 0 if not specified.
550+ """
440551 retry_str = headers .get ("Retry-After" , "" )
441552 return int (retry_str ) if retry_str else 0
0 commit comments