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.
- {linode_cli-5.56.2/linode_cli.egg-info → linode_cli-5.57.0}/PKG-INFO +4 -3
- {linode_cli-5.56.2 → linode_cli-5.57.0/linode_cli.egg-info}/PKG-INFO +4 -3
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/__init__.py +61 -13
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/api_request.py +182 -50
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/arg_helpers.py +63 -12
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/operation.py +12 -3
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/request.py +25 -13
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/response.py +25 -3
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/cli.py +76 -8
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/config.py +73 -34
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/helpers.py +1 -3
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/data-3 +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/helpers.py +4 -1
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/__init__.py +8 -4
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/objects.py +8 -2
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/ssh.py +8 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/version.py +1 -1
- {linode_cli-5.56.2 → linode_cli-5.57.0}/pyproject.toml +1 -1
- {linode_cli-5.56.2 → linode_cli-5.57.0}/LICENSE +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/MANIFEST.in +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/README.md +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/SOURCES.txt +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/dependency_links.txt +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/entry_points.txt +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/requires.txt +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linode_cli.egg-info/top_level.txt +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/__main__.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/__init__.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/parsing.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/baked/util.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/completion.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/__init__.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/configuration/auth.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/exit_codes.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/help_pages.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/oauth-landing-page.html +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/output/__init__.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/output/helpers.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/output/output_handler.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/overrides.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/__init__.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/firewall-editor.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/get-kubeconfig.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/image-upload.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/metadata.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/buckets.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/config.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/helpers.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/list.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/obj/website.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/plugins.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/linodecli/plugins/region-table.py +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/setup.cfg +0 -0
- {linode_cli-5.56.2 → linode_cli-5.57.0}/setup.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: linode-cli
|
|
3
|
-
Version: 5.
|
|
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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: linode-cli
|
|
3
|
-
Version: 5.
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
64
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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(
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
+
return result
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _format_response_for_log(
|
|
404
|
+
response: requests.Response,
|
|
405
|
+
):
|
|
301
406
|
"""
|
|
302
|
-
|
|
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
|
-
|
|
309
|
-
f"< HTTP/{http_version:.1f} {response.status_code} {response.reason}"
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|