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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +135 -91
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +17 -12
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +99 -79
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +315 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/aws_decorators.py +2 -3
  113. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  114. runbooks/inventory/check_controltower_readiness.py +152 -151
  115. runbooks/inventory/check_landingzone_readiness.py +85 -84
  116. runbooks/inventory/cloud_foundations_integration.py +144 -149
  117. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  118. runbooks/inventory/collectors/aws_networking.py +109 -99
  119. runbooks/inventory/collectors/base.py +4 -0
  120. runbooks/inventory/core/collector.py +495 -313
  121. runbooks/inventory/core/formatter.py +11 -0
  122. runbooks/inventory/draw_org_structure.py +8 -9
  123. runbooks/inventory/drift_detection_cli.py +69 -96
  124. runbooks/inventory/ec2_vpc_utils.py +2 -2
  125. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  126. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  127. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  128. runbooks/inventory/find_ec2_security_groups.py +48 -42
  129. runbooks/inventory/find_landingzone_versions.py +4 -6
  130. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  131. runbooks/inventory/inventory_mcp_cli.py +48 -46
  132. runbooks/inventory/inventory_modules.py +103 -91
  133. runbooks/inventory/list_cfn_stacks.py +9 -10
  134. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  135. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  136. runbooks/inventory/list_cfn_stacksets.py +8 -10
  137. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  138. runbooks/inventory/list_ds_directories.py +65 -53
  139. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  140. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  141. runbooks/inventory/list_ec2_instances.py +23 -28
  142. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  143. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  144. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  145. runbooks/inventory/list_guardduty_detectors.py +2 -4
  146. runbooks/inventory/list_iam_policies.py +2 -4
  147. runbooks/inventory/list_iam_roles.py +5 -7
  148. runbooks/inventory/list_iam_saml_providers.py +4 -6
  149. runbooks/inventory/list_lambda_functions.py +38 -38
  150. runbooks/inventory/list_org_accounts.py +6 -8
  151. runbooks/inventory/list_org_accounts_users.py +55 -44
  152. runbooks/inventory/list_rds_db_instances.py +31 -33
  153. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  154. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  155. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  156. runbooks/inventory/list_sns_topics.py +2 -4
  157. runbooks/inventory/list_ssm_parameters.py +4 -7
  158. runbooks/inventory/list_vpc_subnets.py +2 -4
  159. runbooks/inventory/list_vpcs.py +7 -10
  160. runbooks/inventory/mcp_inventory_validator.py +554 -468
  161. runbooks/inventory/mcp_vpc_validator.py +359 -442
  162. runbooks/inventory/organizations_discovery.py +63 -55
  163. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  164. runbooks/inventory/requirements.txt +0 -1
  165. runbooks/inventory/rich_inventory_display.py +35 -34
  166. runbooks/inventory/run_on_multi_accounts.py +3 -5
  167. runbooks/inventory/unified_validation_engine.py +281 -253
  168. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  169. runbooks/inventory/vpc_analyzer.py +735 -697
  170. runbooks/inventory/vpc_architecture_validator.py +293 -348
  171. runbooks/inventory/vpc_dependency_analyzer.py +384 -380
  172. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  173. runbooks/main.py +49 -34
  174. runbooks/main_final.py +91 -60
  175. runbooks/main_minimal.py +22 -10
  176. runbooks/main_optimized.py +131 -100
  177. runbooks/main_ultra_minimal.py +7 -2
  178. runbooks/mcp/__init__.py +36 -0
  179. runbooks/mcp/integration.py +679 -0
  180. runbooks/monitoring/performance_monitor.py +9 -4
  181. runbooks/operate/dynamodb_operations.py +3 -1
  182. runbooks/operate/ec2_operations.py +145 -137
  183. runbooks/operate/iam_operations.py +146 -152
  184. runbooks/operate/networking_cost_heatmap.py +29 -8
  185. runbooks/operate/rds_operations.py +223 -254
  186. runbooks/operate/s3_operations.py +107 -118
  187. runbooks/operate/vpc_operations.py +646 -616
  188. runbooks/remediation/base.py +1 -1
  189. runbooks/remediation/commons.py +10 -7
  190. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  191. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  192. runbooks/remediation/multi_account.py +24 -21
  193. runbooks/remediation/rds_snapshot_list.py +86 -60
  194. runbooks/remediation/remediation_cli.py +92 -146
  195. runbooks/remediation/universal_account_discovery.py +83 -79
  196. runbooks/remediation/workspaces_list.py +46 -41
  197. runbooks/security/__init__.py +19 -0
  198. runbooks/security/assessment_runner.py +1150 -0
  199. runbooks/security/baseline_checker.py +812 -0
  200. runbooks/security/cloudops_automation_security_validator.py +509 -535
  201. runbooks/security/compliance_automation_engine.py +17 -17
  202. runbooks/security/config/__init__.py +2 -2
  203. runbooks/security/config/compliance_config.py +50 -50
  204. runbooks/security/config_template_generator.py +63 -76
  205. runbooks/security/enterprise_security_framework.py +1 -1
  206. runbooks/security/executive_security_dashboard.py +519 -508
  207. runbooks/security/multi_account_security_controls.py +959 -1210
  208. runbooks/security/real_time_security_monitor.py +422 -444
  209. runbooks/security/security_baseline_tester.py +1 -1
  210. runbooks/security/security_cli.py +143 -112
  211. runbooks/security/test_2way_validation.py +439 -0
  212. runbooks/security/two_way_validation_framework.py +852 -0
  213. runbooks/sre/production_monitoring_framework.py +167 -177
  214. runbooks/tdd/__init__.py +15 -0
  215. runbooks/tdd/cli.py +1071 -0
  216. runbooks/utils/__init__.py +14 -17
  217. runbooks/utils/logger.py +7 -2
  218. runbooks/utils/version_validator.py +50 -47
  219. runbooks/validation/__init__.py +6 -6
  220. runbooks/validation/cli.py +9 -3
  221. runbooks/validation/comprehensive_2way_validator.py +745 -704
  222. runbooks/validation/mcp_validator.py +906 -228
  223. runbooks/validation/terraform_citations_validator.py +104 -115
  224. runbooks/validation/terraform_drift_detector.py +461 -454
  225. runbooks/vpc/README.md +617 -0
  226. runbooks/vpc/__init__.py +8 -1
  227. runbooks/vpc/analyzer.py +577 -0
  228. runbooks/vpc/cleanup_wrapper.py +476 -413
  229. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  230. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  231. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  232. runbooks/vpc/config.py +92 -97
  233. runbooks/vpc/cost_engine.py +411 -148
  234. runbooks/vpc/cost_explorer_integration.py +553 -0
  235. runbooks/vpc/cross_account_session.py +101 -106
  236. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  237. runbooks/vpc/eni_gate_validator.py +961 -0
  238. runbooks/vpc/heatmap_engine.py +185 -160
  239. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  240. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  241. runbooks/vpc/networking_wrapper.py +15 -8
  242. runbooks/vpc/pdca_remediation_planner.py +528 -0
  243. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  244. runbooks/vpc/runbooks_adapter.py +1167 -241
  245. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  246. runbooks/vpc/test_data_loader.py +358 -0
  247. runbooks/vpc/tests/conftest.py +314 -4
  248. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  249. runbooks/vpc/tests/test_cost_engine.py +0 -2
  250. runbooks/vpc/topology_generator.py +326 -0
  251. runbooks/vpc/unified_scenarios.py +1297 -1124
  252. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  253. runbooks-1.1.6.dist-info/METADATA +327 -0
  254. runbooks-1.1.6.dist-info/RECORD +489 -0
  255. runbooks/finops/README.md +0 -414
  256. runbooks/finops/accuracy_cross_validator.py +0 -647
  257. runbooks/finops/business_cases.py +0 -950
  258. runbooks/finops/dashboard_router.py +0 -922
  259. runbooks/finops/ebs_optimizer.py +0 -973
  260. runbooks/finops/embedded_mcp_validator.py +0 -1629
  261. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  262. runbooks/finops/finops_dashboard.py +0 -584
  263. runbooks/finops/finops_scenarios.py +0 -1218
  264. runbooks/finops/legacy_migration.py +0 -730
  265. runbooks/finops/multi_dashboard.py +0 -1519
  266. runbooks/finops/single_dashboard.py +0 -1113
  267. runbooks/finops/unlimited_scenarios.py +0 -393
  268. runbooks-1.1.4.dist-info/METADATA +0 -800
  269. runbooks-1.1.4.dist-info/RECORD +0 -468
  270. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  271. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  272. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  273. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
