aws-cost-calculator-cli 1.0.3__py3-none-any.whl → 1.5.2__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.
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.5.2.dist-info}/METADATA +129 -2
- aws_cost_calculator_cli-1.5.2.dist-info/RECORD +13 -0
- cost_calculator/api_client.py +84 -0
- cost_calculator/cli.py +332 -0
- cost_calculator/drill.py +323 -0
- cost_calculator/executor.py +241 -0
- cost_calculator/monthly.py +242 -0
- cost_calculator/trends.py +353 -0
- aws_cost_calculator_cli-1.0.3.dist-info/RECORD +0 -8
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.5.2.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.5.2.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.5.2.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.5.2.dist-info}/top_level.txt +0 -0
cost_calculator/drill.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Drill-down cost analysis module.
|
|
3
|
+
Allows filtering by service, account, and usage type for detailed cost investigation.
|
|
4
|
+
"""
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_filtered_costs(ce_client, accounts, time_start, time_end, granularity='DAILY',
|
|
10
|
+
service_filter=None, account_filter=None, usage_type_filter=None):
|
|
11
|
+
"""
|
|
12
|
+
Get costs with optional filters, grouped by the next level of detail.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
ce_client: boto3 Cost Explorer client
|
|
16
|
+
accounts: List of account IDs
|
|
17
|
+
time_start: datetime for start
|
|
18
|
+
time_end: datetime for end
|
|
19
|
+
granularity: 'WEEKLY' or 'MONTHLY'
|
|
20
|
+
service_filter: Optional service name to filter
|
|
21
|
+
account_filter: Optional account ID to filter
|
|
22
|
+
usage_type_filter: Optional usage type to filter
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
dict: {dimension_value: total_cost}
|
|
26
|
+
"""
|
|
27
|
+
# Build filter
|
|
28
|
+
filters = []
|
|
29
|
+
|
|
30
|
+
# Account filter (either from account_filter or all accounts)
|
|
31
|
+
if account_filter:
|
|
32
|
+
filters.append({
|
|
33
|
+
"Dimensions": {
|
|
34
|
+
"Key": "LINKED_ACCOUNT",
|
|
35
|
+
"Values": [account_filter]
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
else:
|
|
39
|
+
filters.append({
|
|
40
|
+
"Dimensions": {
|
|
41
|
+
"Key": "LINKED_ACCOUNT",
|
|
42
|
+
"Values": accounts
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
# Service filter
|
|
47
|
+
if service_filter:
|
|
48
|
+
filters.append({
|
|
49
|
+
"Dimensions": {
|
|
50
|
+
"Key": "SERVICE",
|
|
51
|
+
"Values": [service_filter]
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
# Usage type filter
|
|
56
|
+
if usage_type_filter:
|
|
57
|
+
filters.append({
|
|
58
|
+
"Dimensions": {
|
|
59
|
+
"Key": "USAGE_TYPE",
|
|
60
|
+
"Values": [usage_type_filter]
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# Determine what to group by (next level of detail)
|
|
65
|
+
if usage_type_filter:
|
|
66
|
+
# Already at usage type level, group by region
|
|
67
|
+
group_by_key = 'REGION'
|
|
68
|
+
elif service_filter and account_filter:
|
|
69
|
+
# Have service and account, show usage types
|
|
70
|
+
group_by_key = 'USAGE_TYPE'
|
|
71
|
+
elif service_filter:
|
|
72
|
+
# Have service, show accounts
|
|
73
|
+
group_by_key = 'LINKED_ACCOUNT'
|
|
74
|
+
elif account_filter:
|
|
75
|
+
# Have account, show services
|
|
76
|
+
group_by_key = 'SERVICE'
|
|
77
|
+
else:
|
|
78
|
+
# No filters, show services (same as trends)
|
|
79
|
+
group_by_key = 'SERVICE'
|
|
80
|
+
|
|
81
|
+
# Build final filter
|
|
82
|
+
if len(filters) > 1:
|
|
83
|
+
final_filter = {"And": filters}
|
|
84
|
+
else:
|
|
85
|
+
final_filter = filters[0]
|
|
86
|
+
|
|
87
|
+
response = ce_client.get_cost_and_usage(
|
|
88
|
+
TimePeriod={
|
|
89
|
+
'Start': time_start.strftime('%Y-%m-%d'),
|
|
90
|
+
'End': time_end.strftime('%Y-%m-%d')
|
|
91
|
+
},
|
|
92
|
+
Granularity=granularity,
|
|
93
|
+
Filter=final_filter,
|
|
94
|
+
Metrics=['NetAmortizedCost'],
|
|
95
|
+
GroupBy=[
|
|
96
|
+
{'Type': 'DIMENSION', 'Key': group_by_key}
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
costs = defaultdict(float)
|
|
101
|
+
for result in response['ResultsByTime']:
|
|
102
|
+
for group in result['Groups']:
|
|
103
|
+
dimension_value = group['Keys'][0]
|
|
104
|
+
cost = float(group['Metrics']['NetAmortizedCost']['Amount'])
|
|
105
|
+
costs[dimension_value] += cost
|
|
106
|
+
|
|
107
|
+
return costs, group_by_key
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def compare_periods(prev_costs, curr_costs):
|
|
111
|
+
"""
|
|
112
|
+
Compare two periods and find increases/decreases.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
list of dicts with dimension, prev_cost, curr_cost, change, pct_change
|
|
116
|
+
"""
|
|
117
|
+
changes = []
|
|
118
|
+
|
|
119
|
+
# Get all dimensions from both periods
|
|
120
|
+
all_dimensions = set(prev_costs.keys()) | set(curr_costs.keys())
|
|
121
|
+
|
|
122
|
+
for dimension in all_dimensions:
|
|
123
|
+
prev_cost = prev_costs.get(dimension, 0)
|
|
124
|
+
curr_cost = curr_costs.get(dimension, 0)
|
|
125
|
+
|
|
126
|
+
change = curr_cost - prev_cost
|
|
127
|
+
pct_change = (change / prev_cost * 100) if prev_cost > 0 else (100 if curr_cost > 0 else 0)
|
|
128
|
+
|
|
129
|
+
# Only include if change is significant (>$10 and >5%)
|
|
130
|
+
if abs(change) > 10 and abs(pct_change) > 5:
|
|
131
|
+
changes.append({
|
|
132
|
+
'dimension': dimension,
|
|
133
|
+
'prev_cost': prev_cost,
|
|
134
|
+
'curr_cost': curr_cost,
|
|
135
|
+
'change': change,
|
|
136
|
+
'pct_change': pct_change
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
return changes
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def analyze_drill_down(ce_client, accounts, num_weeks=4, service_filter=None,
|
|
143
|
+
account_filter=None, usage_type_filter=None):
|
|
144
|
+
"""
|
|
145
|
+
Analyze cost trends with drill-down filters.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
ce_client: boto3 Cost Explorer client
|
|
149
|
+
accounts: List of account IDs
|
|
150
|
+
num_weeks: Number of weeks to analyze (default: 4)
|
|
151
|
+
service_filter: Optional service name to filter
|
|
152
|
+
account_filter: Optional account ID to filter
|
|
153
|
+
usage_type_filter: Optional usage type to filter
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
dict with weekly comparisons and metadata
|
|
157
|
+
"""
|
|
158
|
+
today = datetime.now()
|
|
159
|
+
|
|
160
|
+
# Calculate week boundaries (Monday to Sunday)
|
|
161
|
+
days_since_sunday = (today.weekday() + 1) % 7
|
|
162
|
+
most_recent_sunday = today - timedelta(days=days_since_sunday)
|
|
163
|
+
|
|
164
|
+
weeks = []
|
|
165
|
+
for i in range(num_weeks):
|
|
166
|
+
week_end = most_recent_sunday - timedelta(weeks=i)
|
|
167
|
+
week_start = week_end - timedelta(days=7)
|
|
168
|
+
weeks.append({
|
|
169
|
+
'start': week_start,
|
|
170
|
+
'end': week_end,
|
|
171
|
+
'label': f"Week of {week_start.strftime('%b %d')}"
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
# Reverse so oldest is first
|
|
175
|
+
weeks.reverse()
|
|
176
|
+
|
|
177
|
+
# Get costs for each week
|
|
178
|
+
weekly_costs = []
|
|
179
|
+
group_by_key = None
|
|
180
|
+
for week in weeks:
|
|
181
|
+
costs, group_by_key = get_filtered_costs(
|
|
182
|
+
ce_client, accounts, week['start'], week['end'],
|
|
183
|
+
service_filter=service_filter,
|
|
184
|
+
account_filter=account_filter,
|
|
185
|
+
usage_type_filter=usage_type_filter
|
|
186
|
+
)
|
|
187
|
+
weekly_costs.append({
|
|
188
|
+
'week': week,
|
|
189
|
+
'costs': costs
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
# Compare consecutive weeks
|
|
193
|
+
comparisons = []
|
|
194
|
+
for i in range(1, len(weekly_costs)):
|
|
195
|
+
prev = weekly_costs[i-1]
|
|
196
|
+
curr = weekly_costs[i]
|
|
197
|
+
|
|
198
|
+
changes = compare_periods(prev['costs'], curr['costs'])
|
|
199
|
+
|
|
200
|
+
# Sort by absolute change
|
|
201
|
+
changes.sort(key=lambda x: abs(x['change']), reverse=True)
|
|
202
|
+
increases = [c for c in changes if c['change'] > 0][:10]
|
|
203
|
+
decreases = [c for c in changes if c['change'] < 0][:10]
|
|
204
|
+
|
|
205
|
+
comparisons.append({
|
|
206
|
+
'prev_week': prev['week'],
|
|
207
|
+
'curr_week': curr['week'],
|
|
208
|
+
'increases': increases,
|
|
209
|
+
'decreases': decreases,
|
|
210
|
+
'total_increase': sum(c['change'] for c in increases),
|
|
211
|
+
'total_decrease': sum(c['change'] for c in decreases)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
# Reverse so most recent is first
|
|
215
|
+
comparisons.reverse()
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
'weeks': weeks,
|
|
219
|
+
'comparisons': comparisons,
|
|
220
|
+
'group_by': group_by_key,
|
|
221
|
+
'filters': {
|
|
222
|
+
'service': service_filter,
|
|
223
|
+
'account': account_filter,
|
|
224
|
+
'usage_type': usage_type_filter
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def format_drill_down_markdown(drill_data):
|
|
230
|
+
"""
|
|
231
|
+
Format drill-down data as markdown.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
str: Markdown formatted report
|
|
235
|
+
"""
|
|
236
|
+
lines = []
|
|
237
|
+
lines.append("# AWS Cost Drill-Down Report")
|
|
238
|
+
lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
239
|
+
lines.append("")
|
|
240
|
+
|
|
241
|
+
# Show active filters
|
|
242
|
+
filters = drill_data['filters']
|
|
243
|
+
lines.append("## Filters Applied")
|
|
244
|
+
lines.append("")
|
|
245
|
+
if filters['service']:
|
|
246
|
+
lines.append(f"- **Service:** {filters['service']}")
|
|
247
|
+
if filters['account']:
|
|
248
|
+
lines.append(f"- **Account:** {filters['account']}")
|
|
249
|
+
if filters['usage_type']:
|
|
250
|
+
lines.append(f"- **Usage Type:** {filters['usage_type']}")
|
|
251
|
+
if not any(filters.values()):
|
|
252
|
+
lines.append("- No filters (showing all services)")
|
|
253
|
+
lines.append("")
|
|
254
|
+
|
|
255
|
+
# Show what dimension we're grouping by
|
|
256
|
+
group_by = drill_data['group_by']
|
|
257
|
+
dimension_label = {
|
|
258
|
+
'SERVICE': 'Service',
|
|
259
|
+
'LINKED_ACCOUNT': 'Account',
|
|
260
|
+
'USAGE_TYPE': 'Usage Type',
|
|
261
|
+
'REGION': 'Region'
|
|
262
|
+
}.get(group_by, group_by)
|
|
263
|
+
|
|
264
|
+
lines.append(f"## Grouped By: {dimension_label}")
|
|
265
|
+
lines.append("")
|
|
266
|
+
lines.append("---")
|
|
267
|
+
lines.append("")
|
|
268
|
+
|
|
269
|
+
for comparison in drill_data['comparisons']:
|
|
270
|
+
prev_week = comparison['prev_week']
|
|
271
|
+
curr_week = comparison['curr_week']
|
|
272
|
+
|
|
273
|
+
lines.append(f"## {prev_week['label']} → {curr_week['label']}")
|
|
274
|
+
lines.append("")
|
|
275
|
+
|
|
276
|
+
# Top increases
|
|
277
|
+
if comparison['increases']:
|
|
278
|
+
lines.append("### 🔴 Top 10 Increases")
|
|
279
|
+
lines.append("")
|
|
280
|
+
lines.append(f"| {dimension_label} | Previous | Current | Change | % |")
|
|
281
|
+
lines.append("|---------|----------|---------|--------|---|")
|
|
282
|
+
|
|
283
|
+
for item in comparison['increases']:
|
|
284
|
+
dimension = item['dimension'][:60]
|
|
285
|
+
prev = f"${item['prev_cost']:,.2f}"
|
|
286
|
+
curr = f"${item['curr_cost']:,.2f}"
|
|
287
|
+
change = f"${item['change']:,.2f}"
|
|
288
|
+
pct = f"{item['pct_change']:+.1f}%"
|
|
289
|
+
|
|
290
|
+
lines.append(f"| {dimension} | {prev} | {curr} | {change} | {pct} |")
|
|
291
|
+
|
|
292
|
+
# Add total row
|
|
293
|
+
total_increase = comparison.get('total_increase', 0)
|
|
294
|
+
lines.append(f"| **TOTAL** | | | **${total_increase:,.2f}** | |")
|
|
295
|
+
|
|
296
|
+
lines.append("")
|
|
297
|
+
|
|
298
|
+
# Top decreases
|
|
299
|
+
if comparison['decreases']:
|
|
300
|
+
lines.append("### 🟢 Top 10 Decreases")
|
|
301
|
+
lines.append("")
|
|
302
|
+
lines.append(f"| {dimension_label} | Previous | Current | Change | % |")
|
|
303
|
+
lines.append("|---------|----------|---------|--------|---|")
|
|
304
|
+
|
|
305
|
+
for item in comparison['decreases']:
|
|
306
|
+
dimension = item['dimension'][:60]
|
|
307
|
+
prev = f"${item['prev_cost']:,.2f}"
|
|
308
|
+
curr = f"${item['curr_cost']:,.2f}"
|
|
309
|
+
change = f"${item['change']:,.2f}"
|
|
310
|
+
pct = f"{item['pct_change']:+.1f}%"
|
|
311
|
+
|
|
312
|
+
lines.append(f"| {dimension} | {prev} | {curr} | {change} | {pct} |")
|
|
313
|
+
|
|
314
|
+
# Add total row
|
|
315
|
+
total_decrease = comparison.get('total_decrease', 0)
|
|
316
|
+
lines.append(f"| **TOTAL** | | | **${total_decrease:,.2f}** | |")
|
|
317
|
+
|
|
318
|
+
lines.append("")
|
|
319
|
+
|
|
320
|
+
lines.append("---")
|
|
321
|
+
lines.append("")
|
|
322
|
+
|
|
323
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Executor that routes to either API or local execution.
|
|
3
|
+
"""
|
|
4
|
+
import boto3
|
|
5
|
+
import click
|
|
6
|
+
from cost_calculator.api_client import is_api_configured, call_lambda_api
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_credentials_dict(config):
|
|
10
|
+
"""
|
|
11
|
+
Extract credentials from config in format needed for API.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
dict with access_key, secret_key, session_token, or None if profile is 'dummy'
|
|
15
|
+
"""
|
|
16
|
+
if 'aws_profile' in config:
|
|
17
|
+
# Skip credential loading for dummy profile (API-only mode)
|
|
18
|
+
if config['aws_profile'] == 'dummy':
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
# Get temporary credentials from SSO session
|
|
22
|
+
try:
|
|
23
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
24
|
+
credentials = session.get_credentials()
|
|
25
|
+
frozen_creds = credentials.get_frozen_credentials()
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
'access_key': frozen_creds.access_key,
|
|
29
|
+
'secret_key': frozen_creds.secret_key,
|
|
30
|
+
'session_token': frozen_creds.token
|
|
31
|
+
}
|
|
32
|
+
except Exception:
|
|
33
|
+
# If profile not found, return None (API will handle)
|
|
34
|
+
return None
|
|
35
|
+
else:
|
|
36
|
+
# Use static credentials
|
|
37
|
+
creds = config.get('credentials', {})
|
|
38
|
+
if not creds:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
result = {
|
|
42
|
+
'access_key': creds['aws_access_key_id'],
|
|
43
|
+
'secret_key': creds['aws_secret_access_key']
|
|
44
|
+
}
|
|
45
|
+
if 'aws_session_token' in creds:
|
|
46
|
+
result['session_token'] = creds['aws_session_token']
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def execute_trends(config, weeks):
|
|
51
|
+
"""
|
|
52
|
+
Execute trends analysis via API or locally.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict: trends data
|
|
56
|
+
"""
|
|
57
|
+
accounts = config['accounts']
|
|
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)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def execute_monthly(config, months):
|
|
88
|
+
"""
|
|
89
|
+
Execute monthly analysis via API or locally.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
dict: monthly data
|
|
93
|
+
"""
|
|
94
|
+
accounts = config['accounts']
|
|
95
|
+
|
|
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)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None):
|
|
125
|
+
"""
|
|
126
|
+
Execute drill-down analysis via API or locally.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
dict: drill data
|
|
130
|
+
"""
|
|
131
|
+
accounts = config['accounts']
|
|
132
|
+
|
|
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
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
|
|
174
|
+
"""
|
|
175
|
+
Execute pandas-based analysis via API.
|
|
176
|
+
Note: This only works via API (requires pandas layer).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
dict: analysis results
|
|
180
|
+
"""
|
|
181
|
+
accounts = config['accounts']
|
|
182
|
+
|
|
183
|
+
if not is_api_configured():
|
|
184
|
+
raise click.ClickException(
|
|
185
|
+
"Analyze command requires API configuration.\n"
|
|
186
|
+
"Set COST_API_SECRET environment variable."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
credentials = get_credentials_dict(config)
|
|
190
|
+
kwargs = {'weeks': weeks, 'type': analysis_type}
|
|
191
|
+
|
|
192
|
+
if pattern:
|
|
193
|
+
kwargs['pattern'] = pattern
|
|
194
|
+
if min_cost:
|
|
195
|
+
kwargs['min_cost'] = min_cost
|
|
196
|
+
|
|
197
|
+
return call_lambda_api('analyze', credentials, accounts, **kwargs)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def execute_profile_operation(operation, profile_name=None, accounts=None, description=None):
|
|
201
|
+
"""
|
|
202
|
+
Execute profile CRUD operations via API.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
dict: operation result
|
|
206
|
+
"""
|
|
207
|
+
if not is_api_configured():
|
|
208
|
+
raise click.ClickException(
|
|
209
|
+
"Profile commands require API configuration.\n"
|
|
210
|
+
"Set COST_API_SECRET environment variable."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Profile operations don't need AWS credentials, just API secret
|
|
214
|
+
import os
|
|
215
|
+
import requests
|
|
216
|
+
import json
|
|
217
|
+
|
|
218
|
+
api_secret = os.environ.get('COST_API_SECRET', '')
|
|
219
|
+
|
|
220
|
+
# Use profiles endpoint (hardcoded URL)
|
|
221
|
+
url = 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
|
|
222
|
+
|
|
223
|
+
payload = {'operation': operation}
|
|
224
|
+
if profile_name:
|
|
225
|
+
payload['profile_name'] = profile_name
|
|
226
|
+
if accounts:
|
|
227
|
+
payload['accounts'] = accounts
|
|
228
|
+
if description:
|
|
229
|
+
payload['description'] = description
|
|
230
|
+
|
|
231
|
+
headers = {
|
|
232
|
+
'X-API-Secret': api_secret,
|
|
233
|
+
'Content-Type': 'application/json'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
|
237
|
+
|
|
238
|
+
if response.status_code != 200:
|
|
239
|
+
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
240
|
+
|
|
241
|
+
return response.json()
|