Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/fabric_cli/core/fab_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def __init__(
error_code: Optional[str] = None,
data: Optional[Any] = None,
hidden_data: Optional[Any] = None,
show_key_value_list: bool = False,
):
"""Initialize a new FabricCLIOutput instance.

Expand All @@ -89,6 +90,7 @@ def __init__(
error_code: Optional error code. Only included when status is Failed.
data: The main output data to be displayed
hidden_data: Additional data shown only when --all flag or FAB_SHOW_HIDDEN is true
show_key_value_list: Whether to show output in key-value list format

Note:
The data parameter is always converted to a list format internally.
Expand All @@ -100,6 +102,7 @@ def __init__(
self._subcommand = subcommand
self._output_format_type = output_format_type
self._show_headers = show_headers
self._show_key_value_list = show_key_value_list

self._result = OutputResult(
data=data,
Expand All @@ -124,6 +127,10 @@ def result(self) -> OutputResult:
def show_headers(self) -> bool:
return self._show_headers

@property
def show_key_value_list(self) -> bool:
return self._show_key_value_list

def to_json(self, indent: int = 4) -> str:
try:
from fabric_cli.utils.fab_util import dumps
Expand Down
4 changes: 4 additions & 0 deletions src/fabric_cli/errors/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

class CommonErrors:

@staticmethod
def invalid_entries_format() -> str:
return "Invalid entries format"

@staticmethod
def invalid_jmespath_query() -> str:
return f"Invalid jmespath query (https://jmespath.org)"
Expand Down
87 changes: 86 additions & 1 deletion src/fabric_cli/utils/fab_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import builtins
import html
import json
import re
import sys
import unicodedata
from argparse import Namespace
Expand Down Expand Up @@ -95,7 +96,7 @@ def print_output_format(
data: Optional[Any] = None,
hidden_data: Optional[Any] = None,
show_headers: bool = False,
# print_callback: bool = True,
show_key_value_list: bool = False,
) -> None:
"""Create a FabricCLIOutput instance and print it depends on the format.

Expand All @@ -105,6 +106,7 @@ def print_output_format(
data: Optional data to include in output
hidden_data: Optional hidden data to include in output
show_headers: Whether to show headers in the output (default: False)
show_key_value_list: Whether to show output in key-value list format (default: False)

Returns:
FabricCLIOutput: Configured output instance ready for printing
Expand All @@ -121,6 +123,7 @@ def print_output_format(
data=data,
hidden_data=hidden_data,
show_headers=show_headers,
show_key_value_list=show_key_value_list,
)

# Get format from output or config
Expand Down Expand Up @@ -355,6 +358,8 @@ def _print_output_format_result_text(output: FabricCLIOutput) -> None:
):
data_keys = output.result.get_data_keys() if output_result.data else []
print_entries_unix_style(output_result.data, data_keys, header=show_headers)
elif output.show_key_value_list:
_print_entries_key_value_list_style(output_result.data)
else:
_print_raw_data(output_result.data)

Expand Down Expand Up @@ -486,3 +491,83 @@ def _get_visual_length(string: str) -> int:
else:
length += 1
return length


def _print_entries_key_value_list_style(entries: Any) -> None:
"""Print entries in a key-value list format with formatted keys.

Args:
entries: Dictionary or list of dictionaries to print

Example output:
Logged In: true
Account: johndoe@example.com
"""
if isinstance(entries, dict):
_entries = [entries]
elif isinstance(entries, list):
if not entries:
return
_entries = entries
else:
raise FabricCLIError(
ErrorMessages.Common.invalid_entries_format(),
fab_constant.ERROR_INVALID_ENTRIES_FORMAT,
)

for entry in _entries:
for key, value in entry.items():
pretty_key = _format_key_to_convert_to_title_case(key)
print_grey(f"{pretty_key}: {value}", to_stderr=False)
if len(_entries) > 1:
print_grey("", to_stderr=False) # Empty line between entries


def _format_key_to_convert_to_title_case(key: str) -> str:
"""Convert a snake_case key to a Title Case name.

Args:
key: The key to format in snake_case format (e.g. 'user_id', 'account_name')

Returns:
str: Formatted to title case name (e.g. 'User ID', 'Account Name')

Raises:
ValueError: If the key is not in the expected underscore-separated format
"""
# Allow letters, numbers, and underscores only
if not key.replace('_', '').replace(' ', '').isalnum():
raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")

# Check for invalid patterns (camelCase, spaces mixed with underscores, etc.)
if ' ' in key and '_' in key:
raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")

# Check for camelCase pattern (uppercase letters not at the start)
if any(char.isupper() for char in key[1:]) and '_' not in key:
raise ValueError(f"Invalid key format: '{key}'. Only underscore-separated words are allowed.")

# Single words without underscores are allowed
if '_' not in key and key.islower():
pretty = key.title()
else:
# Replace underscores with spaces and title case
pretty = key.replace('_', ' ').title()

pretty = key.replace('_', ' ').title()

return _check_special_cases(pretty)


def _check_special_cases(pretty: str) -> str:
"""Check for special cases and replace them with the correct value."""
# Here add special cases for specific keys that need to be formatted differently
special_cases = {
"Id": "ID",
"Powerbi": "PowerBI",
}

for case_key, case_value in special_cases.items():
pretty = pretty.replace(case_key.title(), case_value)

return pretty
15 changes: 15 additions & 0 deletions tests/test_core/test_fab_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,18 @@ def test_fabric_cli_output_error_handling_success():

json_output = json.loads(output.to_json())
assert json_output["result"]["error_code"] == "UnexpectedError"


def test_fabric_cli_output_show_key_value_list_success():
"""Test show_key_value_list property is handled correctly."""
# Test with show_key_value_list True
output = FabricCLIOutput(data={"test": "data"}, show_key_value_list=True)
assert output.show_key_value_list is True

# Test with show_key_value_list False (default)
output = FabricCLIOutput(data={"test": "data"})
assert output.show_key_value_list is False

# Test with explicit False
output = FabricCLIOutput(data={"test": "data"}, show_key_value_list=False)
assert output.show_key_value_list is False
Loading
Loading