Skip to content

Commit f70c3af

Browse files
Resolve issues
2 parents 23cab4e + d5136db commit f70c3af

File tree

86 files changed

+5395
-5124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+5395
-5124
lines changed

.github/workflows/e2e-suite.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ jobs:
294294
steps:
295295
- name: Notify Slack
296296
id: main_message
297-
uses: slackapi/slack-github-action@v2.0.0
297+
uses: slackapi/slack-github-action@v2.1.0
298298
with:
299299
method: chat.postMessage
300300
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -326,7 +326,7 @@ jobs:
326326
327327
- name: Test summary thread
328328
if: success()
329-
uses: slackapi/slack-github-action@v2.0.0
329+
uses: slackapi/slack-github-action@v2.1.0
330330
with:
331331
method: chat.postMessage
332332
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/nightly-smoke-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646

4747
- name: Notify Slack
4848
if: always() && github.repository == 'linode/linode-cli' # Run even if integration tests fail and only on main repository
49-
uses: slackapi/slack-github-action@v2.0.0
49+
uses: slackapi/slack-github-action@v2.1.0
5050
with:
5151
method: chat.postMessage
5252
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
steps:
1212
- name: Notify Slack - Main Message
1313
id: main_message
14-
uses: slackapi/slack-github-action@v2.0.0
14+
uses: slackapi/slack-github-action@v2.1.0
1515
with:
1616
method: chat.postMessage
1717
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -42,7 +42,7 @@ jobs:
4242
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # [email protected]
4343

4444
- name: Set up Docker Buildx
45-
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # pin@v3.10.0
45+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # pin@v3.11.1
4646

4747
- name: Login to Docker Hub
4848
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # [email protected]
@@ -67,7 +67,7 @@ jobs:
6767
result-encoding: string
6868

6969
- name: Build and push to DockerHub
70-
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # pin@v6.16.0
70+
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # pin@v6.18.0
7171
with:
7272
context: .
7373
file: Dockerfile

.github/workflows/remote-release-trigger.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
commit_sha: ${{ steps.calculate_head_sha.outputs.commit_sha }}
6767

6868
- name: Release
69-
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # pin@v2.2.2
69+
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # pin@v2.3.2
7070
with:
7171
target_commitish: 'main'
7272
token: ${{ steps.generate_token.outputs.token }}

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: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ExplicitNullValue,
2424
OpenAPIOperation,
2525
)
26+
from .baked.util import get_path_segments
2627
from .helpers import handle_url_overrides
2728

