aws-cost-calculator-cli 1.6.0__py3-none-any.whl → 1.6.3__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,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,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)