runbooks 1.1.4__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 (228) 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/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +138 -35
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +11 -0
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +63 -74
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +201 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/cloud_foundations_integration.py +144 -149
  113. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  114. runbooks/inventory/collectors/aws_networking.py +109 -99
  115. runbooks/inventory/collectors/base.py +4 -0
  116. runbooks/inventory/core/collector.py +495 -313
  117. runbooks/inventory/drift_detection_cli.py +69 -96
  118. runbooks/inventory/inventory_mcp_cli.py +48 -46
  119. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  120. runbooks/inventory/mcp_inventory_validator.py +549 -465
  121. runbooks/inventory/mcp_vpc_validator.py +359 -442
  122. runbooks/inventory/organizations_discovery.py +55 -51
  123. runbooks/inventory/rich_inventory_display.py +33 -32
  124. runbooks/inventory/unified_validation_engine.py +278 -251
  125. runbooks/inventory/vpc_analyzer.py +732 -695
  126. runbooks/inventory/vpc_architecture_validator.py +293 -348
  127. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  128. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  129. runbooks/main.py +49 -34
  130. runbooks/main_final.py +91 -60
  131. runbooks/main_minimal.py +22 -10
  132. runbooks/main_optimized.py +131 -100
  133. runbooks/main_ultra_minimal.py +7 -2
  134. runbooks/mcp/__init__.py +36 -0
  135. runbooks/mcp/integration.py +679 -0
  136. runbooks/monitoring/performance_monitor.py +9 -4
  137. runbooks/operate/dynamodb_operations.py +3 -1
  138. runbooks/operate/ec2_operations.py +145 -137
  139. runbooks/operate/iam_operations.py +146 -152
  140. runbooks/operate/networking_cost_heatmap.py +29 -8
  141. runbooks/operate/rds_operations.py +223 -254
  142. runbooks/operate/s3_operations.py +107 -118
  143. runbooks/operate/vpc_operations.py +646 -616
  144. runbooks/remediation/base.py +1 -1
  145. runbooks/remediation/commons.py +10 -7
  146. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  147. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  148. runbooks/remediation/multi_account.py +24 -21
  149. runbooks/remediation/rds_snapshot_list.py +86 -60
  150. runbooks/remediation/remediation_cli.py +92 -146
  151. runbooks/remediation/universal_account_discovery.py +83 -79
  152. runbooks/remediation/workspaces_list.py +46 -41
  153. runbooks/security/__init__.py +19 -0
  154. runbooks/security/assessment_runner.py +1150 -0
  155. runbooks/security/baseline_checker.py +812 -0
  156. runbooks/security/cloudops_automation_security_validator.py +509 -535
  157. runbooks/security/compliance_automation_engine.py +17 -17
  158. runbooks/security/config/__init__.py +2 -2
  159. runbooks/security/config/compliance_config.py +50 -50
  160. runbooks/security/config_template_generator.py +63 -76
  161. runbooks/security/enterprise_security_framework.py +1 -1
  162. runbooks/security/executive_security_dashboard.py +519 -508
  163. runbooks/security/multi_account_security_controls.py +959 -1210
  164. runbooks/security/real_time_security_monitor.py +422 -444
  165. runbooks/security/security_baseline_tester.py +1 -1
  166. runbooks/security/security_cli.py +143 -112
  167. runbooks/security/test_2way_validation.py +439 -0
  168. runbooks/security/two_way_validation_framework.py +852 -0
  169. runbooks/sre/production_monitoring_framework.py +167 -177
  170. runbooks/tdd/__init__.py +15 -0
  171. runbooks/tdd/cli.py +1071 -0
  172. runbooks/utils/__init__.py +14 -17
  173. runbooks/utils/logger.py +7 -2
  174. runbooks/utils/version_validator.py +50 -47
  175. runbooks/validation/__init__.py +6 -6
  176. runbooks/validation/cli.py +9 -3
  177. runbooks/validation/comprehensive_2way_validator.py +745 -704
  178. runbooks/validation/mcp_validator.py +906 -228
  179. runbooks/validation/terraform_citations_validator.py +104 -115
  180. runbooks/validation/terraform_drift_detector.py +447 -451
  181. runbooks/vpc/README.md +617 -0
  182. runbooks/vpc/__init__.py +8 -1
  183. runbooks/vpc/analyzer.py +577 -0
  184. runbooks/vpc/cleanup_wrapper.py +476 -413
  185. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  186. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  187. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  188. runbooks/vpc/config.py +92 -97
  189. runbooks/vpc/cost_engine.py +411 -148
  190. runbooks/vpc/cost_explorer_integration.py +553 -0
  191. runbooks/vpc/cross_account_session.py +101 -106
  192. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  193. runbooks/vpc/eni_gate_validator.py +961 -0
  194. runbooks/vpc/heatmap_engine.py +185 -160
  195. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  196. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  197. runbooks/vpc/networking_wrapper.py +15 -8
  198. runbooks/vpc/pdca_remediation_planner.py +528 -0
  199. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  200. runbooks/vpc/runbooks_adapter.py +1167 -241
  201. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  202. runbooks/vpc/test_data_loader.py +358 -0
  203. runbooks/vpc/tests/conftest.py +314 -4
  204. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  205. runbooks/vpc/tests/test_cost_engine.py +0 -2
  206. runbooks/vpc/topology_generator.py +326 -0
  207. runbooks/vpc/unified_scenarios.py +1297 -1124
  208. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  209. runbooks-1.1.5.dist-info/METADATA +328 -0
  210. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
  211. runbooks/finops/README.md +0 -414
  212. runbooks/finops/accuracy_cross_validator.py +0 -647
  213. runbooks/finops/business_cases.py +0 -950
  214. runbooks/finops/dashboard_router.py +0 -922
  215. runbooks/finops/ebs_optimizer.py +0 -973
  216. runbooks/finops/embedded_mcp_validator.py +0 -1629
  217. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  218. runbooks/finops/finops_dashboard.py +0 -584
  219. runbooks/finops/finops_scenarios.py +0 -1218
  220. runbooks/finops/legacy_migration.py +0 -730
  221. runbooks/finops/multi_dashboard.py +0 -1519
  222. runbooks/finops/single_dashboard.py +0 -1113
  223. runbooks/finops/unlimited_scenarios.py +0 -393
  224. runbooks-1.1.4.dist-info/METADATA +0 -800
  225. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  226. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  227. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  228. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,7 @@ 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
