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.

@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 1.6.0
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/yourusername/cost-calculator
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/yourusername/cost-calculator/blob/main/README.md
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,8 +43,12 @@ A CLI tool to quickly calculate AWS costs across multiple accounts.
43
43
  ## Installation
44
44
 
45
45
  ```bash
46
- cd ~/cost-calculator
47
- pip install -e .
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
@@ -56,68 +60,183 @@ The CLI supports three authentication methods:
56
60
  #### 1. SSO (Recommended)
57
61
  ```bash
58
62
  # The CLI will automatically trigger SSO login if needed
59
- cc calculate --profile khoros --sso khoros_umbrella
63
+ cc calculate --profile myprofile --sso my_sso_profile
60
64
  ```
61
65
 
62
66
  #### 2. Static Credentials
63
67
  ```bash
64
- cc calculate --profile khoros \
65
- --access-key-id ASIA3D3QOXPO6EBAPXVI \
66
- --secret-access-key /9ijZEUoszN/S2A8IlCrHpW+1fMZ7aUb7fPvU0dL \
67
- --session-token IQoJb3JpZ2luX2VjENr...
68
+ cc calculate --profile myprofile \
69
+ --access-key-id ASIA... \
70
+ --secret-access-key ... \
71
+ --session-token ...
68
72
  ```
69
73
 
70
74
  #### 3. Environment Variables
71
75
  ```bash
72
76
  # For SSO
73
- export AWS_PROFILE=khoros_umbrella
74
- cc calculate --profile khoros
77
+ export AWS_PROFILE=my_sso_profile
78
+ cc calculate --profile myprofile
75
79
 
76
80
  # For static credentials
77
81
  export AWS_ACCESS_KEY_ID=ASIA...
78
82
  export AWS_SECRET_ACCESS_KEY=...
79
83
  export AWS_SESSION_TOKEN=...
80
- cc calculate --profile khoros
84
+ cc calculate --profile myprofile
81
85
  ```
82
86
 
83
87
  ### Basic Usage
84
88
 
85
89
  ```bash
86
90
  # Default: Today minus 2 days, going back 30 days
87
- cc calculate --profile khoros --sso khoros_umbrella
91
+ cc calculate --profile myprofile --sso my_sso_profile
88
92
 
89
93
  # Specific start date
90
- cc calculate --profile khoros --sso khoros_umbrella --start-date 2025-11-04
94
+ cc calculate --profile myprofile --sso my_sso_profile --start-date 2025-11-04
91
95
 
92
96
  # Custom offset and window
93
- cc calculate --profile khoros --sso khoros_umbrella --offset 2 --window 30
97
+ cc calculate --profile myprofile --sso my_sso_profile --offset 2 --window 30
94
98
 
95
99
  # JSON output
96
- cc calculate --profile khoros --sso khoros_umbrella --json-output
100
+ cc calculate --profile myprofile --sso my_sso_profile --json-output
97
101
  ```
98
102
 
99
103
  ### 4. Analyze cost trends
100
104
 
105
+ All commands support the same authentication options:
106
+
101
107
  ```bash
102
- # Analyze last 3 weeks (default)
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
103
116
  cc trends --profile myprofile
104
117
 
105
118
  # Analyze more weeks
106
- cc trends --profile myprofile --weeks 5
119
+ cc trends --profile myprofile --sso my_sso_profile --weeks 5
107
120
 
108
121
  # Custom output file
109
- cc trends --profile myprofile --output weekly_trends.md
122
+ cc trends --profile myprofile --sso my_sso_profile --output weekly_trends.md
110
123
 
111
124
  # JSON output
112
- cc trends --profile myprofile --json-output
125
+ cc trends --profile myprofile --sso my_sso_profile --json-output
113
126
  ```
114
127
 
115
- ### 5. List profiles
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
116
142
 
117
143
  ```bash
118
144
  cc list-profiles
119
145
  ```
120
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
+
121
240
  ## How It Works
122
241
 
123
242
  ### Date Calculation
@@ -277,7 +396,7 @@ cc drill --profile myprofile --service "EC2 - Other"
277
396
  Showing top accounts:
278
397
  Week of Oct 19 → Week of Oct 26
279
398
  Increases: 3, Decreases: 2
280
- Top: 887649220066 (+$450.23)
399
+ Top: 123456789012 (+$450.23)
281
400
  ```
282
401
 
283
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
+ }