Source code for utils.json_output_viewer

"""
Script
------
json_output_viewer.py

Path
----
python/hillstar/utils/json_output_viewer.py

Purpose
-------
Generic utility to parse, validate, and view JSON output files in full.

Provides CLI and programmatic access to any JSON outputs file with complete
untruncated text. Works with any structure containing node outputs, test
results, workflow outputs, or similar data requiring full text inspection.

Features
--------
- Load and validate JSON structure
- View individual node/key outputs in full
- Summary statistics (character counts, line counts)
- Line-numbered output for detailed review
- Raw JSON export
- Validation reporting
- File auto-detection or explicit path specification

Usage
-----
View all outputs from file:
 python json_output_viewer.py /path/to/outputs.json

View specific node:
 python json_output_viewer.py /path/to/outputs.json --key node_name

View with line numbers:
 python json_output_viewer.py /path/to/outputs.json --key node_name --lines

View summary only:
 python json_output_viewer.py /path/to/outputs.json --summary

View raw JSON:
 python json_output_viewer.py /path/to/outputs.json --raw

Validation report:
 python json_output_viewer.py /path/to/outputs.json --validate

Programmatic Usage
------------------
from json_output_viewer import JSONOutputViewer

viewer = JSONOutputViewer('/path/to/outputs.json')
if viewer.load_and_validate():
 viewer.print_all_outputs()
 summary = viewer.get_summary()

 # Access data directly
 all_data = viewer.data

Author: Julen Gamboa <julen.gamboa.ds@gmail.com>

Created
-------
2026-02-22

Last Edited
-----------
2026-02-22
"""

import json
import sys
from pathlib import Path
from typing import Optional, Dict, Any
import argparse


