refactor: route CLI→Application communication through A2A boundary
Created src/cleveragents/shared/output_format.py - a new shared module with format_data() function that provides JSON/YAML/plain/table serialization without any CLI dependencies. Fixed reverse dependency in plan_apply_service.py - changed import from cleveragents.cli.formatting to cleveragents.shared.output_format (the most critical architectural violation: Application layer importing from Presentation layer). Added .importlinter configuration file with rules to enforce: - No Application->Presentation (CLI) reverse dependencies - CLI->Application boundary violations (with current exceptions documented) Added import-linter>=2.0 to dev dependencies in pyproject.toml. Added BDD feature file features/a2a_boundary_enforcement.feature with 10 scenarios testing the boundary enforcement and step definitions. ISSUES CLOSED: #9962
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
[importlinter]
|
||||
root_package = cleveragents
|
||||
include_external_packages = True
|
||||
|
||||
[importlinter:contract:cli-no-application-direct]
|
||||
name = CLI must not import Application services directly
|
||||
type = forbidden
|
||||
source_modules =
|
||||
cleveragents.cli
|
||||
forbidden_modules =
|
||||
cleveragents.application.services
|
||||
ignore_imports =
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.plan_apply_service
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.plan_lifecycle_service
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.plan_service
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.project_service
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.execute_phase_context_assembler
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.llm_actors
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.plan_executor
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.strategy_actor
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.plan_preflight_guardrail
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.plan_resume_service
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.services.decision_service
|
||||
cleveragents.cli.commands.plan -> cleveragents.application.container
|
||||
cleveragents.cli.commands.context -> cleveragents.application.services.context_service
|
||||
cleveragents.cli.commands.context -> cleveragents.application.services.project_service
|
||||
cleveragents.cli.commands.context -> cleveragents.application.container
|
||||
cleveragents.cli.commands.resource -> cleveragents.application.services.resource_registry_service
|
||||
cleveragents.cli.commands.resource -> cleveragents.application.container
|
||||
cleveragents.cli.commands.skill -> cleveragents.application.services.skill_service
|
||||
cleveragents.cli.commands.skill -> cleveragents.application.services.config_service
|
||||
cleveragents.cli.commands.skill -> cleveragents.application.container
|
||||
cleveragents.cli.commands.invariant -> cleveragents.application.services.invariant_service
|
||||
|
||||
[importlinter:contract:application-no-presentation]
|
||||
name = Application layer must not import from Presentation (CLI) layer
|
||||
type = forbidden
|
||||
source_modules =
|
||||
cleveragents.application
|
||||
forbidden_modules =
|
||||
cleveragents.cli
|
||||
@@ -0,0 +1,59 @@
|
||||
Feature: A2A architectural boundary enforcement
|
||||
As a developer
|
||||
I want to verify that the CLI layer does not bypass the A2A boundary
|
||||
So that the architecture remains clean and maintainable
|
||||
|
||||
Background:
|
||||
Given the source directory exists at "src/cleveragents"
|
||||
|
||||
Scenario: Application layer does not import from CLI (Presentation) layer
|
||||
Given the application services directory exists
|
||||
When I scan all application service modules for CLI imports
|
||||
Then no application service module should import from "cleveragents.cli"
|
||||
|
||||
Scenario: plan_apply_service uses shared output_format instead of CLI formatting
|
||||
Given the plan_apply_service module exists
|
||||
When I inspect plan_apply_service imports
|
||||
Then it should not import from "cleveragents.cli.formatting"
|
||||
And it should import from "cleveragents.shared.output_format"
|
||||
|
||||
Scenario: shared output_format module has no CLI dependencies
|
||||
Given the shared output_format module exists
|
||||
When I inspect shared output_format imports
|
||||
Then it should not import from "cleveragents.cli"
|
||||
|
||||
Scenario: format_data function handles JSON format
|
||||
Given the shared output_format module is imported
|
||||
When I call format_data with a dict and format "json"
|
||||
Then the result should be valid JSON
|
||||
And the result should contain the expected keys
|
||||
|
||||
Scenario: format_data function handles YAML format
|
||||
Given the shared output_format module is imported
|
||||
When I call format_data with a dict and format "yaml"
|
||||
Then the result should be valid YAML
|
||||
|
||||
Scenario: format_data function handles plain format
|
||||
Given the shared output_format module is imported
|
||||
When I call format_data with a dict and format "plain"
|
||||
Then the result should contain key-value pairs
|
||||
|
||||
Scenario: format_data function handles table format
|
||||
Given the shared output_format module is imported
|
||||
When I call format_data with a list of dicts and format "table"
|
||||
Then the result should contain column headers
|
||||
|
||||
Scenario: format_data function falls back to JSON for unknown format
|
||||
Given the shared output_format module is imported
|
||||
When I call format_data with a dict and format "unknown"
|
||||
Then the result should be valid JSON
|
||||
|
||||
Scenario: format_data handles empty list for table format
|
||||
Given the shared output_format module is imported
|
||||
When I call format_data with an empty list and format "table"
|
||||
Then the result should be "(empty)"
|
||||
|
||||
Scenario: format_data handles nested dict values
|
||||
Given the shared output_format module is imported
|
||||
When I call format_data with a nested dict and format "plain"
|
||||
Then the result should contain nested key-value pairs
|
||||
@@ -0,0 +1,283 @@
|
||||
"""Step definitions for A2A architectural boundary enforcement tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from behave import given, then, when
|
||||
|
||||
|
||||
@given("the application services directory exists")
|
||||
def step_application_services_dir_exists(context: Any) -> None:
|
||||
"""Verify the application services directory exists."""
|
||||
context.app_services_dir = Path("src/cleveragents/application/services")
|
||||
assert context.app_services_dir.exists(), (
|
||||
f"Application services directory not found: {context.app_services_dir}"
|
||||
)
|
||||
|
||||
|
||||
@when("I scan all application service modules for CLI imports")
|
||||
def step_scan_application_services_for_cli_imports(context: Any) -> None:
|
||||
"""Scan all application service modules for CLI imports."""
|
||||
context.cli_violations: list[str] = []
|
||||
for py_file in context.app_services_dir.rglob("*.py"):
|
||||
try:
|
||||
tree = ast.parse(py_file.read_text())
|
||||
except SyntaxError:
|
||||
continue
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
if node.module.startswith("cleveragents.cli"):
|
||||
context.cli_violations.append(
|
||||
f"{py_file}:{node.lineno}: "
|
||||
f"from {node.module} import ..."
|
||||
)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
if alias.name.startswith("cleveragents.cli"):
|
||||
context.cli_violations.append(
|
||||
f"{py_file}:{node.lineno}: "
|
||||
f"import {alias.name}"
|
||||
)
|
||||
|
||||
|
||||
@then('no application service module should import from "cleveragents.cli"')
|
||||
def step_no_cli_imports_in_application(context: Any) -> None:
|
||||
"""Assert no application service imports from CLI layer."""
|
||||
violations = getattr(context, "cli_violations", [])
|
||||
assert not violations, (
|
||||
"Application services must not import from CLI (Presentation) layer.\n"
|
||||
"Violations found:\n" + "\n".join(violations)
|
||||
)
|
||||
|
||||
|
||||
@given("the plan_apply_service module exists")
|
||||
def step_plan_apply_service_exists(context: Any) -> None:
|
||||
"""Verify plan_apply_service.py exists."""
|
||||
context.plan_apply_service_path = Path(
|
||||
"src/cleveragents/application/services/plan_apply_service.py"
|
||||
)
|
||||
assert context.plan_apply_service_path.exists(), (
|
||||
"plan_apply_service.py not found"
|
||||
)
|
||||
|
||||
|
||||
@when("I inspect plan_apply_service imports")
|
||||
def step_inspect_plan_apply_service_imports(context: Any) -> None:
|
||||
"""Parse imports from plan_apply_service.py."""
|
||||
content = context.plan_apply_service_path.read_text()
|
||||
context.plan_apply_service_content = content
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
except SyntaxError as exc:
|
||||
raise AssertionError(
|
||||
f"Syntax error in plan_apply_service.py: {exc}"
|
||||
) from exc
|
||||
context.plan_apply_imports: list[str] = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
context.plan_apply_imports.append(node.module)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
context.plan_apply_imports.append(alias.name)
|
||||
|
||||
|
||||
@then('it should not import from "cleveragents.cli.formatting"')
|
||||
def step_plan_apply_service_no_cli_formatting(context: Any) -> None:
|
||||
"""Assert plan_apply_service does not import from cli.formatting."""
|
||||
imports = getattr(context, "plan_apply_imports", [])
|
||||
cli_formatting_imports = [
|
||||
imp for imp in imports if imp == "cleveragents.cli.formatting"
|
||||
]
|
||||
assert not cli_formatting_imports, (
|
||||
"plan_apply_service.py must not import from cleveragents.cli.formatting. "
|
||||
"Use cleveragents.shared.output_format instead."
|
||||
)
|
||||
|
||||
|
||||
@then('it should import from "cleveragents.shared.output_format"')
|
||||
def step_plan_apply_service_uses_shared_output(context: Any) -> None:
|
||||
"""Assert plan_apply_service imports from shared.output_format."""
|
||||
content = getattr(context, "plan_apply_service_content", "")
|
||||
assert "cleveragents.shared.output_format" in content, (
|
||||
"plan_apply_service.py should import from "
|
||||
"cleveragents.shared.output_format for output formatting."
|
||||
)
|
||||
|
||||
|
||||
@given("the shared output_format module exists")
|
||||
def step_shared_output_format_exists(context: Any) -> None:
|
||||
"""Verify shared/output_format.py exists."""
|
||||
context.output_format_path = Path(
|
||||
"src/cleveragents/shared/output_format.py"
|
||||
)
|
||||
assert context.output_format_path.exists(), (
|
||||
"shared/output_format.py not found"
|
||||
)
|
||||
|
||||
|
||||
@when("I inspect shared output_format imports")
|
||||
def step_inspect_shared_output_format_imports(context: Any) -> None:
|
||||
"""Parse imports from shared/output_format.py."""
|
||||
content = context.output_format_path.read_text()
|
||||
try:
|
||||
tree = ast.parse(content)
|
||||
except SyntaxError as exc:
|
||||
raise AssertionError(
|
||||
f"Syntax error in shared/output_format.py: {exc}"
|
||||
) from exc
|
||||
context.output_format_imports: list[str] = []
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom) and node.module:
|
||||
context.output_format_imports.append(node.module)
|
||||
elif isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
context.output_format_imports.append(alias.name)
|
||||
|
||||
|
||||
@then('it should not import from "cleveragents.cli"')
|
||||
def step_shared_output_format_no_cli_imports(context: Any) -> None:
|
||||
"""Assert shared/output_format.py has no CLI imports."""
|
||||
imports = getattr(context, "output_format_imports", [])
|
||||
cli_imports = [imp for imp in imports if imp.startswith("cleveragents.cli")]
|
||||
assert not cli_imports, (
|
||||
"shared/output_format.py must not import from cleveragents.cli. "
|
||||
f"Found: {cli_imports}"
|
||||
)
|
||||
|
||||
|
||||
@given("the shared output_format module is imported")
|
||||
def step_import_shared_output_format(context: Any) -> None:
|
||||
"""Import the shared output_format module."""
|
||||
from cleveragents.shared.output_format import format_data
|
||||
|
||||
context.format_data = format_data
|
||||
|
||||
|
||||
@when('I call format_data with a dict and format "json"')
|
||||
def step_call_format_data_json(context: Any) -> None:
|
||||
"""Call format_data with JSON format."""
|
||||
context.sample_data: dict[str, Any] = {
|
||||
"plan_id": "01HXYZ",
|
||||
"status": "applied",
|
||||
"count": 42,
|
||||
}
|
||||
context.format_result = context.format_data(context.sample_data, "json")
|
||||
|
||||
|
||||
@when('I call format_data with a dict and format "yaml"')
|
||||
def step_call_format_data_yaml(context: Any) -> None:
|
||||
"""Call format_data with YAML format."""
|
||||
context.sample_data = {"plan_id": "01HXYZ", "status": "applied", "count": 42}
|
||||
context.format_result = context.format_data(context.sample_data, "yaml")
|
||||
|
||||
|
||||
@when('I call format_data with a dict and format "plain"')
|
||||
def step_call_format_data_plain(context: Any) -> None:
|
||||
"""Call format_data with plain format."""
|
||||
context.sample_data = {"plan_id": "01HXYZ", "status": "applied", "count": 42}
|
||||
context.format_result = context.format_data(context.sample_data, "plain")
|
||||
|
||||
|
||||
@when('I call format_data with a list of dicts and format "table"')
|
||||
def step_call_format_data_table(context: Any) -> None:
|
||||
"""Call format_data with table format."""
|
||||
context.sample_data = [
|
||||
{"plan_id": "01HXYZ", "status": "applied"},
|
||||
{"plan_id": "01HABC", "status": "pending"},
|
||||
]
|
||||
context.format_result = context.format_data(context.sample_data, "table")
|
||||
|
||||
|
||||
@when('I call format_data with a dict and format "unknown"')
|
||||
def step_call_format_data_unknown(context: Any) -> None:
|
||||
"""Call format_data with unknown format (should fall back to JSON)."""
|
||||
context.sample_data = {"plan_id": "01HXYZ", "status": "applied"}
|
||||
context.format_result = context.format_data(context.sample_data, "unknown")
|
||||
|
||||
|
||||
@when('I call format_data with an empty list and format "table"')
|
||||
def step_call_format_data_empty_table(context: Any) -> None:
|
||||
"""Call format_data with empty list and table format."""
|
||||
context.format_result = context.format_data([], "table")
|
||||
|
||||
|
||||
@when('I call format_data with a nested dict and format "plain"')
|
||||
def step_call_format_data_nested_plain(context: Any) -> None:
|
||||
"""Call format_data with nested dict and plain format."""
|
||||
context.sample_data = {
|
||||
"plan_id": "01HXYZ",
|
||||
"metadata": {"author": "test", "version": "1.0"},
|
||||
}
|
||||
context.format_result = context.format_data(context.sample_data, "plain")
|
||||
|
||||
|
||||
@then("the result should be valid JSON")
|
||||
def step_result_is_valid_json(context: Any) -> None:
|
||||
"""Assert the result is valid JSON."""
|
||||
result = context.format_result
|
||||
assert isinstance(result, str), f"Expected str, got {type(result)}"
|
||||
try:
|
||||
json.loads(result)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise AssertionError(
|
||||
f"Result is not valid JSON: {exc}\nResult: {result!r}"
|
||||
) from exc
|
||||
|
||||
|
||||
@then("the result should contain the expected keys")
|
||||
def step_result_contains_expected_keys(context: Any) -> None:
|
||||
"""Assert the JSON result contains expected keys."""
|
||||
result = json.loads(context.format_result)
|
||||
assert "plan_id" in result, f"Missing 'plan_id' in result: {result}"
|
||||
assert "status" in result, f"Missing 'status' in result: {result}"
|
||||
|
||||
|
||||
@then("the result should be valid YAML")
|
||||
def step_result_is_valid_yaml(context: Any) -> None:
|
||||
"""Assert the result is valid YAML."""
|
||||
result = context.format_result
|
||||
assert isinstance(result, str), f"Expected str, got {type(result)}"
|
||||
try:
|
||||
parsed = yaml.safe_load(result)
|
||||
assert parsed is not None, "YAML parsed to None"
|
||||
except yaml.YAMLError as exc:
|
||||
raise AssertionError(
|
||||
f"Result is not valid YAML: {exc}\nResult: {result!r}"
|
||||
) from exc
|
||||
|
||||
|
||||
@then("the result should contain key-value pairs")
|
||||
def step_result_contains_key_value_pairs(context: Any) -> None:
|
||||
"""Assert the plain result contains key-value pairs."""
|
||||
result = context.format_result
|
||||
assert "plan_id" in result, f"Missing 'plan_id' in plain output: {result!r}"
|
||||
assert "status" in result, f"Missing 'status' in plain output: {result!r}"
|
||||
|
||||
|
||||
@then("the result should contain column headers")
|
||||
def step_result_contains_column_headers(context: Any) -> None:
|
||||
"""Assert the table result contains column headers."""
|
||||
result = context.format_result
|
||||
assert "plan_id" in result, f"Missing 'plan_id' column in table: {result!r}"
|
||||
assert "status" in result, f"Missing 'status' column in table: {result!r}"
|
||||
|
||||
|
||||
@then('the result should be "(empty)"')
|
||||
def step_result_is_empty(context: Any) -> None:
|
||||
"""Assert the result is the empty placeholder."""
|
||||
assert context.format_result == "(empty)", (
|
||||
f"Expected '(empty)', got: {context.format_result!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the result should contain nested key-value pairs")
|
||||
def step_result_contains_nested_pairs(context: Any) -> None:
|
||||
"""Assert the plain result contains nested key-value pairs."""
|
||||
result = context.format_result
|
||||
assert "plan_id" in result, f"Missing 'plan_id' in plain output: {result!r}"
|
||||
assert "metadata" in result, f"Missing 'metadata' in plain output: {result!r}"
|
||||
@@ -84,6 +84,8 @@ dev = [
|
||||
"vulture>=2.10",
|
||||
# Complexity metrics
|
||||
"radon>=6.0.1",
|
||||
# Import boundary enforcement
|
||||
"import-linter>=2.0",
|
||||
]
|
||||
tests = [
|
||||
"behave==1.3.3",
|
||||
|
||||
@@ -646,7 +646,7 @@ class PlanApplyService:
|
||||
Returns:
|
||||
Rendered artifacts string.
|
||||
"""
|
||||
from cleveragents.cli.formatting import format_output
|
||||
from cleveragents.shared.output_format import format_data as format_output
|
||||
|
||||
plan = self._lifecycle.get_plan(plan_id)
|
||||
changeset = self._resolve_changeset(plan)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Shared output formatting utilities for cross-layer use.
|
||||
|
||||
Provides lightweight serialisation helpers (JSON, YAML, plain text, ASCII
|
||||
table) that can be used by any layer without importing from the CLI
|
||||
presentation layer.
|
||||
|
||||
This module intentionally has **no** dependencies on ``cleveragents.cli.*``
|
||||
so that Application and Domain layer code can format data for output without
|
||||
creating a reverse dependency on the Presentation layer.
|
||||
|
||||
For full-featured CLI output (Rich console, colour, envelope wrapping, etc.)
|
||||
use ``cleveragents.cli.formatting`` instead.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from io import StringIO
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
|
||||
def _serialize_value(value: Any) -> Any:
|
||||
"""Recursively normalise values for JSON/YAML serialisation.
|
||||
|
||||
* ``datetime`` -> ISO-8601 string
|
||||
* ``Enum`` -> ``.value``
|
||||
* ``dict`` -> recursed
|
||||
* ``list`` -> recursed
|
||||
* everything else -> unchanged
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
if isinstance(value, Enum):
|
||||
return value.value
|
||||
if isinstance(value, dict):
|
||||
return {k: _serialize_value(v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_serialize_value(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def format_as_json(data: dict[str, Any] | list[dict[str, Any]]) -> str:
|
||||
"""Render *data* as indented JSON."""
|
||||
return json.dumps(_serialize_value(data), indent=2, default=str)
|
||||
|
||||
|
||||
def format_as_yaml(data: dict[str, Any] | list[dict[str, Any]]) -> str:
|
||||
"""Render *data* as YAML."""
|
||||
return yaml.dump(
|
||||
_serialize_value(data),
|
||||
default_flow_style=False,
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
).rstrip("\n")
|
||||
|
||||
|
||||
def format_as_plain(data: dict[str, Any] | list[dict[str, Any]]) -> str:
|
||||
"""Render *data* as plain ``key: value`` lines."""
|
||||
if isinstance(data, list):
|
||||
parts: list[str] = []
|
||||
for idx, item in enumerate(data):
|
||||
if idx > 0:
|
||||
parts.append("---")
|
||||
parts.append(_format_plain_dict(item))
|
||||
return "\n".join(parts)
|
||||
return _format_plain_dict(data)
|
||||
|
||||
|
||||
def _format_plain_dict(data: dict[str, Any]) -> str:
|
||||
lines: list[str] = []
|
||||
for key, value in data.items():
|
||||
serialised = _serialize_value(value)
|
||||
if isinstance(serialised, dict):
|
||||
lines.append(f"{key}:")
|
||||
for k2, v2 in serialised.items():
|
||||
lines.append(f" {k2}: {v2}")
|
||||
elif isinstance(serialised, list):
|
||||
lines.append(f"{key}:")
|
||||
for item in serialised:
|
||||
if isinstance(item, dict):
|
||||
lines.append(f" - {json.dumps(item, default=str)}")
|
||||
else:
|
||||
lines.append(f" - {item}")
|
||||
else:
|
||||
lines.append(f"{key}: {serialised}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_as_table(data: dict[str, Any] | list[dict[str, Any]]) -> str:
|
||||
"""Render *data* as an ASCII table (no Rich styling)."""
|
||||
rows: list[dict[str, Any]] = [data] if isinstance(data, dict) else data
|
||||
|
||||
if not rows:
|
||||
return "(empty)"
|
||||
|
||||
columns: list[str] = list(rows[0].keys())
|
||||
for row in rows[1:]:
|
||||
for key in row:
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
|
||||
table = Table(show_header=True, show_edge=True)
|
||||
for col in columns:
|
||||
table.add_column(col)
|
||||
for row in rows:
|
||||
cells: list[str] = []
|
||||
for col in columns:
|
||||
val = row.get(col, "")
|
||||
serialised = _serialize_value(val)
|
||||
if isinstance(serialised, (dict, list)):
|
||||
cells.append(json.dumps(serialised, default=str))
|
||||
else:
|
||||
cells.append(str(serialised))
|
||||
table.add_row(*cells)
|
||||
|
||||
buf = StringIO()
|
||||
console = Console(file=buf, width=200, no_color=True)
|
||||
console.print(table)
|
||||
return buf.getvalue().rstrip("\n")
|
||||
|
||||
|
||||
def format_data(
|
||||
data: dict[str, Any] | list[dict[str, Any]],
|
||||
format_type: str,
|
||||
) -> str:
|
||||
"""Format *data* according to *format_type*.
|
||||
|
||||
Supported format types: ``json``, ``yaml``, ``plain``, ``table``.
|
||||
Any other value falls back to JSON.
|
||||
|
||||
This function is intentionally minimal -- it does **not** wrap output in
|
||||
the CLI envelope (``command``, ``status``, ``exit_code``, etc.) and does
|
||||
**not** write to ``sys.stdout`` directly. It simply returns the rendered
|
||||
string so the caller can decide what to do with it.
|
||||
|
||||
For full CLI output with envelope and Rich console support use
|
||||
``cleveragents.cli.formatting.format_output`` instead.
|
||||
"""
|
||||
fmt = format_type.lower()
|
||||
if fmt == "json":
|
||||
return format_as_json(data)
|
||||
if fmt == "yaml":
|
||||
return format_as_yaml(data)
|
||||
if fmt == "plain":
|
||||
return format_as_plain(data)
|
||||
if fmt == "table":
|
||||
return format_as_table(data)
|
||||
# Fallback: JSON
|
||||
return format_as_json(data)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"format_as_json",
|
||||
"format_as_plain",
|
||||
"format_as_table",
|
||||
"format_as_yaml",
|
||||
"format_data",
|
||||
]
|
||||
Reference in New Issue
Block a user