runbooks 0.7.9__py3-none-any.whl → 0.9.1__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 (122) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/README.md +12 -1
  3. runbooks/cfat/__init__.py +1 -1
  4. runbooks/cfat/assessment/compliance.py +4 -1
  5. runbooks/cfat/assessment/runner.py +42 -34
  6. runbooks/cfat/models.py +1 -1
  7. runbooks/cloudops/__init__.py +123 -0
  8. runbooks/cloudops/base.py +385 -0
  9. runbooks/cloudops/cost_optimizer.py +811 -0
  10. runbooks/cloudops/infrastructure_optimizer.py +29 -0
  11. runbooks/cloudops/interfaces.py +828 -0
  12. runbooks/cloudops/lifecycle_manager.py +29 -0
  13. runbooks/cloudops/mcp_cost_validation.py +678 -0
  14. runbooks/cloudops/models.py +251 -0
  15. runbooks/cloudops/monitoring_automation.py +29 -0
  16. runbooks/cloudops/notebook_framework.py +676 -0
  17. runbooks/cloudops/security_enforcer.py +449 -0
  18. runbooks/common/__init__.py +152 -0
  19. runbooks/common/accuracy_validator.py +1039 -0
  20. runbooks/common/context_logger.py +440 -0
  21. runbooks/common/cross_module_integration.py +594 -0
  22. runbooks/common/enhanced_exception_handler.py +1108 -0
  23. runbooks/common/enterprise_audit_integration.py +634 -0
  24. runbooks/common/mcp_cost_explorer_integration.py +900 -0
  25. runbooks/common/mcp_integration.py +548 -0
  26. runbooks/common/performance_monitor.py +387 -0
  27. runbooks/common/profile_utils.py +216 -0
  28. runbooks/common/rich_utils.py +172 -1
  29. runbooks/feedback/user_feedback_collector.py +440 -0
  30. runbooks/finops/README.md +377 -458
  31. runbooks/finops/__init__.py +4 -21
  32. runbooks/finops/account_resolver.py +279 -0
  33. runbooks/finops/accuracy_cross_validator.py +638 -0
  34. runbooks/finops/aws_client.py +721 -36
  35. runbooks/finops/budget_integration.py +313 -0
  36. runbooks/finops/cli.py +59 -5
  37. runbooks/finops/cost_optimizer.py +1340 -0
  38. runbooks/finops/cost_processor.py +211 -37
  39. runbooks/finops/dashboard_router.py +900 -0
  40. runbooks/finops/dashboard_runner.py +990 -232
  41. runbooks/finops/embedded_mcp_validator.py +288 -0
  42. runbooks/finops/enhanced_dashboard_runner.py +8 -7
  43. runbooks/finops/enhanced_progress.py +327 -0
  44. runbooks/finops/enhanced_trend_visualization.py +423 -0
  45. runbooks/finops/finops_dashboard.py +184 -1829
  46. runbooks/finops/helpers.py +509 -196
  47. runbooks/finops/iam_guidance.py +400 -0
  48. runbooks/finops/markdown_exporter.py +466 -0
  49. runbooks/finops/multi_dashboard.py +1502 -0
  50. runbooks/finops/optimizer.py +15 -15
  51. runbooks/finops/profile_processor.py +2 -2
  52. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  53. runbooks/finops/runbooks.security.report_generator.log +0 -0
  54. runbooks/finops/runbooks.security.run_script.log +0 -0
  55. runbooks/finops/runbooks.security.security_export.log +0 -0
  56. runbooks/finops/schemas.py +589 -0
  57. runbooks/finops/service_mapping.py +195 -0
  58. runbooks/finops/single_dashboard.py +710 -0
  59. runbooks/finops/tests/test_reference_images_validation.py +1 -1
  60. runbooks/inventory/README.md +12 -1
  61. runbooks/inventory/core/collector.py +157 -29
  62. runbooks/inventory/list_ec2_instances.py +9 -6
  63. runbooks/inventory/list_ssm_parameters.py +10 -10
  64. runbooks/inventory/organizations_discovery.py +210 -164
  65. runbooks/inventory/rich_inventory_display.py +74 -107
  66. runbooks/inventory/run_on_multi_accounts.py +13 -13
  67. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  68. runbooks/inventory/runbooks.security.security_export.log +0 -0
  69. runbooks/main.py +1371 -240
  70. runbooks/metrics/dora_metrics_engine.py +711 -17
  71. runbooks/monitoring/performance_monitor.py +433 -0
  72. runbooks/operate/README.md +394 -0
  73. runbooks/operate/base.py +215 -47
  74. runbooks/operate/ec2_operations.py +435 -5
  75. runbooks/operate/iam_operations.py +598 -3
  76. runbooks/operate/privatelink_operations.py +1 -1
  77. runbooks/operate/rds_operations.py +508 -0
  78. runbooks/operate/s3_operations.py +508 -0
  79. runbooks/operate/vpc_endpoints.py +1 -1
  80. runbooks/remediation/README.md +489 -13
  81. runbooks/remediation/base.py +5 -3
  82. runbooks/remediation/commons.py +8 -4
  83. runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
  84. runbooks/security/README.md +12 -1
  85. runbooks/security/__init__.py +265 -33
  86. runbooks/security/cloudops_automation_security_validator.py +1164 -0
  87. runbooks/security/compliance_automation.py +12 -10
  88. runbooks/security/compliance_automation_engine.py +1021 -0
  89. runbooks/security/enterprise_security_framework.py +930 -0
  90. runbooks/security/enterprise_security_policies.json +293 -0
  91. runbooks/security/executive_security_dashboard.py +1247 -0
  92. runbooks/security/integration_test_enterprise_security.py +879 -0
  93. runbooks/security/module_security_integrator.py +641 -0
  94. runbooks/security/multi_account_security_controls.py +2254 -0
  95. runbooks/security/real_time_security_monitor.py +1196 -0
  96. runbooks/security/report_generator.py +1 -1
  97. runbooks/security/run_script.py +4 -8
  98. runbooks/security/security_baseline_tester.py +39 -52
  99. runbooks/security/security_export.py +99 -120
  100. runbooks/sre/README.md +472 -0
  101. runbooks/sre/__init__.py +33 -0
  102. runbooks/sre/mcp_reliability_engine.py +1049 -0
  103. runbooks/sre/performance_optimization_engine.py +1032 -0
  104. runbooks/sre/production_monitoring_framework.py +584 -0
  105. runbooks/sre/reliability_monitoring_framework.py +1011 -0
  106. runbooks/validation/__init__.py +2 -2
  107. runbooks/validation/benchmark.py +154 -149
  108. runbooks/validation/cli.py +159 -147
  109. runbooks/validation/mcp_validator.py +291 -248
  110. runbooks/vpc/README.md +478 -0
  111. runbooks/vpc/__init__.py +2 -2
  112. runbooks/vpc/manager_interface.py +366 -351
  113. runbooks/vpc/networking_wrapper.py +68 -36
  114. runbooks/vpc/rich_formatters.py +22 -8
  115. runbooks-0.9.1.dist-info/METADATA +308 -0
  116. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/RECORD +120 -59
  117. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/entry_points.txt +1 -1
  118. runbooks/finops/cross_validation.py +0 -375
  119. runbooks-0.7.9.dist-info/METADATA +0 -636
  120. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/WHEEL +0 -0
  121. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/licenses/LICENSE +0 -0
  122. {runbooks-0.7.9.dist-info → runbooks-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Rich-styled Markdown Export Module for CloudOps Runbooks FinOps
4
+
5
+ This module provides Rich table to markdown conversion functionality with
6
+ MkDocs compatibility for copy-pasteable documentation tables.
7
+
8
+ Features:
9
+ - Rich table to markdown conversion with styled borders
10
+ - 10-column format for multi-account analysis
11
+ - MkDocs compatible table syntax
12
+ - Intelligent file management and organization
13
+ - Preserves color coding through markdown syntax
14
+ - Automated timestamping and metadata
15
+
16
+ Author: CloudOps Runbooks Team
17
+ Version: 0.7.8
18
+ """
19
+
20
+ import os
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+ from typing import Any, Dict, List, Optional, Union
24
+
25
+ from rich import box
26
+ from rich.table import Table
27
+ from rich.text import Text
28
+
29
+ from runbooks.common.rich_utils import (
30
+ STATUS_INDICATORS,
31
+ console,
32
+ create_table,
33
+ format_cost,
34
+ print_info,
35
+ print_success,
36
+ print_warning,
37
+ )
38
+
39
+
40
+ class MarkdownExporter:
41
+ """Rich-styled markdown export functionality for FinOps analysis."""
42
+
43
+ def __init__(self, output_dir: str = "./exports"):
44
+ """
45
+ Initialize the markdown exporter.
46
+
47
+ Args:
48
+ output_dir: Directory to save markdown exports
49
+ """
50
+ self.output_dir = Path(output_dir)
51
+ self.output_dir.mkdir(exist_ok=True)
52
+
53
+ def rich_table_to_markdown(self, table: Table, preserve_styling: bool = True) -> str:
54
+ """
55
+ Convert Rich table to markdown format with optional styling preservation.
56
+
57
+ Args:
58
+ table: Rich Table object to convert
59
+ preserve_styling: Whether to preserve Rich styling through markdown syntax
60
+
61
+ Returns:
62
+ Markdown formatted table string
63
+ """
64
+ if not table.columns:
65
+ return ""
66
+
67
+ # Extract column headers
68
+ headers = []
69
+ for column in table.columns:
70
+ header_text = column.header or ""
71
+ if hasattr(header_text, "plain"):
72
+ headers.append(header_text.plain)
73
+ else:
74
+ headers.append(str(header_text))
75
+
76
+ # Create markdown table header
77
+ markdown_lines = []
78
+ markdown_lines.append("| " + " | ".join(headers) + " |")
79
+
80
+ # Create GitHub-compliant separator line with proper alignment syntax
81
+ separators = []
82
+ for column in table.columns:
83
+ if column.justify == "right":
84
+ separators.append("---:") # GitHub right alignment (minimum 3 hyphens)
85
+ elif column.justify == "center":
86
+ separators.append(":---:") # GitHub center alignment
87
+ else:
88
+ separators.append("---") # GitHub left alignment (default, minimum 3 hyphens)
89
+ markdown_lines.append("| " + " | ".join(separators) + " |")
90
+
91
+ # Extract and format data rows
92
+ for row in table.rows:
93
+ row_cells = []
94
+ for cell in row:
95
+ if isinstance(cell, Text):
96
+ if preserve_styling:
97
+ # Convert Rich Text to markdown with styling
98
+ cell_text = self._rich_text_to_markdown(cell)
99
+ else:
100
+ cell_text = cell.plain
101
+ else:
102
+ cell_text = str(cell)
103
+
104
+ # GitHub tables don't support multi-line content - convert to single line
105
+ cell_text = cell_text.replace("\n", " • ").strip()
106
+
107
+ # Escape pipes in cell content for GitHub compatibility
108
+ cell_text = cell_text.replace("|", "\\|")
109
+
110
+ # Remove excessive Rich formatting that doesn't render well in GitHub
111
+ cell_text = self._clean_rich_formatting_for_github(cell_text)
112
+
113
+ row_cells.append(cell_text)
114
+
115
+ markdown_lines.append("| " + " | ".join(row_cells) + " |")
116
+
117
+ return "\n".join(markdown_lines)
118
+
119
+ def _rich_text_to_markdown(self, rich_text: Text) -> str:
120
+ """
121
+ Convert Rich Text object to markdown with style preservation.
122
+
123
+ Args:
124
+ rich_text: Rich Text object with styling
125
+
126
+ Returns:
127
+ Markdown formatted string with preserved styling
128
+ """
129
+ # Start with plain text
130
+ text = rich_text.plain
131
+
132
+ # Extract style information and apply markdown equivalents
133
+ if hasattr(rich_text, "_spans") and rich_text._spans:
134
+ for span in reversed(rich_text._spans): # Reverse to handle overlapping spans
135
+ style = span.style
136
+ start, end = span.start, span.end
137
+
138
+ # Apply markdown formatting based on Rich styles
139
+ if style and hasattr(style, "color"):
140
+ if "green" in str(style.color):
141
+ # Green text (success/positive) - use ✅ emoji
142
+ text = text[:start] + "✅ " + text[start:end] + text[end:]
143
+ elif "red" in str(style.color):
144
+ # Red text (error/negative) - use ❌ emoji
145
+ text = text[:start] + "❌ " + text[start:end] + text[end:]
146
+ elif "yellow" in str(style.color):
147
+ # Yellow text (warning) - use ⚠️ emoji
148
+ text = text[:start] + "⚠️ " + text[start:end] + text[end:]
149
+ elif "cyan" in str(style.color):
150
+ # Cyan text (info) - use **bold** markdown
151
+ text = text[:start] + "**" + text[start:end] + "**" + text[end:]
152
+
153
+ if style and hasattr(style, "bold") and style.bold:
154
+ text = text[:start] + "**" + text[start:end] + "**" + text[end:]
155
+
156
+ if style and hasattr(style, "italic") and style.italic:
157
+ text = text[:start] + "*" + text[start:end] + "*" + text[end:]
158
+
159
+ return text
160
+
161
+ def _clean_rich_formatting_for_github(self, text: str) -> str:
162
+ """
163
+ Clean Rich formatting for better GitHub markdown compatibility.
164
+
165
+ Args:
166
+ text: Text with Rich formatting tags
167
+
168
+ Returns:
169
+ Cleaned text suitable for GitHub markdown tables
170
+ """
171
+ # Remove Rich color/style tags that don't render well in GitHub
172
+ import re
173
+
174
+ # Remove Rich markup tags but preserve content
175
+ text = re.sub(r"\[/?(?:red|green|yellow|cyan|blue|magenta|white|black|bright_\w+|dim|bold|italic)\]", "", text)
176
+ text = re.sub(r"\[/?[^\]]*\]", "", text) # Remove any remaining Rich tags
177
+
178
+ # Clean up multiple spaces and trim
179
+ text = re.sub(r"\s+", " ", text).strip()
180
+
181
+ return text
182
+
183
+ def create_single_account_export(self, profile_data: Dict[str, Any], account_id: str, profile_name: str) -> str:
184
+ """
185
+ Create markdown export for single account analysis.
186
+
187
+ Args:
188
+ profile_data: Single profile cost data
189
+ account_id: AWS account ID
190
+ profile_name: AWS profile name
191
+
192
+ Returns:
193
+ Markdown formatted single account analysis
194
+ """
195
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
196
+
197
+ # Create markdown content
198
+ markdown_content = f"""# AWS Cost Analysis - Account {account_id}
199
+
200
+ **Generated**: {timestamp}
201
+ **Profile**: {profile_name}
202
+ **Organization**: Single Account Analysis
203
+
204
+ ## Executive Summary
205
+
206
+ | Metric | Value | Status |
207
+ |--------|-------|--------|
208
+ | Total Cost | ${profile_data.get("total_cost", 0):,.2f} | {self._get_cost_status_emoji(profile_data.get("total_cost", 0))} |
209
+ | Service Count | {len(profile_data.get("service_breakdown", []))} services | 📊 |
210
+ | Cost Trend | {profile_data.get("cost_trend", "Stable")} | {self._get_trend_emoji(profile_data.get("cost_trend", ""))} |
211
+
212
+ ## Service Breakdown
213
+
214
+ | Service | Current Cost | Percentage | Trend | Optimization |
215
+ |---------|--------------|------------|-------|--------------|
216
+ """
217
+
218
+ # Add service breakdown rows
219
+ services = profile_data.get("service_breakdown", [])
220
+ for service in services[:10]: # Top 10 services
221
+ service_name = service.get("service", "Unknown")
222
+ cost = service.get("cost", 0)
223
+ percentage = service.get("percentage", 0)
224
+ trend = service.get("trend", "Stable")
225
+ optimization = service.get("optimization_opportunity", "Monitor")
226
+
227
+ markdown_content += f"| {service_name} | ${cost:,.2f} | {percentage:.1f}% | {self._get_trend_emoji(trend)} {trend} | {optimization} |\n"
228
+
229
+ # Add resource optimization section
230
+ markdown_content += f"""
231
+
232
+ ## Resource Optimization Opportunities
233
+
234
+ | Resource Type | Count | Potential Savings | Action Required |
235
+ |---------------|-------|------------------|-----------------|
236
+ | Stopped EC2 Instances | {profile_data.get("stopped_ec2", 0)} | ${profile_data.get("stopped_ec2_savings", 0):,.2f} | Review and terminate |
237
+ | Unused EBS Volumes | {profile_data.get("unused_volumes", 0)} | ${profile_data.get("unused_volume_savings", 0):,.2f} | Clean up unused storage |
238
+ | Unused Elastic IPs | {profile_data.get("unused_eips", 0)} | ${profile_data.get("unused_eip_savings", 0):,.2f} | Release unused IPs |
239
+ | Untagged Resources | {profile_data.get("untagged_resources", 0)} | N/A | Implement tagging strategy |
240
+
241
+ ---
242
+ *Generated by CloudOps Runbooks FinOps Module v0.7.8*
243
+ """
244
+
245
+ return markdown_content
246
+
247
+ def create_multi_account_export(
248
+ self, multi_profile_data: List[Dict[str, Any]], organization_info: Optional[Dict[str, Any]] = None
249
+ ) -> str:
250
+ """
251
+ Create 10-column markdown export for multi-account analysis.
252
+
253
+ Args:
254
+ multi_profile_data: List of profile data dictionaries
255
+ organization_info: Optional organization metadata
256
+
257
+ Returns:
258
+ Markdown formatted multi-account analysis with 10 columns
259
+ """
260
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
261
+ account_count = len(multi_profile_data)
262
+
263
+ # Calculate organization totals
264
+ total_cost = sum(profile.get("total_cost", 0) for profile in multi_profile_data)
265
+ total_savings = sum(profile.get("potential_savings", 0) for profile in multi_profile_data)
266
+
267
+ # Create markdown content with 10-column format
268
+ markdown_content = f"""# AWS Multi-Account Cost Analysis
269
+
270
+ **Generated**: {timestamp}
271
+ **Organization**: {account_count} accounts
272
+ **Total Cost**: ${total_cost:,.2f}
273
+ **Potential Savings**: ${total_savings:,.2f}
274
+
275
+ ## Executive Dashboard
276
+
277
+ | Metric | Value | Status |
278
+ |--------|-------|--------|
279
+ | Total Monthly Cost | ${total_cost:,.2f} | 💰 |
280
+ | Average per Account | ${total_cost / account_count:,.2f} | 📊 |
281
+ | Optimization Opportunity | ${total_savings:,.2f} ({total_savings / total_cost * 100:.1f}%) | 🎯 |
282
+
283
+ ## Multi-Account Analysis (10-Column Format)
284
+
285
+ | Profile | Last Month | Current Month | Top 3 Services | Budget | Stopped EC2 | Unused Vol | Unused EIP | Savings | Untagged |
286
+ |---------|------------|---------------|-----------------|---------|-------------|------------|------------|---------|----------|
287
+ """
288
+
289
+ # Add data rows for each profile
290
+ for profile_data in multi_profile_data:
291
+ profile_name = profile_data.get("profile_name", "Unknown")[:15] # Truncate for table width
292
+ last_month = profile_data.get("last_month_cost", 0)
293
+ current_month = profile_data.get("total_cost", 0)
294
+
295
+ # Get top 3 services
296
+ services = profile_data.get("service_breakdown", [])
297
+ top_services = [s.get("service", "")[:6] for s in services[:3]] # Truncate service names
298
+ top_services_str = ",".join(top_services) if top_services else "N/A"
299
+
300
+ # Budget status
301
+ budget_status = self._get_budget_status_emoji(profile_data.get("budget_status", "unknown"))
302
+
303
+ # Resource optimization data
304
+ stopped_ec2 = profile_data.get("stopped_ec2", 0)
305
+ unused_volumes = profile_data.get("unused_volumes", 0)
306
+ unused_eips = profile_data.get("unused_eips", 0)
307
+ potential_savings = profile_data.get("potential_savings", 0)
308
+ untagged_resources = profile_data.get("untagged_resources", 0)
309
+
310
+ # Add row to table
311
+ markdown_content += f"| {profile_name} | ${last_month:,.0f} | ${current_month:,.0f} | {top_services_str} | {budget_status} | {stopped_ec2} | {unused_volumes} | {unused_eips} | ${potential_savings:,.0f} | {untagged_resources} |\n"
312
+
313
+ # Add summary section
314
+ markdown_content += f"""
315
+
316
+ ## Organization Summary
317
+
318
+ ### Cost Trends
319
+ - **Month-over-Month Change**: {self._calculate_mom_change(multi_profile_data):.1f}%
320
+ - **Highest Cost Account**: {self._get_highest_cost_account(multi_profile_data)}
321
+ - **Most Opportunities**: {self._get_most_optimization_account(multi_profile_data)}
322
+
323
+ ### Optimization Recommendations
324
+ 1. **Immediate Actions**: Review {sum(p.get("stopped_ec2", 0) for p in multi_profile_data)} stopped EC2 instances
325
+ 2. **Storage Cleanup**: Clean up {sum(p.get("unused_volumes", 0) for p in multi_profile_data)} unused EBS volumes
326
+ 3. **Network Optimization**: Release {sum(p.get("unused_eips", 0) for p in multi_profile_data)} unused Elastic IPs
327
+ 4. **Governance**: Tag {sum(p.get("untagged_resources", 0) for p in multi_profile_data)} untagged resources
328
+
329
+ ---
330
+ *Generated by CloudOps Runbooks FinOps Module v0.7.8*
331
+ """
332
+
333
+ return markdown_content
334
+
335
+ def export_to_file(self, markdown_content: str, filename: str, account_type: str = "single") -> str:
336
+ """
337
+ Export markdown content to file with intelligent naming.
338
+
339
+ Args:
340
+ markdown_content: Markdown content to export
341
+ filename: Base filename (without extension)
342
+ account_type: Type of analysis (single, multi, organization)
343
+
344
+ Returns:
345
+ Path to exported file
346
+ """
347
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
348
+
349
+ if not filename.endswith(".md"):
350
+ filename = f"{filename}_{account_type}_account_{timestamp}.md"
351
+
352
+ filepath = self.output_dir / filename
353
+
354
+ # Show progress indication
355
+ print_info(f"📝 Generating markdown export: {filename}")
356
+
357
+ try:
358
+ with open(filepath, "w", encoding="utf-8") as f:
359
+ f.write(markdown_content)
360
+
361
+ print_success(f"Rich-styled markdown export saved: {filepath}")
362
+ print_info(f"🔗 Ready for MkDocs or GitHub documentation sharing")
363
+ return str(filepath)
364
+
365
+ except Exception as e:
366
+ print_warning(f"Failed to save markdown export: {e}")
367
+ return ""
368
+
369
+ def _get_cost_status_emoji(self, cost: float) -> str:
370
+ """Get emoji based on cost level."""
371
+ if cost >= 10000:
372
+ return "🔴 High"
373
+ elif cost >= 1000:
374
+ return "🟡 Medium"
375
+ else:
376
+ return "🟢 Low"
377
+
378
+ def _get_trend_emoji(self, trend: str) -> str:
379
+ """Get emoji for cost trend."""
380
+ trend_lower = trend.lower()
381
+ if "up" in trend_lower or "increas" in trend_lower:
382
+ return "📈"
383
+ elif "down" in trend_lower or "decreas" in trend_lower:
384
+ return "📉"
385
+ else:
386
+ return "➡️"
387
+
388
+ def _get_budget_status_emoji(self, status: str) -> str:
389
+ """Get emoji for budget status."""
390
+ status_lower = status.lower()
391
+ if "over" in status_lower or "exceeded" in status_lower:
392
+ return "❌ Over"
393
+ elif "warn" in status_lower:
394
+ return "⚠️ Warn"
395
+ elif "ok" in status_lower or "good" in status_lower:
396
+ return "✅ OK"
397
+ else:
398
+ return "❓ Unknown"
399
+
400
+ def _calculate_mom_change(self, profiles: List[Dict[str, Any]]) -> float:
401
+ """Calculate month-over-month change percentage."""
402
+ total_current = sum(p.get("total_cost", 0) for p in profiles)
403
+ total_last = sum(p.get("last_month_cost", 0) for p in profiles)
404
+
405
+ if total_last == 0:
406
+ return 0.0
407
+
408
+ return ((total_current - total_last) / total_last) * 100
409
+
410
+ def _get_highest_cost_account(self, profiles: List[Dict[str, Any]]) -> str:
411
+ """Get the account with highest cost."""
412
+ if not profiles:
413
+ return "N/A"
414
+
415
+ highest = max(profiles, key=lambda p: p.get("total_cost", 0))
416
+ return highest.get("profile_name", "Unknown")[:20]
417
+
418
+ def _get_most_optimization_account(self, profiles: List[Dict[str, Any]]) -> str:
419
+ """Get the account with most optimization opportunities."""
420
+ if not profiles:
421
+ return "N/A"
422
+
423
+ highest = max(profiles, key=lambda p: p.get("potential_savings", 0))
424
+ return highest.get("profile_name", "Unknown")[:20]
425
+
426
+
427
+ def export_finops_to_markdown(
428
+ profile_data: Union[Dict[str, Any], List[Dict[str, Any]]],
429
+ output_dir: str = "./exports",
430
+ filename: str = "finops_analysis",
431
+ account_type: str = "auto",
432
+ ) -> str:
433
+ """
434
+ Export FinOps analysis to markdown format.
435
+
436
+ Args:
437
+ profile_data: Single profile dict or list of profiles
438
+ output_dir: Output directory for exports
439
+ filename: Base filename for export
440
+ account_type: Type of analysis (single, multi, auto)
441
+
442
+ Returns:
443
+ Path to exported markdown file
444
+ """
445
+ exporter = MarkdownExporter(output_dir)
446
+
447
+ # Determine account type if auto
448
+ if account_type == "auto":
449
+ account_type = "multi" if isinstance(profile_data, list) else "single"
450
+
451
+ # Generate appropriate markdown content
452
+ if account_type == "single" and isinstance(profile_data, dict):
453
+ markdown_content = exporter.create_single_account_export(
454
+ profile_data, profile_data.get("account_id", "Unknown"), profile_data.get("profile_name", "Unknown")
455
+ )
456
+ elif account_type == "multi" and isinstance(profile_data, list):
457
+ markdown_content = exporter.create_multi_account_export(profile_data)
458
+ else:
459
+ raise ValueError(f"Invalid combination: account_type={account_type}, data_type={type(profile_data)}")
460
+
461
+ # Export to file
462
+ return exporter.export_to_file(markdown_content, filename, account_type)
463
+
464
+
465
+ # Export public interface
466
+ __all__ = ["MarkdownExporter", "export_finops_to_markdown"]