[docs] class JSONOutputViewer: """Generic parser and display tool for JSON output files."""
[docs] def __init__(self, output_file: Path): """Initialize viewer with output file path.""" self.output_file = Path(output_file) self.data = None self.is_valid = False self.validation_errors = []
[docs] def load_and_validate(self) -> bool: """Load and validate the JSON output file.""" if not self.output_file.exists(): self.validation_errors.append( f"File not found: {self.output_file}" ) return False try: with open(self.output_file) as f: self.data = json.load(f) except json.JSONDecodeError as e: self.validation_errors.append(f"Invalid JSON: {e}") return False except Exception as e: self.validation_errors.append(f"Error loading file: {e}") return False # Validate structure - must be dict-like if not isinstance(self.data, dict): self.validation_errors.append( f"Root must be a dictionary, got {type(self.data).__name__}" ) return False # Validate entries are string-like (or JSON-serializable) for key, content in self.data.items(): if not isinstance(content, (str, int, float, bool, type(None))): if isinstance(content, (dict, list)): # JSON-serializable complex types are okay continue self.validation_errors.append( f"Entry '{key}' is not a JSON-serializable type: {type(content)}" ) return False self.is_valid = True return True
[docs] def get_summary(self) -> Dict[str, Any]: """Get summary statistics about the outputs.""" if not self.is_valid or self.data is None: return {} summary = {} for key, content in self.data.items(): if isinstance(content, str): content_str = content else: content_str = str(content) char_count = len(content_str) if content_str else 0 summary[key] = { "type": type(content).__name__, "characters": char_count, "lines": (content_str.count("\n") + 1) if content_str else 0, "preview": ( content_str[:100] + "..." if content_str and len(content_str) > 100 else content_str ), } return summary
[docs] def print_summary(self) -> None: """Print summary of all outputs.""" if not self.is_valid: print("ERROR: Invalid or missing outputs file") return summary = self.get_summary() print("\n" + "=" * 80) print("JSON OUTPUT SUMMARY") print("=" * 80) print(f"\nFile: {self.output_file}") print(f"Total entries: {len(summary)}\n") for i, (key, stats) in enumerate(summary.items(), 1): print(f"[{i}] {key}") print(f" Type: {stats['type']}") print(f" Size: {stats['characters']} characters") print(f" Lines: {stats['lines']}") print(f" Preview: {stats['preview']}") print() total_chars = sum(s["characters"] for s in summary.values()) print(f"Total size: {total_chars} characters") print("=" * 80)
[docs] def print_all_outputs(self, with_lines: bool = False) -> None: """Print all outputs in full.""" if not self.is_valid: print("ERROR: Invalid or missing outputs file") return if self.data is None: print("ERROR: No data loaded") return print("\n" + "=" * 80) print("JSON OUTPUT - FULL VIEW") print("=" * 80) print(f"\nFile: {self.output_file}\n") for i, (key, content) in enumerate(self.data.items(), 1): if isinstance(content, str): content_str = content else: content_str = json.dumps(content) content_len = len(content_str) if content_str else 0 print(f"\n{'-' * 80}") print(f"[{i}] {key}") print(f"{'-' * 80}") print(f"Type: {type(content).__name__}") print(f"Length: {content_len} characters\n") if with_lines: for line_num, line in enumerate(content_str.split("\n"), 1): print(f"{line_num:4d} | {line}") else: print(content_str) print() print("=" * 80)
[docs] def print_key( self, key: str, with_lines: bool = False ) -> None: """Print a specific key's output in full.""" if not self.is_valid: print("ERROR: Invalid or missing outputs file") return if self.data is None: print("ERROR: No data loaded") return if key not in self.data: available = ", ".join(self.data.keys()) print(f"ERROR: Key '{key}' not found") print(f"Available keys: {available}") return content = self.data[key] if isinstance(content, str): content_str = content else: content_str = json.dumps(content) content_len = len(content_str) if content_str else 0 print("\n" + "=" * 80) print(f"OUTPUT: {key}") print("=" * 80) print(f"Type: {type(content).__name__}") print(f"Length: {content_len} characters\n") if with_lines: for line_num, line in enumerate(content_str.split("\n"), 1): print(f"{line_num:4d} | {line}") else: print(content_str) print("\n" + "=" * 80)
[docs] def print_raw_json(self) -> None: """Print raw JSON with formatting.""" if not self.is_valid: print("ERROR: Invalid or missing outputs file") return if self.data is None: print("ERROR: No data loaded") return print("\n" + "=" * 80) print("RAW JSON") print("=" * 80 + "\n") print(json.dumps(self.data, indent=2)) print()
[docs] def print_validation_report(self) -> None: """Print validation report.""" print("\n" + "=" * 80) print("VALIDATION REPORT") print("=" * 80) if self.is_valid and self.data is not None: print("PASS: Valid JSON structure") print(f"File: {self.output_file}") print(f"Entries: {len(self.data)}") if self.data: print("\nContents:") for key in self.data: content = self.data[key] if isinstance(content, str): content_str = content else: content_str = json.dumps(content) chars = len(content_str) if content_str else 0 print(f" [{key}] {type(content).__name__} ({chars} chars)") else: print("FAIL: Validation failed") if self.validation_errors: print("\nErrors:") for error in self.validation_errors: print(f" - {error}") print("=" * 80 + "\n")
[docs] def export_markdown(self, output_path: Optional[Path] = None) -> Path: """Export all outputs to a markdown file.""" if not self.is_valid or self.data is None: raise ValueError("Cannot export: data not loaded or invalid") if output_path is None: output_path = self.output_file.parent / "outputs.md" output_path = Path(output_path) with open(output_path, "w") as f: f.write("# JSON Output Report\n\n") f.write(f"Source: `{self.output_file}`\n\n") f.write(f"Total entries: {len(self.data)}\n\n") # Table of contents f.write("## Contents\n\n") for i, key in enumerate(self.data.keys(), 1): content = self.data[key] if isinstance(content, str): content_str = content else: content_str = json.dumps(content) chars = len(content_str) if content_str else 0 f.write(f"{i}. [{key}](#section-{i}-{key.lower()}) - {chars} chars\n") f.write("\n---\n\n") # Content sections for i, (key, content) in enumerate(self.data.items(), 1): if isinstance(content, str): content_str = content else: content_str = json.dumps(content) content_len = len(content_str) if content_str else 0 f.write(f"## Section {i}: {key}\n\n") f.write(f"**Type:** {type(content).__name__}\n\n") f.write(f"**Length:** {content_len} characters\n\n") f.write("```\n") f.write(content_str) f.write("\n```\n\n") return output_path
[docs] def main(): """CLI entry point.""" parser = argparse.ArgumentParser( description="View JSON output files in full", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python json_output_viewer.py /path/to/outputs.json View all outputs in full python json_output_viewer.py /path/to/outputs.json --key node_name View specific key output python json_output_viewer.py /path/to/outputs.json --summary View summary statistics only python json_output_viewer.py /path/to/outputs.json --key node_name --lines View with line numbers for detailed review python json_output_viewer.py /path/to/outputs.json --raw Export raw JSON data python json_output_viewer.py /path/to/outputs.json --validate Show validation report """, ) parser.add_argument( "output_file", type=Path, help="Path to JSON output file to view", ) parser.add_argument( "--key", help="View specific key by name", ) parser.add_argument( "--summary", action="store_true", help="Show summary statistics only", ) parser.add_argument( "--raw", action="store_true", help="Show raw JSON", ) parser.add_argument( "--lines", action="store_true", help="Show output with line numbers", ) parser.add_argument( "--validate", action="store_true", help="Show validation report", ) parser.add_argument( "--markdown", nargs="?", const="auto", help="Export to markdown file (auto-filename or specify path)", ) args = parser.parse_args() # Create viewer viewer = JSONOutputViewer(args.output_file) viewer.load_and_validate() # Execute requested action if args.markdown is not None: try: if args.markdown == "auto": # Auto-generate filename md_path = viewer.export_markdown() else: # Use specified path md_path = viewer.export_markdown(Path(args.markdown)) print(f"Markdown exported to: {md_path}") except ValueError as e: print(f"ERROR: {e}") sys.exit(1) elif args.validate: viewer.print_validation_report() elif args.raw: viewer.print_raw_json() elif args.summary: viewer.print_summary() elif args.key: viewer.print_key(args.key, with_lines=args.lines) else: # Default: print all outputs viewer.print_all_outputs(with_lines=args.lines)
if __name__ == "__main__": main()