runbooks 1.1.4__py3-none-any.whl → 1.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +135 -91
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +17 -12
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +99 -79
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +315 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/aws_decorators.py +2 -3
- runbooks/inventory/check_cloudtrail_compliance.py +2 -4
- runbooks/inventory/check_controltower_readiness.py +152 -151
- runbooks/inventory/check_landingzone_readiness.py +85 -84
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/core/formatter.py +11 -0
- runbooks/inventory/draw_org_structure.py +8 -9
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/ec2_vpc_utils.py +2 -2
- runbooks/inventory/find_cfn_drift_detection.py +5 -7
- runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
- runbooks/inventory/find_cfn_stackset_drift.py +5 -6
- runbooks/inventory/find_ec2_security_groups.py +48 -42
- runbooks/inventory/find_landingzone_versions.py +4 -6
- runbooks/inventory/find_vpc_flow_logs.py +7 -9
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/inventory_modules.py +103 -91
- runbooks/inventory/list_cfn_stacks.py +9 -10
- runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
- runbooks/inventory/list_cfn_stackset_operations.py +79 -57
- runbooks/inventory/list_cfn_stacksets.py +8 -10
- runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
- runbooks/inventory/list_ds_directories.py +65 -53
- runbooks/inventory/list_ec2_availability_zones.py +2 -4
- runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
- runbooks/inventory/list_ec2_instances.py +23 -28
- runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
- runbooks/inventory/list_elbs_load_balancers.py +22 -20
- runbooks/inventory/list_enis_network_interfaces.py +26 -33
- runbooks/inventory/list_guardduty_detectors.py +2 -4
- runbooks/inventory/list_iam_policies.py +2 -4
- runbooks/inventory/list_iam_roles.py +5 -7
- runbooks/inventory/list_iam_saml_providers.py +4 -6
- runbooks/inventory/list_lambda_functions.py +38 -38
- runbooks/inventory/list_org_accounts.py +6 -8
- runbooks/inventory/list_org_accounts_users.py +55 -44
- runbooks/inventory/list_rds_db_instances.py +31 -33
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/list_route53_hosted_zones.py +3 -5
- runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
- runbooks/inventory/list_sns_topics.py +2 -4
- runbooks/inventory/list_ssm_parameters.py +4 -7
- runbooks/inventory/list_vpc_subnets.py +2 -4
- runbooks/inventory/list_vpcs.py +7 -10
- runbooks/inventory/mcp_inventory_validator.py +554 -468
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +63 -55
- runbooks/inventory/recover_cfn_stack_ids.py +7 -8
- runbooks/inventory/requirements.txt +0 -1
- runbooks/inventory/rich_inventory_display.py +35 -34
- runbooks/inventory/run_on_multi_accounts.py +3 -5
- runbooks/inventory/unified_validation_engine.py +281 -253
- runbooks/inventory/verify_ec2_security_groups.py +1 -1
- runbooks/inventory/vpc_analyzer.py +735 -697
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +384 -380
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +461 -454
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.6.dist-info/METADATA +327 -0
- runbooks-1.1.6.dist-info/RECORD +489 -0
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- runbooks-1.1.4.dist-info/RECORD +0 -468
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/top_level.txt +0 -0
runbooks/common/aws_pricing.py
CHANGED
@@ -37,6 +37,7 @@ logger = logging.getLogger(__name__)
|
|
37
37
|
@dataclass
|
38
38
|
class AWSPricingResult:
|
39
39
|
"""Result of AWS pricing calculation."""
|
40
|
+
|
40
41
|
service_key: str
|
41
42
|
region: str
|
42
43
|
monthly_cost: float
|
@@ -48,9 +49,9 @@ class AWSPricingResult:
|
|
48
49
|
class DynamicAWSPricing:
|
49
50
|
"""
|
50
51
|
Enterprise AWS Pricing Service - Universal Compatibility & Real-time Integration
|
51
|
-
|
52
|
+
|
52
53
|
Strategic Features:
|
53
|
-
- Universal AWS region/partition compatibility
|
54
|
+
- Universal AWS region/partition compatibility
|
54
55
|
- Enterprise performance: <1s response time with intelligent caching
|
55
56
|
- Real-time AWS Pricing API integration with thread-safe operations
|
56
57
|
- Complete profile integration with --profile and --all patterns
|
@@ -62,7 +63,7 @@ class DynamicAWSPricing:
|
|
62
63
|
def __init__(self, cache_ttl_hours: int = 24, enable_fallback: bool = True, profile: Optional[str] = None):
|
63
64
|
"""
|
64
65
|
Initialize enterprise dynamic pricing engine.
|
65
|
-
|
66
|
+
|
66
67
|
Args:
|
67
68
|
cache_ttl_hours: Cache time-to-live in hours
|
68
69
|
enable_fallback: Enable fallback to estimated pricing
|
@@ -74,28 +75,28 @@ class DynamicAWSPricing:
|
|
74
75
|
self._pricing_cache = {}
|
75
76
|
self._cache_lock = threading.RLock()
|
76
77
|
self._executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="pricing")
|
77
|
-
|
78
|
+
|
78
79
|
# Regional pricing cache - populated dynamically from AWS Pricing API
|
79
80
|
# NO hardcoded multipliers - all pricing retrieved in real-time
|
80
81
|
self._regional_pricing_cache = {}
|
81
82
|
self._region_cache_lock = threading.RLock()
|
82
|
-
|
83
|
+
|
83
84
|
console.print("[dim]Enterprise AWS Pricing Engine initialized with universal compatibility[/]")
|
84
85
|
logger.info(f"Dynamic AWS Pricing Engine initialized with profile: {profile or 'default'}")
|
85
86
|
|
86
87
|
def get_ec2_instance_pricing(self, instance_type: str, region: str = "us-east-1") -> AWSPricingResult:
|
87
88
|
"""
|
88
89
|
Get dynamic pricing for EC2 instance type.
|
89
|
-
|
90
|
+
|
90
91
|
Args:
|
91
92
|
instance_type: EC2 instance type (e.g., t3.micro)
|
92
93
|
region: AWS region for pricing lookup
|
93
|
-
|
94
|
+
|
94
95
|
Returns:
|
95
96
|
AWSPricingResult with current EC2 pricing information
|
96
97
|
"""
|
97
98
|
cache_key = f"ec2_instance:{instance_type}:{region}"
|
98
|
-
|
99
|
+
|
99
100
|
with self._cache_lock:
|
100
101
|
# Check cache first
|
101
102
|
if cache_key in self._pricing_cache:
|
@@ -106,20 +107,20 @@ class DynamicAWSPricing:
|
|
106
107
|
else:
|
107
108
|
# Cache expired, remove it
|
108
109
|
del self._pricing_cache[cache_key]
|
109
|
-
|
110
|
+
|
110
111
|
# Try to get real pricing from AWS API
|
111
112
|
try:
|
112
113
|
pricing_result = self._get_ec2_api_pricing(instance_type, region)
|
113
|
-
|
114
|
+
|
114
115
|
# Cache the result
|
115
116
|
with self._cache_lock:
|
116
117
|
self._pricing_cache[cache_key] = pricing_result
|
117
|
-
|
118
|
+
|
118
119
|
return pricing_result
|
119
|
-
|
120
|
+
|
120
121
|
except Exception as e:
|
121
122
|
logger.error(f"Failed to get AWS API pricing for {instance_type}: {e}")
|
122
|
-
|
123
|
+
|
123
124
|
if self.enable_fallback:
|
124
125
|
return self._get_ec2_fallback_pricing(instance_type, region)
|
125
126
|
else:
|
@@ -131,40 +132,40 @@ class DynamicAWSPricing:
|
|
131
132
|
def _get_ec2_api_pricing(self, instance_type: str, region: str) -> AWSPricingResult:
|
132
133
|
"""
|
133
134
|
Get EC2 instance pricing from AWS Pricing API.
|
134
|
-
|
135
|
+
|
135
136
|
Args:
|
136
137
|
instance_type: EC2 instance type
|
137
138
|
region: AWS region
|
138
|
-
|
139
|
+
|
139
140
|
Returns:
|
140
141
|
AWSPricingResult with real AWS pricing
|
141
142
|
"""
|
142
143
|
import json
|
143
|
-
|
144
|
+
|
144
145
|
try:
|
145
146
|
# AWS Pricing API is only available in us-east-1 region
|
146
147
|
# Use enhanced session management for universal AWS environment support
|
147
148
|
if self.profile:
|
148
149
|
# Use profile-aware session creation with proper credential resolution
|
149
|
-
session = create_cost_session(self.profile)
|
150
|
-
pricing_client = session.client(
|
150
|
+
session = create_cost_session(profile_name=self.profile)
|
151
|
+
pricing_client = session.client("pricing", region_name="us-east-1")
|
151
152
|
logger.debug(f"Created EC2 pricing client with profile: {self.profile}")
|
152
153
|
else:
|
153
154
|
# Try environment-based credentials with fallback chain
|
154
155
|
try:
|
155
156
|
# First attempt: Use default credential chain
|
156
|
-
pricing_client = boto3.client(
|
157
|
+
pricing_client = boto3.client("pricing", region_name="us-east-1")
|
157
158
|
logger.debug("Created EC2 pricing client with default credentials")
|
158
159
|
except NoCredentialsError:
|
159
160
|
# Second attempt: Try with AWS_PROFILE if set
|
160
|
-
aws_profile = os.getenv(
|
161
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
161
162
|
if aws_profile:
|
162
163
|
session = boto3.Session(profile_name=aws_profile)
|
163
|
-
pricing_client = session.client(
|
164
|
+
pricing_client = session.client("pricing", region_name="us-east-1")
|
164
165
|
logger.debug(f"Created EC2 pricing client with AWS_PROFILE: {aws_profile}")
|
165
166
|
else:
|
166
167
|
raise NoCredentialsError("No AWS credentials available for Pricing API")
|
167
|
-
|
168
|
+
|
168
169
|
# Query AWS Pricing API for EC2 instances - get multiple results to find on-demand pricing
|
169
170
|
response = pricing_client.get_products(
|
170
171
|
ServiceCode="AmazonEC2",
|
@@ -175,35 +176,35 @@ class DynamicAWSPricing:
|
|
175
176
|
{"Type": "TERM_MATCH", "Field": "tenancy", "Value": "Shared"},
|
176
177
|
{"Type": "TERM_MATCH", "Field": "operatingSystem", "Value": "Linux"},
|
177
178
|
{"Type": "TERM_MATCH", "Field": "preInstalledSw", "Value": "NA"},
|
178
|
-
{"Type": "TERM_MATCH", "Field": "licenseModel", "Value": "No License required"}
|
179
|
+
{"Type": "TERM_MATCH", "Field": "licenseModel", "Value": "No License required"},
|
179
180
|
],
|
180
|
-
MaxResults=10 # Get more results to find on-demand pricing
|
181
|
+
MaxResults=10, # Get more results to find on-demand pricing
|
181
182
|
)
|
182
|
-
|
183
|
-
if not response.get(
|
183
|
+
|
184
|
+
if not response.get("PriceList"):
|
184
185
|
raise ValueError(f"No pricing data found for {instance_type} in {region}")
|
185
|
-
|
186
|
+
|
186
187
|
# Extract pricing from response - prioritize on-demand over reservation pricing
|
187
188
|
hourly_rate = None
|
188
189
|
|
189
|
-
for price_item in response[
|
190
|
+
for price_item in response["PriceList"]:
|
190
191
|
try:
|
191
192
|
price_data = json.loads(price_item)
|
192
|
-
product = price_data.get(
|
193
|
-
attributes = product.get(
|
193
|
+
product = price_data.get("product", {})
|
194
|
+
attributes = product.get("attributes", {})
|
194
195
|
|
195
196
|
# Skip reservation instances, focus on on-demand
|
196
|
-
usage_type = attributes.get(
|
197
|
-
market_option = attributes.get(
|
197
|
+
usage_type = attributes.get("usagetype", "")
|
198
|
+
market_option = attributes.get("marketoption", "")
|
198
199
|
|
199
200
|
# Skip if this is reservation pricing
|
200
|
-
if
|
201
|
+
if "reservation" in usage_type.lower() or "reserved" in market_option.lower():
|
201
202
|
logger.debug(f"Skipping reservation pricing for {instance_type}")
|
202
203
|
continue
|
203
204
|
|
204
205
|
# Navigate the pricing structure
|
205
|
-
terms = price_data.get(
|
206
|
-
on_demand = terms.get(
|
206
|
+
terms = price_data.get("terms", {})
|
207
|
+
on_demand = terms.get("OnDemand", {})
|
207
208
|
|
208
209
|
if not on_demand:
|
209
210
|
continue
|
@@ -212,7 +213,7 @@ class DynamicAWSPricing:
|
|
212
213
|
term_key = list(on_demand.keys())[0]
|
213
214
|
term_data = on_demand[term_key]
|
214
215
|
|
215
|
-
price_dimensions = term_data.get(
|
216
|
+
price_dimensions = term_data.get("priceDimensions", {})
|
216
217
|
if not price_dimensions:
|
217
218
|
continue
|
218
219
|
|
@@ -220,10 +221,10 @@ class DynamicAWSPricing:
|
|
220
221
|
price_dim_key = list(price_dimensions.keys())[0]
|
221
222
|
price_dim = price_dimensions[price_dim_key]
|
222
223
|
|
223
|
-
price_per_unit = price_dim.get(
|
224
|
-
usd_price = price_per_unit.get(
|
224
|
+
price_per_unit = price_dim.get("pricePerUnit", {})
|
225
|
+
usd_price = price_per_unit.get("USD")
|
225
226
|
|
226
|
-
if usd_price and usd_price !=
|
227
|
+
if usd_price and usd_price != "0.0000000000":
|
227
228
|
hourly_rate = float(usd_price)
|
228
229
|
logger.info(f"Found AWS API on-demand pricing for {instance_type}: ${hourly_rate}/hour")
|
229
230
|
# Log the pricing source for debugging
|
@@ -233,24 +234,24 @@ class DynamicAWSPricing:
|
|
233
234
|
except (KeyError, ValueError, IndexError, json.JSONDecodeError) as parse_error:
|
234
235
|
logger.debug(f"Failed to parse EC2 pricing data: {parse_error}")
|
235
236
|
continue
|
236
|
-
|
237
|
+
|
237
238
|
if hourly_rate is None:
|
238
239
|
raise ValueError(f"Could not extract valid pricing for {instance_type}")
|
239
|
-
|
240
|
+
|
240
241
|
# Convert hourly to monthly (24 hours * 30 days)
|
241
242
|
monthly_cost = hourly_rate * 24 * 30
|
242
|
-
|
243
|
+
|
243
244
|
logger.info(f"AWS API pricing for {instance_type} in {region}: ${monthly_cost:.4f}/month")
|
244
|
-
|
245
|
+
|
245
246
|
return AWSPricingResult(
|
246
247
|
service_key=f"ec2_instance:{instance_type}",
|
247
248
|
region=region,
|
248
249
|
monthly_cost=monthly_cost,
|
249
250
|
pricing_source="aws_api",
|
250
251
|
last_updated=datetime.now(),
|
251
|
-
currency="USD"
|
252
|
+
currency="USD",
|
252
253
|
)
|
253
|
-
|
254
|
+
|
254
255
|
except (ClientError, NoCredentialsError) as e:
|
255
256
|
logger.warning(f"AWS Pricing API unavailable for {instance_type}: {e}")
|
256
257
|
raise e
|
@@ -261,118 +262,122 @@ class DynamicAWSPricing:
|
|
261
262
|
# ============================================================================
|
262
263
|
# ENTERPRISE SERVICE PRICING METHODS - Strategic Requirements Implementation
|
263
264
|
# ============================================================================
|
264
|
-
|
265
|
+
|
265
266
|
def get_ec2_instance_hourly_cost(self, instance_type: str, region: str = "us-east-1") -> float:
|
266
267
|
"""
|
267
268
|
Get EC2 instance hourly cost (Strategic Requirement #1).
|
268
|
-
|
269
|
+
|
269
270
|
Args:
|
270
271
|
instance_type: EC2 instance type (e.g., t3.micro)
|
271
272
|
region: AWS region for pricing lookup
|
272
|
-
|
273
|
+
|
273
274
|
Returns:
|
274
275
|
Hourly cost in USD
|
275
276
|
"""
|
276
277
|
result = self.get_ec2_instance_pricing(instance_type, region)
|
277
278
|
return result.monthly_cost / (24 * 30) # Convert monthly to hourly
|
278
|
-
|
279
|
+
|
279
280
|
def get_eip_monthly_cost(self, region: str = "us-east-1") -> float:
|
280
281
|
"""
|
281
282
|
Get Elastic IP monthly cost (Strategic Requirement #2).
|
282
|
-
|
283
|
+
|
283
284
|
Args:
|
284
285
|
region: AWS region for pricing lookup
|
285
|
-
|
286
|
+
|
286
287
|
Returns:
|
287
288
|
Monthly cost in USD for unassociated EIP
|
288
289
|
"""
|
289
290
|
result = self.get_service_pricing("elastic_ip", region)
|
290
291
|
return result.monthly_cost
|
291
|
-
|
292
|
+
|
292
293
|
def get_nat_gateway_monthly_cost(self, region: str = "us-east-1") -> float:
|
293
294
|
"""
|
294
295
|
Get NAT Gateway monthly cost (Strategic Requirement #3).
|
295
|
-
|
296
|
+
|
296
297
|
Args:
|
297
298
|
region: AWS region for pricing lookup
|
298
|
-
|
299
|
+
|
299
300
|
Returns:
|
300
301
|
Monthly cost in USD for NAT Gateway
|
301
302
|
"""
|
302
303
|
result = self.get_service_pricing("nat_gateway", region)
|
303
304
|
return result.monthly_cost
|
304
|
-
|
305
|
+
|
305
306
|
def get_ebs_gb_monthly_cost(self, volume_type: str = "gp3", region: str = "us-east-1") -> float:
|
306
307
|
"""
|
307
308
|
Get EBS per-GB monthly cost (Strategic Requirement #4).
|
308
|
-
|
309
|
+
|
309
310
|
Args:
|
310
311
|
volume_type: EBS volume type (gp3, gp2, io1, io2, st1, sc1)
|
311
312
|
region: AWS region for pricing lookup
|
312
|
-
|
313
|
+
|
313
314
|
Returns:
|
314
315
|
Monthly cost per GB in USD
|
315
316
|
"""
|
316
317
|
result = self.get_service_pricing(f"ebs_{volume_type}", region)
|
317
318
|
return result.monthly_cost
|
318
|
-
|
319
|
+
|
319
320
|
# Additional Enterprise Service Methods
|
320
321
|
def get_vpc_endpoint_monthly_cost(self, region: str = "us-east-1") -> float:
|
321
322
|
"""Get VPC Endpoint monthly cost."""
|
322
323
|
result = self.get_service_pricing("vpc_endpoint", region)
|
323
324
|
return result.monthly_cost
|
324
|
-
|
325
|
+
|
325
326
|
def get_transit_gateway_monthly_cost(self, region: str = "us-east-1") -> float:
|
326
327
|
"""Get Transit Gateway monthly cost."""
|
327
328
|
result = self.get_service_pricing("transit_gateway", region)
|
328
329
|
return result.monthly_cost
|
329
|
-
|
330
|
+
|
330
331
|
def get_load_balancer_monthly_cost(self, lb_type: str = "application", region: str = "us-east-1") -> float:
|
331
332
|
"""
|
332
333
|
Get Load Balancer monthly cost.
|
333
|
-
|
334
|
+
|
334
335
|
Args:
|
335
336
|
lb_type: Load balancer type (application, network, gateway)
|
336
337
|
region: AWS region
|
337
|
-
|
338
|
+
|
338
339
|
Returns:
|
339
340
|
Monthly cost in USD
|
340
341
|
"""
|
341
342
|
result = self.get_service_pricing(f"loadbalancer_{lb_type}", region)
|
342
343
|
return result.monthly_cost
|
343
|
-
|
344
|
-
def get_rds_instance_monthly_cost(
|
344
|
+
|
345
|
+
def get_rds_instance_monthly_cost(
|
346
|
+
self, instance_class: str, engine: str = "mysql", region: str = "us-east-1"
|
347
|
+
) -> float:
|
345
348
|
"""
|
346
349
|
Get RDS instance monthly cost.
|
347
|
-
|
350
|
+
|
348
351
|
Args:
|
349
352
|
instance_class: RDS instance class (e.g., db.t3.micro)
|
350
353
|
engine: Database engine (mysql, postgres, oracle, etc.)
|
351
354
|
region: AWS region
|
352
|
-
|
355
|
+
|
353
356
|
Returns:
|
354
357
|
Monthly cost in USD
|
355
358
|
"""
|
356
359
|
result = self.get_service_pricing(f"rds_{engine}_{instance_class}", region)
|
357
360
|
return result.monthly_cost
|
358
|
-
|
361
|
+
|
359
362
|
# ============================================================================
|
360
363
|
# ENTERPRISE PERFORMANCE METHODS - <1s Response Time Requirements
|
361
364
|
# ============================================================================
|
362
|
-
|
363
|
-
def get_multi_service_pricing(
|
365
|
+
|
366
|
+
def get_multi_service_pricing(
|
367
|
+
self, service_requests: List[Tuple[str, str]], max_workers: int = 4
|
368
|
+
) -> Dict[str, AWSPricingResult]:
|
364
369
|
"""
|
365
370
|
Get pricing for multiple services concurrently for enterprise performance.
|
366
|
-
|
371
|
+
|
367
372
|
Args:
|
368
373
|
service_requests: List of (service_key, region) tuples
|
369
374
|
max_workers: Maximum concurrent workers
|
370
|
-
|
375
|
+
|
371
376
|
Returns:
|
372
377
|
Dictionary mapping service_key:region to AWSPricingResult
|
373
378
|
"""
|
374
379
|
results = {}
|
375
|
-
|
380
|
+
|
376
381
|
def fetch_pricing(service_request):
|
377
382
|
service_key, region = service_request
|
378
383
|
try:
|
@@ -380,13 +385,11 @@ class DynamicAWSPricing:
|
|
380
385
|
except Exception as e:
|
381
386
|
logger.error(f"Failed to fetch pricing for {service_key} in {region}: {e}")
|
382
387
|
return f"{service_key}:{region}", None
|
383
|
-
|
388
|
+
|
384
389
|
# Use existing executor for thread management
|
385
390
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
386
|
-
future_to_service = {
|
387
|
-
|
388
|
-
}
|
389
|
-
|
391
|
+
future_to_service = {executor.submit(fetch_pricing, req): req for req in service_requests}
|
392
|
+
|
390
393
|
for future in as_completed(future_to_service):
|
391
394
|
service_request = future_to_service[future]
|
392
395
|
try:
|
@@ -396,30 +399,35 @@ class DynamicAWSPricing:
|
|
396
399
|
except Exception as e:
|
397
400
|
service_key, region = service_request
|
398
401
|
logger.error(f"Concurrent pricing fetch failed for {service_key}:{region}: {e}")
|
399
|
-
|
402
|
+
|
400
403
|
return results
|
401
|
-
|
404
|
+
|
402
405
|
def warm_cache_for_region(self, region: str, services: Optional[List[str]] = None) -> None:
|
403
406
|
"""
|
404
407
|
Pre-warm pricing cache for a region to ensure <1s response times.
|
405
|
-
|
408
|
+
|
406
409
|
Args:
|
407
410
|
region: AWS region to warm cache for
|
408
411
|
services: List of services to warm (default: common services)
|
409
412
|
"""
|
410
413
|
if services is None:
|
411
414
|
services = [
|
412
|
-
"ec2_instance",
|
413
|
-
"
|
415
|
+
"ec2_instance",
|
416
|
+
"elastic_ip",
|
417
|
+
"nat_gateway",
|
418
|
+
"ebs_gp3",
|
419
|
+
"vpc_endpoint",
|
420
|
+
"transit_gateway",
|
421
|
+
"loadbalancer_application",
|
414
422
|
]
|
415
|
-
|
423
|
+
|
416
424
|
service_requests = [(service, region) for service in services]
|
417
|
-
|
425
|
+
|
418
426
|
console.print(f"[dim]Warming pricing cache for {region} with {len(services)} services...[/]")
|
419
427
|
start_time = time.time()
|
420
|
-
|
428
|
+
|
421
429
|
self.get_multi_service_pricing(service_requests)
|
422
|
-
|
430
|
+
|
423
431
|
elapsed = time.time() - start_time
|
424
432
|
console.print(f"[dim]Cache warming completed in {elapsed:.2f}s[/]")
|
425
433
|
logger.info(f"Pricing cache warmed for {region} in {elapsed:.2f}s")
|
@@ -427,141 +435,141 @@ class DynamicAWSPricing:
|
|
427
435
|
def _get_ec2_fallback_pricing(self, instance_type: str, region: str) -> AWSPricingResult:
|
428
436
|
"""
|
429
437
|
ENTERPRISE CRITICAL: EC2 fallback pricing for absolute last resort.
|
430
|
-
|
438
|
+
|
431
439
|
Args:
|
432
440
|
instance_type: EC2 instance type
|
433
441
|
region: AWS region
|
434
|
-
|
442
|
+
|
435
443
|
Returns:
|
436
444
|
AWSPricingResult with estimated pricing
|
437
445
|
"""
|
438
446
|
console.print(f"[red]⚠ ENTERPRISE WARNING: Using fallback pricing for EC2 {instance_type}[/red]")
|
439
|
-
|
447
|
+
|
440
448
|
# Calculate base hourly rate from AWS documentation patterns
|
441
449
|
hourly_rate = self._calculate_ec2_from_aws_patterns(instance_type)
|
442
|
-
|
450
|
+
|
443
451
|
if hourly_rate <= 0:
|
444
452
|
raise RuntimeError(
|
445
453
|
f"ENTERPRISE VIOLATION: No dynamic pricing available for {instance_type} "
|
446
454
|
f"in region {region}. Cannot proceed without hardcoded values."
|
447
455
|
)
|
448
|
-
|
456
|
+
|
449
457
|
# Apply dynamic regional multiplier from AWS Pricing API
|
450
458
|
region_multiplier = self.get_regional_pricing_multiplier("ec2_instance", region, "us-east-1")
|
451
459
|
adjusted_hourly_rate = hourly_rate * region_multiplier
|
452
460
|
monthly_cost = adjusted_hourly_rate * 24 * 30
|
453
|
-
|
461
|
+
|
454
462
|
logger.warning(f"Using calculated EC2 fallback for {instance_type} in {region}: ${monthly_cost:.4f}/month")
|
455
|
-
|
463
|
+
|
456
464
|
return AWSPricingResult(
|
457
465
|
service_key=f"ec2_instance:{instance_type}",
|
458
466
|
region=region,
|
459
467
|
monthly_cost=monthly_cost,
|
460
468
|
pricing_source="calculated_fallback",
|
461
469
|
last_updated=datetime.now(),
|
462
|
-
currency="USD"
|
470
|
+
currency="USD",
|
463
471
|
)
|
464
472
|
|
465
473
|
def _calculate_ec2_from_aws_patterns(self, instance_type: str) -> float:
|
466
474
|
"""
|
467
475
|
Calculate EC2 pricing using AWS documented patterns and ratios.
|
468
|
-
|
476
|
+
|
469
477
|
Based on AWS instance family patterns, not hardcoded business values.
|
470
|
-
|
478
|
+
|
471
479
|
Returns:
|
472
480
|
Hourly rate or 0 if cannot be calculated
|
473
481
|
"""
|
474
482
|
instance_type = instance_type.lower()
|
475
|
-
|
483
|
+
|
476
484
|
# Parse instance type (e.g., "t3.micro" -> family="t3", size="micro")
|
477
485
|
try:
|
478
|
-
family, size = instance_type.split(
|
486
|
+
family, size = instance_type.split(".", 1)
|
479
487
|
except ValueError:
|
480
488
|
logger.error(f"Invalid instance type format: {instance_type}")
|
481
489
|
return 0.0
|
482
|
-
|
490
|
+
|
483
491
|
# Instance family base rates from AWS pricing patterns
|
484
492
|
# These represent documented relative pricing, not hardcoded business values
|
485
493
|
family_base_factors = {
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
+
"t3": 1.0, # Burstable performance baseline
|
495
|
+
"t2": 1.12, # Previous generation, slightly higher
|
496
|
+
"m5": 1.85, # General purpose, balanced
|
497
|
+
"c5": 1.63, # Compute optimized
|
498
|
+
"r5": 2.42, # Memory optimized
|
499
|
+
"m4": 1.75, # Previous generation general purpose
|
500
|
+
"c4": 1.54, # Previous generation compute
|
501
|
+
"r4": 2.28, # Previous generation memory
|
494
502
|
}
|
495
|
-
|
503
|
+
|
496
504
|
# Size multipliers based on AWS documented scaling
|
497
505
|
size_multipliers = {
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
+
"nano": 0.25, # Quarter of micro
|
507
|
+
"micro": 1.0, # Base unit
|
508
|
+
"small": 2.0, # Double micro
|
509
|
+
"medium": 4.0, # Double small
|
510
|
+
"large": 8.0, # Double medium
|
511
|
+
"xlarge": 16.0, # Double large
|
512
|
+
"2xlarge": 32.0, # Double xlarge
|
513
|
+
"4xlarge": 64.0, # Double 2xlarge
|
506
514
|
}
|
507
|
-
|
515
|
+
|
508
516
|
family_factor = family_base_factors.get(family, 0.0)
|
509
517
|
size_multiplier = size_multipliers.get(size, 0.0)
|
510
|
-
|
518
|
+
|
511
519
|
if family_factor == 0.0:
|
512
520
|
logger.warning(f"Unknown EC2 family: {family}")
|
513
521
|
return 0.0
|
514
|
-
|
522
|
+
|
515
523
|
if size_multiplier == 0.0:
|
516
524
|
logger.warning(f"Unknown EC2 size: {size}")
|
517
525
|
return 0.0
|
518
|
-
|
526
|
+
|
519
527
|
# Calculate using AWS documented scaling patterns
|
520
528
|
# Instead of hardcoded baseline, use the family and size factors
|
521
529
|
# This calculates relative pricing without hardcoded base rates
|
522
|
-
|
530
|
+
|
523
531
|
# Use the smallest family factor as baseline to avoid hardcoded values
|
524
532
|
baseline_factor = min(family_base_factors.values()) # t3 = 1.0
|
525
|
-
|
533
|
+
|
526
534
|
# Try to get real baseline pricing from AWS API for any known instance type
|
527
535
|
baseline_rate = None
|
528
|
-
known_instance_types = [
|
529
|
-
|
536
|
+
known_instance_types = ["t3.micro", "t2.micro", "m5.large"]
|
537
|
+
|
530
538
|
for baseline_instance in known_instance_types:
|
531
539
|
try:
|
532
540
|
pricing_engine = get_aws_pricing_engine(enable_fallback=False, profile=self.profile)
|
533
|
-
real_pricing = pricing_engine._get_ec2_api_pricing(baseline_instance,
|
541
|
+
real_pricing = pricing_engine._get_ec2_api_pricing(baseline_instance, "us-east-1")
|
534
542
|
baseline_rate = real_pricing.monthly_cost / (24 * 30) # Convert to hourly
|
535
543
|
logger.info(f"Using {baseline_instance} as baseline: ${baseline_rate}/hour")
|
536
544
|
break
|
537
545
|
except Exception as e:
|
538
546
|
logger.debug(f"Could not get pricing for {baseline_instance}: {e}")
|
539
547
|
continue
|
540
|
-
|
548
|
+
|
541
549
|
if baseline_rate is None:
|
542
550
|
# If we can't get any real pricing, we cannot calculate reliably
|
543
551
|
logger.error(f"ENTERPRISE COMPLIANCE: Cannot calculate {instance_type} without AWS API baseline")
|
544
552
|
return 0.0
|
545
|
-
|
553
|
+
|
546
554
|
# Calculate relative pricing based on AWS documented ratios and real baseline
|
547
555
|
calculated_rate = baseline_rate * family_factor * size_multiplier
|
548
|
-
|
556
|
+
|
549
557
|
logger.info(f"Calculated {instance_type} rate: ${calculated_rate}/hour using AWS patterns")
|
550
558
|
return calculated_rate
|
551
559
|
|
552
560
|
def get_service_pricing(self, service_key: str, region: str = "us-east-1") -> AWSPricingResult:
|
553
561
|
"""
|
554
562
|
Get dynamic pricing for AWS service.
|
555
|
-
|
563
|
+
|
556
564
|
Args:
|
557
565
|
service_key: Service identifier (vpc, nat_gateway, elastic_ip, etc.)
|
558
566
|
region: AWS region for pricing lookup
|
559
|
-
|
567
|
+
|
560
568
|
Returns:
|
561
569
|
AWSPricingResult with current pricing information
|
562
570
|
"""
|
563
571
|
cache_key = f"{service_key}:{region}"
|
564
|
-
|
572
|
+
|
565
573
|
with self._cache_lock:
|
566
574
|
# Check cache first
|
567
575
|
if cache_key in self._pricing_cache:
|
@@ -572,20 +580,20 @@ class DynamicAWSPricing:
|
|
572
580
|
else:
|
573
581
|
# Cache expired, remove it
|
574
582
|
del self._pricing_cache[cache_key]
|
575
|
-
|
583
|
+
|
576
584
|
# Try to get real pricing from AWS API
|
577
585
|
try:
|
578
586
|
pricing_result = self._get_aws_api_pricing(service_key, region)
|
579
|
-
|
587
|
+
|
580
588
|
# Cache the result
|
581
589
|
with self._cache_lock:
|
582
590
|
self._pricing_cache[cache_key] = pricing_result
|
583
|
-
|
591
|
+
|
584
592
|
return pricing_result
|
585
|
-
|
593
|
+
|
586
594
|
except Exception as e:
|
587
595
|
logger.error(f"Failed to get AWS API pricing for {service_key}: {e}")
|
588
|
-
|
596
|
+
|
589
597
|
if self.enable_fallback:
|
590
598
|
return self._get_fallback_pricing(service_key, region)
|
591
599
|
else:
|
@@ -612,21 +620,21 @@ class DynamicAWSPricing:
|
|
612
620
|
# Use enhanced session management for universal AWS environment support
|
613
621
|
if self.profile:
|
614
622
|
# Use profile-aware session creation with proper credential resolution
|
615
|
-
session = create_cost_session(self.profile)
|
616
|
-
pricing_client = session.client(
|
623
|
+
session = create_cost_session(profile_name=self.profile)
|
624
|
+
pricing_client = session.client("pricing", region_name="us-east-1")
|
617
625
|
logger.debug(f"Created pricing client with profile: {self.profile}")
|
618
626
|
else:
|
619
627
|
# Try environment-based credentials with fallback chain
|
620
628
|
try:
|
621
629
|
# First attempt: Use default credential chain
|
622
|
-
pricing_client = boto3.client(
|
630
|
+
pricing_client = boto3.client("pricing", region_name="us-east-1")
|
623
631
|
logger.debug("Created pricing client with default credentials")
|
624
632
|
except NoCredentialsError:
|
625
633
|
# Second attempt: Try with AWS_PROFILE if set
|
626
|
-
aws_profile = os.getenv(
|
634
|
+
aws_profile = os.getenv("AWS_PROFILE")
|
627
635
|
if aws_profile:
|
628
636
|
session = boto3.Session(profile_name=aws_profile)
|
629
|
-
pricing_client = session.client(
|
637
|
+
pricing_client = session.client("pricing", region_name="us-east-1")
|
630
638
|
logger.debug(f"Created pricing client with AWS_PROFILE: {aws_profile}")
|
631
639
|
else:
|
632
640
|
# Enhanced credential guidance
|
@@ -636,7 +644,7 @@ class DynamicAWSPricing:
|
|
636
644
|
console.print(" 2. AWS SSO: [cyan]aws sso login --profile your-profile[/]")
|
637
645
|
console.print(" 3. Environment: [cyan]export AWS_ACCESS_KEY_ID=...[/]")
|
638
646
|
raise NoCredentialsError("No AWS credentials available for Pricing API")
|
639
|
-
|
647
|
+
|
640
648
|
# Enterprise Service Mapping for AWS Pricing API - Complete Coverage
|
641
649
|
service_mapping = {
|
642
650
|
# Core Networking Services - NAT Gateway (fallback to broad search)
|
@@ -645,33 +653,32 @@ class DynamicAWSPricing:
|
|
645
653
|
"location": self._get_aws_location_name(region),
|
646
654
|
"filters": [
|
647
655
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
648
|
-
] # Simplified - will search for NAT Gateway in response
|
656
|
+
], # Simplified - will search for NAT Gateway in response
|
649
657
|
},
|
650
658
|
"elastic_ip": {
|
651
|
-
"service_code": "AmazonEC2",
|
659
|
+
"service_code": "AmazonEC2",
|
652
660
|
"location": self._get_aws_location_name(region),
|
653
661
|
"filters": [
|
654
662
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
655
|
-
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "IP Address"}
|
656
|
-
]
|
663
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "IP Address"},
|
664
|
+
],
|
657
665
|
},
|
658
666
|
"vpc_endpoint": {
|
659
667
|
"service_code": "AmazonVPC",
|
660
668
|
"location": self._get_aws_location_name(region),
|
661
669
|
"filters": [
|
662
670
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
663
|
-
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "VpcEndpoint"}
|
664
|
-
]
|
671
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "VpcEndpoint"},
|
672
|
+
],
|
665
673
|
},
|
666
674
|
"transit_gateway": {
|
667
675
|
"service_code": "AmazonVPC",
|
668
676
|
"location": self._get_aws_location_name(region),
|
669
677
|
"filters": [
|
670
678
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
671
|
-
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Transit Gateway"}
|
672
|
-
]
|
679
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Transit Gateway"},
|
680
|
+
],
|
673
681
|
},
|
674
|
-
|
675
682
|
# Compute Services
|
676
683
|
"ec2_instance": {
|
677
684
|
"service_code": "AmazonEC2",
|
@@ -680,10 +687,9 @@ class DynamicAWSPricing:
|
|
680
687
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
681
688
|
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Compute Instance"},
|
682
689
|
{"Type": "TERM_MATCH", "Field": "tenancy", "Value": "Shared"},
|
683
|
-
{"Type": "TERM_MATCH", "Field": "operatingSystem", "Value": "Linux"}
|
684
|
-
]
|
690
|
+
{"Type": "TERM_MATCH", "Field": "operatingSystem", "Value": "Linux"},
|
691
|
+
],
|
685
692
|
},
|
686
|
-
|
687
693
|
# Storage Services
|
688
694
|
"ebs_gp3": {
|
689
695
|
"service_code": "AmazonEC2",
|
@@ -692,8 +698,8 @@ class DynamicAWSPricing:
|
|
692
698
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
693
699
|
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
|
694
700
|
{"Type": "TERM_MATCH", "Field": "volumeType", "Value": "General Purpose"},
|
695
|
-
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp3"}
|
696
|
-
]
|
701
|
+
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp3"},
|
702
|
+
],
|
697
703
|
},
|
698
704
|
"ebs_gp2": {
|
699
705
|
"service_code": "AmazonEC2",
|
@@ -702,8 +708,8 @@ class DynamicAWSPricing:
|
|
702
708
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
703
709
|
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
|
704
710
|
{"Type": "TERM_MATCH", "Field": "volumeType", "Value": "General Purpose"},
|
705
|
-
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp2"}
|
706
|
-
]
|
711
|
+
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "gp2"},
|
712
|
+
],
|
707
713
|
},
|
708
714
|
"ebs_io1": {
|
709
715
|
"service_code": "AmazonEC2",
|
@@ -712,8 +718,8 @@ class DynamicAWSPricing:
|
|
712
718
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
713
719
|
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
|
714
720
|
{"Type": "TERM_MATCH", "Field": "volumeType", "Value": "Provisioned IOPS"},
|
715
|
-
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "io1"}
|
716
|
-
]
|
721
|
+
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "io1"},
|
722
|
+
],
|
717
723
|
},
|
718
724
|
"ebs_io2": {
|
719
725
|
"service_code": "AmazonEC2",
|
@@ -722,44 +728,43 @@ class DynamicAWSPricing:
|
|
722
728
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
723
729
|
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Storage"},
|
724
730
|
{"Type": "TERM_MATCH", "Field": "volumeType", "Value": "Provisioned IOPS"},
|
725
|
-
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "io2"}
|
726
|
-
]
|
731
|
+
{"Type": "TERM_MATCH", "Field": "volumeApiName", "Value": "io2"},
|
732
|
+
],
|
727
733
|
},
|
728
|
-
|
729
734
|
# Load Balancer Services
|
730
735
|
"loadbalancer_application": {
|
731
736
|
"service_code": "AWSELB",
|
732
737
|
"location": self._get_aws_location_name(region),
|
733
738
|
"filters": [
|
734
739
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
735
|
-
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Load Balancer-Application"}
|
736
|
-
]
|
740
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Load Balancer-Application"},
|
741
|
+
],
|
737
742
|
},
|
738
743
|
"loadbalancer_network": {
|
739
744
|
"service_code": "AWSELB",
|
740
745
|
"location": self._get_aws_location_name(region),
|
741
746
|
"filters": [
|
742
747
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
743
|
-
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Load Balancer-Network"}
|
744
|
-
]
|
748
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Load Balancer-Network"},
|
749
|
+
],
|
745
750
|
},
|
746
751
|
"loadbalancer_gateway": {
|
747
752
|
"service_code": "AWSELB",
|
748
753
|
"location": self._get_aws_location_name(region),
|
749
754
|
"filters": [
|
750
755
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
751
|
-
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Load Balancer-Gateway"}
|
752
|
-
]
|
753
|
-
}
|
756
|
+
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Load Balancer-Gateway"},
|
757
|
+
],
|
758
|
+
},
|
754
759
|
}
|
755
|
-
|
760
|
+
|
756
761
|
# Handle dynamic RDS service keys (rds_engine_instanceclass)
|
757
762
|
if service_key.startswith("rds_"):
|
758
763
|
parts = service_key.split("_")
|
759
764
|
if len(parts) >= 3:
|
760
765
|
engine = parts[1]
|
761
766
|
instance_class = "_".join(parts[2:])
|
762
|
-
|
767
|
+
|
763
768
|
service_mapping[service_key] = {
|
764
769
|
"service_code": "AmazonRDS",
|
765
770
|
"location": self._get_aws_location_name(region),
|
@@ -767,10 +772,10 @@ class DynamicAWSPricing:
|
|
767
772
|
{"Type": "TERM_MATCH", "Field": "location", "Value": self._get_aws_location_name(region)},
|
768
773
|
{"Type": "TERM_MATCH", "Field": "productFamily", "Value": "Database Instance"},
|
769
774
|
{"Type": "TERM_MATCH", "Field": "databaseEngine", "Value": engine.title()},
|
770
|
-
{"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_class}
|
771
|
-
]
|
775
|
+
{"Type": "TERM_MATCH", "Field": "instanceType", "Value": instance_class},
|
776
|
+
],
|
772
777
|
}
|
773
|
-
|
778
|
+
|
774
779
|
# Handle data_transfer service with graceful fallback
|
775
780
|
if service_key == "data_transfer":
|
776
781
|
print_warning("data_transfer service not supported by AWS Pricing API - using standard rates")
|
@@ -780,43 +785,43 @@ class DynamicAWSPricing:
|
|
780
785
|
region=region,
|
781
786
|
monthly_cost=0.045, # $0.045/GB for NAT Gateway data processing
|
782
787
|
pricing_source="aws_standard_rates",
|
783
|
-
last_updated=datetime.now()
|
788
|
+
last_updated=datetime.now(),
|
784
789
|
)
|
785
790
|
|
786
791
|
if service_key not in service_mapping:
|
787
792
|
raise ValueError(f"Service {service_key} not supported by AWS Pricing API integration")
|
788
|
-
|
793
|
+
|
789
794
|
service_info = service_mapping[service_key]
|
790
|
-
|
795
|
+
|
791
796
|
# Query AWS Pricing API
|
792
797
|
response = pricing_client.get_products(
|
793
798
|
ServiceCode=service_info["service_code"],
|
794
799
|
Filters=service_info["filters"],
|
795
|
-
MaxResults=5 # Get more results to find best match
|
800
|
+
MaxResults=5, # Get more results to find best match
|
796
801
|
)
|
797
|
-
|
798
|
-
if not response.get(
|
802
|
+
|
803
|
+
if not response.get("PriceList"):
|
799
804
|
raise ValueError(f"No pricing data found for {service_key} in {region}")
|
800
|
-
|
805
|
+
|
801
806
|
# Extract pricing from response with service-specific filtering
|
802
807
|
hourly_rate = None
|
803
808
|
|
804
|
-
for price_item in response[
|
809
|
+
for price_item in response["PriceList"]:
|
805
810
|
try:
|
806
811
|
price_data = json.loads(price_item)
|
807
|
-
product = price_data.get(
|
808
|
-
attributes = product.get(
|
812
|
+
product = price_data.get("product", {})
|
813
|
+
attributes = product.get("attributes", {})
|
809
814
|
|
810
815
|
# Service-specific filtering for broad searches
|
811
816
|
if service_key == "nat_gateway":
|
812
817
|
# Look for NAT Gateway specific attributes
|
813
818
|
item_text = json.dumps(attributes).lower()
|
814
|
-
if not any(keyword in item_text for keyword in [
|
819
|
+
if not any(keyword in item_text for keyword in ["nat", "natgateway", "nat-gateway"]):
|
815
820
|
continue # Skip items that don't contain NAT references
|
816
821
|
|
817
822
|
# Navigate the pricing structure
|
818
|
-
terms = price_data.get(
|
819
|
-
on_demand = terms.get(
|
823
|
+
terms = price_data.get("terms", {})
|
824
|
+
on_demand = terms.get("OnDemand", {})
|
820
825
|
|
821
826
|
if not on_demand:
|
822
827
|
continue
|
@@ -825,7 +830,7 @@ class DynamicAWSPricing:
|
|
825
830
|
term_key = list(on_demand.keys())[0]
|
826
831
|
term_data = on_demand[term_key]
|
827
832
|
|
828
|
-
price_dimensions = term_data.get(
|
833
|
+
price_dimensions = term_data.get("priceDimensions", {})
|
829
834
|
if not price_dimensions:
|
830
835
|
continue
|
831
836
|
|
@@ -833,15 +838,17 @@ class DynamicAWSPricing:
|
|
833
838
|
price_dim_key = list(price_dimensions.keys())[0]
|
834
839
|
price_dim = price_dimensions[price_dim_key]
|
835
840
|
|
836
|
-
price_per_unit = price_dim.get(
|
837
|
-
usd_price = price_per_unit.get(
|
841
|
+
price_per_unit = price_dim.get("pricePerUnit", {})
|
842
|
+
usd_price = price_per_unit.get("USD")
|
838
843
|
|
839
|
-
if usd_price and usd_price !=
|
844
|
+
if usd_price and usd_price != "0.0000000000":
|
840
845
|
hourly_rate = float(usd_price)
|
841
846
|
monthly_cost = hourly_rate * 24 * 30
|
842
847
|
|
843
848
|
# Honest success reporting
|
844
|
-
console.print(
|
849
|
+
console.print(
|
850
|
+
f"[green]✅ Real-time AWS API pricing[/]: {service_key} = ${monthly_cost:.2f}/month"
|
851
|
+
)
|
845
852
|
logger.info(f"Found AWS API pricing for {service_key}: ${hourly_rate}/hour")
|
846
853
|
|
847
854
|
# Log what we found for debugging
|
@@ -852,24 +859,24 @@ class DynamicAWSPricing:
|
|
852
859
|
except (KeyError, ValueError, IndexError, json.JSONDecodeError) as parse_error:
|
853
860
|
logger.debug(f"Failed to parse pricing data: {parse_error}")
|
854
861
|
continue
|
855
|
-
|
862
|
+
|
856
863
|
if hourly_rate is None:
|
857
864
|
raise ValueError(f"Could not extract valid pricing for {service_key}")
|
858
|
-
|
865
|
+
|
859
866
|
# Convert hourly to monthly (24 hours * 30 days)
|
860
867
|
monthly_cost = hourly_rate * 24 * 30
|
861
|
-
|
868
|
+
|
862
869
|
logger.info(f"AWS API pricing for {service_key} in {region}: ${monthly_cost:.4f}/month")
|
863
|
-
|
870
|
+
|
864
871
|
return AWSPricingResult(
|
865
872
|
service_key=service_key,
|
866
873
|
region=region,
|
867
874
|
monthly_cost=monthly_cost,
|
868
875
|
pricing_source="aws_api",
|
869
876
|
last_updated=datetime.now(),
|
870
|
-
currency="USD"
|
877
|
+
currency="USD",
|
871
878
|
)
|
872
|
-
|
879
|
+
|
873
880
|
except (ClientError, NoCredentialsError) as e:
|
874
881
|
logger.warning(f"AWS Pricing API unavailable for {service_key}: {e}")
|
875
882
|
raise e
|
@@ -906,9 +913,9 @@ class DynamicAWSPricing:
|
|
906
913
|
monthly_cost=override_cost,
|
907
914
|
pricing_source="environment_override",
|
908
915
|
last_updated=datetime.now(),
|
909
|
-
currency="USD"
|
916
|
+
currency="USD",
|
910
917
|
)
|
911
|
-
|
918
|
+
|
912
919
|
# Try alternative approach: Query public AWS docs or use Cloud Formation cost estimation
|
913
920
|
try:
|
914
921
|
estimated_cost = self._query_alternative_pricing_sources(service_key, region)
|
@@ -919,22 +926,22 @@ class DynamicAWSPricing:
|
|
919
926
|
monthly_cost=estimated_cost,
|
920
927
|
pricing_source="alternative_source",
|
921
928
|
last_updated=datetime.now(),
|
922
|
-
currency="USD"
|
929
|
+
currency="USD",
|
923
930
|
)
|
924
931
|
except Exception as e:
|
925
932
|
logger.debug(f"Alternative pricing source failed: {e}")
|
926
|
-
|
933
|
+
|
927
934
|
# LAST RESORT: Calculated estimates from AWS documentation
|
928
935
|
# These are NOT hardcoded business values but technical calculations
|
929
936
|
# Based on AWS official documentation and calculator methodology
|
930
937
|
base_hourly_rates_from_aws_docs = self._calculate_from_aws_documentation(service_key)
|
931
|
-
|
938
|
+
|
932
939
|
if not base_hourly_rates_from_aws_docs:
|
933
940
|
raise RuntimeError(
|
934
941
|
f"ENTERPRISE VIOLATION: No dynamic pricing available for {service_key} "
|
935
942
|
f"in region {region}. Cannot proceed without hardcoded values."
|
936
943
|
)
|
937
|
-
|
944
|
+
|
938
945
|
# Apply dynamic regional multiplier from AWS Pricing API
|
939
946
|
region_multiplier = self.get_regional_pricing_multiplier(service_key, region, "us-east-1")
|
940
947
|
hourly_rate = base_hourly_rates_from_aws_docs * region_multiplier
|
@@ -951,7 +958,7 @@ class DynamicAWSPricing:
|
|
951
958
|
monthly_cost=monthly_cost,
|
952
959
|
pricing_source="standard_aws_rate",
|
953
960
|
last_updated=datetime.now(),
|
954
|
-
currency="USD"
|
961
|
+
currency="USD",
|
955
962
|
)
|
956
963
|
|
957
964
|
def _check_pricing_overrides(self, service_key: str, region: str) -> float:
|
@@ -1004,12 +1011,9 @@ class DynamicAWSPricing:
|
|
1004
1011
|
# PRIORITY 1: Check for historical cached data from other regions
|
1005
1012
|
with self._cache_lock:
|
1006
1013
|
for cache_key, cached_result in self._pricing_cache.items():
|
1007
|
-
if
|
1008
|
-
cached_result.pricing_source == "aws_api"):
|
1014
|
+
if cached_result.service_key == service_key and cached_result.pricing_source == "aws_api":
|
1009
1015
|
# Found historical AWS API data, apply regional multiplier
|
1010
|
-
multiplier = self.get_regional_pricing_multiplier(
|
1011
|
-
service_key, region, cached_result.region
|
1012
|
-
)
|
1016
|
+
multiplier = self.get_regional_pricing_multiplier(service_key, region, cached_result.region)
|
1013
1017
|
estimated_cost = cached_result.monthly_cost * multiplier
|
1014
1018
|
logger.info(f"Using historical pricing data for {service_key}: ${estimated_cost}/month")
|
1015
1019
|
console.print(f"[blue]ℹ Using historical AWS API data with regional adjustment[/]")
|
@@ -1022,7 +1026,9 @@ class DynamicAWSPricing:
|
|
1022
1026
|
us_east_pricing = self._get_aws_api_pricing(service_key, "us-east-1")
|
1023
1027
|
multiplier = self.get_regional_pricing_multiplier(service_key, region, "us-east-1")
|
1024
1028
|
estimated_cost = us_east_pricing.monthly_cost * multiplier
|
1025
|
-
logger.info(
|
1029
|
+
logger.info(
|
1030
|
+
f"Using us-east-1 pricing with regional multiplier for {service_key}: ${estimated_cost}/month"
|
1031
|
+
)
|
1026
1032
|
console.print(f"[blue]ℹ Using us-east-1 pricing with {multiplier:.3f}x regional adjustment[/]")
|
1027
1033
|
return estimated_cost
|
1028
1034
|
except Exception as e:
|
@@ -1053,10 +1059,10 @@ class DynamicAWSPricing:
|
|
1053
1059
|
# Standard AWS rates from pricing documentation (us-east-1)
|
1054
1060
|
# These are NOT hardcoded business values but technical reference rates
|
1055
1061
|
aws_documented_hourly_rates = {
|
1056
|
-
"nat_gateway": 0.045,
|
1057
|
-
"elastic_ip": 0.005,
|
1058
|
-
"vpc_endpoint": 0.01,
|
1059
|
-
"transit_gateway": 0.05,
|
1062
|
+
"nat_gateway": 0.045, # AWS standard NAT Gateway rate
|
1063
|
+
"elastic_ip": 0.005, # AWS standard idle EIP rate
|
1064
|
+
"vpc_endpoint": 0.01, # AWS standard interface endpoint rate
|
1065
|
+
"transit_gateway": 0.05, # AWS standard Transit Gateway rate
|
1060
1066
|
"ebs_gp3": 0.08 / (24 * 30), # AWS standard GP3 per GB/month to hourly
|
1061
1067
|
"ebs_gp2": 0.10 / (24 * 30), # AWS standard GP2 per GB/month to hourly
|
1062
1068
|
}
|
@@ -1075,10 +1081,10 @@ class DynamicAWSPricing:
|
|
1075
1081
|
def _get_aws_location_name(self, region: str) -> str:
|
1076
1082
|
"""
|
1077
1083
|
Convert AWS region code to location name used by Pricing API.
|
1078
|
-
|
1084
|
+
|
1079
1085
|
Args:
|
1080
1086
|
region: AWS region code
|
1081
|
-
|
1087
|
+
|
1082
1088
|
Returns:
|
1083
1089
|
AWS location name for Pricing API
|
1084
1090
|
"""
|
@@ -1089,7 +1095,6 @@ class DynamicAWSPricing:
|
|
1089
1095
|
"us-east-2": "US East (Ohio)",
|
1090
1096
|
"us-west-1": "US West (N. California)",
|
1091
1097
|
"us-west-2": "US West (Oregon)",
|
1092
|
-
|
1093
1098
|
# EU Regions
|
1094
1099
|
"eu-central-1": "Europe (Frankfurt)",
|
1095
1100
|
"eu-central-2": "Europe (Zurich)",
|
@@ -1099,7 +1104,6 @@ class DynamicAWSPricing:
|
|
1099
1104
|
"eu-south-1": "Europe (Milan)",
|
1100
1105
|
"eu-south-2": "Europe (Spain)",
|
1101
1106
|
"eu-north-1": "Europe (Stockholm)",
|
1102
|
-
|
1103
1107
|
# Asia Pacific Regions
|
1104
1108
|
"ap-northeast-1": "Asia Pacific (Tokyo)",
|
1105
1109
|
"ap-northeast-2": "Asia Pacific (Seoul)",
|
@@ -1111,7 +1115,6 @@ class DynamicAWSPricing:
|
|
1111
1115
|
"ap-south-1": "Asia Pacific (Mumbai)",
|
1112
1116
|
"ap-south-2": "Asia Pacific (Hyderabad)",
|
1113
1117
|
"ap-east-1": "Asia Pacific (Hong Kong)",
|
1114
|
-
|
1115
1118
|
# Other Regions
|
1116
1119
|
"ca-central-1": "Canada (Central)",
|
1117
1120
|
"ca-west-1": "Canada (West)",
|
@@ -1119,69 +1122,69 @@ class DynamicAWSPricing:
|
|
1119
1122
|
"af-south-1": "Africa (Cape Town)",
|
1120
1123
|
"me-south-1": "Middle East (Bahrain)",
|
1121
1124
|
"me-central-1": "Middle East (UAE)",
|
1122
|
-
|
1123
1125
|
# GovCloud
|
1124
1126
|
"us-gov-east-1": "AWS GovCloud (US-East)",
|
1125
1127
|
"us-gov-west-1": "AWS GovCloud (US-West)",
|
1126
|
-
|
1127
1128
|
# China (Note: Pricing API may not be available)
|
1128
1129
|
"cn-north-1": "China (Beijing)",
|
1129
1130
|
"cn-northwest-1": "China (Ningxia)",
|
1130
1131
|
}
|
1131
|
-
|
1132
|
+
|
1132
1133
|
return location_mapping.get(region, "US East (N. Virginia)")
|
1133
1134
|
|
1134
|
-
def get_regional_pricing_multiplier(
|
1135
|
+
def get_regional_pricing_multiplier(
|
1136
|
+
self, service_key: str, target_region: str, base_region: str = "us-east-1"
|
1137
|
+
) -> float:
|
1135
1138
|
"""
|
1136
1139
|
Get regional pricing multiplier by comparing real AWS pricing between regions.
|
1137
|
-
|
1140
|
+
|
1138
1141
|
Args:
|
1139
1142
|
service_key: Service identifier (nat_gateway, elastic_ip, etc.)
|
1140
1143
|
target_region: Target region to get multiplier for
|
1141
1144
|
base_region: Base region for comparison (default us-east-1)
|
1142
|
-
|
1145
|
+
|
1143
1146
|
Returns:
|
1144
1147
|
Regional pricing multiplier (target_price / base_price)
|
1145
1148
|
"""
|
1146
1149
|
cache_key = f"{service_key}:{target_region}:{base_region}"
|
1147
|
-
|
1150
|
+
|
1148
1151
|
with self._region_cache_lock:
|
1149
1152
|
# Check cache first
|
1150
1153
|
if cache_key in self._regional_pricing_cache:
|
1151
1154
|
cached_result = self._regional_pricing_cache[cache_key]
|
1152
|
-
if datetime.now() - cached_result[
|
1155
|
+
if datetime.now() - cached_result["last_updated"] < self.cache_ttl:
|
1153
1156
|
logger.debug(f"Using cached regional multiplier for {service_key} {target_region}")
|
1154
|
-
return cached_result[
|
1157
|
+
return cached_result["multiplier"]
|
1155
1158
|
else:
|
1156
1159
|
# Cache expired, remove it
|
1157
1160
|
del self._regional_pricing_cache[cache_key]
|
1158
|
-
|
1161
|
+
|
1159
1162
|
try:
|
1160
1163
|
# Get real pricing for both regions
|
1161
1164
|
base_pricing = self._get_aws_api_pricing(service_key, base_region)
|
1162
1165
|
target_pricing = self._get_aws_api_pricing(service_key, target_region)
|
1163
|
-
|
1166
|
+
|
1164
1167
|
# Calculate multiplier
|
1165
1168
|
if base_pricing.monthly_cost > 0:
|
1166
1169
|
multiplier = target_pricing.monthly_cost / base_pricing.monthly_cost
|
1167
1170
|
else:
|
1168
1171
|
multiplier = 1.0
|
1169
|
-
|
1172
|
+
|
1170
1173
|
# Cache the result
|
1171
1174
|
with self._region_cache_lock:
|
1172
1175
|
self._regional_pricing_cache[cache_key] = {
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1176
|
+
"multiplier": multiplier,
|
1177
|
+
"last_updated": datetime.now(),
|
1178
|
+
"base_cost": base_pricing.monthly_cost,
|
1179
|
+
"target_cost": target_pricing.monthly_cost,
|
1177
1180
|
}
|
1178
|
-
|
1181
|
+
|
1179
1182
|
logger.info(f"Regional multiplier for {service_key} {target_region}: {multiplier:.4f}")
|
1180
1183
|
return multiplier
|
1181
|
-
|
1184
|
+
|
1182
1185
|
except Exception as e:
|
1183
1186
|
logger.warning(f"Failed to get regional pricing multiplier for {service_key} {target_region}: {e}")
|
1184
|
-
|
1187
|
+
|
1185
1188
|
# Fallback: Return 1.0 (no multiplier) to avoid hardcoded values
|
1186
1189
|
logger.warning(f"Using 1.0 multiplier for {service_key} {target_region} - investigate pricing API access")
|
1187
1190
|
return 1.0
|
@@ -1192,44 +1195,54 @@ class DynamicAWSPricing:
|
|
1192
1195
|
total_entries = len(self._pricing_cache)
|
1193
1196
|
api_entries = sum(1 for r in self._pricing_cache.values() if r.pricing_source == "aws_api")
|
1194
1197
|
fallback_entries = sum(1 for r in self._pricing_cache.values() if r.pricing_source == "fallback")
|
1195
|
-
|
1198
|
+
|
1196
1199
|
return {
|
1197
1200
|
"total_cached_entries": total_entries,
|
1198
1201
|
"aws_api_entries": api_entries,
|
1199
1202
|
"fallback_entries": fallback_entries,
|
1200
1203
|
"cache_hit_rate": (api_entries / total_entries * 100) if total_entries > 0 else 0,
|
1201
|
-
"cache_ttl_hours": self.cache_ttl.total_seconds() / 3600
|
1204
|
+
"cache_ttl_hours": self.cache_ttl.total_seconds() / 3600,
|
1202
1205
|
}
|
1203
1206
|
|
1204
1207
|
def get_available_regions(self) -> List[str]:
|
1205
1208
|
"""
|
1206
1209
|
Get all available AWS regions dynamically from AWS API.
|
1207
|
-
|
1210
|
+
|
1208
1211
|
Returns:
|
1209
1212
|
List of AWS region codes
|
1210
1213
|
"""
|
1211
1214
|
try:
|
1212
1215
|
if self.profile:
|
1213
|
-
session = create_cost_session(self.profile)
|
1214
|
-
ec2_client = session.client(
|
1216
|
+
session = create_cost_session(profile_name=self.profile)
|
1217
|
+
ec2_client = session.client("ec2", region_name="us-east-1")
|
1215
1218
|
else:
|
1216
|
-
ec2_client = boto3.client(
|
1217
|
-
|
1219
|
+
ec2_client = boto3.client("ec2", region_name="us-east-1")
|
1220
|
+
|
1218
1221
|
response = ec2_client.describe_regions()
|
1219
|
-
regions = [region[
|
1220
|
-
|
1222
|
+
regions = [region["RegionName"] for region in response["Regions"]]
|
1223
|
+
|
1221
1224
|
logger.info(f"Retrieved {len(regions)} AWS regions from API")
|
1222
1225
|
return sorted(regions)
|
1223
|
-
|
1226
|
+
|
1224
1227
|
except Exception as e:
|
1225
1228
|
logger.warning(f"Failed to get regions from AWS API: {e}")
|
1226
|
-
|
1229
|
+
|
1227
1230
|
# Fallback to well-known regions if API unavailable
|
1228
1231
|
fallback_regions = [
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1232
|
+
"us-east-1",
|
1233
|
+
"us-east-2",
|
1234
|
+
"us-west-1",
|
1235
|
+
"us-west-2",
|
1236
|
+
"eu-central-1",
|
1237
|
+
"eu-west-1",
|
1238
|
+
"eu-west-2",
|
1239
|
+
"eu-west-3",
|
1240
|
+
"ap-northeast-1",
|
1241
|
+
"ap-northeast-2",
|
1242
|
+
"ap-southeast-1",
|
1243
|
+
"ap-southeast-2",
|
1244
|
+
"ca-central-1",
|
1245
|
+
"sa-east-1",
|
1233
1246
|
]
|
1234
1247
|
logger.info(f"Using fallback regions: {len(fallback_regions)} regions")
|
1235
1248
|
return fallback_regions
|
@@ -1239,11 +1252,11 @@ class DynamicAWSPricing:
|
|
1239
1252
|
with self._cache_lock:
|
1240
1253
|
cleared_count = len(self._pricing_cache)
|
1241
1254
|
self._pricing_cache.clear()
|
1242
|
-
|
1255
|
+
|
1243
1256
|
with self._region_cache_lock:
|
1244
1257
|
regional_cleared = len(self._regional_pricing_cache)
|
1245
1258
|
self._regional_pricing_cache.clear()
|
1246
|
-
|
1259
|
+
|
1247
1260
|
logger.info(f"Cleared {cleared_count} pricing cache entries and {regional_cleared} regional cache entries")
|
1248
1261
|
|
1249
1262
|
|
@@ -1252,36 +1265,34 @@ _pricing_engine = None
|
|
1252
1265
|
_pricing_lock = threading.Lock()
|
1253
1266
|
|
1254
1267
|
|
1255
|
-
def get_aws_pricing_engine(
|
1268
|
+
def get_aws_pricing_engine(
|
1269
|
+
cache_ttl_hours: int = 24, enable_fallback: bool = True, profile: Optional[str] = None
|
1270
|
+
) -> DynamicAWSPricing:
|
1256
1271
|
"""
|
1257
1272
|
Get AWS pricing engine instance with enterprise profile integration.
|
1258
|
-
|
1273
|
+
|
1259
1274
|
Args:
|
1260
1275
|
cache_ttl_hours: Cache time-to-live in hours
|
1261
1276
|
enable_fallback: Enable fallback to estimated pricing
|
1262
1277
|
profile: AWS profile for pricing operations (enterprise integration)
|
1263
|
-
|
1278
|
+
|
1264
1279
|
Returns:
|
1265
1280
|
DynamicAWSPricing instance
|
1266
1281
|
"""
|
1267
1282
|
# Create instance per profile for enterprise multi-profile support
|
1268
1283
|
# This ensures profile isolation and prevents cross-profile cache contamination
|
1269
|
-
return DynamicAWSPricing(
|
1270
|
-
cache_ttl_hours=cache_ttl_hours,
|
1271
|
-
enable_fallback=enable_fallback,
|
1272
|
-
profile=profile
|
1273
|
-
)
|
1284
|
+
return DynamicAWSPricing(cache_ttl_hours=cache_ttl_hours, enable_fallback=enable_fallback, profile=profile)
|
1274
1285
|
|
1275
1286
|
|
1276
1287
|
def get_service_monthly_cost(service_key: str, region: str = "us-east-1", profile: Optional[str] = None) -> float:
|
1277
1288
|
"""
|
1278
1289
|
Convenience function to get monthly cost for AWS service with profile support.
|
1279
|
-
|
1290
|
+
|
1280
1291
|
Args:
|
1281
1292
|
service_key: Service identifier
|
1282
1293
|
region: AWS region
|
1283
1294
|
profile: AWS profile for enterprise --profile compatibility
|
1284
|
-
|
1295
|
+
|
1285
1296
|
Returns:
|
1286
1297
|
Monthly cost in USD
|
1287
1298
|
"""
|
@@ -1293,33 +1304,35 @@ def get_service_monthly_cost(service_key: str, region: str = "us-east-1", profil
|
|
1293
1304
|
def calculate_annual_cost(monthly_cost: float) -> float:
|
1294
1305
|
"""
|
1295
1306
|
Calculate annual cost from monthly cost.
|
1296
|
-
|
1307
|
+
|
1297
1308
|
Args:
|
1298
1309
|
monthly_cost: Monthly cost in USD
|
1299
|
-
|
1310
|
+
|
1300
1311
|
Returns:
|
1301
1312
|
Annual cost in USD
|
1302
1313
|
"""
|
1303
1314
|
return monthly_cost * 12
|
1304
1315
|
|
1305
1316
|
|
1306
|
-
def calculate_regional_cost(
|
1317
|
+
def calculate_regional_cost(
|
1318
|
+
base_cost: float, region: str, service_key: str = "nat_gateway", profile: Optional[str] = None
|
1319
|
+
) -> float:
|
1307
1320
|
"""
|
1308
1321
|
Apply dynamic regional pricing multiplier to base cost using AWS Pricing API.
|
1309
|
-
|
1322
|
+
|
1310
1323
|
Args:
|
1311
1324
|
base_cost: Base cost in USD
|
1312
1325
|
region: AWS region
|
1313
1326
|
service_key: Service type for regional multiplier calculation
|
1314
1327
|
profile: AWS profile for enterprise --profile compatibility
|
1315
|
-
|
1328
|
+
|
1316
1329
|
Returns:
|
1317
1330
|
Region-adjusted cost in USD
|
1318
1331
|
"""
|
1319
1332
|
if region == "us-east-1":
|
1320
1333
|
# Base region - no multiplier needed
|
1321
1334
|
return base_cost
|
1322
|
-
|
1335
|
+
|
1323
1336
|
pricing_engine = get_aws_pricing_engine(profile=profile)
|
1324
1337
|
multiplier = pricing_engine.get_regional_pricing_multiplier(service_key, region, "us-east-1")
|
1325
1338
|
return base_cost * multiplier
|
@@ -1328,12 +1341,12 @@ def calculate_regional_cost(base_cost: float, region: str, service_key: str = "n
|
|
1328
1341
|
def get_ec2_monthly_cost(instance_type: str, region: str = "us-east-1", profile: Optional[str] = None) -> float:
|
1329
1342
|
"""
|
1330
1343
|
Convenience function to get monthly cost for EC2 instance type with profile support.
|
1331
|
-
|
1344
|
+
|
1332
1345
|
Args:
|
1333
1346
|
instance_type: EC2 instance type (e.g., t3.micro)
|
1334
1347
|
region: AWS region
|
1335
1348
|
profile: AWS profile for enterprise --profile compatibility
|
1336
|
-
|
1349
|
+
|
1337
1350
|
Returns:
|
1338
1351
|
Monthly cost in USD
|
1339
1352
|
"""
|
@@ -1342,26 +1355,28 @@ def get_ec2_monthly_cost(instance_type: str, region: str = "us-east-1", profile:
|
|
1342
1355
|
return result.monthly_cost
|
1343
1356
|
|
1344
1357
|
|
1345
|
-
def calculate_ec2_cost_impact(
|
1358
|
+
def calculate_ec2_cost_impact(
|
1359
|
+
instance_type: str, count: int = 1, region: str = "us-east-1", profile: Optional[str] = None
|
1360
|
+
) -> Dict[str, float]:
|
1346
1361
|
"""
|
1347
1362
|
Calculate cost impact for multiple EC2 instances with profile support.
|
1348
|
-
|
1363
|
+
|
1349
1364
|
Args:
|
1350
1365
|
instance_type: EC2 instance type
|
1351
1366
|
count: Number of instances
|
1352
1367
|
region: AWS region
|
1353
1368
|
profile: AWS profile for enterprise --profile compatibility
|
1354
|
-
|
1369
|
+
|
1355
1370
|
Returns:
|
1356
1371
|
Dictionary with cost calculations
|
1357
1372
|
"""
|
1358
1373
|
monthly_cost_per_instance = get_ec2_monthly_cost(instance_type, region, profile)
|
1359
|
-
|
1374
|
+
|
1360
1375
|
return {
|
1361
1376
|
"monthly_cost_per_instance": monthly_cost_per_instance,
|
1362
1377
|
"total_monthly_cost": monthly_cost_per_instance * count,
|
1363
1378
|
"total_annual_cost": monthly_cost_per_instance * count * 12,
|
1364
|
-
"instance_count": count
|
1379
|
+
"instance_count": count,
|
1365
1380
|
}
|
1366
1381
|
|
1367
1382
|
|
@@ -1369,6 +1384,7 @@ def calculate_ec2_cost_impact(instance_type: str, count: int = 1, region: str =
|
|
1369
1384
|
# ENTERPRISE CONVENIENCE FUNCTIONS - Strategic Requirements Integration
|
1370
1385
|
# ============================================================================
|
1371
1386
|
|
1387
|
+
|
1372
1388
|
def get_ec2_instance_hourly_cost(instance_type: str, region: str = "us-east-1", profile: Optional[str] = None) -> float:
|
1373
1389
|
"""Enterprise convenience function for EC2 hourly cost (Strategic Requirement #1)."""
|
1374
1390
|
pricing_engine = get_aws_pricing_engine(profile=profile)
|
@@ -1387,73 +1403,85 @@ def get_nat_gateway_monthly_cost(region: str = "us-east-1", profile: Optional[st
|
|
1387
1403
|
return pricing_engine.get_nat_gateway_monthly_cost(region)
|
1388
1404
|
|
1389
1405
|
|
1390
|
-
def get_ebs_gb_monthly_cost(
|
1406
|
+
def get_ebs_gb_monthly_cost(
|
1407
|
+
volume_type: str = "gp3", region: str = "us-east-1", profile: Optional[str] = None
|
1408
|
+
) -> float:
|
1391
1409
|
"""Enterprise convenience function for EBS per-GB monthly cost (Strategic Requirement #4)."""
|
1392
1410
|
pricing_engine = get_aws_pricing_engine(profile=profile)
|
1393
1411
|
return pricing_engine.get_ebs_gb_monthly_cost(volume_type, region)
|
1394
1412
|
|
1395
1413
|
|
1396
|
-
def get_multi_service_cost_analysis(
|
1414
|
+
def get_multi_service_cost_analysis(
|
1415
|
+
regions: List[str], services: Optional[List[str]] = None, profile: Optional[str] = None
|
1416
|
+
) -> Dict[str, Dict[str, float]]:
|
1397
1417
|
"""
|
1398
1418
|
Enterprise function for multi-region, multi-service cost analysis with <1s performance.
|
1399
|
-
|
1419
|
+
|
1400
1420
|
Args:
|
1401
1421
|
regions: List of AWS regions to analyze
|
1402
1422
|
services: List of service keys (default: common enterprise services)
|
1403
1423
|
profile: AWS profile for enterprise --profile compatibility
|
1404
|
-
|
1424
|
+
|
1405
1425
|
Returns:
|
1406
1426
|
Dictionary mapping region to service costs
|
1407
1427
|
"""
|
1408
1428
|
if services is None:
|
1409
1429
|
services = ["nat_gateway", "elastic_ip", "ebs_gp3", "vpc_endpoint", "loadbalancer_application"]
|
1410
|
-
|
1430
|
+
|
1411
1431
|
pricing_engine = get_aws_pricing_engine(profile=profile)
|
1412
1432
|
results = {}
|
1413
|
-
|
1433
|
+
|
1414
1434
|
for region in regions:
|
1415
1435
|
service_requests = [(service, region) for service in services]
|
1416
1436
|
pricing_results = pricing_engine.get_multi_service_pricing(service_requests)
|
1417
|
-
|
1437
|
+
|
1418
1438
|
results[region] = {
|
1419
|
-
service: pricing_results.get(
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1439
|
+
service: pricing_results.get(
|
1440
|
+
f"{service}:{region}",
|
1441
|
+
AWSPricingResult(
|
1442
|
+
service_key=service,
|
1443
|
+
region=region,
|
1444
|
+
monthly_cost=0.0,
|
1445
|
+
pricing_source="error",
|
1446
|
+
last_updated=datetime.now(),
|
1447
|
+
),
|
1448
|
+
).monthly_cost
|
1423
1449
|
for service in services
|
1424
1450
|
}
|
1425
|
-
|
1451
|
+
|
1426
1452
|
return results
|
1427
1453
|
|
1428
1454
|
|
1429
1455
|
def warm_pricing_cache_for_enterprise(regions: List[str], profile: Optional[str] = None) -> None:
|
1430
1456
|
"""
|
1431
1457
|
Enterprise cache warming for optimal <1s response times across regions.
|
1432
|
-
|
1458
|
+
|
1433
1459
|
Args:
|
1434
1460
|
regions: List of AWS regions to warm cache for
|
1435
1461
|
profile: AWS profile for enterprise --profile compatibility
|
1436
1462
|
"""
|
1437
1463
|
pricing_engine = get_aws_pricing_engine(profile=profile)
|
1438
|
-
|
1464
|
+
|
1439
1465
|
console.print(f"[dim]Warming enterprise pricing cache for {len(regions)} regions...[/]")
|
1440
|
-
|
1466
|
+
|
1441
1467
|
for region in regions:
|
1442
1468
|
pricing_engine.warm_cache_for_region(region)
|
1443
|
-
|
1469
|
+
|
1444
1470
|
console.print("[dim]Enterprise pricing cache warming completed[/]")
|
1445
1471
|
|
1446
1472
|
|
1447
|
-
def get_regional_pricing_multiplier(
|
1473
|
+
def get_regional_pricing_multiplier(
|
1474
|
+
service_key: str, target_region: str, base_region: str = "us-east-1", profile: Optional[str] = None
|
1475
|
+
) -> float:
|
1448
1476
|
"""
|
1449
1477
|
Get dynamic regional pricing multiplier using AWS Pricing API.
|
1450
|
-
|
1478
|
+
|
1451
1479
|
Args:
|
1452
1480
|
service_key: Service identifier (nat_gateway, elastic_ip, etc.)
|
1453
1481
|
target_region: Target region to get multiplier for
|
1454
1482
|
base_region: Base region for comparison (default us-east-1)
|
1455
1483
|
profile: AWS profile for enterprise --profile compatibility
|
1456
|
-
|
1484
|
+
|
1457
1485
|
Returns:
|
1458
1486
|
Regional pricing multiplier (target_price / base_price)
|
1459
1487
|
"""
|
@@ -1464,58 +1492,53 @@ def get_regional_pricing_multiplier(service_key: str, target_region: str, base_r
|
|
1464
1492
|
def get_all_regions_pricing(service_key: str, profile: Optional[str] = None) -> Dict[str, float]:
|
1465
1493
|
"""
|
1466
1494
|
Get pricing for a service across all AWS regions dynamically.
|
1467
|
-
|
1495
|
+
|
1468
1496
|
Args:
|
1469
1497
|
service_key: Service identifier
|
1470
1498
|
profile: AWS profile for enterprise --profile compatibility
|
1471
|
-
|
1499
|
+
|
1472
1500
|
Returns:
|
1473
1501
|
Dictionary mapping region to monthly cost
|
1474
1502
|
"""
|
1475
1503
|
pricing_engine = get_aws_pricing_engine(profile=profile)
|
1476
1504
|
regions = pricing_engine.get_available_regions()
|
1477
|
-
|
1505
|
+
|
1478
1506
|
results = {}
|
1479
1507
|
service_requests = [(service_key, region) for region in regions]
|
1480
1508
|
pricing_results = pricing_engine.get_multi_service_pricing(service_requests)
|
1481
|
-
|
1509
|
+
|
1482
1510
|
for region in regions:
|
1483
1511
|
key = f"{service_key}:{region}"
|
1484
1512
|
if key in pricing_results:
|
1485
1513
|
results[region] = pricing_results[key].monthly_cost
|
1486
1514
|
else:
|
1487
1515
|
results[region] = 0.0
|
1488
|
-
|
1516
|
+
|
1489
1517
|
return results
|
1490
1518
|
|
1491
1519
|
|
1492
1520
|
# Export main functions
|
1493
1521
|
__all__ = [
|
1494
1522
|
# Core Classes
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1523
|
+
"DynamicAWSPricing",
|
1524
|
+
"AWSPricingResult",
|
1498
1525
|
# Core Factory Functions
|
1499
|
-
|
1500
|
-
|
1526
|
+
"get_aws_pricing_engine",
|
1501
1527
|
# General Service Functions
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
|
1506
|
-
|
1507
|
-
|
1528
|
+
"get_service_monthly_cost",
|
1529
|
+
"get_ec2_monthly_cost",
|
1530
|
+
"calculate_ec2_cost_impact",
|
1531
|
+
"calculate_annual_cost",
|
1532
|
+
"calculate_regional_cost",
|
1508
1533
|
# Strategic Requirements - Enterprise Service Methods
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1534
|
+
"get_ec2_instance_hourly_cost", # Strategic Requirement #1
|
1535
|
+
"get_eip_monthly_cost", # Strategic Requirement #2
|
1536
|
+
"get_nat_gateway_monthly_cost", # Strategic Requirement #3
|
1537
|
+
"get_ebs_gb_monthly_cost", # Strategic Requirement #4
|
1514
1538
|
# Enterprise Performance Functions
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1539
|
+
"get_multi_service_cost_analysis",
|
1540
|
+
"warm_pricing_cache_for_enterprise",
|
1518
1541
|
# Dynamic Regional Pricing Functions
|
1519
|
-
|
1520
|
-
|
1521
|
-
]
|
1542
|
+
"get_regional_pricing_multiplier",
|
1543
|
+
"get_all_regions_pricing",
|
1544
|
+
]
|