aws-cost-calculator-cli 1.6.3__py3-none-any.whl → 1.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aws-cost-calculator-cli might be problematic. Click here for more details.

cost_calculator/cur.py ADDED
@@ -0,0 +1,244 @@
1
+ """
2
+ CUR (Cost and Usage Report) queries via Athena for resource-level analysis.
3
+ """
4
+ import time
5
+ from datetime import datetime, timedelta
6
+
7
+
8
+ # Service name to CUR product code mapping
9
+ SERVICE_TO_PRODUCT_CODE = {
10
+ 'EC2 - Other': 'AmazonEC2',
11
+ 'Amazon Elastic Compute Cloud - Compute': 'AmazonEC2',
12
+ 'Amazon Relational Database Service': 'AmazonRDS',
13
+ 'Amazon Simple Storage Service': 'AmazonS3',
14
+ 'Load Balancing': 'AWSELB',
15
+ 'Elastic Load Balancing': 'AWSELB',
16
+ 'Amazon DynamoDB': 'AmazonDynamoDB',
17
+ 'AWS Lambda': 'AWSLambda',
18
+ 'Amazon CloudFront': 'AmazonCloudFront',
19
+ 'Amazon ElastiCache': 'AmazonElastiCache',
20
+ 'Amazon Elastic MapReduce': 'ElasticMapReduce',
21
+ 'Amazon Kinesis': 'AmazonKinesis',
22
+ 'Amazon Redshift': 'AmazonRedshift',
23
+ 'Amazon Simple Notification Service': 'AmazonSNS',
24
+ 'Amazon Simple Queue Service': 'AmazonSQS',
25
+ }
26
+
27
+
28
+ def map_service_to_product_code(service_name):
29
+ """Map service name to CUR product code"""
30
+ # Direct mapping
31
+ if service_name in SERVICE_TO_PRODUCT_CODE:
32
+ return SERVICE_TO_PRODUCT_CODE[service_name]
33
+
34
+ # Fuzzy matching
35
+ service_lower = service_name.lower()
36
+ for key, code in SERVICE_TO_PRODUCT_CODE.items():
37
+ if key.lower() in service_lower or service_lower in key.lower():
38
+ return code
39
+
40
+ # Fallback: try to extract from service name
41
+ # "Amazon Relational Database Service" -> "AmazonRDS"
42
+ return service_name.replace(' ', '').replace('-', '')
43
+
44
+
45
+ def get_cur_config():
46
+ """Load CUR configuration from config file or environment variables"""
47
+ import os
48
+ from pathlib import Path
49
+ import json
50
+
51
+ # Try config file first
52
+ config_file = Path.home() / '.config' / 'cost-calculator' / 'cur_config.json'
53
+ if config_file.exists():
54
+ with open(config_file) as f:
55
+ return json.load(f)
56
+
57
+ # Fall back to environment variables
58
+ return {
59
+ 'database': os.environ.get('CUR_DATABASE', 'cur_database'),
60
+ 'table': os.environ.get('CUR_TABLE', 'cur_table'),
61
+ 's3_output': os.environ.get('CUR_S3_OUTPUT', 's3://your-athena-results-bucket/')
62
+ }
63
+
64
+
65
+ def query_cur_resources(athena_client, accounts, service, account_filter, weeks,
66
+ cur_database=None,
67
+ cur_table=None,
68
+ s3_output=None):
69
+ """
70
+ Query CUR via Athena for resource-level cost details.
71
+
72
+ Args:
73
+ athena_client: boto3 Athena client
74
+ accounts: list of account IDs
75
+ service: service name to filter by
76
+ account_filter: specific account ID or None for all accounts
77
+ weeks: number of weeks to analyze
78
+ cur_database: Athena database name
79
+ cur_table: CUR table name
80
+ s3_output: S3 location for query results
81
+
82
+ Returns:
83
+ list of dicts with resource details
84
+ """
85
+ # Load CUR configuration
86
+ cur_config = get_cur_config()
87
+ if cur_database is None:
88
+ cur_database = cur_config['database']
89
+ if cur_table is None:
90
+ cur_table = cur_config['table']
91
+ if s3_output is None:
92
+ s3_output = cur_config['s3_output']
93
+
94
+ # Calculate date range
95
+ end_date = datetime.now() - timedelta(days=2)
96
+ start_date = end_date - timedelta(weeks=weeks)
97
+
98
+ # Map service to product code
99
+ product_code = map_service_to_product_code(service)
100
+
101
+ # Build account filter
102
+ if account_filter:
103
+ account_clause = f"AND line_item_usage_account_id = '{account_filter}'"
104
+ else:
105
+ account_list = "','".join(accounts)
106
+ account_clause = f"AND line_item_usage_account_id IN ('{account_list}')"
107
+
108
+ # Build query - use generic columns that work for all services
109
+ query = f"""
110
+ SELECT
111
+ line_item_usage_account_id as account_id,
112
+ line_item_resource_id as resource_id,
113
+ line_item_usage_type as usage_type,
114
+ product_region as region,
115
+ SUM(line_item_unblended_cost) as total_cost,
116
+ SUM(line_item_usage_amount) as total_usage
117
+ FROM {cur_database}.{cur_table}
118
+ WHERE line_item_product_code = '{product_code}'
119
+ {account_clause}
120
+ AND line_item_resource_id != ''
121
+ AND line_item_line_item_type IN ('Usage', 'Fee')
122
+ AND line_item_usage_start_date >= DATE '{start_date.strftime('%Y-%m-%d')}'
123
+ AND line_item_usage_start_date < DATE '{end_date.strftime('%Y-%m-%d')}'
124
+ GROUP BY 1, 2, 3, 4
125
+ ORDER BY total_cost DESC
126
+ LIMIT 50
127
+ """
128
+
129
+ # Execute Athena query
130
+ try:
131
+ response = athena_client.start_query_execution(
132
+ QueryString=query,
133
+ QueryExecutionContext={'Database': cur_database},
134
+ ResultConfiguration={'OutputLocation': s3_output}
135
+ )
136
+
137
+ query_execution_id = response['QueryExecutionId']
138
+
139
+ # Wait for query to complete (max 60 seconds)
140
+ max_wait = 60
141
+ wait_interval = 2
142
+ elapsed = 0
143
+
144
+ while elapsed < max_wait:
145
+ status_response = athena_client.get_query_execution(
146
+ QueryExecutionId=query_execution_id
147
+ )
148
+ state = status_response['QueryExecution']['Status']['State']
149
+
150
+ if state == 'SUCCEEDED':
151
+ break
152
+ elif state in ['FAILED', 'CANCELLED']:
153
+ reason = status_response['QueryExecution']['Status'].get(
154
+ 'StateChangeReason', 'Unknown error'
155
+ )
156
+ raise Exception(f"Athena query {state}: {reason}")
157
+
158
+ time.sleep(wait_interval)
159
+ elapsed += wait_interval
160
+
161
+ if elapsed >= max_wait:
162
+ raise Exception("Athena query timeout after 60 seconds")
163
+
164
+ # Get results
165
+ results_response = athena_client.get_query_results(
166
+ QueryExecutionId=query_execution_id,
167
+ MaxResults=100
168
+ )
169
+
170
+ # Parse results
171
+ resources = []
172
+ rows = results_response['ResultSet']['Rows']
173
+
174
+ # Skip header row
175
+ for row in rows[1:]:
176
+ data = row['Data']
177
+ resources.append({
178
+ 'account_id': data[0].get('VarCharValue', ''),
179
+ 'resource_id': data[1].get('VarCharValue', ''),
180
+ 'usage_type': data[2].get('VarCharValue', ''),
181
+ 'region': data[3].get('VarCharValue', ''),
182
+ 'total_cost': float(data[4].get('VarCharValue', 0)),
183
+ 'total_usage': float(data[5].get('VarCharValue', 0))
184
+ })
185
+
186
+ return {
187
+ 'resources': resources,
188
+ 'service': service,
189
+ 'product_code': product_code,
190
+ 'account_filter': account_filter,
191
+ 'period': {
192
+ 'start': start_date.strftime('%Y-%m-%d'),
193
+ 'end': end_date.strftime('%Y-%m-%d'),
194
+ 'weeks': weeks
195
+ }
196
+ }
197
+
198
+ except Exception as e:
199
+ raise Exception(f"CUR query failed: {str(e)}")
200
+
201
+
202
+ def format_resource_output(result):
203
+ """Format resource query results for display"""
204
+ resources = result['resources']
205
+ service = result['service']
206
+ account_filter = result.get('account_filter')
207
+ period = result['period']
208
+
209
+ output = []
210
+ output.append(f"\n📊 Resource Breakdown: {service}")
211
+
212
+ if account_filter:
213
+ output.append(f"Account: {account_filter} | Period: {period['start']} to {period['end']} ({period['weeks']} weeks)")
214
+ else:
215
+ output.append(f"All Accounts | Period: {period['start']} to {period['end']} ({period['weeks']} weeks)")
216
+
217
+ output.append("")
218
+
219
+ if not resources:
220
+ output.append("No resources found with costs in this period.")
221
+ return '\n'.join(output)
222
+
223
+ # Format as table
224
+ output.append(f"Top {len(resources)} Resources by Cost:")
225
+ output.append("┌─────────────────────────────┬──────────────────────────────┬────────────┬─────────┐")
226
+ output.append("│ Resource ID │ Usage Type │ Region │ Cost │")
227
+ output.append("├─────────────────────────────┼──────────────────────────────┼────────────┼─────────┤")
228
+
229
+ for resource in resources:
230
+ resource_id = resource['resource_id'][:27] + '...' if len(resource['resource_id']) > 27 else resource['resource_id']
231
+ usage_type = resource['usage_type'][:28] + '..' if len(resource['usage_type']) > 28 else resource['usage_type']
232
+ region = resource['region'][:10] if resource['region'] else 'N/A'
233
+ cost = resource['total_cost']
234
+
235
+ output.append(
236
+ f"│ {resource_id:<27} │ {usage_type:<28} │ {region:<10} │ ${cost:>6.2f} │"
237
+ )
238
+
239
+ output.append("└─────────────────────────────┴──────────────────────────────┴────────────┴─────────┘")
240
+ output.append("")
241
+ output.append("💡 Tip: Use AWS CLI to investigate specific resources:")
242
+ output.append(f" aws ec2 describe-instances --instance-ids <resource-id>")
243
+
244
+ return '\n'.join(output)
@@ -56,32 +56,18 @@ def execute_trends(config, weeks):
56
56
  """
57
57
  accounts = config['accounts']
58
58
 
59
- if is_api_configured():
60
- # Use API
61
- click.echo("Using Lambda API...")
62
- credentials = get_credentials_dict(config)
63
- return call_lambda_api('trends', credentials, accounts, weeks=weeks)
64
- else:
65
- # Use local execution
66
- click.echo("Using local execution...")
67
- from cost_calculator.trends import analyze_trends
68
-
69
- # Initialize boto3 client
70
- if 'aws_profile' in config:
71
- session = boto3.Session(profile_name=config['aws_profile'])
72
- else:
73
- creds = config['credentials']
74
- session_kwargs = {
75
- 'aws_access_key_id': creds['aws_access_key_id'],
76
- 'aws_secret_access_key': creds['aws_secret_access_key'],
77
- 'region_name': creds.get('region', 'us-east-1')
78
- }
79
- if 'aws_session_token' in creds:
80
- session_kwargs['aws_session_token'] = creds['aws_session_token']
81
- session = boto3.Session(**session_kwargs)
82
-
83
- ce_client = session.client('ce', region_name='us-east-1')
84
- return analyze_trends(ce_client, accounts, weeks)
59
+ if not is_api_configured():
60
+ raise Exception(
61
+ "API not configured. Set COST_API_SECRET environment variable.\n"
62
+ "Local execution is disabled. Use the Lambda API."
63
+ )
64
+
65
+ # Use API only
66
+ click.echo("Using Lambda API...")
67
+ credentials = get_credentials_dict(config)
68
+ if not credentials:
69
+ raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
70
+ return call_lambda_api('trends', credentials, accounts, weeks=weeks)
85
71
 
86
72
 
87
73
  def execute_monthly(config, months):
@@ -93,81 +79,62 @@ def execute_monthly(config, months):
93
79
  """
