aws-cost-calculator-cli 1.0.2__py3-none-any.whl → 1.8.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.

Potentially problematic release.


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

@@ -0,0 +1,242 @@
1
+ """
2
+ Monthly cost trend analysis module.
3
+ Analyzes month-over-month cost changes at the service level.
4
+ """
5
+ from datetime import datetime, timedelta
6
+ from collections import defaultdict
7
+
8
+
9
+ def get_month_costs(ce_client, accounts, month_start, month_end):
10
+ """
11
+ Get costs for a specific month grouped by service.
12
+
13
+ Args:
14
+ ce_client: boto3 Cost Explorer client
15
+ accounts: List of account IDs
16
+ month_start: datetime for start of month
17
+ month_end: datetime for end of month
18
+
19
+ Returns:
20
+ dict: {service: total_cost}
21
+ """
22
+ response = ce_client.get_cost_and_usage(
23
+ TimePeriod={
24
+ 'Start': month_start.strftime('%Y-%m-%d'),
25
+ 'End': month_end.strftime('%Y-%m-%d')
26
+ },
27
+ Granularity='MONTHLY',
28
+ Filter={
29
+ "Dimensions": {
30
+ "Key": "LINKED_ACCOUNT",
31
+ "Values": accounts
32
+ }
33
+ },
34
+ Metrics=['NetAmortizedCost'],
35
+ GroupBy=[
36
+ {'Type': 'DIMENSION', 'Key': 'SERVICE'}
37
+ ]
38
+ )
39
+
40
+ costs = defaultdict(float)
41
+ for result in response['ResultsByTime']:
42
+ for group in result['Groups']:
43
+ service = group['Keys'][0]
44
+ cost = float(group['Metrics']['NetAmortizedCost']['Amount'])
45
+ costs[service] += cost
46
+
47
+ return costs
48
+
49
+
50
+ def compare_months(prev_month_costs, curr_month_costs):
51
+ """
52
+ Compare two months and find increases/decreases at service level.
53
+
54
+ Returns:
55
+ list of dicts with service, prev_cost, curr_cost, change, pct_change
56
+ """
57
+ changes = []
58
+
59
+ # Get all services from both months
60
+ all_services = set(prev_month_costs.keys()) | set(curr_month_costs.keys())
61
+
62
+ for service in all_services:
63
+ prev_cost = prev_month_costs.get(service, 0)
64
+ curr_cost = curr_month_costs.get(service, 0)
65
+
66
+ change = curr_cost - prev_cost
67
+ pct_change = (change / prev_cost * 100) if prev_cost > 0 else (100 if curr_cost > 0 else 0)
68
+
69
+ # Only include if change is significant (>$50 and >5%)
70
+ if abs(change) > 50 and abs(pct_change) > 5:
71
+ changes.append({
72
+ 'service': service,
73
+ 'prev_cost': prev_cost,
74
+ 'curr_cost': curr_cost,
75
+ 'change': change,
76
+ 'pct_change': pct_change
77
+ })
78
+
79
+ return changes
80
+
81
+
82
+ def analyze_monthly_trends(ce_client, accounts, num_months=6):
83
+ """
84
+ Analyze cost trends over the last N months.
85
+
86
+ Args:
87
+ ce_client: boto3 Cost Explorer client
88
+ accounts: List of account IDs
89
+ num_months: Number of months to analyze (default: 6)
90
+
91
+ Returns:
92
+ dict with monthly comparisons
93
+ """
94
+ today = datetime.now()
95
+
96
+ # Calculate month boundaries
97
+ months = []
98
+ for i in range(num_months):
99
+ # Go back i months from today
100
+ if today.month - i <= 0:
101
+ year = today.year - 1
102
+ month = 12 + (today.month - i)
103
+ else:
104
+ year = today.year
105
+ month = today.month - i
106
+
107
+ # First day of month
108
+ month_start = datetime(year, month, 1)
109
+
110
+ # First day of next month
111
+ if month == 12:
112
+ month_end = datetime(year + 1, 1, 1)
113
+ else:
114
+ month_end = datetime(year, month + 1, 1)
115
+
116
+ months.append({
117
+ 'start': month_start,
118
+ 'end': month_end,
119
+ 'label': month_start.strftime('%B %Y')
120
+ })
121
+
122
+ # Reverse so oldest is first
123
+ months.reverse()
124
+
125
+ # Get costs for each month
126
+ monthly_costs = []
127
+ for month in months:
128
+ costs = get_month_costs(ce_client, accounts, month['start'], month['end'])
129
+ monthly_costs.append({
130
+ 'month': month,
131
+ 'costs': costs
132
+ })
133
+
134
+ # Compare consecutive months
135
+ comparisons = []
136
+ for i in range(1, len(monthly_costs)):
137
+ prev = monthly_costs[i-1]
138
+ curr = monthly_costs[i]
139
+
140
+ changes = compare_months(prev['costs'], curr['costs'])
141
+
142
+ # Sort by absolute change
143
+ changes.sort(key=lambda x: abs(x['change']), reverse=True)
144
+ increases = [c for c in changes if c['change'] > 0][:10]
145
+ decreases = [c for c in changes if c['change'] < 0][:10]
146
+
147
+ comparisons.append({
148
+ 'prev_month': prev['month'],
149
+ 'curr_month': curr['month'],
150
+ 'increases': increases,
151
+ 'decreases': decreases,
152
+ 'total_increase': sum(c['change'] for c in increases),
153
+ 'total_decrease': sum(c['change'] for c in decreases)
154
+ })
155
+
156
+ # Reverse so most recent is first
157
+ comparisons.reverse()
158
+
159
+ return {
160
+ 'months': months,
161
+ 'comparisons': comparisons
162
+ }
163
+
164
+
165
+ def format_monthly_markdown(monthly_data):
166
+ """
167
+ Format monthly trends data as markdown.
168
+
169
+ Returns:
170
+ str: Markdown formatted report
171
+ """
172
+ lines = []
173
+ lines.append("# AWS Monthly Cost Trends Report (Service Level)")
174
+ lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
175
+ lines.append("")
176
+ lines.append("## Methodology")
177
+ lines.append("")
178
+ lines.append("This report shows month-over-month cost changes at the service level:")
179
+ lines.append("")
180
+ lines.append("- Compares consecutive calendar months")
181
+ lines.append("- Shows total cost per service (aggregated across all usage types)")
182
+ lines.append("- Filters out noise (>$50 and >5% change)")
183
+ lines.append("- Most recent comparisons first")
184
+ lines.append("")
185
+ lines.append("---")
186
+ lines.append("")
187
+
188
+ for comparison in monthly_data['comparisons']:
189
+ prev_month = comparison['prev_month']
190
+ curr_month = comparison['curr_month']
191
+
192
+ lines.append(f"## {prev_month['label']} → {curr_month['label']}")
193
+ lines.append("")
194
+
195
+ # Top increases
196
+ if comparison['increases']:
197
+ lines.append("### 🔴 Top 10 Increases")
198
+ lines.append("")
199
+ lines.append("| Service | Previous Month | Current Month | Change | % |")
200
+ lines.append("|---------|----------------|---------------|--------|---|")
201
+
202
+ for item in comparison['increases']:
203
+ service = item['service'][:60]
204
+ prev = f"${item['prev_cost']:,.2f}"
205
+ curr = f"${item['curr_cost']:,.2f}"
206
+ change = f"${item['change']:,.2f}"
207
+ pct = f"{item['pct_change']:+.1f}%"
208
+
209
+ lines.append(f"| {service} | {prev} | {curr} | {change} | {pct} |")
210
+
211
+ # Add total row
212
+ total_increase = comparison.get('total_increase', 0)
213
+ lines.append(f"| **TOTAL** | | | **${total_increase:,.2f}** | |")
214
+
215
+ lines.append("")
216
+
217
+ # Top decreases
218
+ if comparison['decreases']:
219
+ lines.append("### 🟢 Top 10 Decreases")
220
+ lines.append("")
221
+ lines.append("| Service | Previous Month | Current Month | Change | % |")
222
+ lines.append("|---------|----------------|---------------|--------|---|")
223
+
224
+ for item in comparison['decreases']:
225
+ service = item['service'][:60]
226
+ prev = f"${item['prev_cost']:,.2f}"
227
+ curr = f"${item['curr_cost']:,.2f}"
228
+ change = f"${item['change']:,.2f}"
229
+ pct = f"{item['pct_change']:+.1f}%"
230
+
231
+ lines.append(f"| {service} | {prev} | {curr} | {change} | {pct} |")
232
+
233
+ # Add total row
234
+ total_decrease = comparison.get('total_decrease', 0)
235
+ lines.append(f"| **TOTAL** | | | **${total_decrease:,.2f}** | |")
236
+
237
+ lines.append("")
238
+
239
+ lines.append("---")
240
+ lines.append("")
241
+
242
+ return "\n".join(lines)
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cost trends analysis module
4
+ """
5
+
6
+ import boto3
7
+ from datetime import datetime, timedelta
8
+ from collections import defaultdict
9
+
10
+
11
+ def get_week_costs(ce_client, accounts, week_start, week_end):
12
+ """
13
+ Get costs for a specific week, grouped by service and usage type.
14
+
15
+ Args:
16
+ ce_client: boto3 Cost Explorer client
17
+ accounts: List of account IDs
18
+ week_start: Start date (datetime)
19
+ week_end: End date (datetime)
20
+
21
+ Returns:
22
+ dict: {service: {usage_type: cost}}
23
+ """
24
+ cost_filter = {
25
+ "And": [
26
+ {
27
+ "Dimensions": {
28
+ "Key": "LINKED_ACCOUNT",
29
+ "Values": accounts
30
+ }
31
+ },
32
+ {
33
+ "Dimensions": {
34
+ "Key": "BILLING_ENTITY",
35
+ "Values": ["AWS"]
36
+ }
37
+ },
38
+ {
39
+ "Not": {
40
+ "Dimensions": {
41
+ "Key": "RECORD_TYPE",
42
+ "Values": ["Tax", "Support"]
43
+ }
44
+ }
45
+ }
46
+ ]
47
+ }
48
+
49
+ response = ce_client.get_cost_and_usage(
50
+ TimePeriod={
51
+ 'Start': week_start.strftime('%Y-%m-%d'),
52
+ 'End': week_end.strftime('%Y-%m-%d')
53
+ },
54
+ Granularity='DAILY',
55
+ Metrics=['NetAmortizedCost'],
56
+ GroupBy=[
57
+ {'Type': 'DIMENSION', 'Key': 'SERVICE'},
58
+ {'Type': 'DIMENSION', 'Key': 'USAGE_TYPE'}
59
+ ],
60
+ Filter=cost_filter
61
+ )
62
+
63
+ # Aggregate by service and usage type
64
+ costs = defaultdict(lambda: defaultdict(float))
65
+
66
+ for day in response['ResultsByTime']:
67
+ for group in day.get('Groups', []):
68
+ service = group['Keys'][0]
69
+ usage_type = group['Keys'][1]
70
+ cost = float(group['Metrics']['NetAmortizedCost']['Amount'])
71
+ costs[service][usage_type] += cost
72
+
73
+ return costs
74
+
75
+
76
+ def compare_weeks(prev_week_costs, curr_week_costs):
77
+ """
78
+ Compare two weeks and find increases/decreases at service level.
79
+
80
+ Returns:
81
+ list of dicts with service, prev_cost, curr_cost, change, pct_change
82
+ """
83
+ changes = []
84
+
85
+ # Get all services from both weeks
86
+ all_services = set(prev_week_costs.keys()) | set(curr_week_costs.keys())
87
+
88
+ for service in all_services:
89
+ prev_service = prev_week_costs.get(service, {})
90
+ curr_service = curr_week_costs.get(service, {})
91
+
92
+ # Sum all usage types for this service
93
+ prev_cost = sum(prev_service.values())
94
+ curr_cost = sum(curr_service.values())
95
+
96
+ change = curr_cost - prev_cost
97
+ pct_change = (change / prev_cost * 100) if prev_cost > 0 else (100 if curr_cost > 0 else 0)
98
+
99
+ # Only include if change is significant (>$10 and >5%)
100
+ if abs(change) > 10 and abs(pct_change) > 5:
101
+ changes.append({
102
+ 'service': service,
103
+ 'prev_cost': prev_cost,
104
+ 'curr_cost': curr_cost,
105
+ 'change': change,
106
+ 'pct_change': pct_change
107
+ })
108
+
109
+ return changes
110
+
111
+
112
+ def analyze_trends(ce_client, accounts, num_weeks=3):
113
+ """
114
+ Analyze cost trends over the last N weeks.
115
+
116
+ Args:
117
+ ce_client: boto3 Cost Explorer client
118
+ accounts: List of account IDs
119
+ num_weeks: Number of weeks to analyze (default: 3)
120
+
121
+ Returns:
122
+ dict with weekly comparisons
123
+ """
124
+ today = datetime.now()
125
+
126
+ # Calculate week boundaries (Monday to Sunday)
127
+ # Go back to most recent Sunday
128
+ days_since_sunday = (today.weekday() + 1) % 7
129
+ most_recent_sunday = today - timedelta(days=days_since_sunday)
130
+
131
+ weeks = []
132
+ for i in range(num_weeks):
133
+ week_end = most_recent_sunday - timedelta(weeks=i)
134
+ week_start = week_end - timedelta(days=7)
135
+ weeks.append({
136
+ 'start': week_start,
137
+ 'end': week_end,
138
+ 'label': f"Week of {week_start.strftime('%b %d')}"
139
+ })
140
+
141
+ # Reverse so oldest is first
142
+ weeks.reverse()
143
+
144
+ # Get costs for each week
145
+ weekly_costs = []
146
+ for week in weeks:
147
+ costs = get_week_costs(ce_client, accounts, week['start'], week['end'])
148
+ weekly_costs.append({
149
+ 'week': week,
150
+ 'costs': costs
151
+ })
152
+
153
+ # Compare consecutive weeks (week-over-week)
154
+ wow_comparisons = []
155
+ for i in range(1, len(weekly_costs)):
156
+ prev = weekly_costs[i-1]
157
+ curr = weekly_costs[i]
158
+
159
+ changes = compare_weeks(prev['costs'], curr['costs'])
160
+
161
+ # Sort by absolute change
162
+ changes.sort(key=lambda x: abs(x['change']), reverse=True)
163
+ increases = [c for c in changes if c['change'] > 0][:10]
164
+ decreases = [c for c in changes if c['change'] < 0][:10]
165
+
166
+ wow_comparisons.append({
167
+ 'prev_week': prev['week'],
168
+ 'curr_week': curr['week'],
169
+ 'increases': increases,
170
+ 'decreases': decreases,
171
+ 'total_increase': sum(c['change'] for c in increases),
172
+ 'total_decrease': sum(c['change'] for c in decreases)
173
+ })
174
+
175
+ # Compare to 30 days ago (T-30)
176
+ t30_comparisons = []
177
+ for i in range(len(weekly_costs)):
178
+ curr = weekly_costs[i]
179
+ # Find week from ~30 days ago (4-5 weeks back)
180
+ baseline_idx = i - 4 if i >= 4 else None
181
+
182
+ if baseline_idx is not None and baseline_idx >= 0:
183
+ baseline = weekly_costs[baseline_idx]
184
+
185
+ changes = compare_weeks(baseline['costs'], curr['costs'])
186
+
187
+ # Sort by absolute change
188
+ changes.sort(key=lambda x: abs(x['change']), reverse=True)
189
+ increases = [c for c in changes if c['change'] > 0][:10]
190
+ decreases = [c for c in changes if c['change'] < 0][:10]
191
+
192
+ t30_comparisons.append({
193
+ 'baseline_week': baseline['week'],
194
+ 'curr_week': curr['week'],
195
+ 'increases': increases,
196
+ 'decreases': decreases,
197
+ 'total_increase': sum(c['change'] for c in increases),
198
+ 'total_decrease': sum(c['change'] for c in decreases)
199
+ })
200
+
201
+ # Reverse so most recent is first
202
+ wow_comparisons.reverse()
203
+ t30_comparisons.reverse()
204
+
205
+ return {
206
+ 'weeks': weeks,
207
+ 'wow_comparisons': wow_comparisons,
208
+ 't30_comparisons': t30_comparisons
209
+ }
210
+
211
+
212
+ def format_trends_markdown(trends_data):
213
+ """
214
+ Format trends data as markdown.
215
+
216
+ Returns:
217
+ str: Markdown formatted report
218
+ """
219
+ lines = []
220
+ lines.append("# AWS Cost Trends Report (Service Level)")
221
+ lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
222
+ lines.append("")
223
+ lines.append("## Methodology")
224
+ lines.append("")
225
+ lines.append("This report shows two types of comparisons:")
226
+ lines.append("")
227
+ lines.append("1. **Week-over-Week (WoW)**: Compares each week to the previous week")
228
+ lines.append(" - Good for catching immediate changes and spikes")
229
+ lines.append(" - Shows short-term volatility")
230
+ lines.append("")
231
+ lines.append("2. **Trailing 30-Day (T-30)**: Compares each week to the same week 4 weeks ago")
232
+ lines.append(" - Filters out weekly noise")
233
+ lines.append(" - Shows sustained trends and real cost changes")
234
+ lines.append("")
235
+ lines.append("---")
236
+ lines.append("")
237
+ lines.append("# Week-over-Week Changes")
238
+ lines.append("")
239
+
240
+ for comparison in trends_data['wow_comparisons']:
241
+ prev_week = comparison['prev_week']
242
+ curr_week = comparison['curr_week']
243
+
244
+ lines.append(f"## {prev_week['label']} → {curr_week['label']}")
245
+ lines.append("")
246
+
247
+ # Top increases
248
+ if comparison['increases']:
249
+ lines.append("### 🔴 Top 10 Increases")
250
+ lines.append("")
251
+ lines.append("| Service | Previous | Current | Change | % |")
252
+ lines.append("|---------|----------|---------|--------|---|")
253
+
254
+ for item in comparison['increases']:
255
+ service = item['service'][:60]
256
+ prev = f"${item['prev_cost']:,.2f}"
257
+ curr = f"${item['curr_cost']:,.2f}"
258
+ change = f"${item['change']:,.2f}"
259
+ pct = f"{item['pct_change']:+.1f}%"
260
+
261
+ lines.append(f"| {service} | {prev} | {curr} | {change} | {pct} |")
262
+
263
+ # Add total row
264
+ total_increase = comparison.get('total_increase', 0)
265
+ lines.append(f"| **TOTAL** | | | **${total_increase:,.2f}** | |")
266
+
267
+ lines.append("")
268
+
269
+ # Top decreases
270
+ if comparison['decreases']:
271
+ lines.append("### 🟢 Top 10 Decreases")
272
+ lines.append("")
273
+ lines.append("| Service | Previous | Current | Change | % |")
274
+ lines.append("|---------|----------|---------|--------|---|")
275
+
276
+ for item in comparison['decreases']:
277
+ service = item['service'][:60]
278
+ prev = f"${item['prev_cost']:,.2f}"
279
+ curr = f"${item['curr_cost']:,.2f}"
280
+ change = f"${item['change']:,.2f}"
281
+ pct = f"{item['pct_change']:+.1f}%"
282
+
283
+ lines.append(f"| {service} | {prev} | {curr} | {change} | {pct} |")
284
+
285
+ # Add total row
286
+ total_decrease = comparison.get('total_decrease', 0)
287
+ lines.append(f"| **TOTAL** | | | **${total_decrease:,.2f}** | |")
288
+
289
+ lines.append("")
290
+
291
+ lines.append("---")
292
+ lines.append("")
293
+
294
+ # Add T-30 comparisons section
295
+ lines.append("")
296
+ lines.append("# Trailing 30-Day Comparisons (T-30)")
297
+ lines.append("")
298
+
299
+ for comparison in trends_data['t30_comparisons']:
300
+ baseline_week = comparison['baseline_week']
301
+ curr_week = comparison['curr_week']
302
+
303
+ lines.append(f"## {curr_week['label']} vs {baseline_week['label']} (30 days ago)")
304
+ lines.append("")
305
+
306
+ # Top increases
307
+ if comparison['increases']:
308
+ lines.append("### 🔴 Top 10 Increases (vs 30 days ago)")
309
+ lines.append("")
310
+ lines.append("| Service | 30 Days Ago | Current | Change | % |")
311
+ lines.append("|---------|-------------|---------|--------|---|")
312
+
313
+ for item in comparison['increases']:
314
+ service = item['service'][:60]
315
+ prev = f"${item['prev_cost']:,.2f}"
316
+ curr = f"${item['curr_cost']:,.2f}"
317
+ change = f"${item['change']:,.2f}"
318
+ pct = f"{item['pct_change']:+.1f}%"
319
+
320
+ lines.append(f"| {service} | {prev} | {curr} | {change} | {pct} |")
321
+
322
+ # Add total row
323
+ total_increase = comparison.get('total_increase', 0)
324
+ lines.append(f"| **TOTAL** | | | **${total_increase:,.2f}** | |")
325
+
326
+ lines.append("")
327
+
328
+ # Top decreases
329
+ if comparison['decreases']:
330
+ lines.append("### 🟢 Top 10 Decreases (vs 30 days ago)")
331
+ lines.append("")
332
+ lines.append("| Service | 30 Days Ago | Current | Change | % |")
333
+ lines.append("|---------|-------------|---------|--------|---|")
334
+
335
+ for item in comparison['decreases']:
336
+ service = item['service'][:60]
337
+ prev = f"${item['prev_cost']:,.2f}"
338
+ curr = f"${item['curr_cost']:,.2f}"
339
+ change = f"${item['change']:,.2f}"
340
+ pct = f"{item['pct_change']:+.1f}%"
341
+
342
+ lines.append(f"| {service} | {prev} | {curr} | {change} | {pct} |")
343
+
344
+ # Add total row
345
+ total_decrease = comparison.get('total_decrease', 0)
346
+ lines.append(f"| **TOTAL** | | | **${total_decrease:,.2f}** | |")
347
+
348
+ lines.append("")
349
+
350
+ lines.append("---")
351
+ lines.append("")
352
+
353
+ return "\n".join(lines)