Skip to content

Commit 2694734

Browse files
authored
Merge branch 'dev' into add_db_config_tests
2 parents 8a621c7 + 2f1bb7c commit 2694734

File tree

6 files changed

+222
-16
lines changed

6 files changed

+222
-16
lines changed

linodecli/api_request.py

Lines changed: 5 additions & 2 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:
@@ -364,13 +365,15 @@ def _build_request_body(
364365
if v is None or k in param_names:
365366
continue
366367

368+
path_segments = get_path_segments(k)
369+
367370
cur = expanded_json
368-
for part in k.split(".")[:-1]:
371+
for part in path_segments[:-1]:
369372
if part not in cur:
370373
cur[part] = {}
371374
cur = cur[part]
372375

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

375378
return json.dumps(_traverse_request_body(expanded_json))
376379

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:
@@ -152,6 +155,8 @@ def _parse_request_model(
152155
return args
153156

154157
for k, v in properties.items():
158+
k = escape_arg_segment(k)
159+
155160
# Handle nested objects which aren't read-only and have properties
156161
if (
157162
v.type == "object"

linodecli/baked/util.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
Provides various utility functions for use in baking logic.
33
"""
44

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

89
from openapi3.schemas import Schema
910

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

linodecli/output/output_handler.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from rich.table import Column, Table
1717

1818
from linodecli.baked.response import OpenAPIResponse, OpenAPIResponseAttr
19+
from linodecli.baked.util import get_terminal_keys
1920

2021

2122
class OutputMode(Enum):
@@ -328,15 +329,30 @@ def _json_output(self, header, data, to):
328329
Prints data in JSON format
329330
"""
330331
# Special handling for JSON headers.
331-
# We're only interested in the last part of the column name.
332-
header = [v.split(".")[-1] for v in header]
332+
# We're only interested in the last part of the column name, unless the last
333+
# part is a dotted key. If the last part is a dotted key, include the entire dotted key.
333334

334335
content = []
335336
if len(data) and isinstance(data[0], dict): # we got delimited json in
337+
parsed_header = []
338+
terminal_keys = get_terminal_keys(data[0])
339+
340+
for v in header:
341+
parts = v.split(".")
342+
if (
343+
len(parts) >= 2
344+
and ".".join([parts[-2], parts[-1]]) in terminal_keys
345+
):
346+
parsed_header.append(".".join([parts[-2], parts[-1]]))
347+
else:
348+
parsed_header.append(parts[-1])
349+
336350
# parse down to the value we display
337351
for row in data:
338-
content.append(self._select_json_elements(header, row))
352+
content.append(self._select_json_elements(parsed_header, row))
339353
else: # this is a list
354+
header = [v.split(".")[-1] for v in header]
355+
340356
for row in data:
341357
content.append(dict(zip(header, row)))
342358

linodecli/overrides.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,30 @@ def inner(func):
4747
return inner
4848

4949

50+
@output_override("databases", "mysql-config-view", OutputMode.table)
51+
def handle_databases_mysql_config_view(
52+
operation, output_handler, json_data
53+
) -> bool:
54+
# pylint: disable=unused-argument
55+
"""
56+
Override the output of 'linode-cli databases mysql-config-view'
57+
to properly display the mysql engine config.
58+
"""
59+
return databases_mysql_config_view_output(json_data)
60+
61+
62+
@output_override("databases", "postgres-config-view", OutputMode.table)
63+
def handle_databases_postgres_config_view(
64+
operation, output_handler, json_data
65+
) -> bool:
66+
# pylint: disable=unused-argument
67+
"""
68+
Override the output of 'linode-cli databases postgres-config-view'
69+
to properly display the postgresql engine config.
70+
"""
71+
return databases_postgres_config_view_output(json_data)
72+
73+
5074
@output_override("domains", "zone-file", OutputMode.delimited)
5175
def handle_domains_zone_file(operation, output_handler, json_data) -> bool:
5276
# pylint: disable=unused-argument
@@ -291,3 +315,97 @@ def pg_view_output(json_data) -> bool:
291315
console.print(output)
292316

293317
return False
318+
319+
320+
def add_param_row(output, param_name, param_data):
321+
"""
322+
Construct and add a row to the output table for DB Config view overrides.
323+
"""
324+
param_type = str(param_data.get("type", ""))
325+
example = str(param_data.get("example", ""))
326+
minimum = str(param_data.get("minimum", ""))
327+
maximum = str(param_data.get("maximum", ""))
328+
min_length = str(param_data.get("minLength", ""))
329+
max_length = str(param_data.get("maxLength", ""))
330+
pattern = str(param_data.get("pattern", ""))
331+
requires_restart = "YES" if param_data.get("requires_restart") else "NO"
332+
description = param_data.get("description", "")
333+
334+
output.add_row(
335+
param_name,
336+
param_type,
337+
example,
338+
minimum,
339+
maximum,
340+
min_length,
341+
max_length,
342+
pattern,
343+
requires_restart,
344+
Align(description, align="left"),
345+
)
346+
347+
348+
def databases_mysql_config_view_output(json_data) -> bool:
349+
"""
350+
Parse and format the MySQL configuration output table.
351+
"""
352+
output = Table(header_style="bold", show_lines=True)
353+
354+
output.add_column("Parameter", style="bold")
355+
output.add_column("Type", justify="center")
356+
output.add_column("Example", justify="center")
357+
output.add_column("Min", justify="center")
358+
output.add_column("Max", justify="center")
359+
output.add_column("Min Length", justify="center")
360+
output.add_column("Max Length", justify="center")
361+
output.add_column("Pattern", justify="center")
362+
output.add_column("Requires Restart", justify="center")
363+
output.add_column("Description", style="dim")
364+
365+
for field, params in json_data.items():
366+
if field in ["binlog_retention_period"]:
367+
add_param_row(output, field, params)
368+
else:
369+
for key, val in params.items():
370+
param_name = f"{field}.{key}"
371+
add_param_row(output, param_name, val)
372+
373+
console = Console()
374+
console.print(output)
375+
376+
return False
377+
378+
379+
def databases_postgres_config_view_output(json_data) -> bool:
380+
"""
381+
Parse and format the PostgreSQL configuration output table.
382+
"""
383+
output = Table(header_style="bold", show_lines=True)
384+
385+
output.add_column("Parameter", style="bold")
386+
output.add_column("Type", justify="center")
387+
output.add_column("Example", justify="center")
388+
output.add_column("Min", justify="center")
389+
output.add_column("Max", justify="center")
390+
output.add_column("Min Length", justify="center")
391+
output.add_column("Max Length", justify="center")
392+
output.add_column("Pattern", justify="center")
393+
output.add_column("Requires Restart", justify="center")
394+
output.add_column("Description", style="dim")
395+
396+
for field, params in json_data.items():
397+
if field in [
398+
"pg_stat_monitor_enable",
399+
"shared_buffers_percentage",
400+
"work_mem",
401+
]:
402+
add_param_row(output, field, params)
403+
else:
404+
for key, val in params.items():
405+
param_name = f"{field}.{key}"
406+
add_param_row(output, param_name, val)
407+
408+
console = Console()
409+
console.print(output)
410+
411+
return False

0 commit comments

Comments
 (0)