@@ -16,361 +16,355 @@ from functools import lru_cache
16
16
  from datetime import datetime, timedelta
17
17
  import os
18
18
 
19
+
19
20
  class AWSPricingAPI:
20
21
  """Real-time AWS Pricing API integration - ZERO hardcoded values."""
21
-
22
+
22
23
  def __init__(self, profile: Optional[str] = None):
23
24
  """Initialize with AWS Pricing API client."""
24
25
  session = boto3.Session(profile_name=profile) if profile else boto3.Session()
25
- self.pricing_client = session.client('pricing', region_name='us-east-1')
26
- self.ce_client = session.client('ce') # Cost Explorer for real costs
26
+ self.pricing_client = session.client("pricing", region_name="us-east-1")
27
+ self.ce_client = session.client("ce") # Cost Explorer for real costs
27
28
  self._cache = {}
28
29
  self._cache_expiry = {}
29
-
30
+
30
31
  @lru_cache(maxsize=128)
31
- def get_ebs_gp3_cost_per_gb(self, region: str = 'us-east-1') -> float:
32
+ def get_ebs_gp3_cost_per_gb(self, region: str = "us-east-1") -> float:
32
33
  """Get real-time EBS GP3 cost per GB per month from AWS Pricing API."""
33
34
  try:
34
35
  response = self.pricing_client.get_products(
35
- ServiceCode='AmazonEC2',
36
+ ServiceCode="AmazonEC2",
36
37
  Filters=[
37
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Storage'},
38
- {'Type': 'TERM_MATCH', 'Field': 'volumeType', 'Value': 'General Purpose'},
39
- {'Type': 'TERM_MATCH', 'Field': 'storageMedia', 'Value': 'SSD-backed'},
40
- {'Type': 'TERM_MATCH', 'Field': 'volumeApiName', 'Value': 'gp3'},
41
- {'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
38
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
39
+ {"Type": "TERM_MATCH", "Field": "volumeType", "Value": "General Purpose"},
40
+ {"Type": "TERM_MATCH", "Field": "storageMedia", "Value": "SSD-backed"},
41
+ {"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp3"},
42
+ {"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
42
43
  ],
43
- MaxResults=1
44
+ MaxResults=1,
44
45
  )
45
-
46
- if response['PriceList']:
47
- price_data = json.loads(response['PriceList'][0])
48
- on_demand = price_data['terms']['OnDemand']
46
+
47
+ if response["PriceList"]:
48
+ price_data = json.loads(response["PriceList"][0])
49
+ on_demand = price_data["terms"]["OnDemand"]
49
50
  for term in on_demand.values():
50
- for price_dimension in term['priceDimensions'].values():
51
- if 'GB-month' in price_dimension.get('unit', ''):
52
- return float(price_dimension['pricePerUnit']['USD'])
53
-
51
+ for price_dimension in term["priceDimensions"].values():
52
+ if "GB-month" in price_dimension.get("unit", ""):
53
+ return float(price_dimension["pricePerUnit"]["USD"])
54
+
54
55
  # Fallback to Cost Explorer actual costs if Pricing API fails
55
- return self._get_from_cost_explorer('EBS', 'gp3')
56
-
56
+ return self._get_from_cost_explorer("EBS", "gp3")
57
+
57
58
  except Exception as e:
58
59
  # Use Cost Explorer as ultimate fallback
59
- return self._get_from_cost_explorer('EBS', 'gp3')
60
-
60
+ return self._get_from_cost_explorer("EBS", "gp3")
61
+
61
62
  @lru_cache(maxsize=128)
62
- def get_ebs_gp2_cost_per_gb(self, region: str = 'us-east-1') -> float:
63
+ def get_ebs_gp2_cost_per_gb(self, region: str = "us-east-1") -> float:
63
64
  """Get real-time EBS GP2 cost per GB per month from AWS Pricing API."""
64
65
  try:
65
66
  response = self.pricing_client.get_products(
66
- ServiceCode='AmazonEC2',
67
+ ServiceCode="AmazonEC2",
67
68
  Filters=[
68
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Storage'},
69
- {'Type': 'TERM_MATCH', 'Field': 'volumeType', 'Value': 'General Purpose'},
70
- {'Type': 'TERM_MATCH', 'Field': 'volumeApiName', 'Value': 'gp2'},
71
- {'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
69
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
70
+ {"Type": "TERM_MATCH", "Field": "volumeType", "Value": "General Purpose"},
71
+ {"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp2"},
72
+ {"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
72
73
  ],
73
- MaxResults=1
74
+ MaxResults=1,
74
75
  )
75
-
76
- if response['PriceList']:
77
- price_data = json.loads(response['PriceList'][0])
78
- on_demand = price_data['terms']['OnDemand']
76
+
77
+ if response["PriceList"]:
78
+ price_data = json.loads(response["PriceList"][0])
79
+ on_demand = price_data["terms"]["OnDemand"]
79
80
  for term in on_demand.values():
80
- for price_dimension in term['priceDimensions'].values():
81
- if 'GB-month' in price_dimension.get('unit', ''):
82
- return float(price_dimension['pricePerUnit']['USD'])
83
-
84
- return self._get_from_cost_explorer('EBS', 'gp2')
85
-
81
+ for price_dimension in term["priceDimensions"].values():
82
+ if "GB-month" in price_dimension.get("unit", ""):
83
+ return float(price_dimension["pricePerUnit"]["USD"])
84
+
85
+ return self._get_from_cost_explorer("EBS", "gp2")
86
+
86
87
  except Exception:
87
- return self._get_from_cost_explorer('EBS', 'gp2')
88
-
88
+ return self._get_from_cost_explorer("EBS", "gp2")
89
+
89
90
  @lru_cache(maxsize=128)
90
- def get_rds_snapshot_cost_per_gb(self, region: str = 'us-east-1') -> float:
91
+ def get_rds_snapshot_cost_per_gb(self, region: str = "us-east-1") -> float:
91
92
  """Get real-time RDS snapshot cost per GB per month from AWS Pricing API."""
92
93
  try:
93
94
  response = self.pricing_client.get_products(
94
- ServiceCode='AmazonRDS',
95
+ ServiceCode="AmazonRDS",
95
96
  Filters=[
96
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Storage Snapshot'},
97
- {'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
97
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage Snapshot"},
98
+ {"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
98
99
  ],
99
- MaxResults=1
100
+ MaxResults=1,
100
101
  )
101
-
102
- if response['PriceList']:
103
- price_data = json.loads(response['PriceList'][0])
104
- on_demand = price_data['terms']['OnDemand']
102
+
103
+ if response["PriceList"]:
104
+ price_data = json.loads(response["PriceList"][0])
105
+ on_demand = price_data["terms"]["OnDemand"]
105
106
  for term in on_demand.values():
106
- for price_dimension in term['priceDimensions'].values():
107
- if 'GB-month' in price_dimension.get('unit', ''):
108
- return float(price_dimension['pricePerUnit']['USD'])
109
-
110
- return self._get_from_cost_explorer('RDS', 'Snapshot')
111
-
107
+ for price_dimension in term["priceDimensions"].values():
108
+ if "GB-month" in price_dimension.get("unit", ""):
109
+ return float(price_dimension["pricePerUnit"]["USD"])
110
+
111
+ return self._get_from_cost_explorer("RDS", "Snapshot")
112
+
112
113
  except Exception:
113
- return self._get_from_cost_explorer('RDS', 'Snapshot')
114
-
114
+ return self._get_from_cost_explorer("RDS", "Snapshot")
115
+
115
116
  @lru_cache(maxsize=128)
116
- def get_nat_gateway_monthly_cost(self, region: str = 'us-east-1') -> float:
117
+ def get_nat_gateway_monthly_cost(self, region: str = "us-east-1") -> float:
117
118
  """Get real-time NAT Gateway monthly cost from AWS Pricing API with enterprise regional fallback."""
118
-
119
+
119
120
  # Enterprise Regional Fallback Strategy
120
- fallback_regions = ['us-east-1', 'us-west-2', 'eu-west-1']
121
+ fallback_regions = ["us-east-1", "us-west-2", "eu-west-1"]
121
122
  if region not in fallback_regions:
122
123
  fallback_regions.insert(0, region)
123
-
124
+
124
125
  last_error = None
125
-
126
+
126
127
  for attempt_region in fallback_regions:
127
128
  try:
128
129
  # Try AWS Pricing API for this region
129
130
  response = self.pricing_client.get_products(
130
- ServiceCode='AmazonVPC',
131
+ ServiceCode="AmazonVPC",
131
132
  Filters=[
132
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'NAT Gateway'},
133
- {'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(attempt_region)}
133
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "NAT Gateway"},
134
+ {"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(attempt_region)},
134
135
  ],
135
- MaxResults=1
136
+ MaxResults=1,
136
137
  )
137
-
138
- if response['PriceList']:
139
- price_data = json.loads(response['PriceList'][0])
140
- on_demand = price_data['terms']['OnDemand']
138
+
139
+ if response["PriceList"]:
140
+ price_data = json.loads(response["PriceList"][0])
141
+ on_demand = price_data["terms"]["OnDemand"]
141
142
  for term in on_demand.values():
142
- for price_dimension in term['priceDimensions'].values():
143
- if 'Hrs' in price_dimension.get('unit', ''):
144
- hourly_rate = float(price_dimension['pricePerUnit']['USD'])
143
+ for price_dimension in term["priceDimensions"].values():
144
+ if "Hrs" in price_dimension.get("unit", ""):
145
+ hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
145
146
  monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
146
147
  print(f"✅ NAT Gateway pricing: ${monthly_cost:.2f}/month from {attempt_region}")
147
148
  return monthly_cost
148
-
149
+
149
150
  # Try Cost Explorer for this region
150
- ce_cost = self._get_from_cost_explorer('VPC', 'NAT Gateway', attempt_region)
151
+ ce_cost = self._get_from_cost_explorer("VPC", "NAT Gateway", attempt_region)
151
152
  if ce_cost > 0:
152
153
  print(f"✅ NAT Gateway pricing: ${ce_cost:.2f}/month from Cost Explorer")
153
154
  return ce_cost
154
-
155
+
155
156
  except Exception as e:
156
157
  last_error = e
157
158
  print(f"⚠️ Pricing API failed for region {attempt_region}: {e}")
158
159
  continue
159
-
160
+
160
161
  # Enterprise fallback with graceful degradation
161
- return self._get_enterprise_fallback_pricing('nat_gateway', region, last_error)
162
-
162
+ return self._get_enterprise_fallback_pricing("nat_gateway", region, last_error)
163
+
163
164
  def _get_from_cost_explorer(self, service: str, resource_type: str, region: str = None) -> float:
164
165
  """Get actual costs from Cost Explorer as ultimate source of truth."""
165
166
  try:
166
167
  end_date = datetime.now()
167
168
  start_date = end_date - timedelta(days=30)
168
-
169
+
169
170
  # Build filter with optional region
170
- filter_conditions = [
171
- {'Dimensions': {'Key': 'SERVICE', 'Values': [f'Amazon {service}']}}
172
- ]
173
-
171
+ filter_conditions = [{"Dimensions": {"Key": "SERVICE", "Values": [f"Amazon {service}"]}}]
172
+
174
173
  if region:
175
- filter_conditions.append({
176
- 'Dimensions': {'Key': 'REGION', 'Values': [region]}
177
- })
178
-
174
+ filter_conditions.append({"Dimensions": {"Key": "REGION", "Values": [region]}})
175
+
179
176
  # Add resource type filter if it helps
180
177
  if resource_type != service:
181
- filter_conditions.append({
182
- 'Dimensions': {'Key': 'USAGE_TYPE_GROUP', 'Values': [resource_type]}
183
- })
184
-
185
- cost_filter = {'And': filter_conditions} if len(filter_conditions) > 1 else filter_conditions[0]
186
-
178
+ filter_conditions.append({"Dimensions": {"Key": "USAGE_TYPE_GROUP", "Values": [resource_type]}})
179
+
180
+ cost_filter = {"And": filter_conditions} if len(filter_conditions) > 1 else filter_conditions[0]
181
+
187
182
  response = self.ce_client.get_cost_and_usage(
188
- TimePeriod={
189
- 'Start': start_date.strftime('%Y-%m-%d'),
190
- 'End': end_date.strftime('%Y-%m-%d')
191
- },
192
- Granularity='MONTHLY',
193
- Metrics=['UnblendedCost'],
194
- Filter=cost_filter
183
+ TimePeriod={"Start": start_date.strftime("%Y-%m-%d"), "End": end_date.strftime("%Y-%m-%d")},
184
+ Granularity="MONTHLY",
185
+ Metrics=["UnblendedCost"],
186
+ Filter=cost_filter,
195
187
  )
196
-
197
- if response['ResultsByTime'] and response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount']:
198
- total_cost = float(response['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
188
+
189
+ if response["ResultsByTime"] and response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"]:
190
+ total_cost = float(response["ResultsByTime"][0]["Total"]["UnblendedCost"]["Amount"])
199
191
  if total_cost > 0:
200
192
  # Calculate per-unit cost based on usage
201
193
  return self._calculate_unit_cost(total_cost, service, resource_type)
202
-
194
+
203
195
  return 0.0 # No cost data found
204
-
196
+
205
197
  except Exception as e:
206
198
  print(f"⚠️ Cost Explorer query failed: {e}")
207
199
  return 0.0
208
-
200
+
209
201
  def _calculate_unit_cost(self, total_cost: float, service: str, resource_type: str) -> float:
210
202
  """Calculate per-unit cost from total cost and usage metrics."""
211
203
  # This would query CloudWatch for usage metrics and calculate unit cost
212
204
  # For now, returning calculated estimates based on typical usage patterns
213
205
  usage_multipliers = {
214
- 'EBS': {'gp3': 1000, 'gp2': 1200}, # Typical GB usage
215
- 'RDS': {'Snapshot': 5000}, # Typical snapshot GB
216
- 'VPC': {'NAT Gateway': 1} # Per gateway
206
+ "EBS": {"gp3": 1000, "gp2": 1200}, # Typical GB usage
207
+ "RDS": {"Snapshot": 5000}, # Typical snapshot GB
208
+ "VPC": {"NAT Gateway": 1}, # Per gateway
217
209
  }
218
-
210
+
219
211
  divisor = usage_multipliers.get(service, {}).get(resource_type, 1000)
220
212
  return total_cost / divisor
221
-
213
+
222
214
  def _get_enterprise_fallback_pricing(self, resource_type: str, region: str, last_error: Exception = None) -> float:
223
215
  """Enterprise-compliant fallback pricing with graceful degradation."""
224
-
216
+
225
217
  # Check for enterprise configuration override
226
218
  override_env = f"AWS_PRICING_OVERRIDE_{resource_type.upper()}_MONTHLY"
227
219
  override_value = os.getenv(override_env)
228
220
  if override_value:
229
221
  print(f"💼 Using enterprise pricing override: ${override_value}/month")
230
222
  return float(override_value)
231
-
223
+
232
224
  # Check if running in compliance-mode or analysis can proceed with warnings
233
225
  compliance_mode = os.getenv("AWS_PRICING_STRICT_COMPLIANCE", "false").lower() == "true"
234
-
226
+
235
227
  if compliance_mode:
236
228
  # Strict compliance: block operation
237
- error_msg = f"ENTERPRISE VIOLATION: Cannot proceed without dynamic {resource_type} pricing. " \
238
- f"Last error: {last_error}. Set {override_env} or enable fallback pricing."
229
+ error_msg = (
230
+ f"ENTERPRISE VIOLATION: Cannot proceed without dynamic {resource_type} pricing. "
231
+ f"Last error: {last_error}. Set {override_env} or enable fallback pricing."
232
+ )
239
233
  print(f"🚫 {error_msg}")
240
234
  raise ValueError(error_msg)
241
235
  else:
242
236
  # Graceful degradation: allow analysis with standard AWS rates (documented approach)
243
237
  standard_rates = {
244
- 'nat_gateway': 32.40, # AWS standard us-east-1 rate: $0.045/hour * 24 * 30
245
- 'transit_gateway': 36.00, # AWS standard us-east-1 rate: $0.05/hour * 24 * 30
246
- 'vpc_endpoint_interface': 7.20, # AWS standard us-east-1 rate: $0.01/hour * 24 * 30
247
- 'elastic_ip_idle': 3.60, # AWS standard us-east-1 rate: $0.005/hour * 24 * 30
238
+ "nat_gateway": 32.40, # AWS standard us-east-1 rate: $0.045/hour * 24 * 30
239
+ "transit_gateway": 36.00, # AWS standard us-east-1 rate: $0.05/hour * 24 * 30
240
+ "vpc_endpoint_interface": 7.20, # AWS standard us-east-1 rate: $0.01/hour * 24 * 30
241
+ "elastic_ip_idle": 3.60, # AWS standard us-east-1 rate: $0.005/hour * 24 * 30
248
242
  }
249
-
243
+
250
244
  if resource_type in standard_rates:
251
245
  fallback_cost = standard_rates[resource_type]
252
246
  print(f"⚠️ FALLBACK PRICING: Using standard AWS rate ${fallback_cost}/month for {resource_type}")
253
247
  print(f" ℹ️ To fix: Check IAM permissions for pricing:GetProducts and ce:GetCostAndUsage")
254
248
  print(f" ℹ️ Or set {override_env} for enterprise override")
255
249
  return fallback_cost
256
-
257
- # Last resort: query MCP servers for validation
250
+
251
+ # Last resort: query MCP servers for validation
258
252
  return self._query_mcp_servers(resource_type, region, last_error)
259
-
253
+
260
254
  @lru_cache(maxsize=128)
261
- def get_vpc_endpoint_monthly_cost(self, region: str = 'us-east-1') -> float:
255
+ def get_vpc_endpoint_monthly_cost(self, region: str = "us-east-1") -> float:
262
256
  """Get real-time VPC Endpoint monthly cost from AWS Pricing API."""
263
257
  try:
264
258
  response = self.pricing_client.get_products(
265
- ServiceCode='AmazonVPC',
259
+ ServiceCode="AmazonVPC",
266
260
  Filters=[
267
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'VpcEndpoint'},
268
- {'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
261
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "VpcEndpoint"},
262
+ {"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
269
263
  ],
270
- MaxResults=1
264
+ MaxResults=1,
271
265
  )
272
-
273
- if response['PriceList']:
274
- price_data = json.loads(response['PriceList'][0])
275
- on_demand = price_data['terms']['OnDemand']
266
+
267
+ if response["PriceList"]:
268
+ price_data = json.loads(response["PriceList"][0])
269
+ on_demand = price_data["terms"]["OnDemand"]
276
270
  for term in on_demand.values():
277
- for price_dimension in term['priceDimensions'].values():
278
- if 'Hrs' in price_dimension.get('unit', ''):
279
- hourly_rate = float(price_dimension['pricePerUnit']['USD'])
271
+ for price_dimension in term["priceDimensions"].values():
272
+ if "Hrs" in price_dimension.get("unit", ""):
273
+ hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
280
274
  monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
281
275
  return monthly_cost
282
-
276
+
283
277
  # Fallback to Cost Explorer
284
- return self._get_from_cost_explorer('VPC', 'VpcEndpoint', region)
285
-
278
+ return self._get_from_cost_explorer("VPC", "VpcEndpoint", region)
279
+
286
280
  except Exception as e:
287
- return self._get_from_cost_explorer('VPC', 'VpcEndpoint', region)
281
+ return self._get_from_cost_explorer("VPC", "VpcEndpoint", region)
288
282
 
289
283
  @lru_cache(maxsize=128)
290
- def get_transit_gateway_monthly_cost(self, region: str = 'us-east-1') -> float:
284
+ def get_transit_gateway_monthly_cost(self, region: str = "us-east-1") -> float:
291
285
  """Get real-time Transit Gateway monthly cost from AWS Pricing API."""
292
286
  try:
293
287
  response = self.pricing_client.get_products(
294
- ServiceCode='AmazonVPC',
288
+ ServiceCode="AmazonVPC",
295
289
  Filters=[
296
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Transit Gateway'},
297
- {'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
290
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Transit Gateway"},
291
+ {"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
298
292
  ],
299
- MaxResults=1
293
+ MaxResults=1,
300
294
  )
301
-
302
- if response['PriceList']:
303
- price_data = json.loads(response['PriceList'][0])
304
- on_demand = price_data['terms']['OnDemand']
295
+
296
+ if response["PriceList"]:
297
+ price_data = json.loads(response["PriceList"][0])
298
+ on_demand = price_data["terms"]["OnDemand"]
305
299
  for term in on_demand.values():
306
- for price_dimension in term['priceDimensions'].values():
307
- if 'Hrs' in price_dimension.get('unit', ''):
308
- hourly_rate = float(price_dimension['pricePerUnit']['USD'])
300
+ for price_dimension in term["priceDimensions"].values():
301
+ if "Hrs" in price_dimension.get("unit", ""):
302
+ hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
309
303
  monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
310
304
  return monthly_cost
311
-
305
+
312
306
  # Fallback to Cost Explorer
313
- return self._get_from_cost_explorer('VPC', 'Transit Gateway', region)
314
-
307
+ return self._get_from_cost_explorer("VPC", "Transit Gateway", region)
308
+
315
309
  except Exception as e:
316
- return self._get_from_cost_explorer('VPC', 'Transit Gateway', region)
310
+ return self._get_from_cost_explorer("VPC", "Transit Gateway", region)
317
311
 
318
312
  @lru_cache(maxsize=128)
319
- def get_elastic_ip_monthly_cost(self, region: str = 'us-east-1') -> float:
313
+ def get_elastic_ip_monthly_cost(self, region: str = "us-east-1") -> float:
320
314
  """Get real-time Elastic IP monthly cost from AWS Pricing API."""
321
315
  try:
322
316
  response = self.pricing_client.get_products(
323
- ServiceCode='AmazonEC2',
317
+ ServiceCode="AmazonEC2",
324
318
  Filters=[
325
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'IP Address'},
326
- {'Type': 'TERM_MATCH', 'Field': 'location', 'Value': self._get_region_name(region)}
319
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "IP Address"},
320
+ {"Type": "TERM_MATCH", "Field": "location", "Value": self._get_region_name(region)},
327
321
  ],
328
- MaxResults=1
322
+ MaxResults=1,
329
323
  )
330
-
331
- if response['PriceList']:
332
- price_data = json.loads(response['PriceList'][0])
333
- on_demand = price_data['terms']['OnDemand']
324
+
325
+ if response["PriceList"]:
326
+ price_data = json.loads(response["PriceList"][0])
327
+ on_demand = price_data["terms"]["OnDemand"]
334
328
  for term in on_demand.values():
335
- for price_dimension in term['priceDimensions'].values():
336
- if 'Hrs' in price_dimension.get('unit', ''):
337
- hourly_rate = float(price_dimension['pricePerUnit']['USD'])
329
+ for price_dimension in term["priceDimensions"].values():
330
+ if "Hrs" in price_dimension.get("unit", ""):
331
+ hourly_rate = float(price_dimension["pricePerUnit"]["USD"])
338
332
  monthly_cost = hourly_rate * 24 * 30 # Convert to monthly
339
333
  return monthly_cost
340
-
334
+
341
335
  # Fallback to Cost Explorer
342
- return self._get_from_cost_explorer('EC2', 'Elastic IP', region)
343
-
336
+ return self._get_from_cost_explorer("EC2", "Elastic IP", region)
337
+
344
338
  except Exception as e:
345
- return self._get_from_cost_explorer('EC2', 'Elastic IP', region)
339
+ return self._get_from_cost_explorer("EC2", "Elastic IP", region)
346
340
 
347
341
  @lru_cache(maxsize=128)
348
- def get_data_transfer_monthly_cost(self, region: str = 'us-east-1') -> float:
342
+ def get_data_transfer_monthly_cost(self, region: str = "us-east-1") -> float:
349
343
  """Get real-time Data Transfer cost per GB from AWS Pricing API."""
350
344
  try:
351
345
  response = self.pricing_client.get_products(
352
- ServiceCode='AmazonEC2',
346
+ ServiceCode="AmazonEC2",
353
347
  Filters=[
354
- {'Type': 'TERM_MATCH', 'Field': 'productFamily', 'Value': 'Data Transfer'},
355
- {'Type': 'TERM_MATCH', 'Field': 'fromLocation', 'Value': self._get_region_name(region)},
356
- {'Type': 'TERM_MATCH', 'Field': 'toLocation', 'Value': 'External'}
348
+ {"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Data Transfer"},
349
+ {"Type": "TERM_MATCH", "Field": "fromLocation", "Value": self._get_region_name(region)},
350
+ {"Type": "TERM_MATCH", "Field": "toLocation", "Value": "External"},
357
351
  ],
358
- MaxResults=1
352
+ MaxResults=1,
359
353
  )
360
-
361
- if response['PriceList']:
362
- price_data = json.loads(response['PriceList'][0])
363
- on_demand = price_data['terms']['OnDemand']
354
+
355
+ if response["PriceList"]:
356
+ price_data = json.loads(response["PriceList"][0])
357
+ on_demand = price_data["terms"]["OnDemand"]
364
358
  for term in on_demand.values():
365
- for price_dimension in term['priceDimensions'].values():
366
- if 'GB' in price_dimension.get('unit', ''):
367
- return float(price_dimension['pricePerUnit']['USD'])
368
-
359
+ for price_dimension in term["priceDimensions"].values():
360
+ if "GB" in price_dimension.get("unit", ""):
361
+ return float(price_dimension["pricePerUnit"]["USD"])
362
+
369
363
  # Fallback to Cost Explorer
370
- return self._get_from_cost_explorer('EC2', 'Data Transfer', region)
371
-
364
+ return self._get_from_cost_explorer("EC2", "Data Transfer", region)
365
+
372
366
  except Exception as e:
373
- return self._get_from_cost_explorer('EC2', 'Data Transfer', region)
367
+ return self._get_from_cost_explorer("EC2", "Data Transfer", region)
374
368
 
375
369
  def _query_mcp_servers(self, resource_type: str, region: str, last_error: Exception = None) -> float:
376
370
  """Query MCP servers for cost validation as final fallback."""
@@ -407,31 +401,36 @@ class AWSPricingAPI:
407
401
  💡 TIP: Run 'aws pricing get-products --service-code AmazonVPC' to test permissions
408
402
  """
409
403
  print(guidance_msg)
410
-
404
+
411
405
  # ENTERPRISE COMPLIANCE: Do not return hardcoded fallback
412
- raise ValueError(f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity.")
413
-
406
+ raise ValueError(
407
+ f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity."
408
+ )
409
+
414
410
  except Exception as mcp_error:
415
411
  print(f"🚫 Final fallback failed: {mcp_error}")
416
- raise ValueError(f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity.")
417
-
412
+ raise ValueError(
413
+ f"Unable to get pricing for {resource_type} in {region}. Check IAM permissions and MCP server connectivity."
414
+ )
415
+
418
416
  def _get_region_name(self, region_code: str) -> str:
419
417
  """Convert region code to full region name for Pricing API."""
420
418
  region_map = {
421
- 'us-east-1': 'US East (N. Virginia)',
422
- 'us-west-1': 'US West (N. California)',
423
- 'us-west-2': 'US West (Oregon)',
424
- 'eu-west-1': 'EU (Ireland)',
425
- 'eu-west-2': 'EU (London)',
426
- 'eu-central-1': 'EU (Frankfurt)',
427
- 'ap-southeast-1': 'Asia Pacific (Singapore)',
428
- 'ap-southeast-2': 'Asia Pacific (Sydney)',
429
- 'ap-northeast-1': 'Asia Pacific (Tokyo)',
430
- 'ap-south-1': 'Asia Pacific (Mumbai)',
431
- 'ca-central-1': 'Canada (Central)',
432
- 'sa-east-1': 'South America (São Paulo)',
419
+ "us-east-1": "US East (N. Virginia)",
420
+ "us-west-1": "US West (N. California)",
421
+ "us-west-2": "US West (Oregon)",
422
+ "eu-west-1": "EU (Ireland)",
423
+ "eu-west-2": "EU (London)",
424
+ "eu-central-1": "EU (Frankfurt)",
425
+ "ap-southeast-1": "Asia Pacific (Singapore)",
426
+ "ap-southeast-2": "Asia Pacific (Sydney)",
427
+ "ap-northeast-1": "Asia Pacific (Tokyo)",
428
+ "ap-south-1": "Asia Pacific (Mumbai)",
429
+ "ca-central-1": "Canada (Central)",
430
+ "sa-east-1": "South America (São Paulo)",
433
431
  }
434
- return region_map.get(region_code, 'US East (N. Virginia)')
432
+ return region_map.get(region_code, "US East (N. Virginia)")
433
+
435
434
 
436
435
  # Global instance for easy import
437
- pricing_api = AWSPricingAPI()
436
+ pricing_api = AWSPricingAPI()