Coverage for src/dataknobs_fsm/core/result_formatter.py: 46%

1"""Utility for formatting execution results in a standardized way.

2

3 This module provides consistent result formatting across Simple and Advanced APIs,

4 eliminating code duplication and ensuring uniform output structure.

5"""

6

7 from typing import Any, Dict, List

8 from ..execution.context import ExecutionContext

9

10

11 class ResultFormatter:

12 """Formatter for execution results across different processing modes."""

13

14 @staticmethod

15 def format_single_result(

16 context: ExecutionContext,

17 success: bool,

18 result: Any = None,

19 error: Exception | None = None

20 ) -> Dict[str, Any]:

21 """Format a single execution result.

22

23 Args:

24 context: The execution context

25 success: Whether execution was successful

26 result: Execution result (if any)

27 error: Exception if execution failed

28

29 Returns:

30 Formatted result dictionary

31 """

32 return {

33 'final_state': context.current_state,

34 'data': context.data,

35 'path': ResultFormatter._get_complete_path(context),

36 'success': success,

37 'error': str(error) if error else (str(result) if not success and result else None),

38 'metadata': context.metadata.copy() if context.metadata else {}

39 }

40

41 @staticmethod

42 def format_batch_result(

43 context: ExecutionContext,

44 batch_results: List[Dict[str, Any]]

45 ) -> List[Dict[str, Any]]:

46 """Format batch execution results.

47

48 Args:

49 context: The execution context

50 batch_results: Raw batch results

51

52 Returns:

53 List of formatted result dictionaries

54 """

55 formatted_results = []

56

57 for i, raw_result in enumerate(batch_results):

58 # Check if this item had an error

59 error = None

60 for err_idx, err_exc in context.batch_errors:

61 if err_idx == i:

62 error = err_exc

63 break

64

65 formatted_result = {

66 'index': i,

67 'final_state': raw_result.get('final_state', context.current_state),

68 'data': raw_result.get('data', {}),

69 'path': raw_result.get('path', []),

70 'success': error is None,

71 'error': str(error) if error else None

72 }

73 formatted_results.append(formatted_result)

74

75 return formatted_results

76

77 @staticmethod

78 def format_stream_result(

79 context: ExecutionContext,

80 chunk_result: Any,

81 chunk_index: int

82 ) -> Dict[str, Any]:

83 """Format a stream chunk result.

84

85 Args:

86 context: The execution context

87 chunk_result: Result from processing a chunk

88 chunk_index: Index of the processed chunk

89

90 Returns:

91 Formatted chunk result dictionary

92 """

93 return {

94 'chunk_index': chunk_index,

95 'chunks_processed': context.processed_chunks,

96 'current_state': context.current_state,

97 'data': chunk_result,

98 'metadata': context.metadata.copy() if context.metadata else {}

99 }

100

101 @staticmethod

102 def format_async_result(

103 context: ExecutionContext,

104 success: bool,

105 result: Any = None,

106 error: Exception | None = None

107 ) -> Dict[str, Any]:

108 """Format an async execution result.

109

110 This is identical to format_single_result but provided for clarity.

111

112 Args:

113 context: The execution context

114 success: Whether execution was successful

115 result: Execution result (if any)

116 error: Exception if execution failed

117

118 Returns:

119 Formatted result dictionary

120 """

121 return ResultFormatter.format_single_result(context, success, result, error)

122

123 @staticmethod

124 def format_step_result(

125 context: ExecutionContext,

126 new_state: str | None = None,

127 transition_taken: bool = False

128 ) -> Dict[str, Any]:

129 """Format a step-by-step execution result.

130

131 Args:

132 context: The execution context

133 new_state: The new state after the step (if any)

134 transition_taken: Whether a transition was taken

135

136 Returns:

137 Formatted step result dictionary

138 """

139 return {

140 'previous_state': context.previous_state,

141 'current_state': context.current_state,

142 'new_state': new_state,

143 'transition_taken': transition_taken,

144 'path': ResultFormatter._get_complete_path(context),

145 'data': context.data,

146 'metadata': context.metadata.copy() if context.metadata else {}

147 }

148

149 @staticmethod

150 def format_error_result(

151 context: ExecutionContext,

152 error: Exception,

153 error_state: str | None = None

154 ) -> Dict[str, Any]:

155 """Format an error result with context.

156

157 Args:

158 context: The execution context

159 error: The exception that occurred

160 error_state: State where error occurred (if known)

161

162 Returns:

163 Formatted error result dictionary

164 """

165 return {

166 'success': False,

167 'error': str(error),

168 'error_type': type(error).__name__,

169 'error_state': error_state or context.current_state,

170 'final_state': context.current_state,

171 'path': ResultFormatter._get_complete_path(context),

172 'data': context.data,

173 'metadata': context.metadata.copy() if context.metadata else {}

174 }

175

176 @staticmethod

177 def _get_complete_path(context: ExecutionContext) -> List[str]:

178 """Get the complete state traversal path.

179

180 Args:

181 context: The execution context

182

183 Returns:

184 List of state names in traversal order

185 """

186 # Build complete path from history plus current state

187 path = context.state_history.copy() if context.state_history else []

188

189 # Add current state if not already in path and if it exists

190 if context.current_state and (not path or path[-1] != context.current_state):

191 path.append(context.current_state)

192

193 return path

194

195 @staticmethod

196 def format_performance_result(

197 context: ExecutionContext,

198 result: Dict[str, Any]

199 ) -> Dict[str, Any]:

200 """Format result with performance metrics.

201

202 Args:

203 context: The execution context

204 result: Base result dictionary

205

206 Returns:

207 Result with added performance metrics

208 """

209 # Add performance stats to existing result

210 result['performance'] = context.get_performance_stats()

211 result['resource_usage'] = context.get_resource_usage()

212

213 return result