aws-cost-calculator-cli 1.5.1__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.
- {aws_cost_calculator_cli-1.5.1.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/METADATA +158 -24
- aws_cost_calculator_cli-1.6.3.dist-info/RECORD +25 -0
- {aws_cost_calculator_cli-1.5.1.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/WHEEL +1 -1
- {aws_cost_calculator_cli-1.5.1.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/top_level.txt +1 -0
- backend/__init__.py +1 -0
- backend/algorithms/__init__.py +1 -0
- backend/algorithms/analyze.py +272 -0
- backend/algorithms/drill.py +323 -0
- backend/algorithms/monthly.py +242 -0
- backend/algorithms/trends.py +353 -0
- backend/handlers/__init__.py +1 -0
- backend/handlers/analyze.py +112 -0
- backend/handlers/drill.py +117 -0
- backend/handlers/monthly.py +106 -0
- backend/handlers/profiles.py +148 -0
- backend/handlers/trends.py +106 -0
- cost_calculator/cli.py +254 -41
- cost_calculator/executor.py +22 -11
- aws_cost_calculator_cli-1.5.1.dist-info/RECORD +0 -13
- {aws_cost_calculator_cli-1.5.1.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.5.1.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/licenses/LICENSE +0 -0
{aws_cost_calculator_cli-1.5.1.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/METADATA
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.3
|
|
4
4
|
Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
|
|
5
|
-
Home-page: https://github.com/
|
|
5
|
+
Home-page: https://github.com/trilogy-group/aws-cost-calculator
|
|
6
6
|
Author: Cost Optimization Team
|
|
7
7
|
Author-email:
|
|
8
|
-
Project-URL: Documentation, https://github.com/
|
|
8
|
+
Project-URL: Documentation, https://github.com/trilogy-group/aws-cost-calculator/blob/main/README.md
|
|
9
9
|
Keywords: aws cost calculator billing optimization cloud
|
|
10
10
|
Classifier: Development Status :: 4 - Beta
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
@@ -43,66 +43,200 @@ A CLI tool to quickly calculate AWS costs across multiple accounts.
|
|
|
43
43
|
## Installation
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
pip install aws-cost-calculator-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or upgrade to the latest version:
|
|
50
|
+
```bash
|
|
51
|
+
pip install --upgrade aws-cost-calculator-cli
|
|
48
52
|
```
|
|
49
53
|
|
|
50
54
|
## Quick Start
|
|
51
55
|
|
|
52
|
-
###
|
|
56
|
+
### Authentication Methods
|
|
57
|
+
|
|
58
|
+
The CLI supports three authentication methods:
|
|
53
59
|
|
|
60
|
+
#### 1. SSO (Recommended)
|
|
54
61
|
```bash
|
|
55
|
-
|
|
62
|
+
# The CLI will automatically trigger SSO login if needed
|
|
63
|
+
cc calculate --profile myprofile --sso my_sso_profile
|
|
56
64
|
```
|
|
57
65
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
#### 2. Static Credentials
|
|
67
|
+
```bash
|
|
68
|
+
cc calculate --profile myprofile \
|
|
69
|
+
--access-key-id ASIA... \
|
|
70
|
+
--secret-access-key ... \
|
|
71
|
+
--session-token ...
|
|
72
|
+
```
|
|
61
73
|
|
|
74
|
+
#### 3. Environment Variables
|
|
62
75
|
```bash
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
# For SSO
|
|
77
|
+
export AWS_PROFILE=my_sso_profile
|
|
78
|
+
cc calculate --profile myprofile
|
|
79
|
+
|
|
80
|
+
# For static credentials
|
|
81
|
+
export AWS_ACCESS_KEY_ID=ASIA...
|
|
82
|
+
export AWS_SECRET_ACCESS_KEY=...
|
|
83
|
+
export AWS_SESSION_TOKEN=...
|
|
84
|
+
cc calculate --profile myprofile
|
|
66
85
|
```
|
|
67
86
|
|
|
68
|
-
###
|
|
87
|
+
### Basic Usage
|
|
69
88
|
|
|
70
89
|
```bash
|
|
71
90
|
# Default: Today minus 2 days, going back 30 days
|
|
72
|
-
cc calculate --profile myprofile
|
|
91
|
+
cc calculate --profile myprofile --sso my_sso_profile
|
|
73
92
|
|
|
74
93
|
# Specific start date
|
|
75
|
-
cc calculate --profile myprofile --start-date 2025-11-04
|
|
94
|
+
cc calculate --profile myprofile --sso my_sso_profile --start-date 2025-11-04
|
|
76
95
|
|
|
77
96
|
# Custom offset and window
|
|
78
|
-
cc calculate --profile myprofile --offset 2 --window 30
|
|
97
|
+
cc calculate --profile myprofile --sso my_sso_profile --offset 2 --window 30
|
|
79
98
|
|
|
80
99
|
# JSON output
|
|
81
|
-
cc calculate --profile myprofile --json-output
|
|
100
|
+
cc calculate --profile myprofile --sso my_sso_profile --json-output
|
|
82
101
|
```
|
|
83
102
|
|
|
84
103
|
### 4. Analyze cost trends
|
|
85
104
|
|
|
105
|
+
All commands support the same authentication options:
|
|
106
|
+
|
|
86
107
|
```bash
|
|
87
|
-
#
|
|
108
|
+
# With SSO
|
|
109
|
+
cc trends --profile myprofile --sso my_sso_profile
|
|
110
|
+
|
|
111
|
+
# With static credentials
|
|
112
|
+
cc trends --profile myprofile --access-key-id ASIA... --secret-access-key ... --session-token ...
|
|
113
|
+
|
|
114
|
+
# With environment variables
|
|
115
|
+
export AWS_PROFILE=my_sso_profile
|
|
88
116
|
cc trends --profile myprofile
|
|
89
117
|
|
|
90
118
|
# Analyze more weeks
|
|
91
|
-
cc trends --profile myprofile --weeks 5
|
|
119
|
+
cc trends --profile myprofile --sso my_sso_profile --weeks 5
|
|
92
120
|
|
|
93
121
|
# Custom output file
|
|
94
|
-
cc trends --profile myprofile --output weekly_trends.md
|
|
122
|
+
cc trends --profile myprofile --sso my_sso_profile --output weekly_trends.md
|
|
95
123
|
|
|
96
124
|
# JSON output
|
|
97
|
-
cc trends --profile myprofile --json-output
|
|
125
|
+
cc trends --profile myprofile --sso my_sso_profile --json-output
|
|
98
126
|
```
|
|
99
127
|
|
|
100
|
-
### 5.
|
|
128
|
+
### 5. Monthly and drill-down analysis
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Monthly trends
|
|
132
|
+
cc monthly --profile myprofile --sso my_sso_profile
|
|
133
|
+
|
|
134
|
+
# Drill down by service
|
|
135
|
+
cc drill --profile myprofile --sso my_sso_profile --service "EC2 - Other"
|
|
136
|
+
|
|
137
|
+
# Drill down by account
|
|
138
|
+
cc drill --profile myprofile --sso my_sso_profile --account 123456789012
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 6. List profiles
|
|
101
142
|
|
|
102
143
|
```bash
|
|
103
144
|
cc list-profiles
|
|
104
145
|
```
|
|
105
146
|
|
|
147
|
+
## Authentication
|
|
148
|
+
|
|
149
|
+
### Overview
|
|
150
|
+
|
|
151
|
+
The CLI supports three authentication methods, all of which work with every command (`calculate`, `trends`, `monthly`, `drill`):
|
|
152
|
+
|
|
153
|
+
### Method 1: SSO (Recommended)
|
|
154
|
+
|
|
155
|
+
The CLI automatically handles SSO login if your session has expired:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
cc calculate --profile myprofile --sso my_sso_profile
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**How it works:**
|
|
162
|
+
1. CLI checks if SSO session is valid using `aws sts get-caller-identity`
|
|
163
|
+
2. If expired, automatically runs `aws sso login --profile my_sso_profile`
|
|
164
|
+
3. Opens browser for authentication
|
|
165
|
+
4. Proceeds with cost calculation once authenticated
|
|
166
|
+
|
|
167
|
+
**Benefits:**
|
|
168
|
+
- No manual SSO login required
|
|
169
|
+
- Automatic session refresh
|
|
170
|
+
- Most secure method
|
|
171
|
+
- Works with AWS IAM Identity Center
|
|
172
|
+
|
|
173
|
+
### Method 2: Static Credentials
|
|
174
|
+
|
|
175
|
+
Pass temporary credentials directly via CLI flags:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
cc calculate --profile myprofile \
|
|
179
|
+
--access-key-id ASIA... \
|
|
180
|
+
--secret-access-key ... \
|
|
181
|
+
--session-token ...
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Use cases:**
|
|
185
|
+
- CI/CD pipelines
|
|
186
|
+
- Temporary credentials from STS
|
|
187
|
+
- Automated scripts
|
|
188
|
+
- When SSO is not available
|
|
189
|
+
|
|
190
|
+
### Method 3: Environment Variables
|
|
191
|
+
|
|
192
|
+
Set credentials in your shell environment:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# For SSO
|
|
196
|
+
export AWS_PROFILE=my_sso_profile
|
|
197
|
+
cc calculate --profile myprofile
|
|
198
|
+
|
|
199
|
+
# For static credentials
|
|
200
|
+
export AWS_ACCESS_KEY_ID=ASIA...
|
|
201
|
+
export AWS_SECRET_ACCESS_KEY=...
|
|
202
|
+
export AWS_SESSION_TOKEN=...
|
|
203
|
+
cc calculate --profile myprofile
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Benefits:**
|
|
207
|
+
- No need to pass credentials with each command
|
|
208
|
+
- Works with existing AWS CLI configuration
|
|
209
|
+
- Can be set in shell profile (~/.zshrc, ~/.bashrc)
|
|
210
|
+
|
|
211
|
+
### Profile Configuration
|
|
212
|
+
|
|
213
|
+
Profiles can be stored locally or fetched from a backend API:
|
|
214
|
+
|
|
215
|
+
**Local storage:** `~/.config/cost-calculator/profiles.json`
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"myprofile": {
|
|
219
|
+
"accounts": ["123456789012", "234567890123", ...]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Backend API:** Set `COST_API_SECRET` environment variable
|
|
225
|
+
|
|
226
|
+
Quick setup (saves to shell profile):
|
|
227
|
+
```bash
|
|
228
|
+
cc setup-api
|
|
229
|
+
# Enter your API secret when prompted (input will be hidden)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Or set manually:
|
|
233
|
+
```bash
|
|
234
|
+
export COST_API_SECRET="your-api-secret"
|
|
235
|
+
cc calculate --profile myprofile --sso my_sso_profile
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The CLI will automatically fetch profile configuration from the backend if `COST_API_SECRET` is set.
|
|
239
|
+
|
|
106
240
|
## How It Works
|
|
107
241
|
|
|
108
242
|
### Date Calculation
|
|
@@ -262,7 +396,7 @@ cc drill --profile myprofile --service "EC2 - Other"
|
|
|
262
396
|
Showing top accounts:
|
|
263
397
|
Week of Oct 19 → Week of Oct 26
|
|
264
398
|
Increases: 3, Decreases: 2
|
|
265
|
-
Top:
|
|
399
|
+
Top: 123456789012 (+$450.23)
|
|
266
400
|
```
|
|
267
401
|
|
|
268
402
|
The report is saved to `drill_down.md` by default.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
aws_cost_calculator_cli-1.6.3.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
backend/__init__.py,sha256=PnH3-V1bIUu7nKDGdfPykzx0sz3x4lsLP0OheoAqY4U,18
|
|
3
|
+
backend/algorithms/__init__.py,sha256=QWrMPtDO_nVOFzKm8yI6_RXdSE0n25RQAFnpS1GsGZs,21
|
|
4
|
+
backend/algorithms/analyze.py,sha256=LvYuY83vIW162km3MvxrL1xsdFdpBqSUOWm7YZ-Tdyc,8922
|
|
5
|
+
backend/algorithms/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
6
|
+
backend/algorithms/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
7
|
+
backend/algorithms/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
8
|
+
backend/handlers/__init__.py,sha256=YGc9-XVhFcT5NOvnu0Vg5qaGy0Md0J_PNj8dJIM5PhE,19
|
|
9
|
+
backend/handlers/analyze.py,sha256=ULeYYMpD5VS4qBd-WvPP_OjgbSLm9VzT6BZYHrt47eE,3546
|
|
10
|
+
backend/handlers/drill.py,sha256=WQbqM5RvOcJHsUpBOR6PS-BtKHqszeCZ7xZu3499jPo,3847
|
|
11
|
+
backend/handlers/monthly.py,sha256=A4B-BLrWHsR9OnhsTEvbYHIATbM5fBRIf_h-liczfE0,3415
|
|
12
|
+
backend/handlers/profiles.py,sha256=tSnHxvGvwH4ynR0R-WrsPgz_VMgxaWHu-SnuhmGPxcs,5107
|
|
13
|
+
backend/handlers/trends.py,sha256=loNvfoc1B-nAb-dTJVLf4TnRZ-UZd_AilyA2l4YahK8,3404
|
|
14
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
15
|
+
cost_calculator/api_client.py,sha256=LUzQmveDF0X9MqAyThp9mbSzJzkOO73Pk4F7IEJjASU,2353
|
|
16
|
+
cost_calculator/cli.py,sha256=zGxFsErjmZAy9v7fqGQDS8qZf4lXoir8Fel0Ka7lw3Y,42235
|
|
17
|
+
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
18
|
+
cost_calculator/executor.py,sha256=tVyyBtXIj9OPyG-xQj8CUmyFjDhb9IVK639360dUZDc,8076
|
|
19
|
+
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
20
|
+
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
21
|
+
aws_cost_calculator_cli-1.6.3.dist-info/METADATA,sha256=VNV5xwWJraHcgf44FpX1TZEGpfkSkXB98BLnkGIo8n0,11506
|
|
22
|
+
aws_cost_calculator_cli-1.6.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
aws_cost_calculator_cli-1.6.3.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
24
|
+
aws_cost_calculator_cli-1.6.3.dist-info/top_level.txt,sha256=YV8sPp9unLPDmK3ixw8-yoyVEKU3O4kskxvZAxFgIK0,24
|
|
25
|
+
aws_cost_calculator_cli-1.6.3.dist-info/RECORD,,
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Backend package
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Algorithms package
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Analysis algorithm using pandas for aggregations.
|
|
3
|
+
Reuses existing algorithms and adds pandas-based analytics.
|
|
4
|
+
"""
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from algorithms.trends import analyze_trends
|
|
9
|
+
from algorithms.drill import analyze_drill_down
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def analyze_aggregated(ce_client, accounts, weeks=12, analysis_type='summary'):
|
|
13
|
+
"""
|
|
14
|
+
Perform pandas-based analysis on cost data.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
ce_client: boto3 Cost Explorer client
|
|
18
|
+
accounts: List of account IDs
|
|
19
|
+
weeks: Number of weeks to analyze
|
|
20
|
+
analysis_type: 'summary', 'volatility', 'trends', 'multi_group'
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
dict with analysis results
|
|
24
|
+
"""
|
|
25
|
+
# Get raw data from trends
|
|
26
|
+
trends_data = analyze_trends(ce_client, accounts, weeks)
|
|
27
|
+
|
|
28
|
+
# Convert to pandas DataFrame
|
|
29
|
+
rows = []
|
|
30
|
+
for comp in trends_data['wow_comparisons']:
|
|
31
|
+
week_label = comp['curr_week']['label']
|
|
32
|
+
for item in comp['increases'] + comp['decreases']:
|
|
33
|
+
rows.append({
|
|
34
|
+
'week': week_label,
|
|
35
|
+
'service': item['service'],
|
|
36
|
+
'prev_cost': item['prev_cost'],
|
|
37
|
+
'curr_cost': item['curr_cost'],
|
|
38
|
+
'change': item['change'],
|
|
39
|
+
'pct_change': item['pct_change']
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
df = pd.DataFrame(rows)
|
|
43
|
+
|
|
44
|
+
if df.empty:
|
|
45
|
+
return {'error': 'No data available'}
|
|
46
|
+
|
|
47
|
+
# Perform requested analysis
|
|
48
|
+
if analysis_type == 'summary':
|
|
49
|
+
return _analyze_summary(df, weeks)
|
|
50
|
+
elif analysis_type == 'volatility':
|
|
51
|
+
return _analyze_volatility(df)
|
|
52
|
+
elif analysis_type == 'trends':
|
|
53
|
+
return _detect_trends(df)
|
|
54
|
+
elif analysis_type == 'multi_group':
|
|
55
|
+
return _multi_group_analysis(ce_client, accounts, weeks)
|
|
56
|
+
else:
|
|
57
|
+
return {'error': f'Unknown analysis type: {analysis_type}'}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _analyze_summary(df, weeks):
|
|
61
|
+
"""Aggregate summary statistics across all weeks."""
|
|
62
|
+
# Group by service and aggregate
|
|
63
|
+
summary = df.groupby('service').agg({
|
|
64
|
+
'change': ['sum', 'mean', 'std', 'min', 'max', 'count'],
|
|
65
|
+
'curr_cost': ['sum', 'mean']
|
|
66
|
+
}).round(2)
|
|
67
|
+
|
|
68
|
+
# Flatten column names
|
|
69
|
+
summary.columns = ['_'.join(col).strip() for col in summary.columns.values]
|
|
70
|
+
summary = summary.reset_index()
|
|
71
|
+
|
|
72
|
+
# Calculate coefficient of variation
|
|
73
|
+
summary['volatility'] = (summary['change_std'] / summary['change_mean'].abs()).fillna(0).round(3)
|
|
74
|
+
|
|
75
|
+
# Sort by total change
|
|
76
|
+
summary = summary.sort_values('change_sum', ascending=False)
|
|
77
|
+
|
|
78
|
+
# Convert to dict
|
|
79
|
+
results = summary.to_dict('records')
|
|
80
|
+
|
|
81
|
+
# Add percentiles
|
|
82
|
+
percentiles = df.groupby('service')['change'].sum().quantile([0.5, 0.9, 0.99]).to_dict()
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
'analysis_type': 'summary',
|
|
86
|
+
'weeks_analyzed': weeks,
|
|
87
|
+
'total_services': len(results),
|
|
88
|
+
'services': results[:50], # Top 50
|
|
89
|
+
'percentiles': {
|
|
90
|
+
'p50': round(percentiles.get(0.5, 0), 2),
|
|
91
|
+
'p90': round(percentiles.get(0.9, 0), 2),
|
|
92
|
+
'p99': round(percentiles.get(0.99, 0), 2)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _analyze_volatility(df):
|
|
98
|
+
"""Identify services with high cost volatility."""
|
|
99
|
+
# Calculate volatility metrics
|
|
100
|
+
volatility = df.groupby('service').agg({
|
|
101
|
+
'change': ['mean', 'std', 'count']
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
volatility.columns = ['mean_change', 'std_change', 'weeks']
|
|
105
|
+
volatility['coefficient_of_variation'] = (volatility['std_change'] / volatility['mean_change'].abs()).fillna(0)
|
|
106
|
+
|
|
107
|
+
# Only services that appear in at least 3 weeks
|
|
108
|
+
volatility = volatility[volatility['weeks'] >= 3]
|
|
109
|
+
|
|
110
|
+
# Sort by CV
|
|
111
|
+
volatility = volatility.sort_values('coefficient_of_variation', ascending=False)
|
|
112
|
+
volatility = volatility.reset_index()
|
|
113
|
+
|
|
114
|
+
# Identify outliers (z-score > 2)
|
|
115
|
+
df['z_score'] = df.groupby('service')['change'].transform(
|
|
116
|
+
lambda x: (x - x.mean()) / x.std() if x.std() > 0 else 0
|
|
117
|
+
)
|
|
118
|
+
outliers = df[df['z_score'].abs() > 2][['week', 'service', 'change', 'z_score']].to_dict('records')
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
'analysis_type': 'volatility',
|
|
122
|
+
'high_volatility_services': volatility.head(20).to_dict('records'),
|
|
123
|
+
'outliers': outliers[:20]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _detect_trends(df):
|
|
128
|
+
"""Detect services with consistent increasing/decreasing trends."""
|
|
129
|
+
# Calculate trend for each service
|
|
130
|
+
trends = []
|
|
131
|
+
|
|
132
|
+
for service in df['service'].unique():
|
|
133
|
+
service_df = df[df['service'] == service].sort_values('week')
|
|
134
|
+
|
|
135
|
+
if len(service_df) < 3:
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
# Calculate linear regression slope
|
|
139
|
+
x = np.arange(len(service_df))
|
|
140
|
+
y = service_df['change'].values
|
|
141
|
+
|
|
142
|
+
if len(x) > 1:
|
|
143
|
+
slope = np.polyfit(x, y, 1)[0]
|
|
144
|
+
avg_change = service_df['change'].mean()
|
|
145
|
+
|
|
146
|
+
# Classify trend
|
|
147
|
+
if slope > avg_change * 0.1: # Increasing by >10% on average
|
|
148
|
+
trend_type = 'increasing'
|
|
149
|
+
elif slope < -avg_change * 0.1: # Decreasing by >10%
|
|
150
|
+
trend_type = 'decreasing'
|
|
151
|
+
else:
|
|
152
|
+
trend_type = 'stable'
|
|
153
|
+
|
|
154
|
+
trends.append({
|
|
155
|
+
'service': service,
|
|
156
|
+
'trend': trend_type,
|
|
157
|
+
'slope': round(slope, 2),
|
|
158
|
+
'avg_change': round(avg_change, 2),
|
|
159
|
+
'weeks_analyzed': len(service_df)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
# Separate by trend type
|
|
163
|
+
increasing = [t for t in trends if t['trend'] == 'increasing']
|
|
164
|
+
decreasing = [t for t in trends if t['trend'] == 'decreasing']
|
|
165
|
+
stable = [t for t in trends if t['trend'] == 'stable']
|
|
166
|
+
|
|
167
|
+
# Sort by slope magnitude
|
|
168
|
+
increasing.sort(key=lambda x: x['slope'], reverse=True)
|
|
169
|
+
decreasing.sort(key=lambda x: x['slope'])
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
'analysis_type': 'trend_detection',
|
|
173
|
+
'increasing_trends': increasing[:20],
|
|
174
|
+
'decreasing_trends': decreasing[:20],
|
|
175
|
+
'stable_services': len(stable)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _multi_group_analysis(ce_client, accounts, weeks):
|
|
180
|
+
"""Multi-dimensional grouping (service + account)."""
|
|
181
|
+
# Get drill-down data for all services
|
|
182
|
+
drill_data = analyze_drill_down(ce_client, accounts, weeks)
|
|
183
|
+
|
|
184
|
+
# Convert to DataFrame
|
|
185
|
+
rows = []
|
|
186
|
+
for comp in drill_data['comparisons']:
|
|
187
|
+
week = comp['curr_week']['label']
|
|
188
|
+
for item in comp['increases'] + comp['decreases']:
|
|
189
|
+
rows.append({
|
|
190
|
+
'week': week,
|
|
191
|
+
'dimension': item['dimension'], # This is account when drilling by service
|
|
192
|
+
'change': item['change'],
|
|
193
|
+
'curr_cost': item['curr_cost']
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
df = pd.DataFrame(rows)
|
|
197
|
+
|
|
198
|
+
if df.empty:
|
|
199
|
+
return {'error': 'No drill-down data available'}
|
|
200
|
+
|
|
201
|
+
# Group by dimension (account) and aggregate
|
|
202
|
+
grouped = df.groupby('dimension').agg({
|
|
203
|
+
'change': ['sum', 'mean', 'count'],
|
|
204
|
+
'curr_cost': 'sum'
|
|
205
|
+
}).round(2)
|
|
206
|
+
|
|
207
|
+
grouped.columns = ['total_change', 'avg_change', 'weeks_appeared', 'total_cost']
|
|
208
|
+
grouped = grouped.sort_values('total_change', ascending=False).reset_index()
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
'analysis_type': 'multi_group',
|
|
212
|
+
'group_by': drill_data.get('group_by', 'account'),
|
|
213
|
+
'groups': grouped.head(50).to_dict('records')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def search_services(ce_client, accounts, weeks, pattern=None, min_cost=None):
|
|
218
|
+
"""
|
|
219
|
+
Search and filter services.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
ce_client: boto3 Cost Explorer client
|
|
223
|
+
accounts: List of account IDs
|
|
224
|
+
weeks: Number of weeks
|
|
225
|
+
pattern: Service name pattern (e.g., "EC2*", "*Compute*")
|
|
226
|
+
min_cost: Minimum total cost threshold
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
dict with matching services
|
|
230
|
+
"""
|
|
231
|
+
# Get trends data
|
|
232
|
+
trends_data = analyze_trends(ce_client, accounts, weeks)
|
|
233
|
+
|
|
234
|
+
# Convert to DataFrame
|
|
235
|
+
rows = []
|
|
236
|
+
for comp in trends_data['wow_comparisons']:
|
|
237
|
+
for item in comp['increases'] + comp['decreases']:
|
|
238
|
+
rows.append({
|
|
239
|
+
'service': item['service'],
|
|
240
|
+
'change': item['change'],
|
|
241
|
+
'curr_cost': item['curr_cost']
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
df = pd.DataFrame(rows)
|
|
245
|
+
|
|
246
|
+
if df.empty:
|
|
247
|
+
return {'matches': []}
|
|
248
|
+
|
|
249
|
+
# Aggregate by service
|
|
250
|
+
summary = df.groupby('service').agg({
|
|
251
|
+
'change': 'sum',
|
|
252
|
+
'curr_cost': 'sum'
|
|
253
|
+
}).reset_index()
|
|
254
|
+
|
|
255
|
+
# Apply filters
|
|
256
|
+
if pattern:
|
|
257
|
+
# Convert glob pattern to regex
|
|
258
|
+
import re
|
|
259
|
+
regex_pattern = pattern.replace('*', '.*').replace('?', '.')
|
|
260
|
+
summary = summary[summary['service'].str.contains(regex_pattern, case=False, regex=True)]
|
|
261
|
+
|
|
262
|
+
if min_cost:
|
|
263
|
+
summary = summary[summary['curr_cost'] >= min_cost]
|
|
264
|
+
|
|
265
|
+
# Sort by total cost
|
|
266
|
+
summary = summary.sort_values('curr_cost', ascending=False)
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
'pattern': pattern,
|
|
270
|
+
'min_cost': min_cost,
|
|
271
|
+
'matches': summary.to_dict('records')
|
|
272
|
+
}
|