94
80
  accounts = config['accounts']
95
81
 
96
- if is_api_configured():
97
- # Use API
98
- click.echo("Using Lambda API...")
99
- credentials = get_credentials_dict(config)
100
- return call_lambda_api('monthly', credentials, accounts, months=months)
101
- else:
102
- # Use local execution
103
- click.echo("Using local execution...")
104
- from cost_calculator.monthly import analyze_monthly_trends
105
-
106
- # Initialize boto3 client
107
- if 'aws_profile' in config:
108
- session = boto3.Session(profile_name=config['aws_profile'])
109
- else:
110
- creds = config['credentials']
111
- session_kwargs = {
112
- 'aws_access_key_id': creds['aws_access_key_id'],
113
- 'aws_secret_access_key': creds['aws_secret_access_key'],
114
- 'region_name': creds.get('region', 'us-east-1')
115
- }
116
- if 'aws_session_token' in creds:
117
- session_kwargs['aws_session_token'] = creds['aws_session_token']
118
- session = boto3.Session(**session_kwargs)
119
-
120
- ce_client = session.client('ce', region_name='us-east-1')
121
- return analyze_monthly_trends(ce_client, accounts, months)
82
+ if not is_api_configured():
83
+ raise Exception(
84
+ "API not configured. Set COST_API_SECRET environment variable.\n"
85
+ "Local execution is disabled. Use the Lambda API."
86
+ )
87
+
88
+ # Use API only
89
+ click.echo("Using Lambda API...")
90
+ credentials = get_credentials_dict(config)
91
+ if not credentials:
92
+ raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
93
+ return call_lambda_api('monthly', credentials, accounts, months=months)
122
94
 
