runbooks 1.1.4__py3-none-any.whl → 1.1.6__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 (273) 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 +135 -91
  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 +17 -12
  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 +99 -79
  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 +315 -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/aws_decorators.py +2 -3
  113. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  114. runbooks/inventory/check_controltower_readiness.py +152 -151
  115. runbooks/inventory/check_landingzone_readiness.py +85 -84
  116. runbooks/inventory/cloud_foundations_integration.py +144 -149
  117. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  118. runbooks/inventory/collectors/aws_networking.py +109 -99
  119. runbooks/inventory/collectors/base.py +4 -0
  120. runbooks/inventory/core/collector.py +495 -313
  121. runbooks/inventory/core/formatter.py +11 -0
  122. runbooks/inventory/draw_org_structure.py +8 -9
  123. runbooks/inventory/drift_detection_cli.py +69 -96
  124. runbooks/inventory/ec2_vpc_utils.py +2 -2
  125. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  126. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  127. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  128. runbooks/inventory/find_ec2_security_groups.py +48 -42
  129. runbooks/inventory/find_landingzone_versions.py +4 -6
  130. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  131. runbooks/inventory/inventory_mcp_cli.py +48 -46
  132. runbooks/inventory/inventory_modules.py +103 -91
  133. runbooks/inventory/list_cfn_stacks.py +9 -10
  134. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  135. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  136. runbooks/inventory/list_cfn_stacksets.py +8 -10
  137. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  138. runbooks/inventory/list_ds_directories.py +65 -53
  139. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  140. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  141. runbooks/inventory/list_ec2_instances.py +23 -28
  142. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  143. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  144. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  145. runbooks/inventory/list_guardduty_detectors.py +2 -4
  146. runbooks/inventory/list_iam_policies.py +2 -4
  147. runbooks/inventory/list_iam_roles.py +5 -7
  148. runbooks/inventory/list_iam_saml_providers.py +4 -6
  149. runbooks/inventory/list_lambda_functions.py +38 -38
  150. runbooks/inventory/list_org_accounts.py +6 -8
  151. runbooks/inventory/list_org_accounts_users.py +55 -44
  152. runbooks/inventory/list_rds_db_instances.py +31 -33
  153. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  154. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  155. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  156. runbooks/inventory/list_sns_topics.py +2 -4
  157. runbooks/inventory/list_ssm_parameters.py +4 -7
  158. runbooks/inventory/list_vpc_subnets.py +2 -4
  159. runbooks/inventory/list_vpcs.py +7 -10
  160. runbooks/inventory/mcp_inventory_validator.py +554 -468
  161. runbooks/inventory/mcp_vpc_validator.py +359 -442
  162. runbooks/inventory/organizations_discovery.py +63 -55
  163. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  164. runbooks/inventory/requirements.txt +0 -1
  165. runbooks/inventory/rich_inventory_display.py +35 -34
  166. runbooks/inventory/run_on_multi_accounts.py +3 -5
  167. runbooks/inventory/unified_validation_engine.py +281 -253
  168. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  169. runbooks/inventory/vpc_analyzer.py +735 -697
  170. runbooks/inventory/vpc_architecture_validator.py +293 -348
  171. runbooks/inventory/vpc_dependency_analyzer.py +384 -380
  172. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  173. runbooks/main.py +49 -34
  174. runbooks/main_final.py +91 -60
  175. runbooks/main_minimal.py +22 -10
  176. runbooks/main_optimized.py +131 -100
  177. runbooks/main_ultra_minimal.py +7 -2
  178. runbooks/mcp/__init__.py +36 -0
  179. runbooks/mcp/integration.py +679 -0
  180. runbooks/monitoring/performance_monitor.py +9 -4
  181. runbooks/operate/dynamodb_operations.py +3 -1
  182. runbooks/operate/ec2_operations.py +145 -137
  183. runbooks/operate/iam_operations.py +146 -152
  184. runbooks/operate/networking_cost_heatmap.py +29 -8
  185. runbooks/operate/rds_operations.py +223 -254
  186. runbooks/operate/s3_operations.py +107 -118
  187. runbooks/operate/vpc_operations.py +646 -616
  188. runbooks/remediation/base.py +1 -1
  189. runbooks/remediation/commons.py +10 -7
  190. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  191. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  192. runbooks/remediation/multi_account.py +24 -21
  193. runbooks/remediation/rds_snapshot_list.py +86 -60
  194. runbooks/remediation/remediation_cli.py +92 -146
  195. runbooks/remediation/universal_account_discovery.py +83 -79
  196. runbooks/remediation/workspaces_list.py +46 -41
  197. runbooks/security/__init__.py +19 -0
  198. runbooks/security/assessment_runner.py +1150 -0
  199. runbooks/security/baseline_checker.py +812 -0
  200. runbooks/security/cloudops_automation_security_validator.py +509 -535
  201. runbooks/security/compliance_automation_engine.py +17 -17
  202. runbooks/security/config/__init__.py +2 -2
  203. runbooks/security/config/compliance_config.py +50 -50
  204. runbooks/security/config_template_generator.py +63 -76
  205. runbooks/security/enterprise_security_framework.py +1 -1
  206. runbooks/security/executive_security_dashboard.py +519 -508
  207. runbooks/security/multi_account_security_controls.py +959 -1210
  208. runbooks/security/real_time_security_monitor.py +422 -444
  209. runbooks/security/security_baseline_tester.py +1 -1
  210. runbooks/security/security_cli.py +143 -112
  211. runbooks/security/test_2way_validation.py +439 -0
  212. runbooks/security/two_way_validation_framework.py +852 -0
  213. runbooks/sre/production_monitoring_framework.py +167 -177
  214. runbooks/tdd/__init__.py +15 -0
  215. runbooks/tdd/cli.py +1071 -0
  216. runbooks/utils/__init__.py +14 -17
  217. runbooks/utils/logger.py +7 -2
  218. runbooks/utils/version_validator.py +50 -47
  219. runbooks/validation/__init__.py +6 -6
  220. runbooks/validation/cli.py +9 -3
  221. runbooks/validation/comprehensive_2way_validator.py +745 -704
  222. runbooks/validation/mcp_validator.py +906 -228
  223. runbooks/validation/terraform_citations_validator.py +104 -115
  224. runbooks/validation/terraform_drift_detector.py +461 -454
  225. runbooks/vpc/README.md +617 -0
  226. runbooks/vpc/__init__.py +8 -1
  227. runbooks/vpc/analyzer.py +577 -0
  228. runbooks/vpc/cleanup_wrapper.py +476 -413
  229. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  230. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  231. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  232. runbooks/vpc/config.py +92 -97
  233. runbooks/vpc/cost_engine.py +411 -148
  234. runbooks/vpc/cost_explorer_integration.py +553 -0
  235. runbooks/vpc/cross_account_session.py +101 -106
  236. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  237. runbooks/vpc/eni_gate_validator.py +961 -0
  238. runbooks/vpc/heatmap_engine.py +185 -160
  239. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  240. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  241. runbooks/vpc/networking_wrapper.py +15 -8
  242. runbooks/vpc/pdca_remediation_planner.py +528 -0
  243. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  244. runbooks/vpc/runbooks_adapter.py +1167 -241
  245. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  246. runbooks/vpc/test_data_loader.py +358 -0
  247. runbooks/vpc/tests/conftest.py +314 -4
  248. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  249. runbooks/vpc/tests/test_cost_engine.py +0 -2
  250. runbooks/vpc/topology_generator.py +326 -0
  251. runbooks/vpc/unified_scenarios.py +1297 -1124
  252. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  253. runbooks-1.1.6.dist-info/METADATA +327 -0
  254. runbooks-1.1.6.dist-info/RECORD +489 -0
  255. runbooks/finops/README.md +0 -414
  256. runbooks/finops/accuracy_cross_validator.py +0 -647
  257. runbooks/finops/business_cases.py +0 -950
  258. runbooks/finops/dashboard_router.py +0 -922
  259. runbooks/finops/ebs_optimizer.py +0 -973
  260. runbooks/finops/embedded_mcp_validator.py +0 -1629
  261. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  262. runbooks/finops/finops_dashboard.py +0 -584
  263. runbooks/finops/finops_scenarios.py +0 -1218
  264. runbooks/finops/legacy_migration.py +0 -730
  265. runbooks/finops/multi_dashboard.py +0 -1519
  266. runbooks/finops/single_dashboard.py +0 -1113
  267. runbooks/finops/unlimited_scenarios.py +0 -393
  268. runbooks-1.1.4.dist-info/METADATA +0 -800
  269. runbooks-1.1.4.dist-info/RECORD +0 -468
  270. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  271. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  272. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  273. {runbooks-1.1.4.dist-info → runbooks-1.1.6.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,169 @@ 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
+ from rich.progress import Progress as RichProgress
54
+
55
+ Console = RichConsole
56
+ Table = RichTable
57
+ Progress = RichProgress
58
+ else:
59
+ # Mock Rich Console for testing - plain text output compatible with Click CliRunner
60
+ class MockConsole:
61
+ """Mock console that prints to stdout without Rich formatting."""
62
+
63
+ def __init__(self, **kwargs):
64
+ """Initialize mock console - ignore all kwargs for compatibility."""
65
+ self._capture_buffer = None
66
+
67
+ def print(self, *args, **kwargs):
68
+ """
69
+ Mock print that outputs plain text to stdout.
70
+
71
+ Accepts all Rich Console.print() parameters but ignores styling.
72
+ Compatible with Click CliRunner's StringIO buffer management.
73
+ """
74
+ # Ignore all kwargs (style, highlight, etc.) - test mode doesn't need them
75
+ if args:
76
+ # Extract text content from Rich markup if present
77
+ text = str(args[0]) if args else ""
78
+ # Remove Rich markup tags for plain output
79
+ text = re.sub(r"\[.*?\]", "", text)
80
+
81
+ # If capturing, append to buffer instead of printing
82
+ if self._capture_buffer is not None:
83
+ self._capture_buffer.append(text)
84
+ else:
85
+ # Use print() to stdout - avoid sys.stdout.write() which causes I/O errors
86
+ # DO NOT use file= parameter or flush= parameter with Click CliRunner
87
+ print(text)
88
+
89
+ def log(self, *args, **kwargs):
90
+ """Mock log method - same as print for testing compatibility."""
91
+ self.print(*args, **kwargs)
92
+
93
+ def capture(self):
94
+ """
95
+ Mock capture context manager for testing.
96
+
97
+ Returns a context manager that captures console output to a buffer
98
+ instead of printing to stdout. Compatible with Rich Console.capture() API.
99
+ """
100
+ class MockCapture:
101
+ def __init__(self, console):
102
+ self.console = console
103
+ self.buffer = []
104
+
105
+ def __enter__(self):
106
+ self.console._capture_buffer = self.buffer
107
+ return self
108
+
109
+ def __exit__(self, *args):
110
+ self.console._capture_buffer = None
111
+
112
+ def get(self):
113
+ """Return captured output as string."""
114
+ return "\n".join(self.buffer)
115
+
116
+ return MockCapture(self)
117
+
118
+ def __enter__(self):
119
+ return self
120
+
121
+ def __exit__(self, *args):
122
+ # CRITICAL: Don't close anything - let Click CliRunner manage streams
123
+ pass
124
+
125
+ class MockTable:
126
+ """Mock table for testing - minimal implementation."""
127
+
128
+ def __init__(self, *args, **kwargs):
129
+ self.title = kwargs.get("title", "")
130
+ self.columns = []
131
+ self.rows = []
132
+
133
+ def add_column(self, header, **kwargs):
134
+ self.columns.append(header)
135
+
136
+ def add_row(self, *args):
137
+ self.rows.append(args)
138
+
139
+ class MockProgress:
140
+ """
141
+ Mock Progress for testing - prevents I/O conflicts with Click CliRunner.
142
+
143
+ Provides complete Rich.Progress API compatibility without any stream operations
144
+ that could interfere with Click's StringIO buffer management.
145
+ """
146
+
147
+ def __init__(self, *columns, **kwargs):
148
+ """Initialize mock progress - ignore all kwargs for test compatibility."""
149
+ self.columns = columns
150
+ self.kwargs = kwargs
151
+ self.tasks = {}
152
+ self.task_counter = 0
153
+ self._started = False
154
+
155
+ def add_task(self, description, total=None, **kwargs):
156
+ """Add a mock task and return task ID."""
157
+ task_id = self.task_counter
158
+ self.tasks[task_id] = {
159
+ "description": description,
160
+ "total": total,
161
+ "completed": 0,
162
+ "kwargs": kwargs
163
+ }
164
+ self.task_counter += 1
165
+ return task_id
166
+
167
+ def update(self, task_id, **kwargs):
168
+ """Update mock task progress."""
169
+ if task_id in self.tasks:
170
+ self.tasks[task_id].update(kwargs)
171
+
172
+ def start(self):
173
+ """Mock start method - no-op for test safety."""
174
+ self._started = True
175
+ return self
176
+
177
+ def stop(self):
178
+ """Mock stop method - CRITICAL: no stream operations."""
179
+ self._started = False
180
+ # IMPORTANT: Do NOT close any streams or file handles
181
+ # Click CliRunner manages its own StringIO lifecycle
182
+
183
+ def __enter__(self):
184
+ """Context manager entry - start progress."""
185
+ self.start()
186
+ return self
187
+
188
+ def __exit__(self, *args):
189
+ """
190
+ Context manager exit - stop progress WITHOUT stream closure.
191
+
192
+ CRITICAL: This method must NOT perform any file operations that could
193
+ close Click CliRunner's StringIO buffer. The stop() method is intentionally
194
+ a no-op to prevent "ValueError: I/O operation on closed file" errors.
195
+ """
196
+ self.stop()
197
+ # Explicitly return None to allow exception propagation
198
+ return None
199
+
200
+ Console = MockConsole
201
+ Table = MockTable
202
+ Progress = MockProgress
203
+
43
204
  # CloudOps Custom Theme
44
205
  CLOUDOPS_THEME = Theme(
45
206
  {
@@ -59,8 +220,11 @@ CLOUDOPS_THEME = Theme(
59
220
  }
60
221
  )
61
222
 
62
- # Initialize console with custom theme
63
- console = Console(theme=CLOUDOPS_THEME)
223
+ # Initialize console with custom theme (test-aware via USE_RICH flag)
224
+ if USE_RICH:
225
+ console = Console(theme=CLOUDOPS_THEME)
226
+ else:
227
+ console = Console() # MockConsole instance
64
228
 
65
229
  # Status indicators
66
230
  STATUS_INDICATORS = {
@@ -109,6 +273,7 @@ def print_header(title: str, version: Optional[str] = None) -> None:
109
273
  """
110
274
  if version is None:
111
275
  from runbooks import __version__
276
+
112
277
  version = __version__
113
278
 
114
279
  header_text = Text()
@@ -124,7 +289,10 @@ def print_header(title: str, version: Optional[str] = None) -> None:
124
289
  def print_banner() -> None:
125
290
  """Print a clean, minimal CloudOps Runbooks banner."""
126
291
  from runbooks import __version__
127
- console.print(f"\n[header]CloudOps Runbooks[/header] [subheader]Enterprise AWS Automation Platform[/subheader] [dim]v{__version__}[/dim]")
292
+
293
+ console.print(
294
+ f"\n[header]CloudOps Runbooks[/header] [subheader]Enterprise AWS Automation Platform[/subheader] [dim]v{__version__}[/dim]"
295
+ )
128
296
  console.print()
129
297
 
130
298
 
@@ -431,7 +599,9 @@ def create_display_profile_name(profile_name: str, max_length: int = 25, context
431
599
  return f"{profile_name[: max_length - 3]}..."
432
600
 
433
601
 
434
- def format_profile_name(profile_name: str, style: str = "cyan", display_max_length: int = 25, secure_logging: bool = True) -> Text:
602
+ def format_profile_name(
603
+ profile_name: str, style: str = "cyan", display_max_length: int = 25, secure_logging: bool = True
604
+ ) -> Text:
435
605
  """
436
606
  Format profile name with consistent styling, intelligent truncation, and security enhancements.
437
607
 
@@ -449,7 +619,7 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
449
619
 
450
620
  Returns:
451
621
  Rich Text object with formatted profile name
452
-
622
+
453
623
  Security Note:
454
624
  When secure_logging=True, account IDs are masked in display to prevent
455
625
  account enumeration while maintaining profile identification.
@@ -458,13 +628,14 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
458
628
  if secure_logging:
459
629
  try:
460
630
  from runbooks.common.aws_utils import AWSProfileSanitizer
631
+
461
632
  display_profile = AWSProfileSanitizer.sanitize_profile_name(profile_name)
462
633
  except ImportError:
463
634
  # Fallback to original profile if aws_utils not available
464
635
  display_profile = profile_name
465
636
  else:
466
637
  display_profile = profile_name
467
-
638
+
468
639
  display_name = create_display_profile_name(display_profile, display_max_length)
469
640
 
470
641
  text = Text()
@@ -476,7 +647,7 @@ def format_profile_name(profile_name: str, style: str = "cyan", display_max_leng
476
647
  else:
477
648
  # Full name - normal style
478
649
  text.append(display_name, style=style)
479
-
650
+
480
651
  # Add security indicator for sanitized profiles
481
652
  if secure_logging and "***masked***" in display_name:
482
653
  text.append(" 🔒", style="dim yellow")
@@ -623,21 +794,21 @@ def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings:
623
794
  Args:
624
795
  workspaces_data: Dictionary containing WorkSpaces cost and utilization data
625
796
  target_savings: Annual savings target (default: $12,518)
626
-
797
+
627
798
  Returns:
628
799
  Rich Panel with formatted WorkSpaces analysis
629
800
  """
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
-
801
+ current_cost = workspaces_data.get("monthly_cost", 0)
802
+ unused_count = workspaces_data.get("unused_count", 0)
803
+ total_count = workspaces_data.get("total_count", 0)
804
+ optimization_potential = workspaces_data.get("optimization_potential", 0)
805
+
635
806
  annual_savings = optimization_potential * 12
636
807
  target_achievement = min(100, (annual_savings / target_savings) * 100) if target_savings > 0 else 0
637
-
808
+
638
809
  status = "🎯 TARGET ACHIEVABLE" if target_achievement >= 90 else "⚠️ TARGET REQUIRES EXPANDED SCOPE"
639
810
  status_style = "bright_green" if target_achievement >= 90 else "yellow"
640
-
811
+
641
812
  content = f"""💼 [bold]Manager's Priority #1: WorkSpaces Cleanup Analysis[/bold]
642
813
 
643
814
  📊 Current State:
@@ -658,35 +829,38 @@ def format_workspaces_analysis(workspaces_data: Dict[str, Any], target_savings:
658
829
 
659
830
  [{status_style}]{status}[/]"""
660
831
 
661
- return Panel(content, title="[bright_cyan]WorkSpaces Cost Optimization[/bright_cyan]",
662
- border_style="bright_green" if target_achievement >= 90 else "yellow")
832
+ return Panel(
833
+ content,
834
+ title="[bright_cyan]WorkSpaces Cost Optimization[/bright_cyan]",
835
+ border_style="bright_green" if target_achievement >= 90 else "yellow",
836
+ )
663
837
 
664
838
 
665
839
  def format_nat_gateway_optimization(nat_data: Dict[str, Any], target_completion: int = 95) -> Panel:
666
840
  """
667
841
  Format NAT Gateway optimization analysis for manager's completion target.
668
-
842
+
669
843
  Manager's requirement to increase NAT Gateway optimization from 75% to 95% completion.
670
-
844
+
671
845
  Args:
672
846
  nat_data: Dictionary containing NAT Gateway configuration and cost data
673
847
  target_completion: Completion target percentage (default: 95% from manager's priority)
674
-
848
+
675
849
  Returns:
676
850
  Rich Panel with formatted NAT Gateway optimization analysis
677
851
  """
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
-
852
+ total_gateways = nat_data.get("total", 0)
853
+ active_gateways = nat_data.get("active", 0)
854
+ monthly_cost = nat_data.get("monthly_cost", 0)
855
+ optimization_ready = nat_data.get("optimization_ready", 0)
856
+
683
857
  current_completion = 75 # Manager specified current state
684
858
  optimization_potential = monthly_cost * 0.75 # 75% can be optimized
685
859
  annual_savings = optimization_potential * 12
686
-
860
+
687
861
  completion_gap = target_completion - current_completion
688
862
  status = "🎯 READY FOR 95% TARGET" if active_gateways > 0 else "❌ NO OPTIMIZATION OPPORTUNITIES"
689
-
863
+
690
864
  content = f"""🌐 [bold]Manager's Priority #2: NAT Gateway Optimization[/bold]
691
865
 
692
866
  🔍 Current Infrastructure:
@@ -711,45 +885,46 @@ def format_nat_gateway_optimization(nat_data: Dict[str, Any], target_completion:
711
885
 
712
886
  [bright_green]{status}[/bright_green]"""
713
887
 
714
- return Panel(content, title="[bright_cyan]Manager's Priority #2: NAT Gateway Optimization[/bright_cyan]",
715
- border_style="cyan")
888
+ return Panel(
889
+ content, title="[bright_cyan]Manager's Priority #2: NAT Gateway Optimization[/bright_cyan]", border_style="cyan"
890
+ )
716
891
 
717
892
 
718
893
  def format_rds_optimization_analysis(rds_data: Dict[str, Any], savings_range: Dict[str, int] = None) -> Panel:
719
894
  """
720
895
  Format RDS Multi-AZ optimization analysis for manager's FinOps-23 scenario.
721
-
896
+
722
897
  Manager's requirement for measurable range annual savings through RDS manual snapshot cleanup
723
898
  and Multi-AZ configuration review.
724
-
899
+
725
900
  Args:
726
901
  rds_data: Dictionary containing RDS instance and snapshot data
727
902
  savings_range: Dict with 'min' and 'max' annual savings (default: {'min': 5000, 'max': 24000})
728
-
903
+
729
904
  Returns:
730
905
  Rich Panel with formatted RDS optimization analysis
731
906
  """
732
907
  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
-
908
+ savings_range = {"min": 5000, "max": 24000}
909
+
910
+ total_instances = rds_data.get("total", 0)
911
+ multi_az_instances = rds_data.get("multi_az_instances", 0)
912
+ manual_snapshots = rds_data.get("manual_snapshots", 0)
913
+ snapshot_storage_gb = rds_data.get("snapshot_storage_gb", 0)
914
+
740
915
  # Calculate savings potential
741
916
  snapshot_savings = snapshot_storage_gb * 0.095 * 12 # $0.095/GB/month
742
917
  multi_az_savings = multi_az_instances * 1000 * 12 # ~$1K/month per instance
743
918
  total_savings = snapshot_savings + multi_az_savings
744
-
745
- savings_min = savings_range['min']
746
- savings_max = savings_range['max']
747
-
919
+
920
+ savings_min = savings_range["min"]
921
+ savings_max = savings_range["max"]
922
+
748
923
  # Check if we're within manager's target range
749
924
  within_range = savings_min <= total_savings <= savings_max
750
925
  range_status = "✅ WITHIN TARGET RANGE" if within_range else "📊 ANALYSIS PENDING"
751
926
  range_style = "bright_green" if within_range else "yellow"
752
-
927
+
753
928
  content = f"""🗄️ [bold]Manager's Priority #3: RDS Cost Optimization[/bold]
754
929
 
755
930
  📊 Current RDS Environment:
@@ -775,42 +950,45 @@ def format_rds_optimization_analysis(rds_data: Dict[str, Any], savings_range: Di
775
950
 
776
951
  [{range_style}]{range_status}[/]"""
777
952
 
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")
953
+ return Panel(
954
+ content,
955
+ title="[bright_cyan]FinOps-23: RDS Multi-AZ & Snapshot Optimization[/bright_cyan]",
956
+ border_style="bright_green" if within_range else "yellow",
957
+ )
780
958
 
781
959
 
782
960
  def format_manager_business_summary(all_scenarios_data: Dict[str, Any]) -> Panel:
783
961
  """
784
962
  Format executive summary panel for manager's complete AWSO business case.
785
-
963
+
786
964
  Combines all three manager priorities into executive-ready decision package:
787
965
  - FinOps-24: WorkSpaces cleanup ($12,518)
788
966
  - Manager Priority #2: NAT Gateway optimization (95% completion)
789
967
  - FinOps-23: RDS optimization (measurable range range)
790
-
968
+
791
969
  Args:
792
970
  all_scenarios_data: Dictionary containing data from all three scenarios
793
-
971
+
794
972
  Returns:
795
973
  Rich Panel with complete executive summary
796
974
  """
797
- workspaces = all_scenarios_data.get('workspaces', {})
798
- nat_gateway = all_scenarios_data.get('nat_gateway', {})
799
- rds = all_scenarios_data.get('rds', {})
800
-
975
+ workspaces = all_scenarios_data.get("workspaces", {})
976
+ nat_gateway = all_scenarios_data.get("nat_gateway", {})
977
+ rds = all_scenarios_data.get("rds", {})
978
+
801
979
  # 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
-
980
+ workspaces_annual = workspaces.get("optimization_potential", 0) * 12
981
+ nat_annual = nat_gateway.get("monthly_cost", 0) * 0.75 * 12
982
+ rds_annual = rds.get("total_savings", 15000) # Mid-range estimate
983
+
806
984
  total_min_savings = workspaces_annual + nat_annual + 5000
807
985
  total_max_savings = workspaces_annual + nat_annual + 24000
808
-
986
+
809
987
  # Overall assessment
810
988
  overall_confidence = 85 # Weighted average of individual confidences
811
989
  payback_months = 2.4 # Quick payback period
812
990
  roi_percentage = 567 # Strong ROI
813
-
991
+
814
992
  content = f"""🏆 [bold]MANAGER'S AWSO BUSINESS CASE - EXECUTIVE SUMMARY[/bold]
815
993
 
816
994
  💼 Three Strategic Priorities:
@@ -837,8 +1015,12 @@ def format_manager_business_summary(all_scenarios_data: Dict[str, Any]) -> Panel
837
1015
 
838
1016
  🎯 [bold]RECOMMENDATION: APPROVED FOR IMPLEMENTATION[/bold]"""
839
1017
 
840
- return Panel(content, title="[bright_green]🏆 MANAGER'S AWSO BUSINESS CASE - DECISION PACKAGE[/bright_green]",
841
- border_style="bright_green", padding=(1, 2))
1018
+ return Panel(
1019
+ content,
1020
+ title="[bright_green]🏆 MANAGER'S AWSO BUSINESS CASE - DECISION PACKAGE[/bright_green]",
1021
+ border_style="bright_green",
1022
+ padding=(1, 2),
1023
+ )
842
1024
 
843
1025
 
844
1026
  # Export all public functions and constants
@@ -846,6 +1028,9 @@ __all__ = [
846
1028
  "CLOUDOPS_THEME",
847
1029
  "STATUS_INDICATORS",
848
1030
  "console",
1031
+ "Console",
1032
+ "Progress",
1033
+ "Table",
849
1034
  "get_console",
850
1035
  "get_context_aware_console",
851
1036
  "print_header",
@@ -891,18 +1076,18 @@ __all__ = [
891
1076
  def create_dual_metric_display(unblended_total: float, amortized_total: float, variance_pct: float) -> Columns:
892
1077
  """
893
1078
  Create dual-metric cost display with technical and financial perspectives.
894
-
1079
+
895
1080
  Args:
896
1081
  unblended_total: Technical total (UnblendedCost)
897
- amortized_total: Financial total (AmortizedCost)
1082
+ amortized_total: Financial total (AmortizedCost)
898
1083
  variance_pct: Variance percentage between metrics
899
-
1084
+
900
1085
  Returns:
901
1086
  Rich Columns object with dual-metric display
902
1087
  """
903
1088
  from rich.columns import Columns
904
1089
  from rich.panel import Panel
905
-
1090
+
906
1091
  # Technical perspective (UnblendedCost)
907
1092
  tech_content = Text()
908
1093
  tech_content.append("🔧 Technical Analysis\n", style="bright_blue bold")
@@ -913,14 +1098,9 @@ def create_dual_metric_display(unblended_total: float, amortized_total: float, v
913
1098
  tech_content.append("Resource optimization\n", style="white")
914
1099
  tech_content.append("Audience: ", style="bright_blue")
915
1100
  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
-
1101
+
1102
+ tech_panel = Panel(tech_content, title="🔧 Technical Perspective", border_style="bright_blue", padding=(1, 2))
1103
+
924
1104
  # Financial perspective (AmortizedCost)
925
1105
  financial_content = Text()
926
1106
  financial_content.append("📊 Financial Reporting\n", style="bright_green bold")
@@ -931,14 +1111,11 @@ def create_dual_metric_display(unblended_total: float, amortized_total: float, v
931
1111
  financial_content.append("Budget planning\n", style="white")
932
1112
  financial_content.append("Audience: ", style="bright_green")
933
1113
  financial_content.append("Finance, Executives", style="white")
934
-
1114
+
935
1115
  financial_panel = Panel(
936
- financial_content,
937
- title="📊 Financial Perspective",
938
- border_style="bright_green",
939
- padding=(1, 2)
1116
+ financial_content, title="📊 Financial Perspective", border_style="bright_green", padding=(1, 2)
940
1117
  )
941
-
1118
+
942
1119
  return Columns([tech_panel, financial_panel])
943
1120
 
944
1121
 
@@ -978,6 +1155,7 @@ def format_metric_variance(variance: float, variance_pct: float) -> Text:
978
1155
  # UNIVERSAL FORMAT EXPORT FUNCTIONS
979
1156
  # ===========================
980
1157
 
1158
+
981
1159
  def export_data(data: Any, format_type: str, output_file: Optional[str] = None, title: Optional[str] = None) -> str:
982
1160
  """
983
1161
  Universal data export function supporting multiple output formats.
@@ -999,7 +1177,7 @@ def export_data(data: Any, format_type: str, output_file: Optional[str] = None,
999
1177
  format_type = format_type.lower().strip()
1000
1178
 
1001
1179
  # Handle table display (default Rich behavior)
1002
- if format_type == 'table':
1180
+ if format_type == "table":
1003
1181
  if isinstance(data, Table):
1004
1182
  # Capture Rich table output
1005
1183
  with console.capture() as capture:
@@ -1009,26 +1187,26 @@ def export_data(data: Any, format_type: str, output_file: Optional[str] = None,
1009
1187
  # Convert data to table format
1010
1188
  output = _convert_to_table_string(data, title)
1011
1189
 
1012
- elif format_type == 'csv':
1190
+ elif format_type == "csv":
1013
1191
  output = export_to_csv(data, title)
1014
1192
 
1015
- elif format_type == 'json':
1193
+ elif format_type == "json":
1016
1194
  output = export_to_json(data, title)
1017
1195
 
1018
- elif format_type == 'markdown':
1196
+ elif format_type == "markdown":
1019
1197
  output = export_to_markdown(data, title)
1020
1198
 
1021
- elif format_type == 'pdf':
1199
+ elif format_type == "pdf":
1022
1200
  output = export_to_pdf(data, title, output_file)
1023
1201
 
1024
1202
  else:
1025
- supported_formats = ['table', 'csv', 'json', 'markdown', 'pdf']
1203
+ supported_formats = ["table", "csv", "json", "markdown", "pdf"]
1026
1204
  raise ValueError(f"Unsupported format: {format_type}. Supported formats: {supported_formats}")
1027
1205
 
1028
1206
  # Write to file if specified
1029
- if output_file and format_type != 'pdf': # PDF handles its own file writing
1207
+ if output_file and format_type != "pdf": # PDF handles its own file writing
1030
1208
  try:
1031
- with open(output_file, 'w', encoding='utf-8') as f:
1209
+ with open(output_file, "w", encoding="utf-8") as f:
1032
1210
  f.write(output)
1033
1211
  print_success(f"Output saved to: {output_file}")
1034
1212
  except IOError as e:
@@ -1078,14 +1256,14 @@ def export_to_csv(data: Any, title: Optional[str] = None) -> str:
1078
1256
  elif isinstance(data, dict):
1079
1257
  # Dictionary - convert to key-value pairs
1080
1258
  writer = csv.writer(output)
1081
- writer.writerow(['Key', 'Value'])
1259
+ writer.writerow(["Key", "Value"])
1082
1260
  for key, value in data.items():
1083
1261
  writer.writerow([key, value])
1084
1262
 
1085
1263
  else:
1086
1264
  # Fallback for other types
1087
1265
  writer = csv.writer(output)
1088
- writer.writerow(['Data'])
1266
+ writer.writerow(["Data"])
1089
1267
  writer.writerow([str(data)])
1090
1268
 
1091
1269
  return output.getvalue()
@@ -1105,7 +1283,7 @@ def export_to_json(data: Any, title: Optional[str] = None) -> str:
1105
1283
  # Prepare data for JSON serialization
1106
1284
  if isinstance(data, Table):
1107
1285
  json_data = _extract_table_data_as_dict(data)
1108
- elif hasattr(data, '__dict__'):
1286
+ elif hasattr(data, "__dict__"):
1109
1287
  # Object with attributes
1110
1288
  json_data = data.__dict__
1111
1289
  else:
@@ -1115,12 +1293,8 @@ def export_to_json(data: Any, title: Optional[str] = None) -> str:
1115
1293
  # Add metadata if title provided
1116
1294
  if title:
1117
1295
  output_data = {
1118
- "metadata": {
1119
- "title": title,
1120
- "generated": datetime.now().isoformat(),
1121
- "format": "json"
1122
- },
1123
- "data": json_data
1296
+ "metadata": {"title": title, "generated": datetime.now().isoformat(), "format": "json"},
1297
+ "data": json_data,
1124
1298
  }
1125
1299
  else:
1126
1300
  output_data = json_data
@@ -1217,13 +1391,11 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1217
1391
  from reportlab.lib.units import inch
1218
1392
  from reportlab.platypus import SimpleDocTemplate, Table as RLTable, TableStyle, Paragraph, Spacer
1219
1393
  except ImportError:
1220
- raise ImportError(
1221
- "PDF export requires reportlab. Install with: pip install reportlab"
1222
- )
1394
+ raise ImportError("PDF export requires reportlab. Install with: pip install reportlab")
1223
1395
 
1224
1396
  if not output_file:
1225
1397
  # Generate temporary file if none provided
1226
- output_file = tempfile.mktemp(suffix='.pdf')
1398
+ output_file = tempfile.mktemp(suffix=".pdf")
1227
1399
 
1228
1400
  # Create PDF document
1229
1401
  doc = SimpleDocTemplate(output_file, pagesize=A4)
@@ -1233,18 +1405,14 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1233
1405
  # Add title
1234
1406
  if title:
1235
1407
  title_style = ParagraphStyle(
1236
- 'CustomTitle',
1237
- parent=styles['Heading1'],
1238
- fontSize=16,
1239
- textColor=colors.darkblue,
1240
- spaceAfter=12
1408
+ "CustomTitle", parent=styles["Heading1"], fontSize=16, textColor=colors.darkblue, spaceAfter=12
1241
1409
  )
1242
1410
  story.append(Paragraph(title, title_style))
1243
1411
  story.append(Spacer(1, 12))
1244
1412
 
1245
1413
  # Add generation info
1246
1414
  info_text = f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
1247
- story.append(Paragraph(info_text, styles['Normal']))
1415
+ story.append(Paragraph(info_text, styles["Normal"]))
1248
1416
  story.append(Spacer(1, 12))
1249
1417
 
1250
1418
  # Handle different data types
@@ -1254,16 +1422,20 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1254
1422
  if table_data:
1255
1423
  # Create ReportLab table
1256
1424
  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
- ]))
1425
+ rl_table.setStyle(
1426
+ TableStyle(
1427
+ [
1428
+ ("BACKGROUND", (0, 0), (-1, 0), colors.lightblue),
1429
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
1430
+ ("ALIGN", (0, 0), (-1, -1), "CENTER"),
1431
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
1432
+ ("FONTSIZE", (0, 0), (-1, 0), 12),
1433
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 12),
1434
+ ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
1435
+ ("GRID", (0, 0), (-1, -1), 1, colors.black),
1436
+ ]
1437
+ )
1438
+ )
1267
1439
  story.append(rl_table)
1268
1440
 
1269
1441
  elif isinstance(data, (list, dict)):
@@ -1275,26 +1447,30 @@ def export_to_pdf(data: Any, title: Optional[str] = None, output_file: Optional[
1275
1447
  table_data = [headers] + rows
1276
1448
 
1277
1449
  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
- ]))
1450
+ rl_table.setStyle(
1451
+ TableStyle(
1452
+ [
1453
+ ("BACKGROUND", (0, 0), (-1, 0), colors.lightblue),
1454
+ ("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
1455
+ ("ALIGN", (0, 0), (-1, -1), "LEFT"),
1456
+ ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
1457
+ ("FONTSIZE", (0, 0), (-1, 0), 10),
1458
+ ("BOTTOMPADDING", (0, 0), (-1, 0), 12),
1459
+ ("BACKGROUND", (0, 1), (-1, -1), colors.beige),
1460
+ ("GRID", (0, 0), (-1, -1), 1, colors.black),
1461
+ ]
1462
+ )
1463
+ )
1288
1464
  story.append(rl_table)
1289
1465
  else:
1290
1466
  # Convert to readable text
1291
1467
  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']))
1468
+ for line in text_content.split("\n"):
1469
+ story.append(Paragraph(line, styles["Code"]))
1294
1470
 
1295
1471
  else:
1296
1472
  # Other data types
1297
- story.append(Paragraph(str(data), styles['Normal']))
1473
+ story.append(Paragraph(str(data), styles["Normal"]))
1298
1474
 
1299
1475
  # Build PDF
1300
1476
  doc.build(story)
@@ -1336,11 +1512,7 @@ def _extract_table_data_as_dict(table: Table) -> Dict[str, Any]:
1336
1512
  headers = table_data[0]
1337
1513
  rows = table_data[1:]
1338
1514
 
1339
- return {
1340
- "headers": headers,
1341
- "rows": rows,
1342
- "row_count": len(rows)
1343
- }
1515
+ return {"headers": headers, "rows": rows, "row_count": len(rows)}
1344
1516
 
1345
1517
 
1346
1518
  def _convert_to_table_string(data: Any, title: Optional[str] = None) -> str:
@@ -1372,7 +1544,9 @@ def _write_csv_data(output: StringIO, csv_data: List[List[str]]) -> None:
1372
1544
  writer.writerows(csv_data)
1373
1545
 
1374
1546
 
1375
- def handle_output_format(data: Any, output_format: str = 'table', output_file: Optional[str] = None, title: Optional[str] = None):
1547
+ def handle_output_format(
1548
+ data: Any, output_format: str = "table", output_file: Optional[str] = None, title: Optional[str] = None
1549
+ ):
1376
1550
  """
1377
1551
  Handle output formatting for CLI commands - unified interface for all modules.
1378
1552
 
@@ -1400,7 +1574,7 @@ def handle_output_format(data: Any, output_format: str = 'table', output_file: O
1400
1574
  handle_output_format(data, output_format='pdf', output_file='report.pdf', title='AWS Resources Report')
1401
1575
  """
1402
1576
  try:
1403
- if output_format == 'table':
1577
+ if output_format == "table":
1404
1578
  # Default Rich table display - just print to console
1405
1579
  if isinstance(data, Table):
1406
1580
  console.print(data)
@@ -1438,10 +1612,10 @@ def handle_output_format(data: Any, output_format: str = 'table', output_file: O
1438
1612
  output = export_data(data, output_format, output_file, title)
1439
1613
 
1440
1614
  # 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':
1615
+ if not output_file and output_format != "pdf":
1616
+ if output_format == "json":
1443
1617
  print_json(json.loads(output))
1444
- elif output_format == 'markdown':
1618
+ elif output_format == "markdown":
1445
1619
  print_markdown(output)
1446
1620
  else:
1447
1621
  console.print(output)