2829
if TYPE_CHECKING:
@@ -347,30 +348,60 @@ def _build_request_body(
347348
348349
:return: A JSON string representing the request body, or None if not applicable.
349350
"""
350-
if operation.method == "get":
351-
# 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+
352360
return None
353361

362+
param_names = {param.name for param in operation.params}
363+
364+
# Returns whether the given argument should be included in the request body
365+
def __should_include(key: str, value: Any) -> bool:
366+
return value is not None and key not in param_names
367+
368+
# If the user has specified the --raw-body argument,
369+
# return it.
370+
if ctx.raw_body is not None:
371+
specified_keys = [
372+
k for k, v in vars(parsed_args).items() if __should_include(k, v)
373+
]
374+
375+
if len(specified_keys) > 0:
376+
print(
377+
"--raw-body cannot be specified with action arguments: "
378+
+ ", ".join(sorted(f"--{key}" for key in specified_keys)),
379+
file=sys.stderr,
380+
)
381+
sys.exit(ExitCodes.ARGUMENT_ERROR)
382+
383+
return ctx.raw_body
384+
354385
# Merge defaults into body if applicable
355386
if ctx.defaults:
356387
parsed_args = ctx.config.update(parsed_args, operation.allowed_defaults)
357388

358-
param_names = {param.name for param in operation.params}
359-
360389
expanded_json = {}
361390

362391
# Expand dotted keys into nested dictionaries
363392
for k, v in vars(parsed_args).items():
364-
if v is None or k in param_names:
393+
if not __should_include(k, v):
365394
continue
366395

396+
path_segments = get_path_segments(k)
397+
367398
cur = expanded_json
368-
for part in k.split(".")[:-1]:
399+
for part in path_segments[:-1]:
369400
if part not in cur:
370401
cur[part] = {}
371402
cur = cur[part]
372403

373-
cur[k.split(".")[-1]] = v
404+
cur[path_segments[-1]] = v
374405

375406
return json.dumps(_traverse_request_body(expanded_json))
376407

linodecli/arg_helpers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ 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 JSON to use as the request body of an action. "
88+
+ "This argument cannot be used if action-specific arguments are specified. "
89+
+ "Additionally, this argument can only be used with POST and PUT actions.",
90+
)
91+
8492
# Register shared argument groups
8593
register_output_args_shared(parser)
8694
register_pagination_args_shared(parser)

linodecli/baked/operation.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
OpenAPIRequestArg,
2626
)
2727
from linodecli.baked.response import OpenAPIResponse
28+
from linodecli.baked.util import unescape_arg_segment
2829
from linodecli.exit_codes import ExitCodes
2930
from linodecli.output.output_handler import OutputHandler
3031
from linodecli.overrides import OUTPUT_OVERRIDES
@@ -649,6 +650,9 @@ def _add_args_post_put(
649650
if arg.read_only:
650651
continue
651652

653+
arg_name_unescaped = unescape_arg_segment(arg.name)
654+
arg_path_unescaped = unescape_arg_segment(arg.path)
655+
652656
arg_type = (
653657
arg.item_type if arg.datatype == "array" else arg.datatype
654658
)
@@ -660,15 +664,17 @@ def _add_args_post_put(
660664
if arg.datatype == "array":
661665
# special handling for input arrays
662666
parser.add_argument(
663-
"--" + arg.path,
664-
metavar=arg.name,
667+
"--" + arg_path_unescaped,
668+
dest=arg.path,
669+
metavar=arg_name_unescaped,
665670
action=ArrayAction,
666671
type=arg_type_handler,
667672
)
668673
elif arg.is_child:
669674
parser.add_argument(
670-
"--" + arg.path,
671-
metavar=arg.name,
675+
"--" + arg_path_unescaped,
676+
dest=arg.path,
677+
metavar=arg_name_unescaped,
672678
action=ListArgumentAction,
673679
type=arg_type_handler,
674680
)
@@ -677,7 +683,7 @@ def _add_args_post_put(
677683
if arg.datatype == "string" and arg.format == "password":
678684
# special case - password input
679685
parser.add_argument(
680-
"--" + arg.path,
686+
"--" + arg_path_unescaped,
681687
nargs="?",
682688
action=PasswordPromptAction,
683689
)
@@ -687,15 +693,17 @@ def _add_args_post_put(
687693
"ssl-key",
688694
):
689695
parser.add_argument(
690-
"--" + arg.path,
691-
metavar=arg.name,
696+
"--" + arg_path_unescaped,
697+
dest=arg.path,
698+
metavar=arg_name_unescaped,
692699
action=OptionalFromFileAction,
693700
type=arg_type_handler,
694701
)
695702
else:
696703
parser.add_argument(
697-
"--" + arg.path,
698-
metavar=arg.name,
704+
"--" + arg_path_unescaped,
705+
dest=arg.path,
706+
metavar=arg_name_unescaped,
699707
type=arg_type_handler,
700708
)
701709

linodecli/baked/request.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
from linodecli.baked.parsing import simplify_description
1111
from linodecli.baked.response import OpenAPIResponse
12-
from linodecli.baked.util import _aggregate_schema_properties
12+
from linodecli.baked.util import (
13+
_aggregate_schema_properties,
14+
escape_arg_segment,
15+
)
1316

1417

1518
class OpenAPIRequestArg:
@@ -155,6 +158,8 @@ def _parse_request_model(
155158
return args
156159

157160
for k, v in properties.items():
161+
k = escape_arg_segment(k)
162+
158163
# Handle nested objects which aren't read-only and have properties
159164
if (
160165
v.type == "object"

linodecli/baked/util.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Provides various utility functions for use in baking logic.
33
"""
44

5+
import re
56
from collections import defaultdict
67
from typing import Any, Dict, List, Set, Tuple
78

@@ -62,3 +63,58 @@ def __inner(
6263
# We only want to mark fields that are required by ALL subschema as required
6364
set(key for key, count in required.items() if count == schema_count),
6465
)
66+
67+
68+
ESCAPED_PATH_DELIMITER_PATTERN = re.compile(r"(?<!\\)\.")
69+
70+
71+
def escape_arg_segment(segment: str) -> str:
72+
"""
73+
Escapes periods in a segment by prefixing them with a backslash.
74+
75+
:param segment: The input string segment to escape.
76+
:return: The escaped segment with periods replaced by '\\.'.
77+
"""
78+
return segment.replace(".", "\\.")
79+
80+
81+
def unescape_arg_segment(segment: str) -> str:
82+
"""
83+
Reverses the escaping of periods in a segment, turning '\\.' back into '.'.
84+
85+
:param segment: The input string segment to unescape.
86+
:return: The unescaped segment with '\\.' replaced by '.'.
87+
"""
88+
return segment.replace("\\.", ".")
89+
90+
91+
def get_path_segments(path: str) -> List[str]:
92+
"""
93+
Splits a path string into segments using a delimiter pattern,
94+
and unescapes any escaped delimiters in the resulting segments.
95+
96+
:param path: The full path string to split and unescape.
97+
:return: A list of unescaped path segments.
98+
"""
99+
return [
100+
unescape_arg_segment(seg)
101+
for seg in ESCAPED_PATH_DELIMITER_PATTERN.split(path)
102+
]
103+
104+
105+
def get_terminal_keys(data: Dict[str, Any]) -> List[str]:
106+
"""
107+
Recursively retrieves all terminal (non-dict) keys from a nested dictionary.
108+
109+
:param data: The input dictionary, possibly nested.
110+
:return: A list of all terminal keys (keys whose values are not dictionaries).
111+
"""
112+
ret = []
113+
114+
for k, v in data.items():
115+
if isinstance(v, dict):
116+
ret.extend(get_terminal_keys(v)) # recurse into nested dicts
117+
else:
118+
ret.append(k) # terminal key
119+
120+
return ret

0 commit comments

Comments
 (0)