runbooks 1.1.4__py3-none-any.whl → 1.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +138 -35
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +11 -0
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +63 -74
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +201 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/cloud_foundations_integration.py +144 -149
  113. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  114. runbooks/inventory/collectors/aws_networking.py +109 -99
  115. runbooks/inventory/collectors/base.py +4 -0
  116. runbooks/inventory/core/collector.py +495 -313
  117. runbooks/inventory/drift_detection_cli.py +69 -96
  118. runbooks/inventory/inventory_mcp_cli.py +48 -46
  119. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  120. runbooks/inventory/mcp_inventory_validator.py +549 -465
  121. runbooks/inventory/mcp_vpc_validator.py +359 -442
  122. runbooks/inventory/organizations_discovery.py +55 -51
  123. runbooks/inventory/rich_inventory_display.py +33 -32
  124. runbooks/inventory/unified_validation_engine.py +278 -251
  125. runbooks/inventory/vpc_analyzer.py +732 -695
  126. runbooks/inventory/vpc_architecture_validator.py +293 -348
  127. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  128. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  129. runbooks/main.py +49 -34
  130. runbooks/main_final.py +91 -60
  131. runbooks/main_minimal.py +22 -10
  132. runbooks/main_optimized.py +131 -100
  133. runbooks/main_ultra_minimal.py +7 -2
  134. runbooks/mcp/__init__.py +36 -0
  135. runbooks/mcp/integration.py +679 -0
  136. runbooks/monitoring/performance_monitor.py +9 -4
  137. runbooks/operate/dynamodb_operations.py +3 -1
  138. runbooks/operate/ec2_operations.py +145 -137
  139. runbooks/operate/iam_operations.py +146 -152
  140. runbooks/operate/networking_cost_heatmap.py +29 -8
  141. runbooks/operate/rds_operations.py +223 -254
  142. runbooks/operate/s3_operations.py +107 -118
  143. runbooks/operate/vpc_operations.py +646 -616
  144. runbooks/remediation/base.py +1 -1
  145. runbooks/remediation/commons.py +10 -7
  146. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  147. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  148. runbooks/remediation/multi_account.py +24 -21
  149. runbooks/remediation/rds_snapshot_list.py +86 -60
  150. runbooks/remediation/remediation_cli.py +92 -146
  151. runbooks/remediation/universal_account_discovery.py +83 -79
  152. runbooks/remediation/workspaces_list.py +46 -41
  153. runbooks/security/__init__.py +19 -0
  154. runbooks/security/assessment_runner.py +1150 -0
  155. runbooks/security/baseline_checker.py +812 -0
  156. runbooks/security/cloudops_automation_security_validator.py +509 -535
  157. runbooks/security/compliance_automation_engine.py +17 -17
  158. runbooks/security/config/__init__.py +2 -2
  159. runbooks/security/config/compliance_config.py +50 -50
  160. runbooks/security/config_template_generator.py +63 -76
  161. runbooks/security/enterprise_security_framework.py +1 -1
  162. runbooks/security/executive_security_dashboard.py +519 -508
  163. runbooks/security/multi_account_security_controls.py +959 -1210
  164. runbooks/security/real_time_security_monitor.py +422 -444
  165. runbooks/security/security_baseline_tester.py +1 -1
  166. runbooks/security/security_cli.py +143 -112
  167. runbooks/security/test_2way_validation.py +439 -0
  168. runbooks/security/two_way_validation_framework.py +852 -0
  169. runbooks/sre/production_monitoring_framework.py +167 -177
  170. runbooks/tdd/__init__.py +15 -0
  171. runbooks/tdd/cli.py +1071 -0
  172. runbooks/utils/__init__.py +14 -17
  173. runbooks/utils/logger.py +7 -2
  174. runbooks/utils/version_validator.py +50 -47
  175. runbooks/validation/__init__.py +6 -6
  176. runbooks/validation/cli.py +9 -3
  177. runbooks/validation/comprehensive_2way_validator.py +745 -704
  178. runbooks/validation/mcp_validator.py +906 -228
  179. runbooks/validation/terraform_citations_validator.py +104 -115
  180. runbooks/validation/terraform_drift_detector.py +447 -451
  181. runbooks/vpc/README.md +617 -0
  182. runbooks/vpc/__init__.py +8 -1
  183. runbooks/vpc/analyzer.py +577 -0
  184. runbooks/vpc/cleanup_wrapper.py +476 -413
  185. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  186. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  187. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  188. runbooks/vpc/config.py +92 -97
  189. runbooks/vpc/cost_engine.py +411 -148
  190. runbooks/vpc/cost_explorer_integration.py +553 -0
  191. runbooks/vpc/cross_account_session.py +101 -106
  192. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  193. runbooks/vpc/eni_gate_validator.py +961 -0
  194. runbooks/vpc/heatmap_engine.py +185 -160
  195. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  196. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  197. runbooks/vpc/networking_wrapper.py +15 -8
  198. runbooks/vpc/pdca_remediation_planner.py +528 -0
  199. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  200. runbooks/vpc/runbooks_adapter.py +1167 -241
  201. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  202. runbooks/vpc/test_data_loader.py +358 -0
  203. runbooks/vpc/tests/conftest.py +314 -4
  204. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  205. runbooks/vpc/tests/test_cost_engine.py +0 -2
  206. runbooks/vpc/topology_generator.py +326 -0
  207. runbooks/vpc/unified_scenarios.py +1297 -1124
  208. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  209. runbooks-1.1.5.dist-info/METADATA +328 -0
  210. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
  211. runbooks/finops/README.md +0 -414
  212. runbooks/finops/accuracy_cross_validator.py +0 -647
  213. runbooks/finops/business_cases.py +0 -950
  214. runbooks/finops/dashboard_router.py +0 -922
  215. runbooks/finops/ebs_optimizer.py +0 -973
  216. runbooks/finops/embedded_mcp_validator.py +0 -1629
  217. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  218. runbooks/finops/finops_dashboard.py +0 -584
  219. runbooks/finops/finops_scenarios.py +0 -1218
  220. runbooks/finops/legacy_migration.py +0 -730
  221. runbooks/finops/multi_dashboard.py +0 -1519
  222. runbooks/finops/single_dashboard.py +0 -1113
  223. runbooks/finops/unlimited_scenarios.py +0 -393
  224. runbooks-1.1.4.dist-info/METADATA +0 -800
  225. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  226. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  227. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  228. {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -28,6 +28,7 @@ NON_ANALYTICAL_SERVICES = ["Tax"] # Services excluded from Top N analysis per u
28
28
  _filter_cache: Dict[str, tuple] = {}
29
29
  _filter_session_id: Optional[str] = None
30
30
 
31
+
31
32
  def _get_filter_session_id() -> str:
32
33
  """Generate filter session ID for cache scoping"""
33
34
  global _filter_session_id
@@ -79,7 +80,9 @@ def filter_analytical_services(
79
80
 
80
81
  # Only log if not already logged in this session
81
82
  if cache_key not in _filter_cache:
82
- console.log(f"[dim yellow]🔍 Filtered {filtered_count} non-analytical services: {', '.join(excluded_names)}[/]")
83
+ console.log(
84
+ f"[dim yellow]🔍 Filtered {filtered_count} non-analytical services: {', '.join(excluded_names)}[/]"
85
+ )
83
86
  _filter_cache[cache_key] = (filtered_count, excluded_names)
84
87
 
85
88
  return filtered_services
@@ -87,29 +90,30 @@ def filter_analytical_services(
87
90
 
88
91
  class DualMetricCostProcessor:
89
92
  """Enhanced processor for UnblendedCost (technical) and AmortizedCost (financial) reporting."""
90
-
91
- def __init__(self, session: Session, profile_name: Optional[str] = None):
93
+
94
+ def __init__(self, session: Session, profile_name: Optional[str] = None, analysis_mode: str = "comprehensive"):
92
95
  """Initialize dual-metric cost processor.
93
-
96
+
94
97
  Args:
95
98
  session: AWS boto3 session
96
99
  profile_name: AWS profile name for error handling
100
+ analysis_mode: Analysis mode - "technical" (UnblendedCost), "financial" (AmortizedCost), or "comprehensive" (both)
97
101
  """
98
102
  self.session = session
99
103
  self.profile_name = profile_name or "default"
104
+ self.analysis_mode = analysis_mode
100
105
  self.ce = session.client("ce")
101
-
102
- def collect_dual_metrics(self,
103
- account_id: Optional[str] = None,
104
- start_date: str = None,
105
- end_date: str = None) -> DualMetricResult:
106
+
107
+ def collect_dual_metrics(
108
+ self, account_id: Optional[str] = None, start_date: str = None, end_date: str = None
109
+ ) -> DualMetricResult:
106
110
  """Collect both UnblendedCost and AmortizedCost for comprehensive reporting.
107
-
111
+
108
112
  Args:
109
113
  account_id: AWS account ID for filtering (multi-account support)
110
114
  start_date: Start date in ISO format (YYYY-MM-DD)
111
115
  end_date: End date in ISO format (YYYY-MM-DD)
112
-
116
+
113
117
  Returns:
114
118
  DualMetricResult with both technical and financial perspectives
115
119
  """
@@ -117,39 +121,47 @@ class DualMetricCostProcessor:
117
121
  filter_param = None
118
122
  if account_id:
119
123
  filter_param = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
120
-
124
+
121
125
  # Set default dates if not provided
122
126
  if not start_date or not end_date:
123
127
  today = date.today()
124
128
  start_date = today.replace(day=1).isoformat()
125
129
  end_date = (today + timedelta(days=1)).isoformat() # AWS CE end date is exclusive
126
-
130
+
127
131
  try:
128
- # Technical Analysis (UnblendedCost)
129
- console.log("[blue]🔧 Collecting technical cost data (UnblendedCost)[/]")
132
+ # Inform user about the metric collection based on analysis mode
133
+ if self.analysis_mode == "technical":
134
+ console.log("[bright_blue]🔧 Collecting UnblendedCost data (Technical Analysis)[/]")
135
+ elif self.analysis_mode == "financial":
136
+ console.log("[bright_green]📊 Collecting AmortizedCost data (Financial Analysis)[/]")
137
+ else:
138
+ console.log(
139
+ "[bright_cyan]💰 Collecting both UnblendedCost and AmortizedCost data (Dual-Metrics Analysis)[/]"
140
+ )
141
+
142
+ # Technical Analysis (UnblendedCost) - always collect for comparison
130
143
  unblended_response = self.ce.get_cost_and_usage(
131
- TimePeriod={'Start': start_date, 'End': end_date},
132
- Granularity='MONTHLY',
133
- Metrics=['UnblendedCost'],
134
- GroupBy=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}],
135
- **({"Filter": filter_param} if filter_param else {})
144
+ TimePeriod={"Start": start_date, "End": end_date},
145
+ Granularity="MONTHLY",
146
+ Metrics=["UnblendedCost"],
147
+ GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
148
+ **({"Filter": filter_param} if filter_param else {}),
136
149
  )
137
-
138
- # Financial Reporting (AmortizedCost)
139
- console.log("[blue]📊 Collecting financial cost data (AmortizedCost)[/]")
150
+
151
+ # Financial Reporting (AmortizedCost) - always collect for comparison
140
152
  amortized_response = self.ce.get_cost_and_usage(
141
- TimePeriod={'Start': start_date, 'End': end_date},
142
- Granularity='MONTHLY',
143
- Metrics=['AmortizedCost'],
144
- GroupBy=[{'Type': 'DIMENSION', 'Key': 'SERVICE'}],
145
- **({"Filter": filter_param} if filter_param else {})
153
+ TimePeriod={"Start": start_date, "End": end_date},
154
+ Granularity="MONTHLY",
155
+ Metrics=["AmortizedCost"],
156
+ GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
157
+ **({"Filter": filter_param} if filter_param else {}),
146
158
  )
147
-
159
+
148
160
  # Parse UnblendedCost data
149
161
  unblended_costs = {}
150
162
  technical_total = 0.0
151
163
  service_breakdown_unblended = []
152
-
164
+
153
165
  for result in unblended_response.get("ResultsByTime", []):
154
166
  for group in result.get("Groups", []):
155
167
  service = group["Keys"][0]
@@ -158,12 +170,12 @@ class DualMetricCostProcessor:
158
170
  unblended_costs[service] = amount
159
171
  technical_total += amount
160
172
  service_breakdown_unblended.append((service, amount))
161
-
173
+
162
174
  # Parse AmortizedCost data
163
175
  amortized_costs = {}
164
176
  financial_total = 0.0
165
177
  service_breakdown_amortized = []
166
-
178
+
167
179
  for result in amortized_response.get("ResultsByTime", []):
168
180
  for group in result.get("Groups", []):
169
181
  service = group["Keys"][0]
@@ -172,17 +184,19 @@ class DualMetricCostProcessor:
172
184
  amortized_costs[service] = amount
173
185
  financial_total += amount
174
186
  service_breakdown_amortized.append((service, amount))
175
-
187
+
176
188
  # Calculate variance
177
189
  variance = abs(technical_total - financial_total)
178
190
  variance_percentage = (variance / financial_total * 100) if financial_total > 0 else 0.0
179
-
191
+
180
192
  # Sort service breakdowns by cost (descending)
181
193
  service_breakdown_unblended.sort(key=lambda x: x[1], reverse=True)
182
194
  service_breakdown_amortized.sort(key=lambda x: x[1], reverse=True)
183
-
184
- console.log(f"[green]✅ Dual-metric collection complete: Technical ${technical_total:.2f}, Financial ${financial_total:.2f}[/]")
185
-
195
+
196
+ console.log(
197
+ f"[green]✅ Dual-metric collection complete: Technical ${technical_total:.2f}, Financial ${financial_total:.2f}[/]"
198
+ )
199
+
186
200
  return DualMetricResult(
187
201
  unblended_costs=unblended_costs,
188
202
  amortized_costs=amortized_costs,
@@ -193,14 +207,14 @@ class DualMetricCostProcessor:
193
207
  period_start=start_date,
194
208
  period_end=end_date,
195
209
  service_breakdown_unblended=service_breakdown_unblended,
196
- service_breakdown_amortized=service_breakdown_amortized
210
+ service_breakdown_amortized=service_breakdown_amortized,
197
211
  )
198
-
212
+
199
213
  except Exception as e:
200
214
  console.log(f"[red]❌ Dual-metric collection failed: {str(e)}[/]")
201
215
  if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
202
216
  handle_cost_explorer_error(e, self.profile_name)
203
-
217
+
204
218
  # Return empty result structure
205
219
  return DualMetricResult(
206
220
  unblended_costs={},
@@ -212,37 +226,34 @@ class DualMetricCostProcessor:
212
226
  period_start=start_date,
213
227
  period_end=end_date,
214
228
  service_breakdown_unblended=[],
215
- service_breakdown_amortized=[]
229
+ service_breakdown_amortized=[],
216
230
  )
217
231
 
218
232
 
219
233
  def get_equal_period_cost_data(
220
- session: Session,
221
- profile_name: Optional[str] = None,
222
- account_id: Optional[str] = None,
223
- months_back: int = 3
234
+ session: Session, profile_name: Optional[str] = None, account_id: Optional[str] = None, months_back: int = 3
224
235
  ) -> Dict[str, Any]:
225
236
  """
226
237
  Get equal-period cost data for accurate trend analysis.
227
-
228
- Addresses the mathematical error where partial current month (e.g., Sept 1-2)
238
+
239
+ Addresses the mathematical error where partial current month (e.g., Sept 1-2)
229
240
  was compared against full previous month (Aug 1-31), resulting in misleading trends.
230
-
241
+
231
242
  Args:
232
243
  session: AWS boto3 session
233
244
  profile_name: AWS profile name for error handling
234
245
  account_id: Optional account ID for filtering
235
246
  months_back: Number of complete months to analyze
236
-
247
+
237
248
  Returns:
238
249
  Dict containing monthly cost data with equal periods for accurate trends
239
250
  """
240
251
  ce = session.client("ce")
241
252
  today = date.today()
242
-
253
+
243
254
  # Calculate complete months for comparison
244
255
  monthly_data = []
245
-
256
+
246
257
  # Get last N complete months (not including current partial month)
247
258
  for i in range(1, months_back + 1): # Start from 1 to skip current month
248
259
  # Calculate the start and end of each complete month
@@ -253,25 +264,25 @@ def get_equal_period_cost_data(
253
264
  # Handle year boundary
254
265
  target_month = 12 + (today.month - i)
255
266
  target_year = today.year - 1
256
-
267
+
257
268
  # First day of target month
258
269
  month_start = date(target_year, target_month, 1)
259
-
270
+
260
271
  # Last day of target month
261
272
  if target_month == 12:
262
273
  month_end = date(target_year + 1, 1, 1) - timedelta(days=1)
263
274
  else:
264
275
  month_end = date(target_year, target_month + 1, 1) - timedelta(days=1)
265
-
276
+
266
277
  # Build filter for account if provided
267
278
  filter_param = None
268
279
  if account_id:
269
280
  filter_param = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
270
-
281
+
271
282
  kwargs = {}
272
283
  if filter_param:
273
284
  kwargs["Filter"] = filter_param
274
-
285
+
275
286
  try:
276
287
  response = ce.get_cost_and_usage(
277
288
  TimePeriod={
@@ -282,41 +293,46 @@ def get_equal_period_cost_data(
282
293
  Metrics=["UnblendedCost"],
283
294
  **kwargs,
284
295
  )
285
-
296
+
286
297
  # Extract cost data
287
298
  total_cost = 0.0
288
299
  for result in response.get("ResultsByTime", []):
289
300
  if "Total" in result and "UnblendedCost" in result["Total"]:
290
301
  total_cost = float(result["Total"]["UnblendedCost"]["Amount"])
291
-
292
- monthly_data.append({
293
- "month": month_start.strftime("%b %Y"),
294
- "start_date": month_start.isoformat(),
295
- "end_date": month_end.isoformat(),
296
- "days": (month_end - month_start).days + 1,
297
- "cost": total_cost
298
- })
299
-
302
+
303
+ monthly_data.append(
304
+ {
305
+ "month": month_start.strftime("%b %Y"),
306
+ "start_date": month_start.isoformat(),
307
+ "end_date": month_end.isoformat(),
308
+ "days": (month_end - month_start).days + 1,
309
+ "cost": total_cost,
310
+ }
311
+ )
312
+
300
313
  except Exception as e:
301
314
  console.log(f"[yellow]Error getting cost data for {month_start.strftime('%b %Y')}: {e}[/]")
302
315
  if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
303
316
  from .iam_guidance import handle_cost_explorer_error
317
+
304
318
  handle_cost_explorer_error(e, profile_name)
305
-
319
+
306
320
  # Add empty data to maintain structure
307
- monthly_data.append({
308
- "month": month_start.strftime("%b %Y"),
309
- "start_date": month_start.isoformat(),
310
- "end_date": month_end.isoformat(),
311
- "days": (month_end - month_start).days + 1,
312
- "cost": 0.0
313
- })
314
-
321
+ monthly_data.append(
322
+ {
323
+ "month": month_start.strftime("%b %Y"),
324
+ "start_date": month_start.isoformat(),
325
+ "end_date": month_end.isoformat(),
326
+ "days": (month_end - month_start).days + 1,
327
+ "cost": 0.0,
328
+ }
329
+ )
330
+
315
331
  return {
316
332
  "account_id": get_account_id(session) or "unknown",
317
333
  "monthly_costs": monthly_data,
318
334
  "analysis_type": "equal_period",
319
- "profile": session.profile_name or profile_name or "default"
335
+ "profile": session.profile_name or profile_name or "default",
320
336
  }
321
337
 
322
338
 
@@ -568,37 +584,41 @@ def get_cost_data(
568
584
  # CRITICAL MATHEMATICAL FIX: Equal period comparisons for accurate trends
569
585
  # Problem: Partial current month vs full previous month = misleading trends
570
586
  # Solution: Same-day comparisons or complete month comparisons
571
-
587
+
572
588
  start_date = today.replace(day=1)
573
589
  end_date = today
574
-
590
+
575
591
  # Detect if we're dealing with a partial month that could cause misleading trends
576
592
  days_into_month = today.day
577
593
  is_partial_month = days_into_month <= 5 # First 5 days are considered "partial"
578
-
594
+
579
595
  if is_partial_month:
580
596
  console.log(f"[yellow]⚠️ Partial month detected ({days_into_month} days into {today.strftime('%B')})[/]")
581
- console.log(f"[dim yellow] Trend calculations may show extreme percentages due to limited current data[/]")
597
+ console.log(
598
+ f"[dim yellow] Trend calculations may show extreme percentages due to limited current data[/]"
599
+ )
582
600
  console.log(f"[dim yellow] Consider using full month comparisons for accurate trend analysis[/]")
583
-
601
+
584
602
  # Current period: start of month to today (include today with +1 day for AWS CE)
585
603
  end_date = today + timedelta(days=1) # AWS Cost Explorer end date is exclusive
586
-
604
+
587
605
  # Previous period: Use same day-of-month from previous month for better comparison
588
606
  # This provides more meaningful trends when current month is partial
589
607
  if is_partial_month and days_into_month > 1:
590
608
  # For partial months, compare same number of days from previous month
591
609
  previous_month_same_day = today.replace(day=1) - timedelta(days=1) # Last day of prev month
592
610
  previous_month_start = previous_month_same_day.replace(day=1)
593
-
611
+
594
612
  # Calculate same day of previous month, handling month boundaries
595
613
  try:
596
614
  previous_month_target_day = previous_month_start.replace(day=today.day)
597
615
  previous_period_start = previous_month_start
598
616
  previous_period_end = previous_month_target_day + timedelta(days=1) # Exclusive end
599
-
600
- console.log(f"[cyan]📊 Using equal-day comparison: {days_into_month} days from current vs previous month[/]")
601
-
617
+
618
+ console.log(
619
+ f"[cyan]📊 Using equal-day comparison: {days_into_month} days from current vs previous month[/]"
620
+ )
621
+
602
622
  except ValueError:
603
623
  # Handle cases where previous month doesn't have the same day (e.g., Feb 30)
604
624
  previous_period_end = previous_month_same_day + timedelta(days=1)
@@ -608,7 +628,14 @@ def get_cost_data(
608
628
  previous_period_end = start_date - timedelta(days=1)
609
629
  previous_period_start = previous_period_end.replace(day=1)
610
630
 
611
- account_id = get_account_id(session)
631
+ # Get account ID with enhanced error handling for AWS-2 accuracy validation
632
+ try:
633
+ account_id = get_account_id(session)
634
+ if not account_id:
635
+ account_id = "unknown"
636
+ except Exception as account_error:
637
+ console.print(f"[yellow]Warning: Could not retrieve account ID: {account_error}[/yellow]")
638
+ account_id = "unknown"
612
639
 
613
640
  try:
614
641
  this_period = ce.get_cost_and_usage(
@@ -707,7 +734,7 @@ def get_cost_data(
707
734
  previous_period_days = (previous_period_end - previous_period_start).days
708
735
  days_difference = abs(current_period_days - previous_period_days)
709
736
  is_partial_comparison = days_difference > 5
710
-
737
+
711
738
  # ENHANCED RELIABILITY ASSESSMENT: Consider MCP validation success in trend reliability
712
739
  trend_reliability = "high"
713
740
  if is_partial_comparison:
@@ -718,17 +745,24 @@ def get_cost_data(
718
745
  else:
719
746
  # Moderate difference - reliability depends on validation accuracy
720
747
  trend_reliability = "medium_with_validation_support"
721
-
748
+
722
749
  # Enhanced period information for trend analysis
750
+ # Calculate is_partial_month for metadata (AWS-2 accuracy enhancement)
751
+ today = date.today()
752
+ days_into_month = today.day
753
+ is_partial_month = days_into_month <= 5 # First 5 days are considered "partial"
754
+
723
755
  period_metadata = {
724
756
  "current_days": current_period_days,
725
757
  "previous_days": previous_period_days,
726
758
  "days_difference": days_difference,
727
759
  "is_partial_comparison": is_partial_comparison,
728
- "comparison_type": "equal_day_comparison" if is_partial_month else "standard_month_comparison",
760
+ "comparison_type": "equal_day_comparison" if is_partial_comparison else "standard_month_comparison",
729
761
  "trend_reliability": trend_reliability,
730
- "period_alignment_strategy": "equal_days" if is_partial_month and days_into_month > 1 else "standard_monthly",
731
- "supports_mcp_validation": True # This data structure supports MCP cross-validation
762
+ "period_alignment_strategy": "equal_days"
763
+ if is_partial_comparison and days_into_month > 1
764
+ else "standard_monthly",
765
+ "supports_mcp_validation": True, # This data structure supports MCP cross-validation
732
766
  }
733
767
 
734
768
  return {
@@ -757,48 +791,48 @@ def get_quarterly_cost_data(
757
791
  ) -> Dict[str, float]:
758
792
  """
759
793
  Get quarterly cost data for enhanced FinOps trend analysis.
760
-
794
+
761
795
  Retrieves cost data for the last complete quarter (3 months) to provide
762
796
  strategic quarterly context for financial planning and trend analysis.
763
-
797
+
764
798
  Args:
765
799
  session: The boto3 session to use
766
800
  profile_name: Optional AWS profile name for enhanced error messaging
767
801
  account_id: Optional account ID to filter costs to specific account
768
-
802
+
769
803
  Returns:
770
804
  Dictionary with service names as keys and quarterly costs as values
771
805
  """
772
806
  ce = session.client("ce")
773
807
  today = date.today()
774
-
808
+
775
809
  # Calculate last quarter date range
776
810
  # Go back 3 months for quarterly analysis
777
811
  quarterly_end_date = today.replace(day=1) - timedelta(days=1) # Last day of previous month
778
812
  quarterly_start_date = (quarterly_end_date.replace(day=1) - timedelta(days=90)).replace(day=1)
779
-
813
+
780
814
  # Build filters for quarterly analysis
781
815
  filters = []
782
816
  if account_id:
783
817
  account_filter = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
784
818
  filters.append(account_filter)
785
-
819
+
786
820
  # Combine filters if needed
787
821
  filter_param: Optional[Dict[str, Any]] = None
788
822
  if len(filters) == 1:
789
823
  filter_param = filters[0]
790
824
  elif len(filters) > 1:
791
825
  filter_param = {"And": filters}
792
-
826
+
793
827
  kwargs = {}
794
828
  if filter_param:
795
829
  kwargs["Filter"] = filter_param
796
-
830
+
797
831
  try:
798
832
  quarterly_period_cost_by_service = ce.get_cost_and_usage(
799
833
  TimePeriod={
800
- "Start": quarterly_start_date.isoformat(),
801
- "End": (quarterly_end_date + timedelta(days=1)).isoformat() # Exclusive end
834
+ "Start": quarterly_start_date.isoformat(),
835
+ "End": (quarterly_end_date + timedelta(days=1)).isoformat(), # Exclusive end
802
836
  },
803
837
  Granularity="MONTHLY",
804
838
  Metrics=["UnblendedCost"],
@@ -810,22 +844,22 @@ def get_quarterly_cost_data(
810
844
  if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
811
845
  handle_cost_explorer_error(e, profile_name)
812
846
  return {}
813
-
847
+
814
848
  # Aggregate quarterly costs by service across the 3-month period
815
849
  quarterly_service_costs: Dict[str, float] = defaultdict(float)
816
-
850
+
817
851
  for result in quarterly_period_cost_by_service.get("ResultsByTime", []):
818
852
  for group in result.get("Groups", []):
819
853
  service = group["Keys"][0]
820
854
  amount = float(group["Metrics"]["UnblendedCost"]["Amount"])
821
855
  quarterly_service_costs[service] += amount
822
-
856
+
823
857
  # Filter out negligible costs and convert to regular dict
824
858
  filtered_quarterly_costs = {}
825
859
  for service, amount in quarterly_service_costs.items():
826
860
  if amount > 0.001: # Filter out negligible costs
827
861
  filtered_quarterly_costs[service] = amount
828
-
862
+
829
863
  console.log(f"[cyan]📊 Retrieved quarterly cost data for {len(filtered_quarterly_costs)} services[/]")
830
864
  return filtered_quarterly_costs
831
865
 
@@ -856,15 +890,15 @@ def process_service_costs(
856
890
 
857
891
 
858
892
  def format_budget_info(budgets: List[BudgetInfo]) -> List[str]:
859
- """Format budget information for display with enhanced error handling."""
893
+ """Format budget information for display with enhanced error handling and concise icons."""
860
894
  budget_info: List[str] = []
861
-
895
+
862
896
  # Check if this is an access denied case (common with read-only profiles)
863
897
  if budgets and len(budgets) == 1:
864
898
  first_budget = budgets[0]
865
899
  if isinstance(first_budget, dict):
866
900
  # Check for access denied pattern
867
- if first_budget.get('name', '').lower() in ['access denied', 'permission denied', 'n/a']:
901
+ if first_budget.get("name", "").lower() in ["access denied", "permission denied", "n/a"]:
868
902
  budget_info.append("ℹ️ Budget data unavailable")
869
903
  budget_info.append("(Read-only profile)")
870
904
  budget_info.append("")
@@ -872,13 +906,39 @@ def format_budget_info(budgets: List[BudgetInfo]) -> List[str]:
872
906
  budget_info.append("Add budgets:ViewBudget")
873
907
  budget_info.append("policy to profile")
874
908
  return budget_info
875
-
876
- # Normal budget formatting
909
+
910
+ # Enhanced budget formatting with concise icons and status
877
911
  for budget in budgets:
878
- budget_info.append(f"{budget['name']} limit: ${budget['limit']}")
879
- budget_info.append(f"{budget['name']} actual: ${budget['actual']:.2f}")
880
- if budget["forecast"] is not None:
881
- budget_info.append(f"{budget['name']} forecast: ${budget['forecast']:.2f}")
912
+ # Calculate budget utilization for status determination
913
+ utilization = (budget["actual"] / budget["limit"]) * 100 if budget["limit"] > 0 else 0
914
+
915
+ # Determine status icon and color based on utilization
916
+ if utilization >= 100:
917
+ status_icon = "🚨" # Over budget - critical
918
+ status_color = "bright_red"
919
+ elif utilization >= 85:
920
+ status_icon = "⚠️" # Near limit - warning
921
+ status_color = "orange1"
922
+ elif utilization >= 70:
923
+ status_icon = "🟡" # Moderate usage - caution
924
+ status_color = "yellow"
925
+ else:
926
+ status_icon = "✅" # Under budget - good
927
+ status_color = "green"
928
+
929
+ # Format budget name (shortened for display)
930
+ display_name = budget["name"].replace(" Budget", "").replace("Budget", "").strip()
931
+ if len(display_name) > 15:
932
+ display_name = display_name[:12] + "..."
933
+
934
+ # Concise budget display with icons
935
+ budget_info.append(f"{status_icon} [{status_color}]{display_name}[/]")
936
+ budget_info.append(f"💰 ${budget['actual']:.0f}/${budget['limit']:.0f} ({utilization:.0f}%)")
937
+
938
+ # Add forecast only if significantly different from actual
939
+ if budget["forecast"] is not None and abs(budget["forecast"] - budget["actual"]) > (budget["actual"] * 0.1):
940
+ trend_icon = "📈" if budget["forecast"] > budget["actual"] else "📉"
941
+ budget_info.append(f"{trend_icon} Est: ${budget['forecast']:.0f}")
882
942
 
883
943
  if not budget_info:
884
944
  budget_info.append("ℹ️ No budgets configured")
@@ -893,7 +953,7 @@ def calculate_quarterly_enhanced_trend(
893
953
  previous: float,
894
954
  quarterly: float,
895
955
  current_days: Optional[int] = None,
896
- previous_days: Optional[int] = None
956
+ previous_days: Optional[int] = None,
897
957
  ) -> str:
898
958
  """
899
959
  Calculate trend with quarterly financial intelligence for strategic decision making.
@@ -963,11 +1023,11 @@ def calculate_quarterly_enhanced_trend(
963
1023
  def format_cost_with_precision(amount: float, context: str = "dashboard") -> str:
964
1024
  """
965
1025
  Format cost with context-aware precision for consistent display.
966
-
1026
+
967
1027
  Args:
968
1028
  amount: Cost amount to format
969
1029
  context: Display context ('executive', 'detailed', 'dashboard')
970
-
1030
+
971
1031
  Returns:
972
1032
  Formatted cost string with appropriate precision
973
1033
  """
@@ -982,22 +1042,22 @@ def format_cost_with_precision(amount: float, context: str = "dashboard") -> str
982
1042
  return f"${amount:,.2f}"
983
1043
 
984
1044
 
985
- def calculate_trend_with_context(current: float, previous: float,
986
- current_days: Optional[int] = None,
987
- previous_days: Optional[int] = None) -> str:
1045
+ def calculate_trend_with_context(
1046
+ current: float, previous: float, current_days: Optional[int] = None, previous_days: Optional[int] = None
1047
+ ) -> str:
988
1048
  """
989
1049
  Calculate trend with statistical context and confidence, handling partial period comparisons.
990
-
991
- CRITICAL MATHEMATICAL FIX: Addresses the business-critical issue where partial current month
1050
+
1051
+ CRITICAL MATHEMATICAL FIX: Addresses the business-critical issue where partial current month
992
1052
  (e.g., September 1-2: $2.50) was compared against full previous month (August 1-31: $155.00),
993
1053
  resulting in misleading -98.4% trend calculations that could cause incorrect business decisions.
994
-
1054
+
995
1055
  Args:
996
1056
  current: Current period cost
997
1057
  previous: Previous period cost
998
1058
  current_days: Number of days in current period (for partial period detection)
999
1059
  previous_days: Number of days in previous period (for partial period detection)
1000
-
1060
+
1001
1061
  Returns:
1002
1062
  Trend string with appropriate context and partial period warnings
1003
1063
  """
@@ -1006,28 +1066,71 @@ def calculate_trend_with_context(current: float, previous: float,
1006
1066
  return "No change (both periods $0)"
1007
1067
  else:
1008
1068
  return "New spend (no historical data)"
1009
-
1010
- # Detect partial period issues
1069
+
1070
+ # Detect partial period issues and apply smart normalization
1011
1071
  partial_period_issue = False
1072
+ normalized_change_percent = None
1073
+ normalization_applied = False
1074
+
1012
1075
  if current_days and previous_days:
1013
1076
  if abs(current_days - previous_days) > 5: # More than 5 days difference
1014
1077
  partial_period_issue = True
1015
-
1016
- # Calculate basic percentage change
1017
- change_percent = ((current - previous) / previous) * 100
1078
+
1079
+ # Apply smart normalization for partial month comparisons
1080
+ if current_days < previous_days:
1081
+ # Current month is partial, previous is full - normalize previous month
1082
+ normalization_factor = current_days / previous_days
1083
+ adjusted_previous = previous * normalization_factor
1084
+ if adjusted_previous > 0:
1085
+ normalized_change_percent = ((current - adjusted_previous) / adjusted_previous) * 100
1086
+ normalization_applied = True
1087
+ from ..common.rich_utils import console
1088
+
1089
+ console.log(
1090
+ f"[dim yellow]📊 Trend normalization: partial current ({current_days} days) vs full previous ({previous_days} days)[/]"
1091
+ )
1092
+ console.log(
1093
+ f"[dim yellow] Adjusted comparison: ${current:.2f} vs ${adjusted_previous:.2f} (factor: {normalization_factor:.2f})[/]"
1094
+ )
1095
+
1096
+ elif current_days > previous_days:
1097
+ # Previous month is partial, current is full - normalize current month
1098
+ normalization_factor = previous_days / current_days
1099
+ adjusted_current = current * normalization_factor
1100
+ if previous > 0:
1101
+ normalized_change_percent = ((adjusted_current - previous) / previous) * 100
1102
+ normalization_applied = True
1103
+ from ..common.rich_utils import console
1104
+
1105
+ console.log(
1106
+ f"[dim yellow]📊 Trend normalization: full current ({current_days} days) vs partial previous ({previous_days} days)[/]"
1107
+ )
1108
+ console.log(
1109
+ f"[dim yellow] Adjusted comparison: ${adjusted_current:.2f} vs ${previous:.2f} (factor: {normalization_factor:.2f})[/]"
1110
+ )
1111
+
1112
+ # Use normalized change if available, otherwise calculate basic percentage change
1113
+ if normalization_applied and normalized_change_percent is not None:
1114
+ change_percent = normalized_change_percent
1115
+ # Add indicator that normalization was applied
1116
+ normalization_indicator = " 📏"
1117
+ else:
1118
+ change_percent = ((current - previous) / previous) * 100
1119
+ normalization_indicator = ""
1018
1120
 
1019
1121
  # FIXED: Show meaningful percentage trends instead of generic messages
1020
1122
  if abs(change_percent) < 0.01: # Less than 0.01%
1021
1123
  if current == previous:
1022
- return "→ 0.0%" # Show actual zero change percentage
1124
+ return f"→ 0.0%{normalization_indicator}" # Show actual zero change percentage
1023
1125
  elif abs(current - previous) < 0.01: # Very small absolute difference
1024
- return "→ <0.1%" # Show near-zero change with percentage
1126
+ return f"→ <0.1%{normalization_indicator}" # Show near-zero change with percentage
1025
1127
  else:
1026
1128
  # Show actual small change with precise percentage
1027
- return f"{'↑' if change_percent > 0 else '↓'} {abs(change_percent):.2f}%"
1129
+ return f"{'↑' if change_percent > 0 else '↓'} {abs(change_percent):.2f}%{normalization_indicator}"
1028
1130
 
1029
1131
  # Handle partial period comparisons with clean display
1030
- if partial_period_issue:
1132
+ if partial_period_issue and not normalization_applied:
1133
+ # Only show warnings if normalization wasn't applied (fallback case)
1031
1134
  if abs(change_percent) > 50:
1032
1135
  return "⚠️ Trend not reliable (partial data)"
1033
1136
  else:
@@ -1037,48 +1140,48 @@ def calculate_trend_with_context(current: float, previous: float,
1037
1140
  # Standard trend analysis for equal periods
1038
1141
  if abs(change_percent) > 90:
1039
1142
  if change_percent > 0:
1040
- return f"↑ {change_percent:.1f}% (significant increase - verify)"
1143
+ return f"↑ {change_percent:.1f}% (significant increase - verify){normalization_indicator}"
1041
1144
  else:
1042
- return f"↓ {abs(change_percent):.1f}% (significant decrease - verify)"
1145
+ return f"↓ {abs(change_percent):.1f}% (significant decrease - verify){normalization_indicator}"
1043
1146
  elif abs(change_percent) < 1:
1044
- return "→ Stable (< 1% change)"
1147
+ return f"→ Stable (< 1% change){normalization_indicator}"
1045
1148
  else:
1046
1149
  if change_percent > 0:
1047
- return f"↑ {change_percent:.1f}%"
1150
+ return f"↑ {change_percent:.1f}%{normalization_indicator}"
1048
1151
  else:
1049
- return f"↓ {abs(change_percent):.1f}%"
1152
+ return f"↓ {abs(change_percent):.1f}%{normalization_indicator}"
1050
1153
 
1051
1154
 
1052
1155
  def format_ec2_summary(ec2_data: EC2Summary) -> List[str]:
1053
1156
  """Format EC2 instance summary with enhanced visual hierarchy."""
1054
1157
  ec2_summary_text: List[str] = []
1055
-
1158
+
1056
1159
  # Enhanced state formatting with icons and context
1057
1160
  state_config = {
1058
1161
  "running": {"color": "bright_green", "icon": "🟢", "priority": 1},
1059
1162
  "stopped": {"color": "bright_yellow", "icon": "🟡", "priority": 2},
1060
1163
  "terminated": {"color": "dim red", "icon": "🔴", "priority": 4},
1061
1164
  "pending": {"color": "bright_cyan", "icon": "🔵", "priority": 3},
1062
- "stopping": {"color": "yellow", "icon": "🟠", "priority": 3}
1165
+ "stopping": {"color": "yellow", "icon": "🟠", "priority": 3},
1063
1166
  }
1064
-
1167
+
1065
1168
  # Sort by priority and then by state name
1066
1169
  sorted_states = sorted(
1067
1170
  [(state, count) for state, count in ec2_data.items() if count > 0],
1068
- key=lambda x: (state_config.get(x[0], {"priority": 99})["priority"], x[0])
1171
+ key=lambda x: (state_config.get(x[0], {"priority": 99})["priority"], x[0]),
1069
1172
  )
1070
-
1173
+
1071
1174
  total_instances = sum(count for _, count in sorted_states)
1072
-
1175
+
1073
1176
  if sorted_states:
1074
1177
  # Header with total count
1075
1178
  ec2_summary_text.append(f"[bright_cyan]📊 EC2 Instances ({total_instances} total)[/bright_cyan]")
1076
-
1179
+
1077
1180
  # Individual states with enhanced styling
1078
1181
  for state, count in sorted_states:
1079
1182
  config = state_config.get(state, {"color": "white", "icon": "⚪", "priority": 99})
1080
1183
  percentage = (count / total_instances * 100) if total_instances > 0 else 0
1081
-
1184
+
1082
1185
  ec2_summary_text.append(
1083
1186
  f" {config['icon']} [{config['color']}]{state.title()}: {count}[/{config['color']}] "
1084
1187
  f"[dim]({percentage:.1f}%)[/dim]"
@@ -1130,25 +1233,29 @@ def export_to_csv(
1130
1233
  previous_period_header,
1131
1234
  current_period_header,
1132
1235
  ]
1133
-
1236
+
1134
1237
  # Add dual-metric columns if requested
1135
1238
  if include_dual_metrics:
1136
- fieldnames.extend([
1137
- f"AmortizedCost {current_period_header}",
1138
- f"AmortizedCost {previous_period_header}",
1139
- "Metric Variance ($)",
1140
- "Metric Variance (%)",
1141
- "Cost By Service (UnblendedCost)",
1142
- "Cost By Service (AmortizedCost)",
1143
- ])
1239
+ fieldnames.extend(
1240
+ [
1241
+ f"AmortizedCost {current_period_header}",
1242
+ f"AmortizedCost {previous_period_header}",
1243
+ "Metric Variance ($)",
1244
+ "Metric Variance (%)",
1245
+ "Cost By Service (UnblendedCost)",
1246
+ "Cost By Service (AmortizedCost)",
1247
+ ]
1248
+ )
1144
1249
  else:
1145
1250
  fieldnames.append("Cost By Service")
1146
-
1147
- fieldnames.extend([
1148
- "Budget Status",
1149
- "EC2 Instances",
1150
- ])
1151
-
1251
+
1252
+ fieldnames.extend(
1253
+ [
1254
+ "Budget Status",
1255
+ "EC2 Instances",
1256
+ ]
1257
+ )
1258
+
1152
1259
  writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
1153
1260
  writer.writeheader()
1154
1261
  for row in data:
@@ -1196,7 +1303,7 @@ def export_to_csv(
1196
1303
  previous_period_header: row.get("previous_month_formatted", "N/A"),
1197
1304
  current_period_header: row.get("current_month_formatted", "N/A"),
1198
1305
  }
1199
-
1306
+
1200
1307
  # Add dual-metric data if requested
1201
1308
  if include_dual_metrics:
1202
1309
  # Calculate variance for dual-metric display
@@ -1205,32 +1312,35 @@ def export_to_csv(
1205
1312
  previous_amortized = row.get("previous_month_amortized", row.get("previous_month", 0))
1206
1313
  variance = abs(current_unblended - current_amortized)
1207
1314
  variance_pct = (variance / current_amortized * 100) if current_amortized > 0 else 0
1208
-
1315
+
1209
1316
  # Format amortized service costs
1210
1317
  amortized_services_data = "No amortized service costs"
1211
1318
  if row.get("service_costs_amortized"):
1212
- amortized_services_data = "\n".join([
1213
- f"{service}: ${cost:.2f}"
1214
- for service, cost in row["service_costs_amortized"]
1215
- ])
1216
-
1217
- row_data.update({
1218
- f"AmortizedCost {current_period_header}": f"${current_amortized:.2f}",
1219
- f"AmortizedCost {previous_period_header}": f"${previous_amortized:.2f}",
1220
- "Metric Variance ($)": f"${variance:.2f}",
1221
- "Metric Variance (%)": f"{variance_pct:.2f}%",
1222
- "Cost By Service (UnblendedCost)": services_data or "No costs",
1223
- "Cost By Service (AmortizedCost)": amortized_services_data,
1224
- })
1319
+ amortized_services_data = "\n".join(
1320
+ [f"{service}: ${cost:.2f}" for service, cost in row["service_costs_amortized"]]
1321
+ )
1322
+
1323
+ row_data.update(
1324
+ {
1325
+ f"AmortizedCost {current_period_header}": f"${current_amortized:.2f}",
1326
+ f"AmortizedCost {previous_period_header}": f"${previous_amortized:.2f}",
1327
+ "Metric Variance ($)": f"${variance:.2f}",
1328
+ "Metric Variance (%)": f"{variance_pct:.2f}%",
1329
+ "Cost By Service (UnblendedCost)": services_data or "No costs",
1330
+ "Cost By Service (AmortizedCost)": amortized_services_data,
1331
+ }
1332
+ )
1225
1333
  else:
1226
1334
  row_data["Cost By Service"] = services_data or "No costs"
1227
-
1335
+
1228
1336
  # Add common fields
1229
- row_data.update({
1230
- "Budget Status": budgets_data or "No budgets",
1231
- "EC2 Instances": ec2_data_summary or "No instances",
1232
- })
1233
-
1337
+ row_data.update(
1338
+ {
1339
+ "Budget Status": budgets_data or "No budgets",
1340
+ "EC2 Instances": ec2_data_summary or "No instances",
1341
+ }
1342
+ )
1343
+
1234
1344
  writer.writerow(row_data)
1235
1345
  except (KeyError, TypeError) as e:
1236
1346
  console.print(f"[yellow]Warning: Could not write CSV row: {e}[/]")
@@ -1238,7 +1348,7 @@ def export_to_csv(
1238
1348
  writer.writerow(
1239
1349
  {
1240
1350
  "CLI Profile": "Error",
1241
- "AWS Account ID": "Error",
1351
+ "AWS Account ID": "Error",
1242
1352
  previous_period_header: "Error",
1243
1353
  current_period_header: "Error",
1244
1354
  "Cost By Service": f"Row processing error: {e}",
@@ -1253,7 +1363,9 @@ def export_to_csv(
1253
1363
  return None
1254
1364
 
1255
1365
 
1256
- def export_to_json(data: List[ProfileData], filename: str, output_dir: Optional[str] = None, include_dual_metrics: bool = False) -> Optional[str]:
1366
+ def export_to_json(
1367
+ data: List[ProfileData], filename: str, output_dir: Optional[str] = None, include_dual_metrics: bool = False
1368
+ ) -> Optional[str]:
1257
1369
  """Export dashboard data to a JSON file."""
1258
1370
  try:
1259
1371
  timestamp = datetime.now().strftime("%Y%m%d_%H%M")
@@ -1271,47 +1383,53 @@ def export_to_json(data: List[ProfileData], filename: str, output_dir: Optional[
1271
1383
  if include_dual_metrics:
1272
1384
  # Enhanced data structure for dual metrics
1273
1385
  enhanced_item = dict(item) # Copy base data
1274
-
1386
+
1275
1387
  # Calculate variance metrics
1276
1388
  current_unblended = item.get("current_month", 0)
1277
1389
  current_amortized = item.get("current_month_amortized", current_unblended)
1278
1390
  variance = abs(current_unblended - current_amortized)
1279
1391
  variance_pct = (variance / current_amortized * 100) if current_amortized > 0 else 0
1280
-
1392
+
1281
1393
  # Add dual-metric metadata
1282
- enhanced_item.update({
1283
- "dual_metric_analysis": {
1284
- "unblended_cost": {
1285
- "current": current_unblended,
1286
- "previous": item.get("previous_month", 0),
1287
- "metric_type": "technical",
1288
- "description": "UnblendedCost - for DevOps/SRE teams"
1394
+ enhanced_item.update(
1395
+ {
1396
+ "dual_metric_analysis": {
1397
+ "unblended_cost": {
1398
+ "current": current_unblended,
1399
+ "previous": item.get("previous_month", 0),
1400
+ "metric_type": "technical",
1401
+ "description": "UnblendedCost - for DevOps/SRE teams",
1402
+ },
1403
+ "amortized_cost": {
1404
+ "current": current_amortized,
1405
+ "previous": item.get("previous_month_amortized", item.get("previous_month", 0)),
1406
+ "metric_type": "financial",
1407
+ "description": "AmortizedCost - for Finance/Executive teams",
1408
+ },
1409
+ "variance_analysis": {
1410
+ "absolute_variance": variance,
1411
+ "percentage_variance": variance_pct,
1412
+ "variance_level": "low"
1413
+ if variance_pct < 1.0
1414
+ else "moderate"
1415
+ if variance_pct < 5.0
1416
+ else "high",
1417
+ },
1289
1418
  },
1290
- "amortized_cost": {
1291
- "current": current_amortized,
1292
- "previous": item.get("previous_month_amortized", item.get("previous_month", 0)),
1293
- "metric_type": "financial",
1294
- "description": "AmortizedCost - for Finance/Executive teams"
1419
+ "export_metadata": {
1420
+ "export_type": "dual_metric",
1421
+ "export_timestamp": datetime.now().isoformat(),
1422
+ "metric_explanation": {
1423
+ "unblended_cost": "Actual costs without Reserved Instance or Savings Plan allocations",
1424
+ "amortized_cost": "Costs with Reserved Instance and Savings Plan benefits applied",
1425
+ },
1295
1426
  },
1296
- "variance_analysis": {
1297
- "absolute_variance": variance,
1298
- "percentage_variance": variance_pct,
1299
- "variance_level": "low" if variance_pct < 1.0 else "moderate" if variance_pct < 5.0 else "high"
1300
- }
1301
- },
1302
- "export_metadata": {
1303
- "export_type": "dual_metric",
1304
- "export_timestamp": datetime.now().isoformat(),
1305
- "metric_explanation": {
1306
- "unblended_cost": "Actual costs without Reserved Instance or Savings Plan allocations",
1307
- "amortized_cost": "Costs with Reserved Instance and Savings Plan benefits applied"
1308
- }
1309
1427
  }
1310
- })
1428
+ )
1311
1429
  export_data.append(enhanced_item)
1312
1430
  else:
1313
1431
  export_data.append(item)
1314
-
1432
+
1315
1433
  with open(output_filename, "w") as jsonfile:
1316
1434
  json.dump(export_data, jsonfile, indent=4)
1317
1435