Skip to content

Commit 3e986b0

Browse files
committed
feat(application): Add option to summarize run describe
1 parent 3e5b9a8 commit 3e986b0

3 files changed

Lines changed: 318 additions & 10 deletions

File tree

src/aignostics/application/_cli.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,13 @@ def run_describe(
919919
str,
920920
typer.Option(help="Output format: 'text' (default) or 'json'"),
921921
] = "text",
922+
summarize: Annotated[
923+
bool,
924+
typer.Option(
925+
"--summarize",
926+
help="Show only run and item status summary (external ID, state, error message)",
927+
),
928+
] = False,
922929
) -> None:
923930
"""Describe run."""
924931
logger.trace("Describing run with ID '{}'", run_id)
@@ -931,7 +938,9 @@ def run_describe(
931938
run_details = run.details(hide_platform_queue_position=not user_info.is_internal_user)
932939
print(json.dumps(run_details.model_dump(mode="json"), indent=2, default=str))
933940
else:
934-
retrieve_and_print_run_details(run, hide_platform_queue_position=not user_info.is_internal_user)
941+
retrieve_and_print_run_details(
942+
run, hide_platform_queue_position=not user_info.is_internal_user, summarize=summarize
943+
)
935944
logger.debug("Described run with ID '{}'", run_id)
936945
except NotFoundException:
937946
logger.warning(f"Run with ID '{run_id}' not found.")

src/aignostics/application/_utils.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
)
2727
from aignostics.platform import (
2828
InputArtifactData,
29+
ItemState,
2930
OutputArtifactData,
3031
OutputArtifactElement,
3132
Run,
@@ -174,17 +175,17 @@ class OutputFormat(StrEnum):
174175
JSON = "json"
175176

176177

177-
def _format_status_string(state: RunState, termination_reason: str | None = None) -> str:
178+
def _format_status_string(state: RunState | ItemState, termination_reason: str | None = None) -> str:
178179
"""Format status string with optional termination reason.
179180
180181
Args:
181-
state (RunState): The run state
182+
state (RunState | ItemState): The run or item state
182183
termination_reason (str | None): Optional termination reason
183184
184185
Returns:
185186
str: Formatted status string
186187
"""
187-
if state is RunState.TERMINATED and termination_reason:
188+
if state.value == "TERMINATED" and termination_reason:
188189
return f"{state.value} ({termination_reason})"
189190
return f"{state.value}"
190191

@@ -277,21 +278,26 @@ def _format_run_details(run: RunData) -> str:
277278
return output
278279

279280

280-
def retrieve_and_print_run_details(run_handle: Run, hide_platform_queue_position: bool) -> None:
281+
def retrieve_and_print_run_details(
282+
run_handle: Run, hide_platform_queue_position: bool, *, summarize: bool = False
283+
) -> None:
281284
"""Retrieve and print detailed information about a run.
282285
283286
Args:
284287
run_handle (Run): The Run handle
285288
hide_platform_queue_position (bool): Whether to hide platform-wide queue position
289+
summarize (bool): If True, show only status summary (external ID, state, error message)
286290
287291
"""
288292
run = run_handle.details(hide_platform_queue_position=hide_platform_queue_position)
289293

290-
run_details = _format_run_details(run)
291-
output = f"[bold]Run Details for {run.run_id}[/bold]\n{'=' * 80}\n{run_details}\n\n[bold]Items:[/bold]"
292-
293-
console.print(output)
294-
_retrieve_and_print_run_items(run_handle)
294+
if summarize:
295+
_print_run_summary(run, run_handle)
296+
else:
297+
run_details = _format_run_details(run)
298+
output = f"[bold]Run Details for {run.run_id}[/bold]\n{'=' * 80}\n{run_details}\n\n[bold]Items:[/bold]"
299+
console.print(output)
300+
_retrieve_and_print_run_items(run_handle)
295301

296302

297303
def _retrieve_and_print_run_items(run_handle: Run) -> None:
@@ -328,6 +334,54 @@ def _retrieve_and_print_run_items(run_handle: Run) -> None:
328334
console.print(f"{item_output}\n")
329335

330336

337+
def _print_run_summary(run: RunData, run_handle: Run) -> None:
338+
"""Print a concise summary of run and item statuses.
339+
340+
Shows only the essential status information: external ID, state, and error message
341+
for each item, plus overall run statistics.
342+
343+
Args:
344+
run (RunData): Run data object
345+
run_handle (Run): The Run handle for fetching item results
346+
"""
347+
status_str = _format_status_string(run.state, run.termination_reason)
348+
duration_str = _format_duration_string(run.submitted_at, run.terminated_at)
349+
350+
# Run summary header
351+
output = (
352+
f"[bold]Run Summary for {run.run_id}[/bold]\n"
353+
f"{'=' * 80}\n"
354+
f"[bold]Application (Version):[/bold] {run.application_id} ({run.version_number})\n"
355+
f"[bold]Status:[/bold] {status_str}\n"
356+
f"[bold]Duration:[/bold] {duration_str}\n"
357+
)
358+
359+
if run.error_message or run.error_code:
360+
output += f"[bold]Error:[/bold] {run.error_message or 'N/A'} ({run.error_code or 'N/A'})\n"
361+
362+
output += f"[bold]Statistics:[/bold]\n{_format_run_statistics(run.statistics)}\n"
363+
console.print(output)
364+
365+
# Items summary
366+
console.print("[bold]Items:[/bold]")
367+
results = run_handle.results()
368+
if not results:
369+
console.print(" No item results available.")
370+
return
371+
372+
for item in results:
373+
item_status = _format_status_string(item.state, item.termination_reason)
374+
item_line = f" [bold]{item.external_id}[/bold]: {item_status}"
375+
376+
if item.error_message or item.error_code:
377+
error_info = item.error_message or item.error_code or ""
378+
if item.error_message and item.error_code:
379+
error_info = f"{item.error_message} ({item.error_code})"
380+
item_line += f" - [red]{error_info}[/red]"
381+
382+
console.print(item_line)
383+
384+
331385
def print_runs_verbose(runs: list[RunData]) -> None:
332386
"""Print detailed information about runs, sorted by submitted_at in descending order.
333387

tests/aignostics/application/utils_test.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,3 +787,248 @@ def test_queue_position_string_from_run_with_only_platform_position() -> None:
787787
num_preceding_items_platform=15,
788788
)
789789
assert queue_position_string_from_run(run) == "15 items ahead across the entire platform"
790+
791+
792+
# Tests for retrieve_and_print_run_details with summarize option
793+
794+
795+
@pytest.mark.unit
796+
@patch("aignostics.application._utils.console")
797+
def test_retrieve_and_print_run_details_summarize_mode(mock_console: Mock) -> None:
798+
"""Test summarize mode shows concise output with external ID, state, and errors."""
799+
submitted_at = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
800+
terminated_at = datetime(2025, 1, 1, 13, 0, 0, tzinfo=UTC)
801+
802+
run_data = RunData(
803+
run_id="run-summarize-test",
804+
application_id="he-tme",
805+
version_number="1.0.0",
806+
state=RunState.TERMINATED,
807+
termination_reason=RunTerminationReason.ALL_ITEMS_PROCESSED,
808+
output=RunOutput.FULL,
809+
statistics=RunItemStatistics(
810+
item_count=2,
811+
item_pending_count=0,
812+
item_processing_count=0,
813+
item_skipped_count=0,
814+
item_succeeded_count=1,
815+
item_user_error_count=1,
816+
item_system_error_count=0,
817+
),
818+
submitted_at=submitted_at,
819+
submitted_by="user@example.com",
820+
terminated_at=terminated_at,
821+
custom_metadata=None,
822+
error_message=None,
823+
error_code=None,
824+
)
825+
826+
from aignx.codegen.models import ItemOutput
827+
828+
item_success = ItemResult(
829+
item_id="item-001",
830+
external_id="slide-success.svs",
831+
state=ItemState.TERMINATED,
832+
termination_reason=ItemTerminationReason.SUCCEEDED,
833+
output=ItemOutput.FULL,
834+
error_message=None,
835+
error_code=None,
836+
custom_metadata=None,
837+
custom_metadata_checksum=None,
838+
terminated_at=terminated_at,
839+
output_artifacts=[],
840+
)
841+
842+
item_error = ItemResult(
843+
item_id="item-002",
844+
external_id="slide-error.svs",
845+
state=ItemState.TERMINATED,
846+
termination_reason=ItemTerminationReason.USER_ERROR,
847+
output=ItemOutput.NONE,
848+
error_message="Invalid file format",
849+
error_code="INVALID_FORMAT",
850+
custom_metadata=None,
851+
custom_metadata_checksum=None,
852+
terminated_at=terminated_at,
853+
output_artifacts=[],
854+
)
855+
856+
mock_run = MagicMock()
857+
mock_run.details.return_value = run_data
858+
mock_run.results.return_value = [item_success, item_error]
859+
860+
retrieve_and_print_run_details(mock_run, hide_platform_queue_position=False, summarize=True)
861+
862+
# Collect all printed output
863+
all_output = " ".join(str(call) for call in mock_console.print.call_args_list)
864+
865+
# Verify summary header is present
866+
assert "Run Summary for run-summarize-test" in all_output
867+
# Verify application info is present
868+
assert "he-tme" in all_output
869+
# Verify items are listed with external IDs
870+
assert "slide-success.svs" in all_output
871+
assert "slide-error.svs" in all_output
872+
# Verify error message is shown for failed item
873+
assert "Invalid file format" in all_output
874+
# Verify artifact details are NOT shown (they are omitted in summary)
875+
assert "Download URL" not in all_output
876+
assert "Artifact ID" not in all_output
877+
878+
879+
@pytest.mark.unit
880+
@patch("aignostics.application._utils.console")
881+
def test_retrieve_and_print_run_details_summarize_no_items(mock_console: Mock) -> None:
882+
"""Test summarize mode with no items shows appropriate message."""
883+
submitted_at = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
884+
885+
run_data = RunData(
886+
run_id="run-no-items",
887+
application_id="test-app",
888+
version_number="0.0.1",
889+
state=RunState.PENDING,
890+
termination_reason=None,
891+
output=RunOutput.NONE,
892+
statistics=RunItemStatistics(
893+
item_count=0,
894+
item_pending_count=0,
895+
item_processing_count=0,
896+
item_skipped_count=0,
897+
item_succeeded_count=0,
898+
item_user_error_count=0,
899+
item_system_error_count=0,
900+
),
901+
submitted_at=submitted_at,
902+
submitted_by="user@example.com",
903+
terminated_at=None,
904+
custom_metadata=None,
905+
error_message=None,
906+
error_code=None,
907+
)
908+
909+
mock_run = MagicMock()
910+
mock_run.details.return_value = run_data
911+
mock_run.results.return_value = []
912+
913+
retrieve_and_print_run_details(mock_run, hide_platform_queue_position=False, summarize=True)
914+
915+
all_output = " ".join(str(call) for call in mock_console.print.call_args_list)
916+
assert "Run Summary for run-no-items" in all_output
917+
assert "No item results available" in all_output
918+
919+
920+
@pytest.mark.unit
921+
@patch("aignostics.application._utils.console")
922+
def test_retrieve_and_print_run_details_summarize_with_run_error(mock_console: Mock) -> None:
923+
"""Test summarize mode shows run-level errors."""
924+
submitted_at = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
925+
terminated_at = datetime(2025, 1, 1, 12, 5, 0, tzinfo=UTC)
926+
927+
run_data = RunData(
928+
run_id="run-with-error",
929+
application_id="test-app",
930+
version_number="0.0.1",
931+
state=RunState.TERMINATED,
932+
termination_reason=RunTerminationReason.CANCELED_BY_SYSTEM,
933+
output=RunOutput.NONE,
934+
statistics=RunItemStatistics(
935+
item_count=1,
936+
item_pending_count=0,
937+
item_processing_count=0,
938+
item_skipped_count=0,
939+
item_succeeded_count=0,
940+
item_user_error_count=0,
941+
item_system_error_count=1,
942+
),
943+
submitted_at=submitted_at,
944+
submitted_by="user@example.com",
945+
terminated_at=terminated_at,
946+
custom_metadata=None,
947+
error_message="System error occurred",
948+
error_code="SYS_ERROR",
949+
)
950+
951+
mock_run = MagicMock()
952+
mock_run.details.return_value = run_data
953+
mock_run.results.return_value = []
954+
955+
retrieve_and_print_run_details(mock_run, hide_platform_queue_position=False, summarize=True)
956+
957+
all_output = " ".join(str(call) for call in mock_console.print.call_args_list)
958+
assert "System error occurred" in all_output
959+
assert "SYS_ERROR" in all_output
960+
961+
962+
@pytest.mark.unit
963+
@patch("aignostics.application._utils.console")
964+
def test_retrieve_and_print_run_details_default_is_detailed(mock_console: Mock) -> None:
965+
"""Test that default mode (summarize=False) shows detailed output with artifacts."""
966+
submitted_at = datetime(2025, 1, 1, 12, 0, 0, tzinfo=UTC)
967+
terminated_at = datetime(2025, 1, 1, 13, 0, 0, tzinfo=UTC)
968+
969+
run_data = RunData(
970+
run_id="run-detailed-test",
971+
application_id="he-tme",
972+
version_number="1.0.0",
973+
state=RunState.TERMINATED,
974+
termination_reason=RunTerminationReason.ALL_ITEMS_PROCESSED,
975+
output=RunOutput.FULL,
976+
statistics=RunItemStatistics(
977+
item_count=1,
978+
item_pending_count=0,
979+
item_processing_count=0,
980+
item_skipped_count=0,
981+
item_succeeded_count=1,
982+
item_user_error_count=0,
983+
item_system_error_count=0,
984+
),
985+
submitted_at=submitted_at,
986+
submitted_by="user@example.com",
987+
terminated_at=terminated_at,
988+
custom_metadata=None,
989+
error_message=None,
990+
error_code=None,
991+
)
992+
993+
from aignx.codegen.models import ArtifactOutput, ArtifactState, ArtifactTerminationReason, ItemOutput
994+
995+
item_result = ItemResult(
996+
item_id="item-123",
997+
external_id="slide-001.svs",
998+
state=ItemState.TERMINATED,
999+
termination_reason=ItemTerminationReason.SUCCEEDED,
1000+
output=ItemOutput.FULL,
1001+
error_message=None,
1002+
error_code=None,
1003+
custom_metadata=None,
1004+
custom_metadata_checksum=None,
1005+
terminated_at=terminated_at,
1006+
output_artifacts=[
1007+
OutputArtifactElement(
1008+
output_artifact_id="artifact-abc",
1009+
name="result.parquet",
1010+
download_url="https://example.com/result.parquet",
1011+
metadata={"media_type": "application/vnd.apache.parquet"},
1012+
state=ArtifactState.TERMINATED,
1013+
termination_reason=ArtifactTerminationReason.SUCCEEDED,
1014+
output=ArtifactOutput.AVAILABLE,
1015+
error_code=None,
1016+
error_message=None,
1017+
)
1018+
],
1019+
)
1020+
1021+
mock_run = MagicMock()
1022+
mock_run.details.return_value = run_data
1023+
mock_run.results.return_value = [item_result]
1024+
1025+
# Call without summarize parameter (default is False)
1026+
retrieve_and_print_run_details(mock_run, hide_platform_queue_position=False)
1027+
1028+
all_output = " ".join(str(call) for call in mock_console.print.call_args_list)
1029+
1030+
# Verify detailed output shows "Run Details" not "Run Summary"
1031+
assert "Run Details for run-detailed-test" in all_output
1032+
# Verify artifact details ARE shown in detailed mode
1033+
assert "Download URL" in all_output
1034+
assert "Artifact ID" in all_output

0 commit comments

Comments
 (0)