omnimancer-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Batch Operation Monitoring and Error Recovery for Omnimancer CLI.
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive feedback system for batch operations
|
|
5
|
+
including progress tracking, result reporting, and error recovery mechanisms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import csv
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.layout import Layout
|
|
20
|
+
from rich.live import Live
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.progress import (
|
|
23
|
+
BarColumn,
|
|
24
|
+
Progress,
|
|
25
|
+
TextColumn,
|
|
26
|
+
TimeElapsedColumn,
|
|
27
|
+
TimeRemainingColumn,
|
|
28
|
+
)
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
|
|
31
|
+
from ..core.agent.approval_manager import BatchApprovalRequest
|
|
32
|
+
from ..core.agent.types import Operation
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OperationStatus(Enum):
|
|
38
|
+
"""Status of individual operations in a batch."""
|
|
39
|
+
|
|
40
|
+
PENDING = "pending"
|
|
41
|
+
IN_PROGRESS = "in_progress"
|
|
42
|
+
COMPLETED = "completed"
|
|
43
|
+
FAILED = "failed"
|
|
44
|
+
SKIPPED = "skipped"
|
|
45
|
+
RETRYING = "retrying"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BatchOperationResult(Enum):
|
|
49
|
+
"""Overall result of batch operation."""
|
|
50
|
+
|
|
51
|
+
SUCCESS = "success"
|
|
52
|
+
PARTIAL_SUCCESS = "partial_success"
|
|
53
|
+
FAILURE = "failure"
|
|
54
|
+
CANCELLED = "cancelled"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class OperationResult:
|
|
59
|
+
"""Result of a single operation execution."""
|
|
60
|
+
|
|
61
|
+
operation_id: int
|
|
62
|
+
operation: Operation
|
|
63
|
+
status: OperationStatus
|
|
64
|
+
start_time: Optional[datetime] = None
|
|
65
|
+
end_time: Optional[datetime] = None
|
|
66
|
+
error_message: Optional[str] = None
|
|
67
|
+
retry_count: int = 0
|
|
68
|
+
execution_details: Dict[str, Any] = field(default_factory=dict)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def duration(self) -> Optional[timedelta]:
|
|
72
|
+
"""Get operation duration."""
|
|
73
|
+
if self.start_time and self.end_time:
|
|
74
|
+
return self.end_time - self.start_time
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class BatchExecutionSummary:
|
|
80
|
+
"""Summary of batch execution results."""
|
|
81
|
+
|
|
82
|
+
batch_id: str
|
|
83
|
+
start_time: datetime
|
|
84
|
+
end_time: Optional[datetime] = None
|
|
85
|
+
total_operations: int = 0
|
|
86
|
+
successful_operations: int = 0
|
|
87
|
+
failed_operations: int = 0
|
|
88
|
+
skipped_operations: int = 0
|
|
89
|
+
retried_operations: int = 0
|
|
90
|
+
result: BatchOperationResult = BatchOperationResult.SUCCESS
|
|
91
|
+
error_summary: List[str] = field(default_factory=list)
|
|
92
|
+
execution_log: List[Dict[str, Any]] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def duration(self) -> Optional[timedelta]:
|
|
96
|
+
"""Get total execution duration."""
|
|
97
|
+
if self.start_time and self.end_time:
|
|
98
|
+
return self.end_time - self.start_time
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def success_rate(self) -> float:
|
|
103
|
+
"""Get success rate as percentage."""
|
|
104
|
+
if self.total_operations == 0:
|
|
105
|
+
return 100.0
|
|
106
|
+
return (self.successful_operations / self.total_operations) * 100
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class BatchOperationMonitor:
|
|
110
|
+
"""
|
|
111
|
+
Monitors and manages batch operation execution with progress tracking,
|
|
112
|
+
error recovery, and comprehensive result reporting.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
console: Optional[Console] = None,
|
|
118
|
+
enable_live_display: bool = True,
|
|
119
|
+
max_retries: int = 3,
|
|
120
|
+
retry_delay: float = 1.0,
|
|
121
|
+
save_results: bool = True,
|
|
122
|
+
):
|
|
123
|
+
"""
|
|
124
|
+
Initialize batch operation monitor.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
console: Rich console for display
|
|
128
|
+
enable_live_display: Whether to show live progress updates
|
|
129
|
+
max_retries: Maximum retry attempts for failed operations
|
|
130
|
+
retry_delay: Delay between retry attempts in seconds
|
|
131
|
+
save_results: Whether to save execution results to files
|
|
132
|
+
"""
|
|
133
|
+
self.console = console or Console()
|
|
134
|
+
self.enable_live_display = enable_live_display
|
|
135
|
+
self.max_retries = max_retries
|
|
136
|
+
self.retry_delay = retry_delay
|
|
137
|
+
self.save_results = save_results
|
|
138
|
+
|
|
139
|
+
# Tracking data
|
|
140
|
+
self.operation_results: Dict[int, OperationResult] = {}
|
|
141
|
+
self.current_summary: Optional[BatchExecutionSummary] = None
|
|
142
|
+
self.progress_tracker: Optional[Progress] = None
|
|
143
|
+
self.live_display: Optional[Live] = None
|
|
144
|
+
|
|
145
|
+
# History storage
|
|
146
|
+
self.results_directory = Path.home() / ".omnimancer" / "batch_results"
|
|
147
|
+
if self.save_results:
|
|
148
|
+
self.results_directory.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
|
|
150
|
+
async def execute_batch(
|
|
151
|
+
self, batch_request: BatchApprovalRequest, operation_executor: callable
|
|
152
|
+
) -> BatchExecutionSummary:
|
|
153
|
+
"""
|
|
154
|
+
Execute a batch of operations with monitoring and error recovery.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
batch_request: Batch approval request to execute
|
|
158
|
+
operation_executor: Async function to execute individual operations
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
BatchExecutionSummary with complete execution results
|
|
162
|
+
"""
|
|
163
|
+
# Initialize tracking
|
|
164
|
+
self.current_summary = BatchExecutionSummary(
|
|
165
|
+
batch_id=batch_request.id,
|
|
166
|
+
start_time=datetime.now(),
|
|
167
|
+
total_operations=len(batch_request.operations),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Setup progress tracking
|
|
171
|
+
if self.enable_live_display:
|
|
172
|
+
await self._setup_progress_display(batch_request)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Execute operations
|
|
176
|
+
await self._execute_operations(batch_request, operation_executor)
|
|
177
|
+
|
|
178
|
+
# Finalize results
|
|
179
|
+
self.current_summary.end_time = datetime.now()
|
|
180
|
+
self._calculate_final_results()
|
|
181
|
+
|
|
182
|
+
# Save results if enabled
|
|
183
|
+
if self.save_results:
|
|
184
|
+
await self._save_execution_results()
|
|
185
|
+
|
|
186
|
+
return self.current_summary
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Batch execution failed: {e}")
|
|
190
|
+
self.current_summary.result = BatchOperationResult.FAILURE
|
|
191
|
+
self.current_summary.error_summary.append(
|
|
192
|
+
f"Batch execution error: {str(e)}"
|
|
193
|
+
)
|
|
194
|
+
return self.current_summary
|
|
195
|
+
finally:
|
|
196
|
+
if self.live_display:
|
|
197
|
+
self.live_display.stop()
|
|
198
|
+
|
|
199
|
+
async def _setup_progress_display(self, batch_request: BatchApprovalRequest):
|
|
200
|
+
"""Setup live progress display."""
|
|
201
|
+
self.progress_tracker = Progress(
|
|
202
|
+
TextColumn("[bold blue]Executing batch operations...", justify="right"),
|
|
203
|
+
BarColumn(bar_width=None),
|
|
204
|
+
"[progress.percentage]{task.percentage:>3.1f}%",
|
|
205
|
+
"•",
|
|
206
|
+
TextColumn("{task.completed}/{task.total} operations"),
|
|
207
|
+
TimeElapsedColumn(),
|
|
208
|
+
TimeRemainingColumn(),
|
|
209
|
+
console=self.console,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Add progress task
|
|
213
|
+
self.progress_task = self.progress_tracker.add_task(
|
|
214
|
+
"batch_execution", total=len(batch_request.operations)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Create live display layout
|
|
218
|
+
layout = Layout()
|
|
219
|
+
layout.split_column(
|
|
220
|
+
Layout(self.progress_tracker, name="progress", size=3),
|
|
221
|
+
Layout(name="details"),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
self.live_display = Live(layout, console=self.console, refresh_per_second=4)
|
|
225
|
+
self.live_display.start()
|
|
226
|
+
|
|
227
|
+
async def _execute_operations(
|
|
228
|
+
self, batch_request: BatchApprovalRequest, operation_executor: callable
|
|
229
|
+
):
|
|
230
|
+
"""Execute all operations in the batch with error handling."""
|
|
231
|
+
approved_indices = batch_request.approved_operations
|
|
232
|
+
|
|
233
|
+
for i, operation in enumerate(batch_request.operations):
|
|
234
|
+
# Skip if not approved
|
|
235
|
+
if i not in approved_indices:
|
|
236
|
+
self._record_operation_result(
|
|
237
|
+
i,
|
|
238
|
+
operation,
|
|
239
|
+
OperationStatus.SKIPPED,
|
|
240
|
+
"Operation not approved",
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Execute operation with retry logic
|
|
245
|
+
await self._execute_single_operation(i, operation, operation_executor)
|
|
246
|
+
|
|
247
|
+
# Update progress display
|
|
248
|
+
if self.progress_tracker:
|
|
249
|
+
self.progress_tracker.update(self.progress_task, advance=1)
|
|
250
|
+
|
|
251
|
+
if self.live_display:
|
|
252
|
+
# Update details section with current status
|
|
253
|
+
details_content = self._create_status_display()
|
|
254
|
+
self.live_display.layout["details"].update(details_content)
|
|
255
|
+
|
|
256
|
+
async def _execute_single_operation(
|
|
257
|
+
self,
|
|
258
|
+
operation_id: int,
|
|
259
|
+
operation: Operation,
|
|
260
|
+
operation_executor: callable,
|
|
261
|
+
):
|
|
262
|
+
"""Execute a single operation with retry logic."""
|
|
263
|
+
result = OperationResult(
|
|
264
|
+
operation_id=operation_id,
|
|
265
|
+
operation=operation,
|
|
266
|
+
status=OperationStatus.PENDING,
|
|
267
|
+
start_time=datetime.now(),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
self.operation_results[operation_id] = result
|
|
271
|
+
|
|
272
|
+
for attempt in range(self.max_retries + 1):
|
|
273
|
+
try:
|
|
274
|
+
result.status = OperationStatus.IN_PROGRESS
|
|
275
|
+
result.retry_count = attempt
|
|
276
|
+
|
|
277
|
+
if attempt > 0:
|
|
278
|
+
result.status = OperationStatus.RETRYING
|
|
279
|
+
await asyncio.sleep(
|
|
280
|
+
self.retry_delay * attempt
|
|
281
|
+
) # Exponential backoff
|
|
282
|
+
|
|
283
|
+
# Execute the operation
|
|
284
|
+
execution_result = await operation_executor(operation)
|
|
285
|
+
|
|
286
|
+
# Success
|
|
287
|
+
result.status = OperationStatus.COMPLETED
|
|
288
|
+
result.end_time = datetime.now()
|
|
289
|
+
result.execution_details = execution_result or {}
|
|
290
|
+
|
|
291
|
+
self._log_operation_event(
|
|
292
|
+
operation_id,
|
|
293
|
+
"COMPLETED",
|
|
294
|
+
f"Successfully executed {operation.type.value}",
|
|
295
|
+
)
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
except Exception as e:
|
|
299
|
+
error_msg = str(e)
|
|
300
|
+
result.error_message = error_msg
|
|
301
|
+
|
|
302
|
+
if attempt < self.max_retries:
|
|
303
|
+
# Will retry
|
|
304
|
+
self._log_operation_event(
|
|
305
|
+
operation_id,
|
|
306
|
+
"RETRY",
|
|
307
|
+
f"Attempt {attempt + 1} failed: {error_msg}. Retrying...",
|
|
308
|
+
)
|
|
309
|
+
continue
|
|
310
|
+
else:
|
|
311
|
+
# Final failure
|
|
312
|
+
result.status = OperationStatus.FAILED
|
|
313
|
+
result.end_time = datetime.now()
|
|
314
|
+
|
|
315
|
+
self._log_operation_event(
|
|
316
|
+
operation_id,
|
|
317
|
+
"FAILED",
|
|
318
|
+
f"Operation failed after {attempt + 1} attempts: {error_msg}",
|
|
319
|
+
)
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
def _record_operation_result(
|
|
323
|
+
self,
|
|
324
|
+
operation_id: int,
|
|
325
|
+
operation: Operation,
|
|
326
|
+
status: OperationStatus,
|
|
327
|
+
message: Optional[str] = None,
|
|
328
|
+
):
|
|
329
|
+
"""Record the result of an operation."""
|
|
330
|
+
result = OperationResult(
|
|
331
|
+
operation_id=operation_id,
|
|
332
|
+
operation=operation,
|
|
333
|
+
status=status,
|
|
334
|
+
start_time=datetime.now(),
|
|
335
|
+
end_time=datetime.now(),
|
|
336
|
+
error_message=(message if status == OperationStatus.FAILED else None),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
self.operation_results[operation_id] = result
|
|
340
|
+
self._log_operation_event(operation_id, status.value.upper(), message or "")
|
|
341
|
+
|
|
342
|
+
def _log_operation_event(self, operation_id: int, event_type: str, message: str):
|
|
343
|
+
"""Log an operation event."""
|
|
344
|
+
if self.current_summary:
|
|
345
|
+
self.current_summary.execution_log.append(
|
|
346
|
+
{
|
|
347
|
+
"timestamp": datetime.now().isoformat(),
|
|
348
|
+
"operation_id": operation_id,
|
|
349
|
+
"event_type": event_type,
|
|
350
|
+
"message": message,
|
|
351
|
+
}
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def _create_status_display(self) -> Panel:
|
|
355
|
+
"""Create status display for live updates."""
|
|
356
|
+
# Count status
|
|
357
|
+
status_counts = {status: 0 for status in OperationStatus}
|
|
358
|
+
recent_events = []
|
|
359
|
+
|
|
360
|
+
for result in self.operation_results.values():
|
|
361
|
+
status_counts[result.status] += 1
|
|
362
|
+
|
|
363
|
+
# Collect recent events
|
|
364
|
+
if result.error_message and result.status == OperationStatus.FAILED:
|
|
365
|
+
recent_events.append(
|
|
366
|
+
f"[red]Failed[/red]: {result.operation.type.value} - {result.error_message}"
|
|
367
|
+
)
|
|
368
|
+
elif result.status == OperationStatus.COMPLETED:
|
|
369
|
+
recent_events.append(
|
|
370
|
+
f"[green]Completed[/green]: {result.operation.type.value}"
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Create status table
|
|
374
|
+
status_table = Table.grid(padding=1)
|
|
375
|
+
status_table.add_column("Status", style="bold")
|
|
376
|
+
status_table.add_column("Count", justify="right")
|
|
377
|
+
|
|
378
|
+
for status, count in status_counts.items():
|
|
379
|
+
if count > 0:
|
|
380
|
+
color = {
|
|
381
|
+
OperationStatus.COMPLETED: "green",
|
|
382
|
+
OperationStatus.FAILED: "red",
|
|
383
|
+
OperationStatus.IN_PROGRESS: "yellow",
|
|
384
|
+
OperationStatus.RETRYING: "orange",
|
|
385
|
+
OperationStatus.SKIPPED: "dim",
|
|
386
|
+
}.get(status, "white")
|
|
387
|
+
|
|
388
|
+
status_table.add_row(
|
|
389
|
+
f"[{color}]{status.value.title()}[/{color}]",
|
|
390
|
+
f"[{color}]{count}[/{color}]",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Recent events
|
|
394
|
+
events_text = (
|
|
395
|
+
"\n".join(recent_events[-5:])
|
|
396
|
+
if recent_events
|
|
397
|
+
else "[dim]No recent events[/dim]"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
content = f"{status_table}\n\n[bold]Recent Events:[/bold]\n{events_text}"
|
|
401
|
+
|
|
402
|
+
return Panel(
|
|
403
|
+
content, title="[bold]Execution Status[/bold]", border_style="blue"
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def _calculate_final_results(self):
|
|
407
|
+
"""Calculate final batch execution results."""
|
|
408
|
+
if not self.current_summary:
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# Count results
|
|
412
|
+
for result in self.operation_results.values():
|
|
413
|
+
if result.status == OperationStatus.COMPLETED:
|
|
414
|
+
self.current_summary.successful_operations += 1
|
|
415
|
+
elif result.status == OperationStatus.FAILED:
|
|
416
|
+
self.current_summary.failed_operations += 1
|
|
417
|
+
if result.error_message:
|
|
418
|
+
self.current_summary.error_summary.append(
|
|
419
|
+
f"Operation {result.operation_id}: {result.error_message}"
|
|
420
|
+
)
|
|
421
|
+
elif result.status == OperationStatus.SKIPPED:
|
|
422
|
+
self.current_summary.skipped_operations += 1
|
|
423
|
+
|
|
424
|
+
if result.retry_count > 0:
|
|
425
|
+
self.current_summary.retried_operations += 1
|
|
426
|
+
|
|
427
|
+
# Determine overall result
|
|
428
|
+
if self.current_summary.failed_operations == 0:
|
|
429
|
+
self.current_summary.result = BatchOperationResult.SUCCESS
|
|
430
|
+
elif self.current_summary.successful_operations > 0:
|
|
431
|
+
self.current_summary.result = BatchOperationResult.PARTIAL_SUCCESS
|
|
432
|
+
else:
|
|
433
|
+
self.current_summary.result = BatchOperationResult.FAILURE
|
|
434
|
+
|
|
435
|
+
async def _save_execution_results(self):
|
|
436
|
+
"""Save execution results to files."""
|
|
437
|
+
if not self.current_summary or not self.save_results:
|
|
438
|
+
return
|
|
439
|
+
|
|
440
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
441
|
+
batch_id_short = self.current_summary.batch_id[:8]
|
|
442
|
+
|
|
443
|
+
# Save JSON summary
|
|
444
|
+
json_path = self.results_directory / f"batch_{batch_id_short}_{timestamp}.json"
|
|
445
|
+
json_data = {
|
|
446
|
+
"summary": {
|
|
447
|
+
"batch_id": self.current_summary.batch_id,
|
|
448
|
+
"start_time": self.current_summary.start_time.isoformat(),
|
|
449
|
+
"end_time": (
|
|
450
|
+
self.current_summary.end_time.isoformat()
|
|
451
|
+
if self.current_summary.end_time
|
|
452
|
+
else None
|
|
453
|
+
),
|
|
454
|
+
"duration_seconds": (
|
|
455
|
+
self.current_summary.duration.total_seconds()
|
|
456
|
+
if self.current_summary.duration
|
|
457
|
+
else None
|
|
458
|
+
),
|
|
459
|
+
"total_operations": self.current_summary.total_operations,
|
|
460
|
+
"successful_operations": self.current_summary.successful_operations,
|
|
461
|
+
"failed_operations": self.current_summary.failed_operations,
|
|
462
|
+
"skipped_operations": self.current_summary.skipped_operations,
|
|
463
|
+
"retried_operations": self.current_summary.retried_operations,
|
|
464
|
+
"result": self.current_summary.result.value,
|
|
465
|
+
"success_rate": self.current_summary.success_rate,
|
|
466
|
+
"error_summary": self.current_summary.error_summary,
|
|
467
|
+
},
|
|
468
|
+
"operations": [
|
|
469
|
+
{
|
|
470
|
+
"id": result.operation_id,
|
|
471
|
+
"type": result.operation.type.value,
|
|
472
|
+
"description": result.operation.description,
|
|
473
|
+
"status": result.status.value,
|
|
474
|
+
"start_time": (
|
|
475
|
+
result.start_time.isoformat() if result.start_time else None
|
|
476
|
+
),
|
|
477
|
+
"end_time": (
|
|
478
|
+
result.end_time.isoformat() if result.end_time else None
|
|
479
|
+
),
|
|
480
|
+
"duration_seconds": (
|
|
481
|
+
result.duration.total_seconds() if result.duration else None
|
|
482
|
+
),
|
|
483
|
+
"retry_count": result.retry_count,
|
|
484
|
+
"error_message": result.error_message,
|
|
485
|
+
"execution_details": result.execution_details,
|
|
486
|
+
}
|
|
487
|
+
for result in self.operation_results.values()
|
|
488
|
+
],
|
|
489
|
+
"execution_log": self.current_summary.execution_log,
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
with open(json_path, "w") as f:
|
|
493
|
+
json.dump(json_data, f, indent=2)
|
|
494
|
+
|
|
495
|
+
logger.info(f"Batch execution results saved to {json_path}")
|
|
496
|
+
|
|
497
|
+
def display_execution_summary(
|
|
498
|
+
self, summary: Optional[BatchExecutionSummary] = None
|
|
499
|
+
):
|
|
500
|
+
"""Display comprehensive execution summary."""
|
|
501
|
+
if not summary:
|
|
502
|
+
summary = self.current_summary
|
|
503
|
+
|
|
504
|
+
if not summary:
|
|
505
|
+
self.console.print("[red]No execution summary available[/red]")
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
# Main summary panel
|
|
509
|
+
summary_table = Table.grid(padding=1)
|
|
510
|
+
summary_table.add_column("Metric", style="bold cyan")
|
|
511
|
+
summary_table.add_column("Value", style="bright_white")
|
|
512
|
+
|
|
513
|
+
summary_table.add_row("Batch ID:", summary.batch_id[:12] + "...")
|
|
514
|
+
summary_table.add_row(
|
|
515
|
+
"Execution Time:",
|
|
516
|
+
str(summary.duration) if summary.duration else "Unknown",
|
|
517
|
+
)
|
|
518
|
+
summary_table.add_row("Total Operations:", str(summary.total_operations))
|
|
519
|
+
summary_table.add_row(
|
|
520
|
+
"Successful:", f"[green]{summary.successful_operations}[/green]"
|
|
521
|
+
)
|
|
522
|
+
summary_table.add_row("Failed:", f"[red]{summary.failed_operations}[/red]")
|
|
523
|
+
summary_table.add_row("Skipped:", f"[dim]{summary.skipped_operations}[/dim]")
|
|
524
|
+
summary_table.add_row(
|
|
525
|
+
"Retried:", f"[yellow]{summary.retried_operations}[/yellow]"
|
|
526
|
+
)
|
|
527
|
+
summary_table.add_row(
|
|
528
|
+
"Success Rate:", f"[green]{summary.success_rate:.1f}%[/green]"
|
|
529
|
+
)
|
|
530
|
+
summary_table.add_row("Result:", self._format_batch_result(summary.result))
|
|
531
|
+
|
|
532
|
+
summary_panel = Panel(
|
|
533
|
+
summary_table,
|
|
534
|
+
title="[bold]Batch Execution Summary[/bold]",
|
|
535
|
+
border_style=(
|
|
536
|
+
"green" if summary.result == BatchOperationResult.SUCCESS else "red"
|
|
537
|
+
),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
self.console.print(summary_panel)
|
|
541
|
+
|
|
542
|
+
# Error details if any
|
|
543
|
+
if summary.error_summary:
|
|
544
|
+
error_text = "\n".join([f"• {error}" for error in summary.error_summary])
|
|
545
|
+
error_panel = Panel(
|
|
546
|
+
error_text,
|
|
547
|
+
title="[bold red]Errors[/bold red]",
|
|
548
|
+
border_style="red",
|
|
549
|
+
)
|
|
550
|
+
self.console.print(error_panel)
|
|
551
|
+
|
|
552
|
+
# Operation details table
|
|
553
|
+
if self.operation_results:
|
|
554
|
+
self._display_operation_details()
|
|
555
|
+
|
|
556
|
+
def _display_operation_details(self):
|
|
557
|
+
"""Display detailed operation results table."""
|
|
558
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
559
|
+
table.add_column("ID", width=4)
|
|
560
|
+
table.add_column("Type", width=12)
|
|
561
|
+
table.add_column("Status", width=10, justify="center")
|
|
562
|
+
table.add_column("Duration", width=8, justify="right")
|
|
563
|
+
table.add_column("Retries", width=7, justify="center")
|
|
564
|
+
table.add_column("Error", width=40)
|
|
565
|
+
|
|
566
|
+
for result in sorted(
|
|
567
|
+
self.operation_results.values(), key=lambda x: x.operation_id
|
|
568
|
+
):
|
|
569
|
+
status_display = self._format_operation_status(result.status)
|
|
570
|
+
duration_display = (
|
|
571
|
+
f"{result.duration.total_seconds():.1f}s" if result.duration else "N/A"
|
|
572
|
+
)
|
|
573
|
+
retry_display = str(result.retry_count) if result.retry_count > 0 else "-"
|
|
574
|
+
error_display = (
|
|
575
|
+
result.error_message[:40] + "..."
|
|
576
|
+
if result.error_message and len(result.error_message) > 40
|
|
577
|
+
else (result.error_message or "")
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
table.add_row(
|
|
581
|
+
str(result.operation_id + 1),
|
|
582
|
+
result.operation.type.value.replace("_", " ").title(),
|
|
583
|
+
status_display,
|
|
584
|
+
duration_display,
|
|
585
|
+
retry_display,
|
|
586
|
+
error_display,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
self.console.print("\n[bold]Operation Details:[/bold]")
|
|
590
|
+
self.console.print(table)
|
|
591
|
+
|
|
592
|
+
def _format_batch_result(self, result: BatchOperationResult) -> str:
|
|
593
|
+
"""Format batch result with colors."""
|
|
594
|
+
color_map = {
|
|
595
|
+
BatchOperationResult.SUCCESS: "green",
|
|
596
|
+
BatchOperationResult.PARTIAL_SUCCESS: "yellow",
|
|
597
|
+
BatchOperationResult.FAILURE: "red",
|
|
598
|
+
BatchOperationResult.CANCELLED: "dim",
|
|
599
|
+
}
|
|
600
|
+
color = color_map.get(result, "white")
|
|
601
|
+
return f"[{color}]{result.value.replace('_', ' ').title()}[/{color}]"
|
|
602
|
+
|
|
603
|
+
def _format_operation_status(self, status: OperationStatus) -> str:
|
|
604
|
+
"""Format operation status with colors."""
|
|
605
|
+
color_map = {
|
|
606
|
+
OperationStatus.COMPLETED: "green",
|
|
607
|
+
OperationStatus.FAILED: "red",
|
|
608
|
+
OperationStatus.SKIPPED: "dim",
|
|
609
|
+
OperationStatus.IN_PROGRESS: "yellow",
|
|
610
|
+
OperationStatus.RETRYING: "orange",
|
|
611
|
+
}
|
|
612
|
+
color = color_map.get(status, "white")
|
|
613
|
+
symbol_map = {
|
|
614
|
+
OperationStatus.COMPLETED: "✓",
|
|
615
|
+
OperationStatus.FAILED: "✗",
|
|
616
|
+
OperationStatus.SKIPPED: "○",
|
|
617
|
+
OperationStatus.IN_PROGRESS: "●",
|
|
618
|
+
OperationStatus.RETRYING: "⟳",
|
|
619
|
+
}
|
|
620
|
+
symbol = symbol_map.get(status, "?")
|
|
621
|
+
return f"[{color}]{symbol} {status.value.title()}[/{color}]"
|
|
622
|
+
|
|
623
|
+
def export_results(
|
|
624
|
+
self, format_type: str = "json", output_path: Optional[Path] = None
|
|
625
|
+
) -> Optional[Path]:
|
|
626
|
+
"""
|
|
627
|
+
Export batch results to file.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
format_type: Export format ('json' or 'csv')
|
|
631
|
+
output_path: Optional custom output path
|
|
632
|
+
|
|
633
|
+
Returns:
|
|
634
|
+
Path to exported file or None if export failed
|
|
635
|
+
"""
|
|
636
|
+
if not self.current_summary:
|
|
637
|
+
self.console.print("[red]No results to export[/red]")
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
if not output_path:
|
|
641
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
642
|
+
batch_id_short = self.current_summary.batch_id[:8]
|
|
643
|
+
filename = f"batch_results_{batch_id_short}_{timestamp}.{format_type}"
|
|
644
|
+
output_path = Path.cwd() / filename
|
|
645
|
+
|
|
646
|
+
try:
|
|
647
|
+
if format_type.lower() == "json":
|
|
648
|
+
return self._export_json(output_path)
|
|
649
|
+
elif format_type.lower() == "csv":
|
|
650
|
+
return self._export_csv(output_path)
|
|
651
|
+
else:
|
|
652
|
+
self.console.print(
|
|
653
|
+
f"[red]Unsupported export format: {format_type}[/red]"
|
|
654
|
+
)
|
|
655
|
+
return None
|
|
656
|
+
except Exception as e:
|
|
657
|
+
self.console.print(f"[red]Export failed: {e}[/red]")
|
|
658
|
+
return None
|
|
659
|
+
|
|
660
|
+
def _export_json(self, output_path: Path) -> Path:
|
|
661
|
+
"""Export results as JSON."""
|
|
662
|
+
data = {
|
|
663
|
+
"batch_summary": {
|
|
664
|
+
"batch_id": self.current_summary.batch_id,
|
|
665
|
+
"result": self.current_summary.result.value,
|
|
666
|
+
"success_rate": self.current_summary.success_rate,
|
|
667
|
+
"execution_time": (
|
|
668
|
+
str(self.current_summary.duration)
|
|
669
|
+
if self.current_summary.duration
|
|
670
|
+
else None
|
|
671
|
+
),
|
|
672
|
+
"operations_summary": {
|
|
673
|
+
"total": self.current_summary.total_operations,
|
|
674
|
+
"successful": self.current_summary.successful_operations,
|
|
675
|
+
"failed": self.current_summary.failed_operations,
|
|
676
|
+
"skipped": self.current_summary.skipped_operations,
|
|
677
|
+
"retried": self.current_summary.retried_operations,
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
"operation_results": [
|
|
681
|
+
{
|
|
682
|
+
"id": result.operation_id,
|
|
683
|
+
"type": result.operation.type.value,
|
|
684
|
+
"status": result.status.value,
|
|
685
|
+
"description": result.operation.description,
|
|
686
|
+
"duration": (str(result.duration) if result.duration else None),
|
|
687
|
+
"retry_count": result.retry_count,
|
|
688
|
+
"error": result.error_message,
|
|
689
|
+
}
|
|
690
|
+
for result in self.operation_results.values()
|
|
691
|
+
],
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
with open(output_path, "w") as f:
|
|
695
|
+
json.dump(data, f, indent=2)
|
|
696
|
+
|
|
697
|
+
return output_path
|
|
698
|
+
|
|
699
|
+
def _export_csv(self, output_path: Path) -> Path:
|
|
700
|
+
"""Export results as CSV."""
|
|
701
|
+
with open(output_path, "w", newline="") as f:
|
|
702
|
+
writer = csv.writer(f)
|
|
703
|
+
|
|
704
|
+
# Write header
|
|
705
|
+
writer.writerow(
|
|
706
|
+
[
|
|
707
|
+
"Operation ID",
|
|
708
|
+
"Type",
|
|
709
|
+
"Description",
|
|
710
|
+
"Status",
|
|
711
|
+
"Duration (s)",
|
|
712
|
+
"Retry Count",
|
|
713
|
+
"Error Message",
|
|
714
|
+
]
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
# Write data rows
|
|
718
|
+
for result in sorted(
|
|
719
|
+
self.operation_results.values(), key=lambda x: x.operation_id
|
|
720
|
+
):
|
|
721
|
+
writer.writerow(
|
|
722
|
+
[
|
|
723
|
+
result.operation_id + 1,
|
|
724
|
+
result.operation.type.value,
|
|
725
|
+
result.operation.description or "",
|
|
726
|
+
result.status.value,
|
|
727
|
+
(result.duration.total_seconds() if result.duration else ""),
|
|
728
|
+
result.retry_count,
|
|
729
|
+
result.error_message or "",
|
|
730
|
+
]
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
return output_path
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def create_batch_monitor(
|
|
737
|
+
console: Optional[Console] = None, **kwargs
|
|
738
|
+
) -> BatchOperationMonitor:
|
|
739
|
+
"""
|
|
740
|
+
Create a batch operation monitor with default configuration.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
console: Rich console for display
|
|
744
|
+
**kwargs: Additional monitor configuration options
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
Configured BatchOperationMonitor instance
|
|
748
|
+
"""
|
|
749
|
+
return BatchOperationMonitor(console=console, **kwargs)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
async def simulate_operation_executor(operation: Operation) -> Dict[str, Any]:
|
|
753
|
+
"""
|
|
754
|
+
Simulate operation execution for testing purposes.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
operation: Operation to simulate
|
|
758
|
+
|
|
759
|
+
Returns:
|
|
760
|
+
Simulated execution result
|
|
761
|
+
"""
|
|
762
|
+
# Simulate execution time
|
|
763
|
+
await asyncio.sleep(0.1)
|
|
764
|
+
|
|
765
|
+
# Simulate occasional failures for testing
|
|
766
|
+
import random
|
|
767
|
+
|
|
768
|
+
if random.random() < 0.1: # 10% failure rate
|
|
769
|
+
raise Exception(f"Simulated failure for {operation.type.value}")
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
"simulated": True,
|
|
773
|
+
"operation_type": operation.type.value,
|
|
774
|
+
"timestamp": datetime.now().isoformat(),
|
|
775
|
+
}
|