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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. 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
+ }