linode-cli 5.56.2__tar.gz → 5.57.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {linode_cli-5.56.2/linode_cli.egg-info → linode_cli-5.57.0}/PKG-INFO +4 -3
  2. {linode_cli-5.56.2 → linode_cli-5.57.0/linode_cli.egg-info}/PKG-INFO +4 -3
  3. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/__init__.py +61 -13
  4. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/api_request.py +182 -50
  5. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/arg_helpers.py +63 -12
  6. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/operation.py +12 -3
  7. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/request.py +25 -13
  8. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/response.py +25 -3
  9. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/cli.py +76 -8
  10. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/config.py +73 -34
  11. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/helpers.py +1 -3
  12. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/data-3 +0 -0
  13. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/helpers.py +4 -1
  14. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/__init__.py +8 -4
  15. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/objects.py +8 -2
  16. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/ssh.py +8 -0
  17. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/version.py +1 -1
  18. {linode_cli-5.56.2 → linode_cli-5.57.0}/pyproject.toml +1 -1
  19. {linode_cli-5.56.2 → linode_cli-5.57.0}/LICENSE +0 -0
  20. {linode_cli-5.56.2 → linode_cli-5.57.0}/MANIFEST.in +0 -0
  21. {linode_cli-5.56.2 → linode_cli-5.57.0}/README.md +0 -0
  22. {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/SOURCES.txt +0 -0
  23. {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/dependency_links.txt +0 -0
  24. {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/entry_points.txt +0 -0
  25. {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/requires.txt +0 -0
  26. {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/top_level.txt +0 -0
  27. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/__main__.py +0 -0
  28. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/__init__.py +0 -0
  29. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/parsing.py +0 -0
  30. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/util.py +0 -0
  31. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/completion.py +0 -0
  32. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/__init__.py +0 -0
  33. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/auth.py +0 -0
  34. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/exit_codes.py +0 -0
  35. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/help_pages.py +0 -0
  36. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/oauth-landing-page.html +0 -0
  37. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/output/__init__.py +0 -0
  38. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/output/helpers.py +0 -0
  39. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/output/output_handler.py +0 -0
  40. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/overrides.py +0 -0
  41. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/__init__.py +0 -0
  42. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/firewall-editor.py +0 -0
  43. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/get-kubeconfig.py +0 -0
  44. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/image-upload.py +0 -0
  45. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/metadata.py +0 -0
  46. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/buckets.py +0 -0
  47. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/config.py +0 -0
  48. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/helpers.py +0 -0
  49. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/list.py +0 -0
  50. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/website.py +0 -0
  51. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/plugins.py +0 -0
  52. {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/region-table.py +0 -0
  53. {linode_cli-5.56.2 → linode_cli-5.57.0}/setup.cfg +0 -0
  54. {linode_cli-5.56.2 → linode_cli-5.57.0}/setup.py +0 -0
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.56.2
3
+ Version: 5.57.0
4
4
  Summary: The official command-line interface for interacting with the Linode API.
5
5
  Author-email: "Akamai Technologies Inc." <developers@linode.com>
6
- License: BSD-3-Clause
6
+ License-Expression: BSD-3-Clause
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
@@ -28,6 +28,7 @@ Requires-Dist: boto3-stubs[s3]; extra == "dev"
28
28
  Requires-Dist: build>=0.10.0; extra == "dev"
29
29
  Requires-Dist: twine>=4.0.2; extra == "dev"
30
30
  Requires-Dist: pytest-rerunfailures; extra == "dev"
31
+ Dynamic: license-file
31
32
 
32
33
  # linode-cli (lin)
33
34
 
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: linode-cli
3
- Version: 5.56.2
3
+ Version: 5.57.0
4
4
  Summary: The official command-line interface for interacting with the Linode API.
5
5
  Author-email: "Akamai Technologies Inc." <developers@linode.com>
6
- License: BSD-3-Clause
6
+ License-Expression: BSD-3-Clause
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
@@ -28,6 +28,7 @@ Requires-Dist: boto3-stubs[s3]; extra == "dev"
28
28
  Requires-Dist: build>=0.10.0; extra == "dev"
29
29
  Requires-Dist: twine>=4.0.2; extra == "dev"
30
30
  Requires-Dist: pytest-rerunfailures; extra == "dev"
31
+ Dynamic: license-file
31
32
 
32
33
  # linode-cli (lin)
33
34
 
@@ -4,6 +4,7 @@ Argument parser for the linode CLI
4
4
  """
5
5
 
6
6
  import argparse
7
+ import logging
7
8
  import os
8
9
  import sys
9
10
  from importlib.metadata import version
@@ -38,12 +39,14 @@ BASE_URL = "https://api.linode.com/v4"
38
39
 
39
40
  TEST_MODE = os.getenv("LINODE_CLI_TEST_MODE") == "1"
40
41
 
42
+ # Configure the `logging` package log level depending on the --debug flag.
43
+ logging.basicConfig(
44
+ level=logging.DEBUG if "--debug" in argv else logging.WARNING,
45
+ )
46
+
41
47
  # if any of these arguments are given, we don't need to prompt for configuration
42
48
  skip_config = (
43
- any(
44
- c in argv
45
- for c in ["--skip-config", "--help", "--version", "completion"]
46
- )
49
+ any(c in argv for c in ["--skip-config", "--version", "completion"])
47
50
  or TEST_MODE
48
51
  )
49
52
 
@@ -104,6 +107,43 @@ def main(): # pylint: disable=too-many-branches,too-many-statements
104
107
  # if not spec was found and we weren't baking, we're doomed
105
108
  sys.exit(ExitCodes.ARGUMENT_ERROR)
106
109
 
110
+ if parsed.command in ("set-custom-alias", "remove-custom-alias"):
111
+ if not parsed.alias_command or not parsed.alias:
112
+ print(
113
+ "Both --alias-command and --alias must be provided.",
114
+ file=sys.stderr,
115
+ )
116
+ sys.exit(ExitCodes.ARGUMENT_ERROR)
117
+
118
+ command = parsed.alias_command
119
+ alias = parsed.alias
120
+
121
+ if command not in cli.ops:
122
+ print(
123
+ f"Error: '{command}' is not a valid command.", file=sys.stderr
124
+ )
125
+ sys.exit(ExitCodes.ARGUMENT_ERROR)
126
+
127
+ if parsed.command == "set-custom-alias":
128
+ if (alias, command) not in cli.config.get_custom_aliases().items():
129
+ cli.config.set_custom_alias(alias, command)
130
+ print(f"Custom alias '{alias}' set for command '{command}'")
131
+ else:
132
+ print(
133
+ f"Custom alias '{alias}' already set for command '{command}'"
134
+ )
135
+
136
+ if parsed.command == "remove-custom-alias":
137
+ if (alias, command) in cli.config.get_custom_aliases().items():
138
+ cli.config.remove_custom_alias(alias, command)
139
+ print(f"Custom alias '{alias}' removed for command '{command}'")
140
+ else:
141
+ print(
142
+ f"Custom alias '{alias}' does not exist for command '{command}'"
143
+ )
144
+
145
+ sys.exit(ExitCodes.SUCCESS)
146
+
107
147
  if parsed.command == "register-plugin":
108
148
  if parsed.action is None:
109
149
  print("register-plugin requires a module name!", file=sys.stderr)
@@ -207,28 +247,36 @@ def main(): # pylint: disable=too-many-branches,too-many-statements
207
247
  plugin_args.remove(parsed.command) # don't include the plugin name
208
248
  plugins.invoke(parsed.command, plugin_args, context)
209
249
  sys.exit(ExitCodes.SUCCESS)
210
-
211
250
  # unknown commands
212
251
  if (
213
252
  parsed.command not in cli.ops
214
253
  and parsed.command not in plugins.available(cli.config)
215
254
  and parsed.command not in HELP_TOPICS
255
+ and parsed.command not in cli.config.get_custom_aliases().keys()
216
256
  ):
217
257
  print(f"Unrecognized command {parsed.command}", file=sys.stderr)
218
258
  sys.exit(ExitCodes.UNRECOGNIZED_COMMAND)
219
259
 
220
260
  # handle a help for a command - either --help or no action triggers this
221
- if (
222
- parsed.command is not None
223
- and parsed.action is None
224
- and parsed.command in cli.ops
225
- ):
226
- print_help_command_actions(cli.ops, parsed.command)
227
- sys.exit(ExitCodes.SUCCESS)
261
+ if parsed.command is not None and parsed.action is None:
262
+ if parsed.command in cli.ops:
263
+ print_help_command_actions(cli.ops, parsed.command)
264
+ sys.exit(ExitCodes.SUCCESS)
265
+ if parsed.command in cli.config.get_custom_aliases().keys():
266
+ print_help_command_actions(
267
+ cli.ops, cli.config.get_custom_aliases()[parsed.command]
268
+ )
228
269
 
229
270
  if parsed.command is not None and parsed.action is not None:
230
271
  if parsed.help:
231
- print_help_action(cli, parsed.command, parsed.action)
272
+ if parsed.command in cli.config.get_custom_aliases().keys():
273
+ print_help_action(
274
+ cli,
275
+ cli.config.get_custom_aliases()[parsed.command],
276
+ parsed.action,
277
+ )
278
+ else:
279
+ print_help_action(cli, parsed.command, parsed.action)
232
280
  sys.exit(ExitCodes.SUCCESS)
233
281
 
234
282
  cli.handle_command(parsed.command, parsed.action, args)
@@ -7,7 +7,8 @@ import json
7
7
  import os
8
8
  import sys
9
9
  import time
10
- from typing import Any, Iterable, List, Optional
10
+ from logging import getLogger
11
+ from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional
11
12
 
12
13
  import requests
13
14
  from packaging import version
@@ -23,13 +24,24 @@ from .baked.operation import (
23
24
  )
24
25
  from .helpers import handle_url_overrides
25
26
 
27
+ if TYPE_CHECKING:
28
+ from linodecli.cli import CLI
26
29
 
27
- def get_all_pages(ctx, operation: OpenAPIOperation, args: List[str]):
30
+ logger = getLogger(__name__)
31
+
32
+
33
+ def get_all_pages(
34
+ ctx: "CLI", operation: OpenAPIOperation, args: List[str]
35
+ ) -> Dict[str, Any]:
28
36
  """
29
- Receive all pages of a resource from multiple
30
- API responses then merge into one page.
37
+ Retrieves all pages of a resource from multiple API responses
38
+ and merges them into a single page.
31
39
 
32
- :param ctx: The main CLI object
40
+ :param ctx: The main CLI object that maintains API request state.
41
+ :param operation: The OpenAPI operation to be executed.
42
+ :param args: A list of arguments passed to the API request.
43
+
44
+ :return: A dictionary containing the merged results from all pages.
33
45
  """
34
46
 
35
47
  ctx.page_size = 500
@@ -38,6 +50,7 @@ def get_all_pages(ctx, operation: OpenAPIOperation, args: List[str]):
38
50
 
39
51
  total_pages = result.get("pages")
40
52
 
53
+ # If multiple pages exist, generate results for all additional pages
41
54
  if total_pages and total_pages > 1:
42
55
  pages_needed = range(2, total_pages + 1)
43
56
 
@@ -51,19 +64,25 @@ def get_all_pages(ctx, operation: OpenAPIOperation, args: List[str]):
51
64
 
52
65
 
53
66
  def do_request(
54
- ctx,
55
- operation,
56
- args,
57
- filter_header=None,
58
- skip_error_handling=False,
67
+ ctx: "CLI",
68
+ operation: OpenAPIOperation,
69
+ args: List[str],
70
+ filter_header: Optional[dict] = None,
71
+ skip_error_handling: bool = False,
59
72
  ) -> (
60
73
  Response
61
74
  ): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
62
75
  """
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
76
+ Makes an HTTP request to an API operation's URL and returns the resulting response.
77
+ Optionally retries the request if specified, handles errors, and supports debugging.
78
+
79
+ :param ctx: The main CLI object that maintains API request state.
80
+ :param operation: The OpenAPI operation to be executed.
81
+ :param args: A list of arguments passed to the API request.
82
+ :param filter_header: Optional filter header to be included in the request (default: None).
83
+ :param skip_error_handling: Whether to skip error handling (default: False).
65
84
 
66
- :param ctx: The main CLI object
85
+ :return: The `Response` object returned from the HTTP request.
67
86
  """
68
87
  # TODO: Revisit using pre-built calls from OpenAPI
69
88
  method = getattr(requests, operation.method)
@@ -87,14 +106,20 @@ def do_request(
87
106
 
88
107
  # Print response debug info is requested
89
108
  if ctx.debug_request:
90
- _print_request_debug_info(method, url, headers, body)
109
+ # Multiline log entries aren't ideal, we should consider
110
+ # using single-line structured logging in the future.
111
+ logger.debug(
112
+ "\n%s",
113
+ "\n".join(_format_request_for_log(method, url, headers, body)),
114
+ )
91
115
 
92
116
  result = method(url, headers=headers, data=body, verify=API_CA_PATH)
93
117
 
94
118
  # Print response debug info is requested
95
119
  if ctx.debug_request:
96
- _print_response_debug_info(result)
120
+ logger.debug("\n%s", "\n".join(_format_response_for_log(result)))
97
121
 
122
+ # Retry the request if necessary
98
123
  while _check_retry(result) and not ctx.no_retry and ctx.retry_count < 3:
99
124
  time.sleep(_get_retry_after(result.headers))
100
125
  ctx.retry_count += 1
@@ -102,24 +127,37 @@ def do_request(
102
127
 
103
128
  _attempt_warn_old_version(ctx, result)
104
129
 
130
+ # If the response is an error and we're not skipping error handling, raise an error
105
131
  if not 199 < result.status_code < 399 and not skip_error_handling:
106
132
  _handle_error(ctx, result)
107
133
 
108
134
  return result
109
135
 
110
136
 
111
- def _merge_results_data(results: Iterable[dict]):
112
- """Merge multiple json response into one"""
137
+ def _merge_results_data(results: Iterable[dict]) -> Optional[Dict[str, Any]]:
138
+ """
139
+ Merges multiple JSON responses into one, combining their 'data' fields
140
+ and setting 'pages' and 'page' to 1 if they exist.
141
+
142
+ :param results: An iterable of dictionaries containing JSON response data.
143
+
144
+ :return: A merged dictionary containing the combined data or None if no results are provided.
145
+ """
113
146
 
114
147
  iterator = iter(results)
115
148
  merged_result = next(iterator, None)
149
+
150
+ # If there are no results to merge, return None
116
151
  if not merged_result:
117
152
  return None
118
153
 
154
+ # Set 'pages' and 'page' to 1 if they exist in the first result
119
155
  if "pages" in merged_result:
120
156
  merged_result["pages"] = 1
121
157
  if "page" in merged_result:
122
158
  merged_result["page"] = 1
159
+
160
+ # Merge the 'data' fields by combining the 'data' from all results
123
161
  if "data" in merged_result:
124
162
  merged_result["data"] += list(
125
163
  itertools.chain.from_iterable(r["data"] for r in iterator)
@@ -128,13 +166,21 @@ def _merge_results_data(results: Iterable[dict]):
128
166
 
129
167
 
130
168
  def _generate_all_pages_results(
131
- ctx,
169
+ ctx: "CLI",
132
170
  operation: OpenAPIOperation,
133
171
  args: List[str],
134
172
  pages_needed: Iterable[int],
135
- ):
173
+ ) -> Iterable[dict]:
136
174
  """
137
- :param ctx: The main CLI object
175
+ Generates results from multiple pages by iterating through the specified page numbers
176
+ and yielding the JSON response for each page.e.
177
+
178
+ :param ctx: The main CLI object that maintains API request state.
179
+ :param operation: The OpenAPI operation to be executed.
180
+ :param args: A list of arguments passed to the API request.
181
+ :param pages_needed: An iterable of page numbers to request.
182
+
183
+ :yield: The JSON response (as a dictionary) for each requested page.
138
184
  """
139
185
  for p in pages_needed:
140
186
  ctx.page = p
@@ -142,8 +188,21 @@ def _generate_all_pages_results(
142
188
 
143
189
 
144
190
  def _build_filter_header(
145
- operation, parsed_args, filter_header=None
191
+ operation: OpenAPIOperation,
192
+ parsed_args: Any,
193
+ filter_header: Optional[dict] = None,
146
194
  ) -> Optional[str]:
195
+ """
196
+ Builds a filter header for a request based on the parsed
197
+ arguments. This is used for GET requests to filter results according
198
+ to the specified arguments. If no filter is provided, returns None.
199
+
200
+ :param operation: The OpenAPI operation to be executed.
201
+ :param parsed_args: The parsed arguments from the CLI or request
202
+ :param filter_header: Optional filter header to be included in the request (default: None).
203
+
204
+ :return: A JSON string representing the filter header, or None if no filters are applied.
205
+ """
147
206
  if operation.method != "get":
148
207
  # Non-GET operations don't support filters
149
208
  return None
@@ -188,7 +247,19 @@ def _build_filter_header(
188
247
  return json.dumps(result) if len(result) > 0 else None
189
248
 
190
249
 
191
- def _build_request_url(ctx, operation, parsed_args) -> str:
250
+ def _build_request_url(
251
+ ctx: "CLI", operation: OpenAPIOperation, parsed_args: Any
252
+ ) -> str:
253
+ """
254
+ Constructs the full request URL for an API operation,
255
+ incorporating user-defined API host and scheme overrides.
256
+
257
+ :param ctx: The main CLI object that maintains API request state.
258
+ :param operation: The OpenAPI operation to be executed.
259
+ :param parsed_args: The parsed arguments from the CLI or request.
260
+
261
+ :return: The fully constructed request URL as a string.
262
+ """
192
263
  url_base = handle_url_overrides(
193
264
  operation.url_base,
194
265
  host=ctx.config.get_value("api_host"),
@@ -206,6 +277,7 @@ def _build_request_url(ctx, operation, parsed_args) -> str:
206
277
  **vars(parsed_args),
207
278
  )
208
279
 
280
+ # Append pagination parameters for GET requests
209
281
  if operation.method == "get":
210
282
  result += f"?page={ctx.page}&page_size={ctx.page_size}"
211
283
 
@@ -214,10 +286,15 @@ def _build_request_url(ctx, operation, parsed_args) -> str:
214
286
 
215
287
  def _traverse_request_body(o: Any) -> Any:
216
288
  """
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.
289
+ Traverses a request body before serialization, handling special cases:
290
+ - Drops keys with `None` values (implicit null values).
291
+ - Converts `ExplicitEmptyListValue` instances to empty lists.
292
+ - Converts `ExplicitNullValue` instances to `None`.
293
+ - Recursively processes nested dictionaries and lists.
294
+
295
+ :param o: The request body object to process.
296
+
297
+ :return: A modified version of `o` with appropriate transformations applied.
221
298
  """
222
299
  if isinstance(o, dict):
223
300
  result = {}
@@ -253,7 +330,18 @@ def _traverse_request_body(o: Any) -> Any:
253
330
  return o
254
331
 
255
332
 
256
- def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
333
+ def _build_request_body(
334
+ ctx: "CLI", operation: OpenAPIOperation, parsed_args: Any
335
+ ) -> Optional[str]:
336
+ """
337
+ Builds the request body for API calls, handling default values and nested structures.
338
+
339
+ :param ctx: The main CLI object that maintains API request state.
340
+ :param operation: The OpenAPI operation to be executed.
341
+ :param parsed_args: The parsed arguments from the CLI or request.
342
+
343
+ :return: A JSON string representing the request body, or None if not applicable.
344
+ """
257
345
  if operation.method == "get":
258
346
  # Get operations don't have a body
259
347
  return None
@@ -266,7 +354,7 @@ def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
266
354
 
267
355
  expanded_json = {}
268
356
 
269
- # expand paths
357
+ # Expand dotted keys into nested dictionaries
270
358
  for k, v in vars(parsed_args).items():
271
359
  if v is None or k in param_names:
272
360
  continue
@@ -282,41 +370,71 @@ def _build_request_body(ctx, operation, parsed_args) -> Optional[str]:
282
370
  return json.dumps(_traverse_request_body(expanded_json))
283
371
 
284
372
 
285
- def _print_request_debug_info(method, url, headers, body):
373
+ def _format_request_for_log(
374
+ method: Any,
375
+ url: str,
376
+ headers: Dict[str, str],
377
+ body: str,
378
+ ) -> List[str]:
286
379
  """
287
- Prints debug info for an HTTP request
380
+ Builds a debug output for the given request.
381
+
382
+ :param method: The HTTP method of the request.
383
+ :param url: The URL of the request.
384
+ :param headers: The headers of the request.
385
+ :param body: The body of the request.
386
+
387
+ :returns: The lines of the generated debug output.
288
388
  """
289
- print(f"> {method.__name__.upper()} {url}", file=sys.stderr)
389
+ result = [f"> {method.__name__.upper()} {url}"]
390
+
290
391
  for k, v in headers.items():
291
392
  # If this is the Authorization header, sanitize the token
292
393
  if k.lower() == "authorization":
293
394
  v = "Bearer " + "*" * 64
294
- print(f"> {k}: {v}", file=sys.stderr)
295
- print("> Body:", file=sys.stderr)
296
- print("> ", body or "", file=sys.stderr)
297
- print("> ", file=sys.stderr)
298
395
 
396
+ result.append(f"> {k}: {v}")
397
+
398
+ result.extend(["> Body:", f"> {body or ''}", "> "])
299
399
 
300
- def _print_response_debug_info(response):
400
+ return result
401
+
402
+
403
+ def _format_response_for_log(
404
+ response: requests.Response,
405
+ ):
301
406
  """
302
- Prints debug info for a response from requests
407
+ Builds a debug output for the given response.
408
+
409
+ :param response: The HTTP response to format.
410
+
411
+ :returns: The lines of the generated debug output.
303
412
  """
413
+
304
414
  # these come back as ints, convert to HTTP version
305
415
  http_version = response.raw.version / 10
306
416
  body = response.content.decode("utf-8", errors="replace")
307
417
 
308
- print(
309
- f"< HTTP/{http_version:.1f} {response.status_code} {response.reason}",
310
- file=sys.stderr,
311
- )
418
+ result = [
419
+ f"< HTTP/{http_version:.1f} {response.status_code} {response.reason}"
420
+ ]
421
+
312
422
  for k, v in response.headers.items():
313
- print(f"< {k}: {v}", file=sys.stderr)
314
- print("< Body:", file=sys.stderr)
315
- print("< ", body or "", file=sys.stderr)
316
- print("< ", file=sys.stderr)
423
+ result.append(f"< {k}: {v}")
317
424
 
425
+ result.extend(["< Body:", f"< {body or ''}", "< "])
426
+
427
+ return result
428
+
429
+
430
+ def _attempt_warn_old_version(ctx: "CLI", result: Any) -> None:
431
+ """
432
+ Checks if the API version is newer than the CLI version and
433
+ warns the user if an upgrade is available.
318
434
 
319
- def _attempt_warn_old_version(ctx, result):
435
+ :param ctx: The main CLI object that maintains API request state.
436
+ :param result: The HTTP response object from the API request.
437
+ """
320
438
  if ctx.suppress_warnings:
321
439
  return
322
440
 
@@ -398,9 +516,13 @@ def _attempt_warn_old_version(ctx, result):
398
516
  )
399
517
 
400
518
 
401
- def _handle_error(ctx, response):
519
+ def _handle_error(ctx: "CLI", response: Any) -> None:
402
520
  """
403
- Given an error message, properly displays the error to the user and exits.
521
+ Handles API error responses by displaying a formatted error message
522
+ and exiting with the appropriate error code.
523
+
524
+ :param ctx: The main CLI object that maintains API request state.
525
+ :param response: The HTTP response object from the API request.
404
526
  """
405
527
  print(f"Request failed: {response.status_code}", file=sys.stderr)
406
528
 
@@ -422,7 +544,9 @@ def _handle_error(ctx, response):
422
544
 
423
545
  def _check_retry(response):
424
546
  """
425
- Check for valid retry scenario, returns true if retry is valid
547
+ Check for valid retry scenario, returns true if retry is valid.
548
+
549
+ :param response: The HTTP response object from the API request.
426
550
  """
427
551
  if response.status_code in (408, 429):
428
552
  # request timed out or rate limit exceeded
@@ -436,6 +560,14 @@ def _check_retry(response):
436
560
  )
437
561
 
438
562
 
439
- def _get_retry_after(headers):
563
+ def _get_retry_after(headers: Dict[str, str]) -> int:
564
+ """
565
+ Extracts the "Retry-After" value from the response headers and returns it
566
+ as an integer representing the number of seconds to wait before retrying.
567
+
568
+ :param headers: The HTTP response headers as a dictionary.
569
+
570
+ :return: The number of seconds to wait before retrying, or 0 if not specified.
571
+ """
440
572
  retry_str = headers.get("Retry-After", "")
441
573
  return int(retry_str) if retry_str else 0