Skip to content

Commit 31d2f23

Browse files
Add --raw-body argument
1 parent 6566cc5 commit 31d2f23

File tree

6 files changed

+174
-7
lines changed

6 files changed

+174
-7
lines changed

linodecli/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def main(): # pylint: disable=too-many-branches,too-many-statements
8080
cli.page = parsed.page
8181
cli.page_size = parsed.page_size
8282
cli.debug_request = parsed.debug
83+
cli.raw_body = parsed.raw_body
8384

8485
if parsed.as_user and not skip_config:
8586
cli.config.set_user(parsed.as_user)

linodecli/api_request.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,15 @@ def _build_request_body(
348348
349349
:return: A JSON string representing the request body, or None if not applicable.
350350
"""
351-
if operation.method == "get":
352-
# Get operations don't have a body
351+
if operation.method in ("get", "delete"):
352+
# GET and DELETE operations don't have a body
353+
if ctx.raw_body is not None:
354+
print(
355+
f"--raw-body cannot be specified for actions with method {operation.method}",
356+
file=sys.stderr,
357+
)
358+
sys.exit(ExitCodes.ARGUMENT_ERROR)
359+
353360
return None
354361

355362
# Merge defaults into body if applicable
@@ -360,11 +367,28 @@ def _build_request_body(
360367

361368
expanded_json = {}
362369

363-
# Expand dotted keys into nested dictionaries
364-
for k, v in vars(parsed_args).items():
365-
if v is None or k in param_names:
366-
continue
370+
body_args = [
371+
(k, v)
372+
for k, v in vars(parsed_args).items()
373+
if v is not None and k not in param_names
374+
]
367375

376+
# If the user has specified the --raw-body argument,
377+
# return it.
378+
if ctx.raw_body is not None:
379+
if len(body_args) > 0:
380+
print(
381+
"--raw-body cannot be specified with action arguments: {}".format(
382+
", ".join(sorted(f"--{key}" for key, _ in body_args)),
383+
),
384+
file=sys.stderr,
385+
)
386+
sys.exit(ExitCodes.ARGUMENT_ERROR)
387+
388+
return ctx.raw_body
389+
390+
# Expand dotted keys into nested dictionaries
391+
for k, v in body_args:
368392
path_segments = get_path_segments(k)
369393

370394
cur = expanded_json

linodecli/arg_helpers.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ def register_args(parser: ArgumentParser) -> ArgumentParser:
8181
help="The alias to set or remove.",
8282
)
8383

84+
parser.add_argument(
85+
"--raw-body",
86+
type=str,
87+
help="The raw body to use for the request body. "
88+
+ "This can only be used if action-specific arguments are not specified.",
89+
)
90+
8491
# Register shared argument groups
8592
register_output_args_shared(parser)
8693
register_pagination_args_shared(parser)

linodecli/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def __init__(self, version, base_url, skip_config=False):
4444
self.base_url = base_url
4545
self.spec_version = "None"
4646
self.suppress_warnings = False
47+
self.raw_body = None
4748

4849
self.output_handler = OutputHandler()
4950
self.config = CLIConfig(self.base_url, skip_config=skip_config)

tests/integration/cli/test_args.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import json
2+
3+
from linodecli.exit_codes import ExitCodes
4+
from tests.integration.helpers import (
5+
exec_failing_test_command,
6+
exec_test_command,
7+
get_random_region_with_caps,
8+
get_random_text,
9+
)
10+
11+
12+
def test_arg_raw_body():
13+
label = get_random_text(12)
14+
region = get_random_region_with_caps(["VPCs"])
15+
16+
res = json.loads(
17+
exec_test_command(
18+
[
19+
"linode-cli",
20+
"vpcs",
21+
"create",
22+
"--json",
23+
"--raw-body",
24+
json.dumps(
25+
{
26+
"label": label,
27+
"region": region,
28+
}
29+
),
30+
],
31+
)
32+
)
33+
34+
exec_test_command(["linode-cli", "vpcs", "delete", str(res[0]["id"])])
35+
36+
assert res[0]["id"] > 0
37+
assert res[0]["label"] == label
38+
assert res[0]["region"] == region
39+
40+
41+
def test_arg_raw_body_conflict():
42+
label = get_random_text(12)
43+
region = get_random_region_with_caps(["VPCs"])
44+
45+
res = exec_failing_test_command(
46+
[
47+
"linode-cli",
48+
"vpcs",
49+
"create",
50+
"--json",
51+
"--label",
52+
label,
53+
"--region",
54+
region,
55+
"--raw-body",
56+
json.dumps(
57+
{
58+
"label": label,
59+
"region": region,
60+
}
61+
),
62+
],
63+
expected_code=ExitCodes.ARGUMENT_ERROR,
64+
)
65+
66+
assert (
67+
"--raw-body cannot be specified with action arguments: --label, --region"
68+
in res
69+
)
70+
71+
72+
def test_arg_raw_body_get():
73+
res = exec_failing_test_command(
74+
[
75+
"linode-cli",
76+
"vpcs",
77+
"list",
78+
"--json",
79+
"--raw-body",
80+
json.dumps({"label": "test"}),
81+
],
82+
expected_code=ExitCodes.ARGUMENT_ERROR,
83+
)
84+
85+
assert "--raw-body cannot be specified for actions with method get" in res

tests/unit/test_api_request.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
import pytest
1212
import requests
13+
from _pytest.capture import CaptureFixture
1314

14-
from linodecli import api_request
15+
from linodecli import ExitCodes, api_request
1516
from linodecli.baked.operation import (
1617
ExplicitEmptyDictValue,
1718
ExplicitEmptyListValue,
@@ -163,6 +164,54 @@ def test_build_request_body_non_null_field(
163164
== result
164165
)
165166

167+
def test_build_request_body(self, mock_cli, create_operation):
168+
body = {"foo": "bar"}
169+
170+
mock_cli.raw_body = json.dumps(body)
171+
172+
result = api_request._build_request_body(
173+
mock_cli,
174+
create_operation,
175+
SimpleNamespace(),
176+
)
177+
assert json.loads(result) == body
178+
179+
def test_build_request_body_conflict(
180+
self, mock_cli, create_operation, capsys: CaptureFixture
181+
):
182+
mock_cli.raw_body = json.dumps({"foo": "bar"})
183+
184+
with pytest.raises(SystemExit) as err:
185+
api_request._build_request_body(
186+
mock_cli,
187+
create_operation,
188+
SimpleNamespace(foo="bar", bar="foo"),
189+
)
190+
191+
assert err.value.code == ExitCodes.ARGUMENT_ERROR
192+
assert (
193+
"--raw-body cannot be specified with action arguments: --bar, --foo"
194+
in capsys.readouterr().err
195+
)
196+
197+
def test_build_request_body_get(
198+
self, mock_cli, list_operation, capsys: CaptureFixture
199+
):
200+
mock_cli.raw_body = json.dumps({"foo": "bar"})
201+
202+
with pytest.raises(SystemExit) as err:
203+
api_request._build_request_body(
204+
mock_cli,
205+
list_operation,
206+
SimpleNamespace(),
207+
)
208+
209+
assert err.value.code == ExitCodes.ARGUMENT_ERROR
210+
assert (
211+
"--raw-body cannot be specified for actions with method get"
212+
in capsys.readouterr().err
213+
)
214+
166215
def test_build_request_url_get(self, mock_cli, list_operation):
167216
result = api_request._build_request_url(
168217
mock_cli, list_operation, SimpleNamespace()

0 commit comments

Comments
 (0)