123
95
 
124
- def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None):
96
+ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None, resources=False):
125
97
  """
126
- Execute drill-down analysis via API or locally.
98
+ Execute drill-down analysis via API.
99
+
100
+ Args:
101
+ config: Profile configuration
102
+ weeks: Number of weeks to analyze
103
+ service_filter: Optional service name filter
104
+ account_filter: Optional account ID filter
105
+ usage_type_filter: Optional usage type filter
106
+ resources: If True, query CUR for resource-level details
127
107
 
128
108
  Returns:
129
- dict: drill data
109
+ dict: drill data or resource data
130
110
  """
131
111
  accounts = config['accounts']
132
112
 
133
- if is_api_configured():
134
- # Use API
135
- click.echo("Using Lambda API...")
136
- credentials = get_credentials_dict(config)
137
- kwargs = {'weeks': weeks}
138
- if service_filter:
139
- kwargs['service'] = service_filter
140
- if account_filter:
141
- kwargs['account'] = account_filter
142
- if usage_type_filter:
143
- kwargs['usage_type'] = usage_type_filter
144
- return call_lambda_api('drill', credentials, accounts, **kwargs)
145
- else:
146
- # Use local execution
147
- click.echo("Using local execution...")
148
- from cost_calculator.drill import analyze_drill_down
149
-
150
- # Initialize boto3 client
151
- if 'aws_profile' in config:
152
- session = boto3.Session(profile_name=config['aws_profile'])
153
- else:
154
- creds = config['credentials']
155
- session_kwargs = {
156
- 'aws_access_key_id': creds['aws_access_key_id'],
157
- 'aws_secret_access_key': creds['aws_secret_access_key'],
158
- 'region_name': creds.get('region', 'us-east-1')
159
- }
160
- if 'aws_session_token' in creds:
161
- session_kwargs['aws_session_token'] = creds['aws_session_token']
162
- session = boto3.Session(**session_kwargs)
163
-
164
- ce_client = session.client('ce', region_name='us-east-1')
165
- return analyze_drill_down(
166
- ce_client, accounts, weeks,
167
- service_filter=service_filter,
168
- account_filter=account_filter,
169
- usage_type_filter=usage_type_filter
113
+ if not is_api_configured():
114
+ raise Exception(
115
+ "API not configured. Set COST_API_SECRET environment variable.\n"
116
+ "Local execution is disabled. Use the Lambda API."
170
117
  )
118
+
119
+ # Use API only
120
+ click.echo("Using Lambda API...")
121
+ credentials = get_credentials_dict(config)
122
+ if not credentials:
123
+ raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
124
+
125
+ kwargs = {'weeks': weeks}
126
+ if service_filter:
127
+ kwargs['service'] = service_filter
128
+ if account_filter:
129
+ kwargs['account'] = account_filter
130
+ if usage_type_filter:
131
+ kwargs['usage_type'] = usage_type_filter
132
+ if resources:
133
+ if not service_filter:
134
+ raise click.ClickException("--service is required when using --resources flag")
135
+ kwargs['resources'] = True
136
+
137
+ return call_lambda_api('drill', credentials, accounts, **kwargs)
171
138
 
172
139
 
173
140
  def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):