blob: 1e55104f4d893975e9b4c7124c623f6a4787f6ea [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Formatting utilities for query results and errors."""
import csv
import io
import json
from abc import ABC, abstractmethod
from typing import Any
class Formatter(ABC):
"""Base class for result formatters."""
@abstractmethod
def format_results(self, data: list[dict[str, Any]]) -> str:
"""Formats query results as a string."""
@abstractmethod
def format_error(self, error_msg: str) -> str:
"""Formats an error message as a string."""
class JsonFormatter(Formatter):
"""Formats results and errors as JSON."""
def format_results(self, data: list[dict[str, Any]]) -> str:
return json.dumps(data, indent=2)
def format_error(self, error_msg: str) -> str:
return json.dumps({"error": error_msg}, indent=2)
def _is_batch(data: list[dict[str, Any]]) -> bool:
"""Checks if the data represents batch query/analysis results."""
return bool(
data
and all(
isinstance(x, dict)
and "name" in x
and ("results" in x or "error" in x)
for x in data
)
)
class MarkdownFormatter(Formatter):
"""Formats results and errors as Markdown."""
def format_results(self, data: list[dict[str, Any]]) -> str:
if _is_batch(data):
output = []
for item in data:
output.append(f"### {item['name']}")
if "error" in item:
output.append(f"Error: {item['error']}")
else:
results = item.get("results", [])
output.append(_table_to_markdown(results))
output.append("")
return "\n".join(output).strip()
return _table_to_markdown(data)
def format_error(self, error_msg: str) -> str:
return f"**Error:** {error_msg}"
class TextFormatter(Formatter):
"""Formats results and errors as plain text (TSV)."""
def format_results(self, data: list[dict[str, Any]]) -> str:
if _is_batch(data):
output = []
for item in data:
output.append(f"Query: {item['name']}")
if "error" in item:
output.append(f"Error: {item['error']}")
else:
results = item.get("results", [])
output.append(_table_to_text(results))
output.append("")
return "\n".join(output).strip()
return _table_to_text(data)
def format_error(self, error_msg: str) -> str:
return f"Error: {error_msg}"
def _table_to_markdown(rows: list[dict[str, Any]]) -> str:
"""Formats a list of dicts as a Markdown table, escaping pipes."""
if not rows:
return "No results."
raw_headers = list(rows[0].keys())
headers = [h.replace("|", "\\|") for h in raw_headers]
header_line = f"| {' | '.join(headers)} |"
separator_line = f"| {' | '.join(['---'] * len(headers))} |"
row_lines = []
for row in rows:
row_values = []
for h in raw_headers:
val_str = str(row.get(h, ""))
val_str = val_str.replace("\r\n", "<br>").replace("\n", "<br>")
row_values.append(val_str.replace("|", "\\|"))
row_line = f"| {' | '.join(row_values)} |"
row_lines.append(row_line)
return "\n".join([header_line, separator_line] + row_lines)
def _table_to_text(rows: list[dict[str, Any]]) -> str:
"""Formats a list of dicts as TSV text using standard csv module."""
if not rows:
return "No results."
headers = list(rows[0].keys())
output = io.StringIO()
writer = csv.writer(output, delimiter="\t", lineterminator="\n")
writer.writerow(headers)
for row in rows:
writer.writerow([row.get(h, "") for h in headers])
return output.getvalue().strip()
FORMATTERS: dict[str, Formatter] = {
"json": JsonFormatter(),
"markdown": MarkdownFormatter(),
"text": TextFormatter(),
}