aws-cost-calculator-cli 1.6.3__py3-none-any.whl → 2.0.0__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.
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-2.0.0.dist-info}/METADATA +13 -1
- aws_cost_calculator_cli-2.0.0.dist-info/RECORD +15 -0
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-2.0.0.dist-info}/WHEEL +1 -1
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-2.0.0.dist-info}/top_level.txt +0 -1
- cost_calculator/api_client.py +2 -1
- cost_calculator/cli.py +1020 -109
- cost_calculator/cur.py +244 -0
- cost_calculator/executor.py +105 -95
- cost_calculator/forensics.py +323 -0
- aws_cost_calculator_cli-1.6.3.dist-info/RECORD +0 -25
- backend/__init__.py +0 -1
- backend/algorithms/__init__.py +0 -1
- backend/algorithms/analyze.py +0 -272
- backend/algorithms/drill.py +0 -323
- backend/algorithms/monthly.py +0 -242
- backend/algorithms/trends.py +0 -353
- backend/handlers/__init__.py +0 -1
- backend/handlers/analyze.py +0 -112
- backend/handlers/drill.py +0 -117
- backend/handlers/monthly.py +0 -106
- backend/handlers/profiles.py +0 -148
- backend/handlers/trends.py +0 -106
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-2.0.0.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.6.3.dist-info → aws_cost_calculator_cli-2.0.0.dist-info}/licenses/LICENSE +0 -0
backend/algorithms/drill.py
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
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)
|
backend/algorithms/monthly.py
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
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)
|