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.
- aws_cost_calculator_cli-1.8.2.dist-info/METADATA +437 -0
- aws_cost_calculator_cli-1.8.2.dist-info/RECORD +15 -0
- cost_calculator/api_client.py +85 -0
- cost_calculator/cli.py +906 -34
- cost_calculator/cur.py +244 -0
- cost_calculator/drill.py +323 -0
- cost_calculator/executor.py +291 -0
- cost_calculator/forensics.py +321 -0
- cost_calculator/monthly.py +242 -0
- cost_calculator/trends.py +353 -0
- aws_cost_calculator_cli-1.0.2.dist-info/METADATA +0 -164
- aws_cost_calculator_cli-1.0.2.dist-info/RECORD +0 -8
- {aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.8.2.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.8.2.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.8.2.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.8.2.dist-info}/top_level.txt +0 -0
|
@@ -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)
|