-
Notifications
You must be signed in to change notification settings - Fork 31
Output Format
This developer guide covers the implementation details, patterns, and best practices for integrating output formatting into Fabric CLI commands. It provides the technical foundation for maintaining consistent output behavior across all CLI commands.
The output format system is built on a centralized approach using utility functions from fab_ui.py that automatically handle format detection and appropriate rendering based on user configuration and command-line flags.
-
Output Functions: Centralized functions in
fab_ui.pyfor consistent output handling - Format Detection: Automatic detection of user's preferred format from config and CLI flags
- Stream Separation: Clear separation between results (stdout) and informational messages (stderr)
- Schema Consistency: Standardized JSON structure across all commands
When implementing CLI commands, always use the designated output functions to ensure consistent behavior across both text and JSON formats.
Use print_output_format() for command results that return data:
from fabric_cli.utils import fab_ui
def command_with_data(args):
# Your command logic here
workspaces = get_workspaces() # Returns list of workspace objects
# Convert to standardized format
data = [
{
"name": ws.name,
"id": ws.id,
}
for ws in workspaces
]
# Output with headers for text format
fab_ui.print_output_format(
args,
data=data,
show_headers=True # Shows table headers in text format
)The print_output_format() function supports several formatting flags that control how data is displayed in text format:
Controls whether table headers are displayed in text format:
# Basic usage - no headers (displays names only)
fab_ui.print_output_format(
args,
data=[{"name": "workspace1"}, {"name": "workspace2"}]
)
# Output:
# workspace1
# workspace2
# With headers - shows tabular format
fab_ui.print_output_format(
args,
data=[{"name": "workspace1", "type": "Workspace"}],
show_headers=True
)
# Output:
# name type
# --------------------
# workspace1 WorkspaceWhen to use show_headers=True:
- Commands that return tabular data with multiple fields (
ls -l,job run-status) - Data with complex structures that benefit from column headers
- When displaying detailed information that users need to understand field meanings
Automatic header behavior:
-
lsanddircommands automatically show headers when multiple fields are present - Commands with
show_headers=Truealways display headers regardless of data complexity
Controls whether data is displayed in a key-value list format instead of raw data:
# Key-value list format - ideal for single record details
fab_ui.print_output_format(
args,
data=[{"logged_in": "true", "account": "user@example.com", "tenant_id": "12345"}],
show_key_value_list=True
)
# Output:
# Logged In: true
# Account: user@example.com
# Tenant ID: 12345When to use show_key_value_list=True:
- Single record display (auth status, configuration details)
- User-friendly formatting of configuration or status information
- When field names should be human-readable rather than technical keys
Key formatting behavior:
- Converts
snake_casekeys toTitle Caseformat - Handles special cases like
id→ID,powerbi→PowerBI - Validates key formats and raises errors for invalid patterns
# Example: Command that shows different formats based on data complexity
def adaptive_display_command(args):
data = get_command_data()
if len(data) == 1 and is_status_like_data(data[0]):
# Single record status - use key-value format
fab_ui.print_output_format(
args,
data=data,
show_key_value_list=True
)
elif len(data) > 1 or has_multiple_fields(data[0]):
# Multiple records or complex data - use table format
fab_ui.print_output_format(
args,
data=data,
show_headers=True
)
else:
# Simple data - use basic format
fab_ui.print_output_format(
args,
data=data
)The formatting behavior follows this priority:
- JSON format: All flags are ignored (JSON structure is always the same)
-
Text format with
show_key_value_list=True: Data displayed as formatted key-value pairs -
Text format with
show_headers=True: Data displayed in Unix-style table format -
Text format with special commands (
ls,dir): Headers shown automatically for complex data - Text format default: Raw data display with intelligent formatting
For operations that complete successfully but don't return structured data:
def command_with_message(args):
# Your command logic here
perform_operation()
# Simple success message
fab_ui.print_output_format(
args,
message="Operation completed successfully"
)For workspace-like commands that may have virtual items (capacities, gateways, etc.):
def workspace_command(args):
items = get_workspace_items()
virtual_items = get_virtual_items() # .capacities, .gateways, etc.
fab_ui.print_output_format(
args,
data=[{"name": item.name, "type": item.type} for item in items],
hidden_data=[item.name for item in virtual_items],
show_headers=True
)Use print_output_error() for error conditions:
from fabric_cli.core.fab_exceptions import FabricCLIError
from fabric_cli.utils import fab_ui
def command_with_error_handling(args):
try:
# Command logic here
result = risky_operation()
fab_ui.print_output_format(
args,
data=result,
show_headers=True
)
except WorkspaceNotFoundError as e:
fab_ui.print_output_error(
FabricCLIError(ErrorMessages.item_not_found(), fab_constant.ERROR_WORKSPACE_NOT_FOUND),
command=args.command
)
except Exception as e:
fab_ui.print_output_error(
FabricCLIError(
ErrorMessages.unexpected_error()",
fab_constant.ERROR_UNEXPECTED
),
command=args.command
)For progress updates, warnings, and debug information that should go to stderr regardless of output format:
from fabric_cli.utils import fab_ui
def long_running_command(args):
# Progress updates (to stderr)
fab_ui.print_progress("Processing items", progress=25)
process_first_batch()
fab_ui.print_progress("Processing items", progress=75)
process_second_batch()
# Warnings (to stderr)
if deprecated_feature_used:
fab_ui.print_warning("This feature will be deprecated in v2.0")
# Debug/info messages (to stderr)
if args.debug:
fab_ui.print_grey(f"Processing {len(items)} items...")
# Final result (to stdout)
fab_ui.print_output_format(
args,
message="Processing completed successfully"
)All successful commands will be printed in the following structure:
# The print_output_format function automatically generates this structure
{
"timestamp": "2026-01-06T08:00:00.000Z", # Auto-generated
"status": "Success", # Auto-set
"command": "command_name", # From args.command
"result": {
"data": [...], # Your data array (optional)
"hidden_data": [...], # Virtual items (optional)
"message": "..." # Success message (optional)
}
}Error responses will be printed in the following structure:
# The print_output_error function automatically generates this structure
{
"timestamp": "2026-01-06T08:00:00.000Z", # Auto-generated
"status": "Failure", # Auto-set
"command": "command_name", # From command parameter
"result": {
"message": "Error description", # From FabricCLIError
"error_code": "ERROR_CODE" # From FabricCLIError
}
}The JSON output format provides access to detailed API response data, particularly for create commands. This feature expands the information available beyond the standard text format.
# Example: Create command with enhanced JSON output
def create_workspace(args):
"""Create workspace with full API response in JSON format."""
try:
# Perform the creation operation
api_response = api_client.create_workspace(args.name)
# For text format: Simple confirmation
# For JSON format: Full API response data
fab_ui.print_output_format(
args,
data=[{
"name": api_response.get("displayName"),
"id": api_response.get("id"),
"type": "Workspace",
"description": api_response.get("description"),
"capacityId": api_response.get("capacityId"),
"created": api_response.get("createdDate"),
"modified": api_response.get("lastModifiedDate")
}],
message=f"Workspace '{args.name}' created successfully"
)
except Exception as e:
fab_ui.print_output_error(
FabricCLIError(ErrorMessages.failed_create_workspace(), fab_constant.ERROR_WORKSPACE_CREATE_FAILED),
command=args.command
)Benefits of Enhanced JSON Output:
- Rich Data: Access to complete API response metadata
- Automation: Scripts can access creation timestamps, IDs, and other metadata
- Debugging: Full response data helps with troubleshooting
- Integration: Downstream tools can access detailed resource information
Planned Expansion: This enhanced JSON response pattern is being extended to more commands in the near future, providing richer data across all create, update, and query operations while maintaining simple text output for interactive use.
Maintain clear separation between result data and informational messages:
def example_command(args):
# Informational messages go to stderr
fab_ui.print_info("Starting operation...")
fab_ui.print_progress("Processing", progress=50)
try:
result = perform_operation()
# Results go to stdout
fab_ui.print_output_format(
args,
data=result,
message="Operation completed"
)
except Exception as e:
# Errors go to stdout (as structured responses)
fab_ui.print_output_error(
FabricCLIError(ErrorMessages.operation_failed(), fab_constant.ERROR_OPERATION_FAILED),
command=args.command
)The CLI maintains strict separation between results (stdout) and informational messages (stderr) to support both interactive and scripted usage:
def example_command(args):
# STDERR: Progress updates, warnings, debug info
# These messages help interactive users but don't interfere with scripts
fab_ui.print_progress("Processing items", progress=25) # → stderr
fab_ui.print_info("Found 150 items to process") # → stderr
fab_ui.print_warning("Feature will be deprecated") # → stderr
process_items()
# STDOUT: Structured command results
# Scripts can safely parse this output
fab_ui.print_output_format( # → stdout
args,
data=process_results(),
show_headers=True
)Why this separation matters:
- Interactive users see helpful progress updates and warnings on stderr
- Scripts and pipelines can safely capture structured results from stdout
- Error handling works consistently: command errors go to stdout as structured JSON/text, informational messages stay on stderr
Stream routing in practice:
# Interactive: See everything
$ fab job run nb.Notebook
Running job (sync) for 'nb.Notebook'... # stderr - visible to user
∟ Job instance 'xxxxx' created # stderr - visible to user
∟ Timeout: no timeout specified # stderr - visible to user
∟ Job instance status: NotStarted # stderr - visible to user
∟ Job instance status: NotStarted # stderr - visible to user
∟ Job instance status: Completed # stderr - visible to user
* Job instance 'xxxxx' completed # stdout - the actual result
# Scripted: Capture only results
$ fab workspace ls > results.txt 2>/dev/null
# results.txt contains only: workspace1, workspace2
# Progress messages are discarded via stderr redirect
# JSON output: Same separation
$ fab workspace ls --output-format json > results.json 2>logs.txt
# results.json: {"status": "Success", "result": {"data": [...]}}
# logs.txt: progress and info messagesAlways test commands with both output formats:
def test_command_text_format(self):
"""Test command with text output format."""
args = create_args(output_format='text')
with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
your_command(args)
output = mock_stdout.getvalue()
# Verify text format output
assert "workspace1" in output
assert "workspace2" in output
def test_command_json_format(self):
"""Test command with JSON output format."""
args = create_args(output_format='json')
with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
your_command(args)
output = mock_stdout.getvalue()
# Parse and verify JSON structure
result = json.loads(output)
assert result["status"] == "Success"
assert result["command"] == "your_command"
assert len(result["result"]["data"]) == 2Test error conditions with both formats:
def test_error_handling(self):
"""Test error output in both formats."""
# Test with JSON format
args = create_args(output_format='json')
with patch('sys.stdout', new_callable=io.StringIO) as mock_stdout:
your_command_with_error(args)
output = mock_stdout.getvalue()
result = json.loads(output)
assert result["status"] == "Failure"
assert result["result"]["error_code"] == "ERROR_WORKSPACE_NOT_FOUND"- Use
print_output_format()for success results - Use
print_output_error()for errors - Use
print_info(),print_warning(),print_grey()for informational output
Critical: Do not check the value of output_format in command implementations. Always use the designated print functions which automatically handle format detection and rendering.
# ❌ WRONG - Don't do this
def bad_command(args):
data = get_data()
if args.output_format == "json":
print(json.dumps(data))
else:
print(data["name"])
# ✅ CORRECT - Use designated functions
def good_command(args):
data = get_data()
fab_ui.print_output_format(args, data=data)Why this matters:
- Format detection logic is centralized and consistent
- Automatic handling of config file defaults vs command-line overrides
- Proper error handling for unsupported formats
- Consistent schema compliance across all commands
- Future format additions require no command changes
- Follow established JSON structure patterns
- Use consistent field names across similar commands
- Maintain consistent data types (strings for IDs, booleans for flags, etc.)
- Use standardized error codes from
fab_constant.py - Provide meaningful error messages
- Always include the command context in error responses
- Send command results to stdout using output functions
- Send informational messages to stderr using info/warning/debug functions
- Never mix result data with progress/debug information
- Test commands with both
--output_format jsonand--output_format text - Verify JSON schema compliance in automated tests
- Test error scenarios with both output formats
- Ensure informational messages don't interfere with structured output