runbooks 1.1.3__py3-none-any.whl → 1.1.5__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 (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -13,17 +13,24 @@ Features:
13
13
  - Error/warning/success message formatting
14
14
  - Tree displays for hierarchical data
15
15
  - Layout templates for complex displays
16
+ - Test mode support to prevent I/O conflicts with Click CliRunner
16
17
 
17
18
  Author: CloudOps Runbooks Team
18
19
  Version: 0.7.8
19
20
  """
20
21
 
22
+ import csv
23
+ import json
24
+ import os
25
+ import re
26
+ import sys
27
+ import tempfile
21
28
  from datetime import datetime
29
+ from io import StringIO
22
30
  from typing import Any, Dict, List, Optional, Union
23
31
 
24
32
  from rich import box
25
33
  from rich.columns import Columns
26
- from rich.console import Console
27
34
  from rich.layout import Layout
28
35
  from rich.markdown import Markdown
29
36
  from rich.panel import Panel
@@ -31,11 +38,58 @@ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn
31
38
  from rich.rule import Rule
32
39
  from rich.style import Style
33
40
  from rich.syntax import Syntax
34
- from rich.table import Table
41
+ from rich.table import Table as RichTable
35
42
  from rich.text import Text
36
43
  from rich.theme import Theme
37
44
  from rich.tree import Tree
38
45
 
46
+ # Test Mode Support: Disable Rich Console in test environments to prevent I/O conflicts
47
+ # Issue: Rich Console writes to StringIO buffer that Click CliRunner closes, causing ValueError
48
+ # Solution: Use plain print() in test mode (RUNBOOKS_TEST_MODE=1), Rich Console in production
49
+ USE_RICH = os.getenv("RUNBOOKS_TEST_MODE") != "1"
50
+
51
+ if USE_RICH:
52
+ from rich.console import Console as RichConsole
53
+
54
+ Console = RichConsole
55
+ Table = RichTable
56
+ else:
57
+ # Mock Rich Console for testing - plain text output compatible with Click CliRunner
58
+ class MockConsole:
59
+ """Mock console that prints to stdout without Rich formatting."""
60
+
61
+ def print(self, *args, **kwargs):
62
+ """Mock print that outputs plain text to stdout."""
63
+ if args:
64
+ # Extract text content from Rich markup if present
65
+ text = str(args[0]) if args else ""
66
+ # Remove Rich markup tags for plain output
67
+ text = re.sub(r"\[.*?\]", "", text)
68
+ print(text, file=sys.stdout)
69
+
70
+ def __enter__(self):
71
+ return self
72
+
73
+ def __exit__(self, *args):
74
+ pass
75
+
76
+ class MockTable:
77
+ """Mock table for testing - minimal implementation."""
78
+
79
+ def __init__(self, *args, **kwargs):
80
+ self.title = kwargs.get("title", "")
81
+ self.columns = []
82
+ self.rows = []
83
+
84
+ def add_column(self, header, **kwargs):
85
+ self.columns.append(header)
86
+
87
+ def add_row(self, *args):
88
+ self.rows.append(args)
89
+
90
+ Console = MockConsole
91
+ Table = MockTable
92
+
39
93
  # CloudOps Custom Theme
40
94
  CLOUDOPS_THEME = Theme(
41
95
  {
@@ -55,8 +109,11 @@ CLOUDOPS_THEME = Theme(
55
109
  }
56
110
  )
57
111
 
58
- # Initialize console with custom theme
59
- console = Console(theme=CLOUDOPS_THEME)
112
+ # Initialize console with custom theme (test-aware via USE_RICH flag)
113
+ if USE_RICH:
114
+ console = Console(theme=CLOUDOPS_THEME)
115
+ else:
116
+ console = Console() # MockConsole instance
60
117
 
61
118
  # Status indicators
62
119
  STATUS_INDICATORS = {
@@ -105,6 +162,7 @@ def print_header(title: str, version: Optional[str] = None) -> None:
105
162
  """
106
163
  if version is None:
107
164
  from runbooks import __version__
165
+
108
166
  version = __version__
109
167
 
110
168
  header_text = Text()
@@ -120,7 +178,10 @@ def print_header(title: str, version: Optional[str] = None) -> None:
120
178
  def print_banner() -> None:
121
179
  """Print a clean, minimal CloudOps Runbooks banner."""
122
180
  from runbooks import __version__
123
- console.print(f"\n[header]CloudOps Runbooks[/header] [subheader]Enterprise AWS Automation Platform[/subheader] [dim]v{__version__}[/dim]")
181
+
182
+ console.print(
183
+ f"\n[header]CloudOps Runbooks[/header] [subheader]Enterprise AWS Automation Platform[/subheader] [dim]v{__version__}[/dim]"
184
+ )
124
185
  console.print()
125
186
 
126
187
 
@@ -427,7 +488,9 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
427
488
  return f"{profile_name[: max_length - 3]}..."
428
489
 
429
490
 
430
- def format_profile_name(profile_name: str, style: str = "cyan", display_max_length: int = 25, secure_logging: bool = True) -> Text:
491
+ def format_profile_name(
492
+ profile_name: str, style: str = "cyan", display_max_length: int = 25, secure_logging: bool = True
493
+ ) -> Text:
431
494
  """
432
495
  Format profile name with consistent styling, intelligent truncation, and security enhancements.
433
496
 
@@ -445,7 +508,7 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
445
508
 
446
509
  Returns:
447
510
  Rich Text object with formatted profile name
448
-
511
+
449
512
  Security Note:
450
513
  When secure_logging=True, account IDs are masked in display to prevent
451
514
  account enumeration while maintaining profile identification.
@@ -454,13 +517,14 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
454
517
  if secure_logging:
455
518
  try:
456
519
  from runbooks.common.aws_utils import AWSProfileSanitizer
520
+
457
521
  display_profile = AWSProfileSanitizer.sanitize_profile_name(profile_name)
458
522
  except ImportError:
459
523
  # Fallback to original profile if aws_utils not available
460
524
  display_profile = profile_name
461
525
  else:
462
526
  display_profile = profile_name
463
-
527
+
464
528
  display_name = create_display_profile_name(display_profile, display_max_length)
465
529
 
466
530
  text = Text()
@@ -472,7 +536,7 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
472
536
  else:
473
537
  # Full name - normal style
474
538
  text.append(display_name, style=style)
475
-
539
+
476
540
  # Add security indicator for sanitized profiles
477
541
  if secure_logging and "***masked***" in display_name:
478
542
  text.append(" 🔒", style="dim yellow")
@@ -613,27 +677,27 @@ def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings:
613
677
  """
614
678
  Format WorkSpaces cost analysis for manager's priority scenario.
615
679
 
616
- Based on manager's requirement for $12,518 annual savings through
680
+ Based on manager's requirement for significant annual savings savings through
617
681
  cleanup of unused WorkSpaces with zero usage in last 6 months.
618
682
 
619
683
  Args:
620
684
  workspaces_data: Dictionary containing WorkSpaces cost and utilization data
621
685
  target_savings: Annual savings target (default: $12,518)
622
-
686
+
623
687
  Returns:
624
688
  Rich Panel with formatted WorkSpaces analysis
625
689
  """
626
- current_cost = workspaces_data.get('monthly_cost', 0)
627
- unused_count = workspaces_data.get('unused_count', 0)
628
- total_count = workspaces_data.get('total_count', 0)
629
- optimization_potential = workspaces_data.get('optimization_potential', 0)
630
-
690
+ current_cost = workspaces_data.get("monthly_cost", 0)
691
+ unused_count = workspaces_data.get("unused_count", 0)
692
+ total_count = workspaces_data.get("total_count", 0)
693
+ optimization_potential = workspaces_data.get("optimization_potential", 0)
694
+
631
695
  annual_savings = optimization_potential * 12
632
696
  target_achievement = min(100, (annual_savings / target_savings) * 100) if target_savings > 0 else 0
633
-
697
+
634
698
  status = "🎯 TARGET ACHIEVABLE" if target_achievement >= 90 else "⚠️ TARGET REQUIRES EXPANDED SCOPE"
635
699
  status_style = "bright_green" if target_achievement >= 90 else "yellow"
636
-
700
+
637
701
  content = f"""💼 [bold]Manager's Priority #1: WorkSpaces Cleanup Analysis[/bold]
638
702
 
639
703
  📊 Current State:
@@ -654,35 +718,38 @@ def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings:
654
718
 
655
719
  [{status_style}]{status}[/]"""
656
720
 
657
- return Panel(content, title="[bright_cyan]WorkSpaces Cost Optimization[/bright_cyan]",
658
- border_style="bright_green" if target_achievement >= 90 else "yellow")
721
+ return Panel(
722
+ content,
723
+ title="[bright_cyan]WorkSpaces Cost Optimization[/bright_cyan]",
724
+ border_style="bright_green" if target_achievement >= 90 else "yellow",
725
+ )
659
726
 
660
727
 
661
728
  def format_nat_gateway_optimization(nat_data: Dict[str, Any], target_completion: int = 95) -> Panel:
662
729
  """
663
730
  Format NAT Gateway optimization analysis for manager's completion target.
664
-
731
+
665
732
  Manager's requirement to increase NAT Gateway optimization from 75% to 95% completion.
666
-
733
+
667
734
  Args:
668
735
  nat_data: Dictionary containing NAT Gateway configuration and cost data
669
736
  target_completion: Completion target percentage (default: 95% from manager's priority)
670
-
737
+
671
738
  Returns:
672
739
  Rich Panel with formatted NAT Gateway optimization analysis
673
740
  """
674
- total_gateways = nat_data.get('total', 0)
675
- active_gateways = nat_data.get('active', 0)
676
- monthly_cost = nat_data.get('monthly_cost', 0)
677
- optimization_ready = nat_data.get('optimization_ready', 0)
678
-
741
+ total_gateways = nat_data.get("total", 0)
742
+ active_gateways = nat_data.get("active", 0)
743
+ monthly_cost = nat_data.get("monthly_cost", 0)
744
+ optimization_ready = nat_data.get("optimization_ready", 0)
745
+
679
746
  current_completion = 75 # Manager specified current state
680
747
  optimization_potential = monthly_cost * 0.75 # 75% can be optimized
681
748
  annual_savings = optimization_potential * 12
682
-
749
+
683
750
  completion_gap = target_completion - current_completion
684
751
  status = "🎯 READY FOR 95% TARGET" if active_gateways > 0 else "❌ NO OPTIMIZATION OPPORTUNITIES"
685
-
752
+
686
753
  content = f"""🌐 [bold]Manager's Priority #2: NAT Gateway Optimization[/bold]
687
754
 
688
755
  🔍 Current Infrastructure:
@@ -698,7 +765,7 @@ def format_nat_gateway_optimization(nat_data: Dict[str, Any], target_completion:
698
765
  💰 Projected Savings:
699
766
  • Monthly Savings Potential: [bright_green]${optimization_potential:,.2f}[/bright_green]
700
767
  • Annual Savings: [bright_green]${annual_savings:,.0f}[/bright_green]
701
- • Per Gateway Savings: [bright_cyan]~$540/year[/bright_cyan]
768
+ • Per Gateway Savings: [bright_cyan]~measurable yearly value[/bright_cyan]
702
769
 
703
770
  ⏰ Implementation:
704
771
  • Timeline: 6-8 weeks
@@ -707,45 +774,46 @@ def format_nat_gateway_optimization(nat_data: Dict[str, Any], target_completion:
707
774
 
708
775
  [bright_green]{status}[/bright_green]"""
709
776
 
710
- return Panel(content, title="[bright_cyan]Manager's Priority #2: NAT Gateway Optimization[/bright_cyan]",
711
- border_style="cyan")
777
+ return Panel(
778
+ content, title="[bright_cyan]Manager's Priority #2: NAT Gateway Optimization[/bright_cyan]", border_style="cyan"
779
+ )
712
780
 
713
781
 
714
782
  def format_rds_optimization_analysis(rds_data: Dict[str, Any], savings_range: Dict[str, int] = None) -> Panel:
715
783
  """
716
784
  Format RDS Multi-AZ optimization analysis for manager's FinOps-23 scenario.
717
-
718
- Manager's requirement for $5K-24K annual savings through RDS manual snapshot cleanup
785
+
786
+ Manager's requirement for measurable range annual savings through RDS manual snapshot cleanup
719
787
  and Multi-AZ configuration review.
720
-
788
+
721
789
  Args:
722
790
  rds_data: Dictionary containing RDS instance and snapshot data
723
791
  savings_range: Dict with 'min' and 'max' annual savings (default: {'min': 5000, 'max': 24000})
724
-
792
+
725
793
  Returns:
726
794
  Rich Panel with formatted RDS optimization analysis
727
795
  """
728
796
  if savings_range is None:
729
- savings_range = {'min': 5000, 'max': 24000}
730
-
731
- total_instances = rds_data.get('total', 0)
732
- multi_az_instances = rds_data.get('multi_az_instances', 0)
733
- manual_snapshots = rds_data.get('manual_snapshots', 0)
734
- snapshot_storage_gb = rds_data.get('snapshot_storage_gb', 0)
735
-
797
+ savings_range = {"min": 5000, "max": 24000}
798
+
799
+ total_instances = rds_data.get("total", 0)
800
+ multi_az_instances = rds_data.get("multi_az_instances", 0)
801
+ manual_snapshots = rds_data.get("manual_snapshots", 0)
802
+ snapshot_storage_gb = rds_data.get("snapshot_storage_gb", 0)
803
+
736
804
  # Calculate savings potential
737
805
  snapshot_savings = snapshot_storage_gb * 0.095 * 12 # $0.095/GB/month
738
806
  multi_az_savings = multi_az_instances * 1000 * 12 # ~$1K/month per instance
739
807
  total_savings = snapshot_savings + multi_az_savings
740
-
741
- savings_min = savings_range['min']
742
- savings_max = savings_range['max']
743
-
808
+
809
+ savings_min = savings_range["min"]
810
+ savings_max = savings_range["max"]
811
+
744
812
  # Check if we're within manager's target range
745
813
  within_range = savings_min <= total_savings <= savings_max
746
814
  range_status = "✅ WITHIN TARGET RANGE" if within_range else "📊 ANALYSIS PENDING"
747
815
  range_style = "bright_green" if within_range else "yellow"
748
-
816
+
749
817
  content = f"""🗄️ [bold]Manager's Priority #3: RDS Cost Optimization[/bold]
750
818
 
751
819
  📊 Current RDS Environment:
@@ -762,7 +830,7 @@ def format_rds_optimization_analysis(rds_data: Dict[str, Any], savings_range: Di
762
830
  🎯 Manager's Target Range:
763
831
  • Minimum Target: [bright_cyan]${savings_min:,.0f}[/bright_cyan]
764
832
  • Maximum Target: [bright_cyan]${savings_max:,.0f}[/bright_cyan]
765
- • Business Case: $5K-24K annual opportunity (FinOps-23)
833
+ • Business Case: measurable range annual opportunity (FinOps-23)
766
834
 
767
835
  ⏰ Implementation:
768
836
  • Timeline: 10-12 weeks
@@ -771,48 +839,51 @@ def format_rds_optimization_analysis(rds_data: Dict[str, Any], savings_range: Di
771
839
 
772
840
  [{range_style}]{range_status}[/]"""
773
841
 
774
- return Panel(content, title="[bright_cyan]FinOps-23: RDS Multi-AZ & Snapshot Optimization[/bright_cyan]",
775
- border_style="bright_green" if within_range else "yellow")
842
+ return Panel(
843
+ content,
844
+ title="[bright_cyan]FinOps-23: RDS Multi-AZ & Snapshot Optimization[/bright_cyan]",
845
+ border_style="bright_green" if within_range else "yellow",
846
+ )
776
847
 
777
848
 
778
849
  def format_manager_business_summary(all_scenarios_data: Dict[str, Any]) -> Panel:
779
850
  """
780
851
  Format executive summary panel for manager's complete AWSO business case.
781
-
852
+
782
853
  Combines all three manager priorities into executive-ready decision package:
783
854
  - FinOps-24: WorkSpaces cleanup ($12,518)
784
855
  - Manager Priority #2: NAT Gateway optimization (95% completion)
785
- - FinOps-23: RDS optimization ($5K-24K range)
786
-
856
+ - FinOps-23: RDS optimization (measurable range range)
857
+
787
858
  Args:
788
859
  all_scenarios_data: Dictionary containing data from all three scenarios
789
-
860
+
790
861
  Returns:
791
862
  Rich Panel with complete executive summary
792
863
  """
793
- workspaces = all_scenarios_data.get('workspaces', {})
794
- nat_gateway = all_scenarios_data.get('nat_gateway', {})
795
- rds = all_scenarios_data.get('rds', {})
796
-
864
+ workspaces = all_scenarios_data.get("workspaces", {})
865
+ nat_gateway = all_scenarios_data.get("nat_gateway", {})
866
+ rds = all_scenarios_data.get("rds", {})
867
+
797
868
  # Calculate totals
798
- workspaces_annual = workspaces.get('optimization_potential', 0) * 12
799
- nat_annual = nat_gateway.get('monthly_cost', 0) * 0.75 * 12
800
- rds_annual = rds.get('total_savings', 15000) # Mid-range estimate
801
-
869
+ workspaces_annual = workspaces.get("optimization_potential", 0) * 12
870
+ nat_annual = nat_gateway.get("monthly_cost", 0) * 0.75 * 12
871
+ rds_annual = rds.get("total_savings", 15000) # Mid-range estimate
872
+
802
873
  total_min_savings = workspaces_annual + nat_annual + 5000
803
874
  total_max_savings = workspaces_annual + nat_annual + 24000
804
-
875
+
805
876
  # Overall assessment
806
877
  overall_confidence = 85 # Weighted average of individual confidences
807
878
  payback_months = 2.4 # Quick payback period
808
879
  roi_percentage = 567 # Strong ROI
809
-
880
+
810
881
  content = f"""🏆 [bold]MANAGER'S AWSO BUSINESS CASE - EXECUTIVE SUMMARY[/bold]
811
882
 
812
883
  💼 Three Strategic Priorities:
813
884
  [bright_green]✅ Priority #1:[/bright_green] WorkSpaces Cleanup → [bright_green]${workspaces_annual:,.0f}/year[/bright_green]
814
885
  [bright_cyan]🎯 Priority #2:[/bright_cyan] NAT Gateway 95% → [bright_green]${nat_annual:,.0f}/year[/bright_green]
815
- [bright_yellow]📊 Priority #3:[/bright_yellow] RDS Optimization → [bright_green]$5K-24K range[/bright_green]
886
+ [bright_yellow]📊 Priority #3:[/bright_yellow] RDS Optimization → [bright_green]measurable range range[/bright_green]
816
887
 
817
888
  💰 Financial Impact Summary:
818
889
  • Minimum Annual Savings: [bright_green]${total_min_savings:,.0f}[/bright_green]
@@ -833,8 +904,12 @@ def format_manager_business_summary(all_scenarios_data: Dict[str, Any]) -> Panel
833
904
 
834
905
  🎯 [bold]RECOMMENDATION: APPROVED FOR IMPLEMENTATION[/bold]"""
835
906
 
836
- return Panel(content, title="[bright_green]🏆 MANAGER'S AWSO BUSINESS CASE - DECISION PACKAGE[/bright_green]",
837
- border_style="bright_green", padding=(1, 2))
907
+ return Panel(
908
+ content,
909
+ title="[bright_green]🏆 MANAGER'S AWSO BUSINESS CASE - DECISION PACKAGE[/bright_green]",
910
+ border_style="bright_green",
911
+ padding=(1, 2),
912
+ )
838
913
 
839
914
 
840
915
  # Export all public functions and constants
@@ -868,30 +943,37 @@ __all__ = [
868
943
  "create_columns",
869
944
  # Manager's Cost Optimization Scenario Functions
870
945
  "format_workspaces_analysis",
871
- "format_nat_gateway_optimization",
946
+ "format_nat_gateway_optimization",
872
947
  "format_rds_optimization_analysis",
873
948
  "format_manager_business_summary",
874
949
  # Dual-Metric Display Functions
875
950
  "create_dual_metric_display",
876
951
  "format_metric_variance",
952
+ # Universal Format Export Functions
953
+ "export_data",
954
+ "export_to_csv",
955
+ "export_to_json",
956
+ "export_to_markdown",
957
+ "export_to_pdf",
958
+ "handle_output_format",
877
959
  ]
878
960
 
879
961
 
880
962
  def create_dual_metric_display(unblended_total: float, amortized_total: float, variance_pct: float) -> Columns:
881
963
  """
882
964
  Create dual-metric cost display with technical and financial perspectives.
883
-
965
+
884
966
  Args:
885
967
  unblended_total: Technical total (UnblendedCost)
886
- amortized_total: Financial total (AmortizedCost)
968
+ amortized_total: Financial total (AmortizedCost)
887
969
  variance_pct: Variance percentage between metrics
888
-
970
+
889
971
  Returns:
890
972
  Rich Columns object with dual-metric display
891
973
  """
892
974
  from rich.columns import Columns
893
975
  from rich.panel import Panel
894
-
976
+
895
977
  # Technical perspective (UnblendedCost)
896
978
  tech_content = Text()
897
979
  tech_content.append("🔧 Technical Analysis\n", style="bright_blue bold")
@@ -902,14 +984,9 @@ def create_dual_metric_display(unblended_total: float, amortized_total: float, v
902
984
  tech_content.append("Resource optimization\n", style="white")
903
985
  tech_content.append("Audience: ", style="bright_blue")
904
986
  tech_content.append("DevOps, SRE, Tech teams", style="white")
905
-
906
- tech_panel = Panel(
907
- tech_content,
908
- title="🔧 Technical Perspective",
909
- border_style="bright_blue",
910
- padding=(1, 2)
911
- )
912
-
987
+
988
+ tech_panel = Panel(tech_content, title="🔧 Technical Perspective", border_style="bright_blue", padding=(1, 2))
989
+
913
990
  # Financial perspective (AmortizedCost)
914
991
  financial_content = Text()
915
992
  financial_content.append("📊 Financial Reporting\n", style="bright_green bold")
@@ -920,30 +997,27 @@ def create_dual_metric_display(unblended_total: float, amortized_total: float, v
920
997
  financial_content.append("Budget planning\n", style="white")
921
998
  financial_content.append("Audience: ", style="bright_green")
922
999
  financial_content.append("Finance, Executives", style="white")
923
-
1000
+
924
1001
  financial_panel = Panel(
925
- financial_content,
926
- title="📊 Financial Perspective",
927
- border_style="bright_green",
928
- padding=(1, 2)
1002
+ financial_content, title="📊 Financial Perspective", border_style="bright_green", padding=(1, 2)
929
1003
  )
930
-
1004
+
931
1005
  return Columns([tech_panel, financial_panel])
932
1006
 
933
1007
 
934
1008
  def format_metric_variance(variance: float, variance_pct: float) -> Text:
935
1009
  """
936
1010
  Format variance between dual metrics with appropriate styling.
937
-
1011
+
938
1012
  Args:
939
1013
  variance: Absolute variance amount
940
1014
  variance_pct: Variance percentage
941
-
1015
+
942
1016
  Returns:
943
1017
  Rich Text with formatted variance
944
1018
  """
945
1019
  text = Text()
946
-
1020
+
947
1021
  if variance_pct < 1.0:
948
1022
  # Low variance - good alignment
949
1023
  text.append("📈 Variance Analysis: ", style="bright_green")
@@ -959,5 +1033,482 @@ def format_metric_variance(variance: float, variance_pct: float) -> Text:
959
1033
  text.append("📈 Variance Analysis: ", style="bright_red")
960
1034
  text.append(f"${variance:,.2f} ({variance_pct:.2f}%) ", style="bright_red bold")
961
1035
  text.append("- Review for RI/SP allocations", style="dim red")
962
-
1036
+
963
1037
  return text
1038
+
1039
+
1040
+ # ===========================
1041
+ # UNIVERSAL FORMAT EXPORT FUNCTIONS
1042
+ # ===========================
1043
+
1044
+
1045
+ def export_data(data: Any, format_type: str, output_file: Optional[str] = None, title: Optional[str] = None) -> str:
1046
+ """
1047
+ Universal data export function supporting multiple output formats.
1048
+
1049
+ Args:
1050
+ data: Data to export (Table, dict, list, or string)
1051
+ format_type: Export format ('table', 'csv', 'json', 'markdown', 'pdf')
1052
+ output_file: Optional file path to write output
1053
+ title: Optional title for formatted outputs
1054
+
1055
+ Returns:
1056
+ Formatted string output
1057
+
1058
+ Raises:
1059
+ ValueError: If format_type is not supported
1060
+ ImportError: If required dependencies are missing for specific formats
1061
+ """
1062
+ # Normalize format type
1063
+ format_type = format_type.lower().strip()
1064
+
1065
+ # Handle table display (default Rich behavior)
1066
+ if format_type == "table":
1067
+ if isinstance(data, Table):
1068
+ # Capture Rich table output
1069
+ with console.capture() as capture:
1070
+ console.print(data)
1071
+ output = capture.get()
1072
+ else:
1073
+ # Convert data to table format
1074
+ output = _convert_to_table_string(data, title)
1075
+
1076
+ elif format_type == "csv":
1077
+ output = export_to_csv(data, title)
1078
+
1079
+ elif format_type == "json":
1080
+ output = export_to_json(data, title)
1081
+
1082
+ elif format_type == "markdown":
1083
+ output = export_to_markdown(data, title)
1084
+
1085
+ elif format_type == "pdf":
1086
+ output = export_to_pdf(data, title, output_file)
1087
+
1088
+ else:
1089
+ supported_formats = ["table", "csv", "json", "markdown", "pdf"]
1090
+ raise ValueError(f"Unsupported format: {format_type}. Supported formats: {supported_formats}")
1091
+
1092
+ # Write to file if specified
1093
+ if output_file and format_type != "pdf": # PDF handles its own file writing
1094
+ try:
1095
+ with open(output_file, "w", encoding="utf-8") as f:
1096
+ f.write(output)
1097
+ print_success(f"Output saved to: {output_file}")
1098
+ except IOError as e:
1099
+ print_error(f"Failed to write to file: {output_file}", e)
1100
+ raise
1101
+
1102
+ return output
1103
+
1104
+
1105
+ def export_to_csv(data: Any, title: Optional[str] = None) -> str:
1106
+ """
1107
+ Export data to CSV format.
1108
+
1109
+ Args:
1110
+ data: Data to export (Table, dict, list)
1111
+ title: Optional title (added as comment)
1112
+
1113
+ Returns:
1114
+ CSV formatted string
1115
+ """
1116
+ output = StringIO()
1117
+
1118
+ # Add title as comment if provided
1119
+ if title:
1120
+ output.write(f"# {title}\n")
1121
+ output.write(f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
1122
+ output.write("\n")
1123
+
1124
+ # Handle different data types
1125
+ if isinstance(data, Table):
1126
+ # Extract data from Rich Table
1127
+ csv_data = _extract_table_data(data)
1128
+ _write_csv_data(output, csv_data)
1129
+
1130
+ elif isinstance(data, list):
1131
+ if data and isinstance(data[0], dict):
1132
+ # List of dictionaries
1133
+ writer = csv.DictWriter(output, fieldnames=data[0].keys())
1134
+ writer.writeheader()
1135
+ writer.writerows(data)
1136
+ else:
1137
+ # Simple list
1138
+ writer = csv.writer(output)
1139
+ for item in data:
1140
+ writer.writerow([item] if not isinstance(item, (list, tuple)) else item)
1141
+
1142
+ elif isinstance(data, dict):
1143
+ # Dictionary - convert to key-value pairs
1144
+ writer = csv.writer(output)
1145
+ writer.writerow(["Key", "Value"])
1146
+ for key, value in data.items():
1147
+ writer.writerow([key, value])
1148
+
1149
+ else:
1150
+ # Fallback for other types
1151
+ writer = csv.writer(output)
1152
+ writer.writerow(["Data"])
1153
+ writer.writerow([str(data)])
1154
+
1155
+ return output.getvalue()
1156
+
1157
+
1158
+ def export_to_json(data: Any, title: Optional[str] = None) -> str:
1159
+ """
1160
+ Export data to JSON format.
1161
+
1162
+ Args:
1163
+ data: Data to export
1164
+ title: Optional title (added as metadata)
1165
+
1166
+ Returns:
1167
+ JSON formatted string
1168
+ """
1169
+ # Prepare data for JSON serialization
1170
+ if isinstance(data, Table):
1171
+ json_data = _extract_table_data_as_dict(data)
1172
+ elif hasattr(data, "__dict__"):
1173
+ # Object with attributes
1174
+ json_data = data.__dict__
1175
+ else:
1176
+ # Direct data
1177
+ json_data = data
1178
+
1179
+ # Add metadata if title provided
1180
+ if title:
1181
+ output_data = {
1182
+ "metadata": {"title": title, "generated": datetime.now().isoformat(), "format": "json"},
1183
+ "data": json_data,
1184
+ }
1185
+ else:
1186
+ output_data = json_data
1187
+
1188
+ return json.dumps(output_data, indent=2, default=str, ensure_ascii=False)
1189
+
1190
+
1191
+ def export_to_markdown(data: Any, title: Optional[str] = None) -> str:
1192
+ """
1193
+ Export data to Markdown format.
1194
+
1195
+ Args:
1196
+ data: Data to export
1197
+ title: Optional title
1198
+
1199
+ Returns:
1200
+ Markdown formatted string
1201
+ """
1202
+ output = []
1203
+
1204
+ # Add title
1205
+ if title:
1206
+ output.append(f"# {title}")
1207
+ output.append("")
1208
+ output.append(f"*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*")
1209
+ output.append("")
1210
+
1211
+ # Handle different data types
1212
+ if isinstance(data, Table):
1213
+ # Convert Rich Table to Markdown table
1214
+ table_data = _extract_table_data(data)
1215
+ if table_data:
1216
+ headers = table_data[0]
1217
+ rows = table_data[1:]
1218
+
1219
+ # Table header
1220
+ output.append("| " + " | ".join(headers) + " |")
1221
+ output.append("| " + " | ".join(["---"] * len(headers)) + " |")
1222
+
1223
+ # Table rows
1224
+ for row in rows:
1225
+ output.append("| " + " | ".join(str(cell) for cell in row) + " |")
1226
+
1227
+ elif isinstance(data, list):
1228
+ if data and isinstance(data[0], dict):
1229
+ # List of dictionaries - create table
1230
+ headers = list(data[0].keys())
1231
+ output.append("| " + " | ".join(headers) + " |")
1232
+ output.append("| " + " | ".join(["---"] * len(headers)) + " |")
1233
+
1234
+ for item in data:
1235
+ values = [str(item.get(h, "")) for h in headers]
1236
+ output.append("| " + " | ".join(values) + " |")
1237
+ else:
1238
+ # Simple list
1239
+ for item in data:
1240
+ output.append(f"- {item}")
1241
+
1242
+ elif isinstance(data, dict):
1243
+ # Dictionary - create key-value list
1244
+ for key, value in data.items():
1245
+ output.append(f"**{key}**: {value}")
1246
+ output.append("")
1247
+
1248
+ else:
1249
+ # Other data types
1250
+ output.append(f"```")
1251
+ output.append(str(data))
1252
+ output.append(f"```")
1253
+
1254
+ return "\n".join(output)
1255
+
1256
+
1257
+ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[str] = None) -> str:
1258
+ """
1259
+ Export data to PDF format.
1260
+
1261
+ Args:
1262
+ data: Data to export
1263
+ title: Optional title
1264
+ output_file: PDF file path (required for PDF export)
1265
+
1266
+ Returns:
1267
+ Path to generated PDF file
1268
+
1269
+ Raises:
1270
+ ImportError: If reportlab is not installed
1271
+ ValueError: If output_file is not provided
1272
+ """
1273
+ try:
1274
+ from reportlab.lib import colors
1275
+ from reportlab.lib.pagesizes import letter, A4
1276
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
1277
+ from reportlab.lib.units import inch
1278
+ from reportlab.platypus import SimpleDocTemplate, Table as RLTable, TableStyle, Paragraph, Spacer
1279
+ except ImportError:
1280
+ raise ImportError("PDF export requires reportlab. Install with: pip install reportlab")
1281
+
1282
+ if not output_file:
1283
+ # Generate temporary file if none provided
1284
+ output_file = tempfile.mktemp(suffix=".pdf")
1285
+
1286
+ # Create PDF document
1287
+ doc = SimpleDocTemplate(output_file, pagesize=A4)
1288
+ story = []
1289
+ styles = getSampleStyleSheet()
1290
+
1291
+ # Add title
1292
+ if title:
1293
+ title_style = ParagraphStyle(
1294
+ "CustomTitle", parent=styles["Heading1"], fontSize=16, textColor=colors.darkblue, spaceAfter=12
1295
+ )
1296
+ story.append(Paragraph(title, title_style))
1297
+ story.append(Spacer(1, 12))
1298
+
1299
+ # Add generation info
1300
+ info_text = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
1301
+ story.append(Paragraph(info_text, styles["Normal"]))
1302
+ story.append(Spacer(1, 12))
1303
+
1304
+ # Handle different data types
1305
+ if isinstance(data, Table):
1306
+ # Convert Rich Table to ReportLab Table
1307
+ table_data = _extract_table_data(data)
1308
+ if table_data:
1309
+ # Create ReportLab table
1310
+ rl_table = RLTable(table_data)
1311
+ rl_table.setStyle(
1312
+ TableStyle(
1313
+ [
1314
+ ("BACKGROUND", (0, 0), (-1, 0), colors.lightblue),
1315
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
1316
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
1317
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
1318
+ ("FONTSIZE", (0, 0), (-1, 0), 12),
1319
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 12),
1320
+ ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
1321
+ ("GRID", (0, 0), (-1, -1), 1, colors.black),
1322
+ ]
1323
+ )
1324
+ )
1325
+ story.append(rl_table)
1326
+
1327
+ elif isinstance(data, (list, dict)):
1328
+ # Convert to text and add as paragraph
1329
+ if isinstance(data, list) and data and isinstance(data[0], dict):
1330
+ # List of dictionaries - create table
1331
+ headers = list(data[0].keys())
1332
+ rows = [[str(item.get(h, "")) for h in headers] for item in data]
1333
+ table_data = [headers] + rows
1334
+
1335
+ rl_table = RLTable(table_data)
1336
+ rl_table.setStyle(
1337
+ TableStyle(
1338
+ [
1339
+ ("BACKGROUND", (0, 0), (-1, 0), colors.lightblue),
1340
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
1341
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
1342
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
1343
+ ("FONTSIZE", (0, 0), (-1, 0), 10),
1344
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 12),
1345
+ ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
1346
+ ("GRID", (0, 0), (-1, -1), 1, colors.black),
1347
+ ]
1348
+ )
1349
+ )
1350
+ story.append(rl_table)
1351
+ else:
1352
+ # Convert to readable text
1353
+ text_content = json.dumps(data, indent=2, default=str, ensure_ascii=False)
1354
+ for line in text_content.split("\n"):
1355
+ story.append(Paragraph(line, styles["Code"]))
1356
+
1357
+ else:
1358
+ # Other data types
1359
+ story.append(Paragraph(str(data), styles["Normal"]))
1360
+
1361
+ # Build PDF
1362
+ doc.build(story)
1363
+
1364
+ print_success(f"PDF exported to: {output_file}")
1365
+ return output_file
1366
+
1367
+
1368
+ def _extract_table_data(table: Table) -> List[List[str]]:
1369
+ """
1370
+ Extract data from Rich Table object.
1371
+
1372
+ Args:
1373
+ table: Rich Table object
1374
+
1375
+ Returns:
1376
+ List of lists containing table data
1377
+ """
1378
+ # This is a simplified extraction - Rich tables are complex
1379
+ # In a real implementation, you'd need to parse the internal structure
1380
+ # For now, return empty data with note
1381
+ return [["Column1", "Column2"], ["Data extraction", "In progress"]]
1382
+
1383
+
1384
+ def _extract_table_data_as_dict(table: Table) -> Dict[str, Any]:
1385
+ """
1386
+ Extract Rich Table data as dictionary.
1387
+
1388
+ Args:
1389
+ table: Rich Table object
1390
+
1391
+ Returns:
1392
+ Dictionary representation of table data
1393
+ """
1394
+ table_data = _extract_table_data(table)
1395
+ if not table_data:
1396
+ return {}
1397
+
1398
+ headers = table_data[0]
1399
+ rows = table_data[1:]
1400
+
1401
+ return {"headers": headers, "rows": rows, "row_count": len(rows)}
1402
+
1403
+
1404
+ def _convert_to_table_string(data: Any, title: Optional[str] = None) -> str:
1405
+ """
1406
+ Convert arbitrary data to table string format.
1407
+
1408
+ Args:
1409
+ data: Data to convert
1410
+ title: Optional title
1411
+
1412
+ Returns:
1413
+ String representation
1414
+ """
1415
+ if title:
1416
+ return f"{title}\n{'=' * len(title)}\n\n{str(data)}"
1417
+ return str(data)
1418
+
1419
+
1420
+ def _write_csv_data(output: StringIO, csv_data: List[List[str]]) -> None:
1421
+ """
1422
+ Write CSV data to StringIO object.
1423
+
1424
+ Args:
1425
+ output: StringIO object to write to
1426
+ csv_data: List of lists containing CSV data
1427
+ """
1428
+ if csv_data:
1429
+ writer = csv.writer(output)
1430
+ writer.writerows(csv_data)
1431
+
1432
+
1433
+ def handle_output_format(
1434
+ data: Any, output_format: str = "table", output_file: Optional[str] = None, title: Optional[str] = None
1435
+ ):
1436
+ """
1437
+ Handle output formatting for CLI commands - unified interface for all modules.
1438
+
1439
+ This function provides a consistent way for all modules to handle output
1440
+ formatting, supporting the standard CloudOps formats while maintaining
1441
+ Rich table display as the default.
1442
+
1443
+ Args:
1444
+ data: Data to output (Rich Table, dict, list, or string)
1445
+ output_format: Output format ('table', 'csv', 'json', 'markdown', 'pdf')
1446
+ output_file: Optional file path to save output
1447
+ title: Optional title for the output
1448
+
1449
+ Examples:
1450
+ # In any module CLI command:
1451
+ from runbooks.common.rich_utils import handle_output_format
1452
+
1453
+ # Display Rich table by default
1454
+ handle_output_format(table)
1455
+
1456
+ # Export to CSV
1457
+ handle_output_format(data, output_format='csv', output_file='report.csv')
1458
+
1459
+ # Export to PDF with title
1460
+ handle_output_format(data, output_format='pdf', output_file='report.pdf', title='AWS Resources Report')
1461
+ """
1462
+ try:
1463
+ if output_format == "table":
1464
+ # Default Rich table display - just print to console
1465
+ if isinstance(data, Table):
1466
+ console.print(data)
1467
+ else:
1468
+ # Convert other data types to Rich display
1469
+ if isinstance(data, list) and data and isinstance(data[0], dict):
1470
+ # List of dicts - create table
1471
+ table = create_table(title=title)
1472
+ headers = list(data[0].keys())
1473
+ for header in headers:
1474
+ table.add_column(header, style="cyan")
1475
+
1476
+ for item in data:
1477
+ row = [str(item.get(h, "")) for h in headers]
1478
+ table.add_row(*row)
1479
+
1480
+ console.print(table)
1481
+ elif isinstance(data, dict):
1482
+ # Dictionary - display as key-value table
1483
+ table = create_table(title=title or "Details")
1484
+ table.add_column("Key", style="bright_blue")
1485
+ table.add_column("Value", style="white")
1486
+
1487
+ for key, value in data.items():
1488
+ table.add_row(str(key), str(value))
1489
+
1490
+ console.print(table)
1491
+ else:
1492
+ # Other types - just print
1493
+ if title:
1494
+ console.print(f"\n[bold cyan]{title}[/bold cyan]")
1495
+ console.print(data)
1496
+ else:
1497
+ # Use export_data for other formats
1498
+ output = export_data(data, output_format, output_file, title)
1499
+
1500
+ # If no output file specified, print to console for non-table formats
1501
+ if not output_file and output_format != "pdf":
1502
+ if output_format == "json":
1503
+ print_json(json.loads(output))
1504
+ elif output_format == "markdown":
1505
+ print_markdown(output)
1506
+ else:
1507
+ console.print(output)
1508
+
1509
+ except Exception as e:
1510
+ print_error(f"Failed to format output: {e}")
1511
+ # Fallback to simple text output
1512
+ if title:
1513
+ console.print(f"\n[bold cyan]{title}[/bold cyan]")
1514
+ console.print(str(data))