@@ -20,6 +21,9 @@ Version: 0.7.8
20
21
 
21
22
  import csv
22
23
  import json
24
+ import os
25
+ import re
26
+ import sys
23
27
  import tempfile
24
28
  from datetime import datetime
25
29
  from io import StringIO
@@ -27,7 +31,6 @@ from typing import Any, Dict, List, Optional, Union
27
31
 
28
32
  from rich import box
29
33
  from rich.columns import Columns
30
- from rich.console import Console
31
34
  from rich.layout import Layout
32
35
  from rich.markdown import Markdown
33
36
  from rich.panel import Panel
@@ -35,11 +38,58 @@ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskProgressColumn
35
38
  from rich.rule import Rule
36
39
  from rich.style import Style
37
40
  from rich.syntax import Syntax
38
- from rich.table import Table
41
+ from rich.table import Table as RichTable
39
42
  from rich.text import Text
40
43
  from rich.theme import Theme
41
44
  from rich.tree import Tree
42
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
+
43
93
  # CloudOps Custom Theme
44
94
  CLOUDOPS_THEME = Theme(
45
95
  {
@@ -59,8 +109,11 @@ CLOUDOPS_THEME = Theme(
59
109
  }
60
110
  )
61
111
 
62
- # Initialize console with custom theme
63
- 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
64
117
 
65
118
  # Status indicators
66
119
  STATUS_INDICATORS = {
@@ -109,6 +162,7 @@ def print_header(title: str, version: Optional[str] = None) -> None:
109
162
  """
110
163
  if version is None:
111
164
  from runbooks import __version__
165
+
112
166
  version = __version__
113
167
 
114
168
  header_text = Text()
@@ -124,7 +178,10 @@ def print_header(title: str, version: Optional[str] = None) -> None:
124
178
  def print_banner() -> None:
125
179
  """Print a clean, minimal CloudOps Runbooks banner."""
126
180
  from runbooks import __version__
127
- 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
+ )
128
185
  console.print()
129
186
 
130
187
 
@@ -431,7 +488,9 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
431
488
  return f"{profile_name[: max_length - 3]}..."
432
489
 
433
490
 
434
- 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:
435
494
  """
436
495
  Format profile name with consistent styling, intelligent truncation, and security enhancements.
437
496
 
@@ -449,7 +508,7 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
449
508
 
450
509
  Returns:
451
510
  Rich Text object with formatted profile name
452
-
511
+
453
512
  Security Note:
454
513
  When secure_logging=True, account IDs are masked in display to prevent
455
514
  account enumeration while maintaining profile identification.
@@ -458,13 +517,14 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
458
517
  if secure_logging:
459
518
  try:
460
519
  from runbooks.common.aws_utils import AWSProfileSanitizer
520
+
461
521
  display_profile = AWSProfileSanitizer.sanitize_profile_name(profile_name)
462
522
  except ImportError:
463
523
  # Fallback to original profile if aws_utils not available
464
524
  display_profile = profile_name
465
525
  else:
466
526
  display_profile = profile_name
467
-
527
+
468
528
  display_name = create_display_profile_name(display_profile, display_max_length)
469
529
 
470
530
  text = Text()
@@ -476,7 +536,7 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
476
536
  else:
477
537
  # Full name - normal style
478
538
  text.append(display_name, style=style)
479
-
539
+
480
540
  # Add security indicator for sanitized profiles
481
541
  if secure_logging and "***masked***" in display_name:
482
542
  text.append(" 🔒", style="dim yellow")
@@ -623,21 +683,21 @@ def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings:
623
683
  Args:
624
684
  workspaces_data: Dictionary containing WorkSpaces cost and utilization data
625
685
  target_savings: Annual savings target (default: $12,518)
626
-
686
+
627
687
  Returns:
628
688
  Rich Panel with formatted WorkSpaces analysis
629
689
  """
630
- current_cost = workspaces_data.get('monthly_cost', 0)
631
- unused_count = workspaces_data.get('unused_count', 0)
632
- total_count = workspaces_data.get('total_count', 0)
633
- optimization_potential = workspaces_data.get('optimization_potential', 0)
634
-
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
+
635
695
  annual_savings = optimization_potential * 12
636
696
  target_achievement = min(100, (annual_savings / target_savings) * 100) if target_savings > 0 else 0
637
-
697
+
638
698
  status = "🎯 TARGET ACHIEVABLE" if target_achievement >= 90 else "⚠️ TARGET REQUIRES EXPANDED SCOPE"
639
699
  status_style = "bright_green" if target_achievement >= 90 else "yellow"
640
-
700
+
641
701
  content = f"""💼 [bold]Manager's Priority #1: WorkSpaces Cleanup Analysis[/bold]
642
702
 
643
703
  📊 Current State:
@@ -658,35 +718,38 @@ def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings:
658
718
 
659
719
  [{status_style}]{status}[/]"""
660
720
 
661
- return Panel(content, title="[bright_cyan]WorkSpaces Cost Optimization[/bright_cyan]",
662
- 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
+ )
663
726
 
664
727
 
665
728
  def format_nat_gateway_optimization(nat_data: Dict[str, Any], target_completion: int = 95) -> Panel:
666
729
  """
667
730
  Format NAT Gateway optimization analysis for manager's completion target.
668
-
731
+
669
732
  Manager's requirement to increase NAT Gateway optimization from 75% to 95% completion.
670
-
733
+
671
734
  Args:
672
735
  nat_data: Dictionary containing NAT Gateway configuration and cost data
673
736
  target_completion: Completion target percentage (default: 95% from manager's priority)
674
-
737
+
675
738
  Returns:
676
739
  Rich Panel with formatted NAT Gateway optimization analysis
677
740
  """
678
- total_gateways = nat_data.get('total', 0)
679
- active_gateways = nat_data.get('active', 0)
680
- monthly_cost = nat_data.get('monthly_cost', 0)
681
- optimization_ready = nat_data.get('optimization_ready', 0)
682
-
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
+
683
746
  current_completion = 75 # Manager specified current state
684
747
  optimization_potential = monthly_cost * 0.75 # 75% can be optimized
685
748
  annual_savings = optimization_potential * 12
686
-
749
+
687
750
  completion_gap = target_completion - current_completion
688
751
  status = "🎯 READY FOR 95% TARGET" if active_gateways > 0 else "❌ NO OPTIMIZATION OPPORTUNITIES"
689
-
752
+
690
753
  content = f"""🌐 [bold]Manager's Priority #2: NAT Gateway Optimization[/bold]
691
754
 
692
755
  🔍 Current Infrastructure:
@@ -711,45 +774,46 @@ def format_nat_gateway_optimization(nat_data: Dict[str, Any], target_completion:
711
774
 
712
775
  [bright_green]{status}[/bright_green]"""
713
776
 
714
- return Panel(content, title="[bright_cyan]Manager's Priority #2: NAT Gateway Optimization[/bright_cyan]",
715
- border_style="cyan")
777
+ return Panel(
778
+ content, title="[bright_cyan]Manager's Priority #2: NAT Gateway Optimization[/bright_cyan]", border_style="cyan"
779
+ )
716
780
 
717
781
 
718
782
  def format_rds_optimization_analysis(rds_data: Dict[str, Any], savings_range: Dict[str, int] = None) -> Panel:
719
783
  """
720
784
  Format RDS Multi-AZ optimization analysis for manager's FinOps-23 scenario.
721
-
785
+
722
786
  Manager's requirement for measurable range annual savings through RDS manual snapshot cleanup
723
787
  and Multi-AZ configuration review.
724
-
788
+
725
789
  Args:
726
790
  rds_data: Dictionary containing RDS instance and snapshot data
727
791
  savings_range: Dict with 'min' and 'max' annual savings (default: {'min': 5000, 'max': 24000})
728
-
792
+
729
793
  Returns:
730
794
  Rich Panel with formatted RDS optimization analysis
731
795
  """
732
796
  if savings_range is None:
733
- savings_range = {'min': 5000, 'max': 24000}
734
-
735
- total_instances = rds_data.get('total', 0)
736
- multi_az_instances = rds_data.get('multi_az_instances', 0)
737
- manual_snapshots = rds_data.get('manual_snapshots', 0)
738
- snapshot_storage_gb = rds_data.get('snapshot_storage_gb', 0)
739
-
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
+
740
804
  # Calculate savings potential
741
805
  snapshot_savings = snapshot_storage_gb * 0.095 * 12 # $0.095/GB/month
742
806
  multi_az_savings = multi_az_instances * 1000 * 12 # ~$1K/month per instance
743
807
  total_savings = snapshot_savings + multi_az_savings
744
-
745
- savings_min = savings_range['min']
746
- savings_max = savings_range['max']
747
-
808
+
809
+ savings_min = savings_range["min"]
810
+ savings_max = savings_range["max"]
811
+
748
812
  # Check if we're within manager's target range
749
813
  within_range = savings_min <= total_savings <= savings_max
750
814
  range_status = "✅ WITHIN TARGET RANGE" if within_range else "📊 ANALYSIS PENDING"
751
815
  range_style = "bright_green" if within_range else "yellow"
752
-
816
+
753
817
  content = f"""🗄️ [bold]Manager's Priority #3: RDS Cost Optimization[/bold]
754
818
 
755
819
  📊 Current RDS Environment:
@@ -775,42 +839,45 @@ def format_rds_optimization_analysis(rds_data: Dict[str, Any], savings_range: Di
775
839
 
776
840
  [{range_style}]{range_status}[/]"""
777
841
 
778
- return Panel(content, title="[bright_cyan]FinOps-23: RDS Multi-AZ & Snapshot Optimization[/bright_cyan]",
779
- 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
+ )
780
847
 
781
848
 
782
849
  def format_manager_business_summary(all_scenarios_data: Dict[str, Any]) -> Panel:
783
850
  """
784
851
  Format executive summary panel for manager's complete AWSO business case.
785
-
852
+
786
853
  Combines all three manager priorities into executive-ready decision package:
787
854
  - FinOps-24: WorkSpaces cleanup ($12,518)
788
855
  - Manager Priority #2: NAT Gateway optimization (95% completion)
789
856
  - FinOps-23: RDS optimization (measurable range range)
790
-
857
+
791
858
  Args:
792
859
  all_scenarios_data: Dictionary containing data from all three scenarios
793
-
860
+
794
861
  Returns:
795
862
  Rich Panel with complete executive summary
796
863
  """
797
- workspaces = all_scenarios_data.get('workspaces', {})
798
- nat_gateway = all_scenarios_data.get('nat_gateway', {})
799
- rds = all_scenarios_data.get('rds', {})
800
-
864
+ workspaces = all_scenarios_data.get("workspaces", {})
865
+ nat_gateway = all_scenarios_data.get("nat_gateway", {})
866
+ rds = all_scenarios_data.get("rds", {})
867
+
801
868
  # Calculate totals
802
- workspaces_annual = workspaces.get('optimization_potential', 0) * 12
803
- nat_annual = nat_gateway.get('monthly_cost', 0) * 0.75 * 12
804
- rds_annual = rds.get('total_savings', 15000) # Mid-range estimate
805
-
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
+
806
873
  total_min_savings = workspaces_annual + nat_annual + 5000
807
874
  total_max_savings = workspaces_annual + nat_annual + 24000
808
-
875
+
809
876
  # Overall assessment
810
877
  overall_confidence = 85 # Weighted average of individual confidences
811
878
  payback_months = 2.4 # Quick payback period
812
879
  roi_percentage = 567 # Strong ROI
813
-
880
+
814
881
  content = f"""🏆 [bold]MANAGER'S AWSO BUSINESS CASE - EXECUTIVE SUMMARY[/bold]
815
882
 
816
883
  💼 Three Strategic Priorities:
@@ -837,8 +904,12 @@ def format_manager_business_summary(all_scenarios_data: Dict[str, Any]) -> Panel
837
904
 
838
905
  🎯 [bold]RECOMMENDATION: APPROVED FOR IMPLEMENTATION[/bold]"""
839
906
 
840
- return Panel(content, title="[bright_green]🏆 MANAGER'S AWSO BUSINESS CASE - DECISION PACKAGE[/bright_green]",
841
- 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
+ )
842
913
 
843
914
 
844
915
  # Export all public functions and constants
@@ -891,18 +962,18 @@ __all__ = [
891
962
  def create_dual_metric_display(unblended_total: float, amortized_total: float, variance_pct: float) -> Columns:
892
963
  """
893
964
  Create dual-metric cost display with technical and financial perspectives.
894
-
965
+
895
966
  Args:
896
967
  unblended_total: Technical total (UnblendedCost)
897
- amortized_total: Financial total (AmortizedCost)
968
+ amortized_total: Financial total (AmortizedCost)
898
969
  variance_pct: Variance percentage between metrics
899
-
970
+
900
971
  Returns:
901
972
  Rich Columns object with dual-metric display
902
973
  """
903
974
  from rich.columns import Columns
904
975
  from rich.panel import Panel
905
-
976
+
906
977
  # Technical perspective (UnblendedCost)
907
978
  tech_content = Text()
908
979
  tech_content.append("🔧 Technical Analysis\n", style="bright_blue bold")
@@ -913,14 +984,9 @@ def create_dual_metric_display(unblended_total: float, amortized_total: float, v
913
984
  tech_content.append("Resource optimization\n", style="white")
914
985
  tech_content.append("Audience: ", style="bright_blue")
915
986
  tech_content.append("DevOps, SRE, Tech teams", style="white")
916
-
917
- tech_panel = Panel(
918
- tech_content,
919
- title="🔧 Technical Perspective",
920
- border_style="bright_blue",
921
- padding=(1, 2)
922
- )
923
-
987
+
988
+ tech_panel = Panel(tech_content, title="🔧 Technical Perspective", border_style="bright_blue", padding=(1, 2))
989
+
924
990
  # Financial perspective (AmortizedCost)
925
991
  financial_content = Text()
926
992
  financial_content.append("📊 Financial Reporting\n", style="bright_green bold")
@@ -931,14 +997,11 @@ def create_dual_metric_display(unblended_total: float, amortized_total: float, v
931
997
  financial_content.append("Budget planning\n", style="white")
932
998
  financial_content.append("Audience: ", style="bright_green")
933
999
  financial_content.append("Finance, Executives", style="white")
934
-
1000
+
935
1001
  financial_panel = Panel(
936
- financial_content,
937
- title="📊 Financial Perspective",
938
- border_style="bright_green",
939
- padding=(1, 2)
1002
+ financial_content, title="📊 Financial Perspective", border_style="bright_green", padding=(1, 2)
940
1003
  )
941
-
1004
+
942
1005
  return Columns([tech_panel, financial_panel])
943
1006
 
944
1007
 
@@ -978,6 +1041,7 @@ def format_metric_variance(variance: float, variance_pct: float) -> Text:
978
1041
  # UNIVERSAL FORMAT EXPORT FUNCTIONS
979
1042
  # ===========================
980
1043
 
1044
+
981
1045
  def export_data(data: Any, format_type: str, output_file: Optional[str] = None, title: Optional[str] = None) -> str:
982
1046
  """
983
1047
  Universal data export function supporting multiple output formats.
@@ -999,7 +1063,7 @@ def export_data(data: Any, format_type: str, output_file: Optional[str] = None,
999
1063
  format_type = format_type.lower().strip()
1000
1064
 
1001
1065
  # Handle table display (default Rich behavior)
1002
- if format_type == 'table':
1066
+ if format_type == "table":
1003
1067
  if isinstance(data, Table):
1004
1068
  # Capture Rich table output
1005
1069
  with console.capture() as capture:
@@ -1009,26 +1073,26 @@ def export_data(data: Any, format_type: str, output_file: Optional[str] = None,
1009
1073
  # Convert data to table format
1010
1074
  output = _convert_to_table_string(data, title)
1011
1075
 
1012
- elif format_type == 'csv':
1076
+ elif format_type == "csv":
1013
1077
  output = export_to_csv(data, title)
1014
1078
 
1015
- elif format_type == 'json':
1079
+ elif format_type == "json":
1016
1080
  output = export_to_json(data, title)
1017
1081
 
1018
- elif format_type == 'markdown':
1082
+ elif format_type == "markdown":
1019
1083
  output = export_to_markdown(data, title)
1020
1084
 
1021
- elif format_type == 'pdf':
1085
+ elif format_type == "pdf":
1022
1086
  output = export_to_pdf(data, title, output_file)
1023
1087
 
1024
1088
  else:
1025
- supported_formats = ['table', 'csv', 'json', 'markdown', 'pdf']
1089
+ supported_formats = ["table", "csv", "json", "markdown", "pdf"]
1026
1090
  raise ValueError(f"Unsupported format: {format_type}. Supported formats: {supported_formats}")
1027
1091
 
1028
1092
  # Write to file if specified
1029
- if output_file and format_type != 'pdf': # PDF handles its own file writing
1093
+ if output_file and format_type != "pdf": # PDF handles its own file writing
1030
1094
  try:
1031
- with open(output_file, 'w', encoding='utf-8') as f:
1095
+ with open(output_file, "w", encoding="utf-8") as f:
1032
1096
  f.write(output)
1033
1097
  print_success(f"Output saved to: {output_file}")
1034
1098
  except IOError as e:
@@ -1078,14 +1142,14 @@ def export_to_csv(data: Any, title: Optional[str] = None) -> str:
1078
1142
  elif isinstance(data, dict):
1079
1143
  # Dictionary - convert to key-value pairs
1080
1144
  writer = csv.writer(output)
1081
- writer.writerow(['Key', 'Value'])
1145
+ writer.writerow(["Key", "Value"])
1082
1146
  for key, value in data.items():
1083
1147
  writer.writerow([key, value])
1084
1148
 
1085
1149
  else:
1086
1150
  # Fallback for other types
1087
1151
  writer = csv.writer(output)
1088
- writer.writerow(['Data'])
1152
+ writer.writerow(["Data"])
1089
1153
  writer.writerow([str(data)])
1090
1154
 
1091
1155
  return output.getvalue()
@@ -1105,7 +1169,7 @@ def export_to_json(data: Any, title: Optional[str] = None) -> str:
1105
1169
  # Prepare data for JSON serialization
1106
1170
  if isinstance(data, Table):
1107
1171
  json_data = _extract_table_data_as_dict(data)
1108
- elif hasattr(data, '__dict__'):
1172
+ elif hasattr(data, "__dict__"):
1109
1173
  # Object with attributes
1110
1174
  json_data = data.__dict__
1111
1175
  else:
@@ -1115,12 +1179,8 @@ def export_to_json(data: Any, title: Optional[str] = None) -> str:
1115
1179
  # Add metadata if title provided
1116
1180
  if title:
1117
1181
  output_data = {
1118
- "metadata": {
1119
- "title": title,
1120
- "generated": datetime.now().isoformat(),
1121
- "format": "json"
1122
- },
1123
- "data": json_data
1182
+ "metadata": {"title": title, "generated": datetime.now().isoformat(), "format": "json"},
1183
+ "data": json_data,
1124
1184
  }
1125
1185
  else:
1126
1186
  output_data = json_data
@@ -1217,13 +1277,11 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1217
1277
  from reportlab.lib.units import inch
1218
1278
  from reportlab.platypus import SimpleDocTemplate, Table as RLTable, TableStyle, Paragraph, Spacer
1219
1279
  except ImportError:
1220
- raise ImportError(
1221
- "PDF export requires reportlab. Install with: pip install reportlab"
1222
- )
1280
+ raise ImportError("PDF export requires reportlab. Install with: pip install reportlab")
1223
1281
 
1224
1282
  if not output_file:
1225
1283
  # Generate temporary file if none provided
1226
- output_file = tempfile.mktemp(suffix='.pdf')
1284
+ output_file = tempfile.mktemp(suffix=".pdf")
1227
1285
 
1228
1286
  # Create PDF document
1229
1287
  doc = SimpleDocTemplate(output_file, pagesize=A4)
@@ -1233,18 +1291,14 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1233
1291
  # Add title
1234
1292
  if title:
1235
1293
  title_style = ParagraphStyle(
1236
- 'CustomTitle',
1237
- parent=styles['Heading1'],
1238
- fontSize=16,
1239
- textColor=colors.darkblue,
1240
- spaceAfter=12
1294
+ "CustomTitle", parent=styles["Heading1"], fontSize=16, textColor=colors.darkblue, spaceAfter=12
1241
1295
  )
1242
1296
  story.append(Paragraph(title, title_style))
1243
1297
  story.append(Spacer(1, 12))
1244
1298
 
1245
1299
  # Add generation info
1246
1300
  info_text = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
1247
- story.append(Paragraph(info_text, styles['Normal']))
1301
+ story.append(Paragraph(info_text, styles["Normal"]))
1248
1302
  story.append(Spacer(1, 12))
1249
1303
 
1250
1304
  # Handle different data types
@@ -1254,16 +1308,20 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1254
1308
  if table_data:
1255
1309
  # Create ReportLab table
1256
1310
  rl_table = RLTable(table_data)
1257
- rl_table.setStyle(TableStyle([
1258
- ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
1259
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
1260
- ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
1261
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
1262
- ('FONTSIZE', (0, 0), (-1, 0), 12),
1263
- ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
1264
- ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
1265
- ('GRID', (0, 0), (-1, -1), 1, colors.black)
1266
- ]))
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
+ )
1267
1325
  story.append(rl_table)
1268
1326
 
1269
1327
  elif isinstance(data, (list, dict)):
@@ -1275,26 +1333,30 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1275
1333
  table_data = [headers] + rows
1276
1334
 
1277
1335
  rl_table = RLTable(table_data)
1278
- rl_table.setStyle(TableStyle([
1279
- ('BACKGROUND', (0, 0), (-1, 0), colors.lightblue),
1280
- ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
1281
- ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
1282
- ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
1283
- ('FONTSIZE', (0, 0), (-1, 0), 10),
1284
- ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
1285
- ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
1286
- ('GRID', (0, 0), (-1, -1), 1, colors.black)
1287
- ]))
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
+ )
1288
1350
  story.append(rl_table)
1289
1351
  else:
1290
1352
  # Convert to readable text
1291
1353
  text_content = json.dumps(data, indent=2, default=str, ensure_ascii=False)
1292
- for line in text_content.split('\n'):
1293
- story.append(Paragraph(line, styles['Code']))
1354
+ for line in text_content.split("\n"):
1355
+ story.append(Paragraph(line, styles["Code"]))
1294
1356
 
1295
1357
  else:
1296
1358
  # Other data types
1297
- story.append(Paragraph(str(data), styles['Normal']))
1359
+ story.append(Paragraph(str(data), styles["Normal"]))
1298
1360
 
1299
1361
  # Build PDF
1300
1362
  doc.build(story)
@@ -1336,11 +1398,7 @@ def _extract_table_data_as_dict(table: Table) -> Dict[str, Any]:
1336
1398
  headers = table_data[0]
1337
1399
  rows = table_data[1:]
1338
1400
 
1339
- return {
1340
- "headers": headers,
1341
- "rows": rows,
1342
- "row_count": len(rows)
1343
- }
1401
+ return {"headers": headers, "rows": rows, "row_count": len(rows)}
1344
1402
 
1345
1403
 
1346
1404
  def _convert_to_table_string(data: Any, title: Optional[str] = None) -> str:
@@ -1372,7 +1430,9 @@ def _write_csv_data(output: StringIO, csv_data: List[List[str]]) -> None:
1372
1430
  writer.writerows(csv_data)
1373
1431
 
1374
1432
 
1375
- def handle_output_format(data: Any, output_format: str = 'table', output_file: Optional[str] = None, title: Optional[str] = None):
1433
+ def handle_output_format(
1434
+ data: Any, output_format: str = "table", output_file: Optional[str] = None, title: Optional[str] = None
1435
+ ):
1376
1436
  """
1377
1437
  Handle output formatting for CLI commands - unified interface for all modules.
1378
1438
 
@@ -1400,7 +1460,7 @@ def handle_output_format(data: Any, output_format: str = 'table', output_file: O
1400
1460
  handle_output_format(data, output_format='pdf', output_file='report.pdf', title='AWS Resources Report')
1401
1461
  """
1402
1462
  try:
1403
- if output_format == 'table':
1463
+ if output_format == "table":
1404
1464
  # Default Rich table display - just print to console
1405
1465
  if isinstance(data, Table):
1406
1466
  console.print(data)
@@ -1438,10 +1498,10 @@ def handle_output_format(data: Any, output_format: str = 'table', output_file: O
1438
1498
  output = export_data(data, output_format, output_file, title)
1439
1499
 
1440
1500
  # If no output file specified, print to console for non-table formats
1441
- if not output_file and output_format != 'pdf':
1442
- if output_format == 'json':
1501
+ if not output_file and output_format != "pdf":
1502
+ if output_format == "json":
1443
1503
  print_json(json.loads(output))
1444
- elif output_format == 'markdown':
1504
+ elif output_format == "markdown":
1445
1505
  print_markdown(output)
1446
1506
  else:
1447
1507
  console.print(output)