From e1507504fac22a3c1b95d823f76f24503774ddde Mon Sep 17 00:00:00 2001 From: Yue Chao Qin Date: Tue, 10 Feb 2026 17:52:09 -0500 Subject: [PATCH] feat: GetExecutionNodes API returns number of total and ended nodes --- cloud_pipelines_backend/api_server_sql.py | 10 +++++++--- tests/test_execution_nodes_api_service.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/cloud_pipelines_backend/api_server_sql.py b/cloud_pipelines_backend/api_server_sql.py index d5f4202..3fdd554 100644 --- a/cloud_pipelines_backend/api_server_sql.py +++ b/cloud_pipelines_backend/api_server_sql.py @@ -522,10 +522,10 @@ class ArtifactNodeIdResponse: id: bts.IdType -@dataclasses.dataclass +@dataclasses.dataclass(kw_only=True) class GetGraphExecutionStateResponse: child_execution_status_stats: dict[bts.IdType, dict[str, int]] - pass + summary: ExecutionStatusSummary @dataclasses.dataclass(kw_only=True) @@ -692,14 +692,18 @@ def get_graph_execution_state( ) + tuple(child_container_execution_stat_rows) child_execution_status_stats: dict[bts.IdType, dict[str, int]] = {} + summary = ExecutionStatusSummary() for row in child_execution_stat_rows: - child_execution_id, status, count = row.tuple() + child_execution_id, status, count = row._tuple() status_stats = child_execution_status_stats.setdefault( child_execution_id, {} ) status_stats[status.value] = count + summary.count_node_status(status=status, count=count) + return GetGraphExecutionStateResponse( child_execution_status_stats=child_execution_status_stats, + summary=summary, ) def get_container_execution_state( diff --git a/tests/test_execution_nodes_api_service.py b/tests/test_execution_nodes_api_service.py index d188717..806aee6 100644 --- a/tests/test_execution_nodes_api_service.py +++ b/tests/test_execution_nodes_api_service.py @@ -68,6 +68,9 @@ def test_no_children_returns_empty_stats(self): assert isinstance(result, GetGraphExecutionStateResponse) assert result.child_execution_status_stats == {} + assert result.summary.total_nodes == 0 + assert result.summary.ended_nodes == 0 + assert result.summary.has_ended is False def test_children_with_no_status_are_excluded(self): """Children whose container_execution_status is None are not counted.""" @@ -87,6 +90,9 @@ def test_children_with_no_status_are_excluded(self): result = self.service.get_graph_execution_state(session, parent.id) assert result.child_execution_status_stats == {} + assert result.summary.total_nodes == 0 + assert result.summary.ended_nodes == 0 + assert result.summary.has_ended is False def test_direct_container_children(self): """Children that are direct container nodes (no descendants via ancestor links).""" @@ -117,6 +123,9 @@ def test_direct_container_children(self): assert stats[child1.id] == {"SUCCEEDED": 1} assert child2.id in stats assert stats[child2.id] == {"RUNNING": 1} + assert result.summary.total_nodes == 2 + assert result.summary.ended_nodes == 1 + assert result.summary.has_ended is False def test_three_level_mixed_stats(self): """3-level deep graph with direct tasks and nested sub-graphs. @@ -302,6 +311,19 @@ def test_three_level_mixed_stats(self): # appear as a key in the stats assert sub_graph_a_b.id not in stats + # -- Summary: total_nodes and ended_nodes -- + # Direct children (Query 2): task_1(1), task_2(1), task_3(1) = 3 nodes + # Descendants of sub_graph_a (Query 1): 6 nodes + # CANCELLED(1), SKIPPED(1), RUNNING(1), INVALID(1), SYSTEM_ERROR(1), SUCCEEDED(1) + # Total = 3 + 6 = 9 + assert result.summary.total_nodes == 9 + # Ended statuses: SUCCEEDED(task_1) + FAILED(task_2) = 2 from Query 2 + # + CANCELLED(1) + SKIPPED(1) + INVALID(1) + SYSTEM_ERROR(1) + SUCCEEDED(1) = 5 from Query 1 + # QUEUED(task_3) and RUNNING(task_sg_a_3) are NOT ended + # Total ended = 2 + 5 = 7 + assert result.summary.ended_nodes == 7 + assert result.summary.has_ended is False + if __name__ == "__main__": pytest.main()