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.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +135 -91
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +17 -12
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +99 -79
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +315 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/aws_decorators.py +2 -3
- runbooks/inventory/check_cloudtrail_compliance.py +2 -4
- runbooks/inventory/check_controltower_readiness.py +152 -151
- runbooks/inventory/check_landingzone_readiness.py +85 -84
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/core/formatter.py +11 -0
- runbooks/inventory/draw_org_structure.py +8 -9
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/ec2_vpc_utils.py +2 -2
- runbooks/inventory/find_cfn_drift_detection.py +5 -7
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
- runbooks/inventory/find_cfn_stackset_drift.py +5 -6
- runbooks/inventory/find_ec2_security_groups.py +48 -42
- runbooks/inventory/find_landingzone_versions.py +4 -6
- runbooks/inventory/find_vpc_flow_logs.py +7 -9
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/inventory_modules.py +103 -91
- runbooks/inventory/list_cfn_stacks.py +9 -10
- runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
- runbooks/inventory/list_cfn_stackset_operations.py +79 -57
- runbooks/inventory/list_cfn_stacksets.py +8 -10
- runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
- runbooks/inventory/list_ds_directories.py +65 -53
- runbooks/inventory/list_ec2_availability_zones.py +2 -4
- runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
- runbooks/inventory/list_ec2_instances.py +23 -28
- runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
- runbooks/inventory/list_elbs_load_balancers.py +22 -20
- runbooks/inventory/list_enis_network_interfaces.py +26 -33
- runbooks/inventory/list_guardduty_detectors.py +2 -4
- runbooks/inventory/list_iam_policies.py +2 -4
- runbooks/inventory/list_iam_roles.py +5 -7
- runbooks/inventory/list_iam_saml_providers.py +4 -6
- runbooks/inventory/list_lambda_functions.py +38 -38
- runbooks/inventory/list_org_accounts.py +6 -8
- runbooks/inventory/list_org_accounts_users.py +55 -44
- runbooks/inventory/list_rds_db_instances.py +31 -33
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/list_route53_hosted_zones.py +3 -5
- runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
- runbooks/inventory/list_sns_topics.py +2 -4
- runbooks/inventory/list_ssm_parameters.py +4 -7
- runbooks/inventory/list_vpc_subnets.py +2 -4
- runbooks/inventory/list_vpcs.py +7 -10
- runbooks/inventory/mcp_inventory_validator.py +554 -468
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +63 -55
- runbooks/inventory/recover_cfn_stack_ids.py +7 -8
- runbooks/inventory/requirements.txt +0 -1
- runbooks/inventory/rich_inventory_display.py +35 -34
- runbooks/inventory/run_on_multi_accounts.py +3 -5
- runbooks/inventory/unified_validation_engine.py +281 -253
- runbooks/inventory/verify_ec2_security_groups.py +1 -1
- runbooks/inventory/vpc_analyzer.py +735 -697
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +384 -380
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +461 -454
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.6.dist-info/METADATA +327 -0
- runbooks-1.1.6.dist-info/RECORD +489 -0
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- runbooks-1.1.4.dist-info/RECORD +0 -468
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
runbooks/common/rich_utils.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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(
|
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(
|
631
|
-
unused_count = workspaces_data.get(
|
632
|
-
total_count = workspaces_data.get(
|
633
|
-
optimization_potential = workspaces_data.get(
|
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(
|
662
|
-
|
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(
|
679
|
-
active_gateways = nat_data.get(
|
680
|
-
monthly_cost = nat_data.get(
|
681
|
-
optimization_ready = nat_data.get(
|
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(
|
715
|
-
|
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 = {
|
734
|
-
|
735
|
-
total_instances = rds_data.get(
|
736
|
-
multi_az_instances = rds_data.get(
|
737
|
-
manual_snapshots = rds_data.get(
|
738
|
-
snapshot_storage_gb = rds_data.get(
|
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[
|
746
|
-
savings_max = savings_range[
|
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(
|
779
|
-
|
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(
|
798
|
-
nat_gateway = all_scenarios_data.get(
|
799
|
-
rds = all_scenarios_data.get(
|
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(
|
803
|
-
nat_annual = nat_gateway.get(
|
804
|
-
rds_annual = rds.get(
|
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(
|
841
|
-
|
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
|
-
|
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 ==
|
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 ==
|
1190
|
+
elif format_type == "csv":
|
1013
1191
|
output = export_to_csv(data, title)
|
1014
1192
|
|
1015
|
-
elif format_type ==
|
1193
|
+
elif format_type == "json":
|
1016
1194
|
output = export_to_json(data, title)
|
1017
1195
|
|
1018
|
-
elif format_type ==
|
1196
|
+
elif format_type == "markdown":
|
1019
1197
|
output = export_to_markdown(data, title)
|
1020
1198
|
|
1021
|
-
elif format_type ==
|
1199
|
+
elif format_type == "pdf":
|
1022
1200
|
output = export_to_pdf(data, title, output_file)
|
1023
1201
|
|
1024
1202
|
else:
|
1025
|
-
supported_formats = [
|
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 !=
|
1207
|
+
if output_file and format_type != "pdf": # PDF handles its own file writing
|
1030
1208
|
try:
|
1031
|
-
with open(output_file,
|
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([
|
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([
|
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,
|
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
|
-
|
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=
|
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
|
-
|
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[
|
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(
|
1258
|
-
(
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
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(
|
1279
|
-
(
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
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(
|
1293
|
-
story.append(Paragraph(line, styles[
|
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[
|
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(
|
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 ==
|
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 !=
|
1442
|
-
if output_format ==
|
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 ==
|
1618
|
+
elif output_format == "markdown":
|
1445
1619
|
print_markdown(output)
|
1446
1620
|
else:
|
1447
1621
|
console.print(output)
|