Skip to content

Output Format

aviatco edited this page Jan 12, 2026 · 4 revisions

Output Format - Developer Guide

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.

Architecture Overview

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.

Core Components

  • Output Functions: Centralized functions in fab_ui.py for 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

Implementation Patterns

Using Output Functions

When implementing CLI commands, always use the designated output functions to ensure consistent behavior across both text and JSON formats.

Success Results with Data

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
    )

Output Format Flags

The print_output_format() function supports several formatting flags that control how data is displayed in text format:

show_headers Flag

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   Workspace

When 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:

  • ls and dir commands automatically show headers when multiple fields are present
  • Commands with show_headers=True always display headers regardless of data complexity
show_key_value_list Flag

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: 12345

When 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_case keys to Title Case format
  • Handles special cases like idID, powerbiPowerBI
  • Validates key formats and raises errors for invalid patterns
Combining Format Flags
# 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
        )
Format Flag Interactions

The formatting behavior follows this priority:

  1. JSON format: All flags are ignored (JSON structure is always the same)
  2. Text format with show_key_value_list=True: Data displayed as formatted key-value pairs
  3. Text format with show_headers=True: Data displayed in Unix-style table format
  4. Text format with special commands (ls, dir): Headers shown automatically for complex data
  5. Text format default: Raw data display with intelligent formatting

Success Results with Message Only

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"
    )

Success Results with Hidden Data

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
    )

Error Results

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
        )

Informational Output

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"
    )

JSON Schema Implementation

Standard Success Schema

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)
    }
}

Standard Error Schema

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
    }
}

JSON Format Enhancements

API Response Data in JSON Format

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.

Stream Separation Implementation

Stdout vs Stderr Usage

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
        )

Interactive vs Scripted Behavior: stdout/stderr Separation

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 messages

Testing Implementation

Test Both Formats

Always 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"]) == 2

Test Error Scenarios

Test 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"

Key Implementation Guidelines

1. Always Use Designated Functions

  • Use print_output_format() for success results
  • Use print_output_error()for errors
  • Use print_info(), print_warning(), print_grey() for informational output

2. Never Check Output Format in Commands

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

3. Maintain Schema Consistency

  • Follow established JSON structure patterns
  • Use consistent field names across similar commands
  • Maintain consistent data types (strings for IDs, booleans for flags, etc.)

4. Error Handling Best Practices

  • Use standardized error codes from fab_constant.py
  • Provide meaningful error messages
  • Always include the command context in error responses

5. Stream Separation

  • 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

6. Testing Requirements

  • Test commands with both --output_format json and --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

Clone this wiki locally