aws-cost-calculator-cli 1.0.3__py3-none-any.whl → 1.4.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.
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.4.0.dist-info}/METADATA +126 -2
- aws_cost_calculator_cli-1.4.0.dist-info/RECORD +13 -0
- cost_calculator/api_client.py +105 -0
- cost_calculator/cli.py +218 -0
- cost_calculator/drill.py +323 -0
- cost_calculator/executor.py +159 -0
- cost_calculator/monthly.py +242 -0
- cost_calculator/trends.py +353 -0
- aws_cost_calculator_cli-1.0.3.dist-info/RECORD +0 -8
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.4.0.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.4.0.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.4.0.dist-info}/top_level.txt +0 -0
{aws_cost_calculator_cli-1.0.3.dist-info → aws_cost_calculator_cli-1.4.0.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
|
|
5
5
|
Home-page: https://github.com/yourusername/cost-calculator
|
|
6
6
|
Author: Cost Optimization Team
|
|
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: click>=8.0.0
|
|
25
25
|
Requires-Dist: boto3>=1.26.0
|
|
26
|
+
Requires-Dist: requests>=2.28.0
|
|
26
27
|
Dynamic: author
|
|
27
28
|
Dynamic: classifier
|
|
28
29
|
Dynamic: description
|
|
@@ -80,7 +81,23 @@ cc calculate --profile myprofile --offset 2 --window 30
|
|
|
80
81
|
cc calculate --profile myprofile --json-output
|
|
81
82
|
```
|
|
82
83
|
|
|
83
|
-
### 4.
|
|
84
|
+
### 4. Analyze cost trends
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Analyze last 3 weeks (default)
|
|
88
|
+
cc trends --profile myprofile
|
|
89
|
+
|
|
90
|
+
# Analyze more weeks
|
|
91
|
+
cc trends --profile myprofile --weeks 5
|
|
92
|
+
|
|
93
|
+
# Custom output file
|
|
94
|
+
cc trends --profile myprofile --output weekly_trends.md
|
|
95
|
+
|
|
96
|
+
# JSON output
|
|
97
|
+
cc trends --profile myprofile --json-output
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 5. List profiles
|
|
84
101
|
|
|
85
102
|
```bash
|
|
86
103
|
cc list-profiles
|
|
@@ -138,8 +155,115 @@ cc calculate --profile myprofile --json-output > costs.json
|
|
|
138
155
|
|
|
139
156
|
# Different window size
|
|
140
157
|
cc calculate --profile myprofile --window 60
|
|
158
|
+
|
|
159
|
+
# Weekly cost trends analysis
|
|
160
|
+
cc trends --profile myprofile
|
|
161
|
+
|
|
162
|
+
# Analyze last 8 weeks
|
|
163
|
+
cc trends --profile myprofile --weeks 8
|
|
164
|
+
|
|
165
|
+
# Monthly cost trends analysis
|
|
166
|
+
cc monthly --profile myprofile
|
|
167
|
+
|
|
168
|
+
# Analyze last 12 months
|
|
169
|
+
cc monthly --profile myprofile --months 12
|
|
170
|
+
|
|
171
|
+
# Drill down into specific service
|
|
172
|
+
cc drill --profile myprofile --service "EC2 - Other"
|
|
173
|
+
|
|
174
|
+
# Drill down into specific account
|
|
175
|
+
cc drill --profile myprofile --account 123456789012
|
|
176
|
+
|
|
177
|
+
# Drill down into service within account
|
|
178
|
+
cc drill --profile myprofile --service "EC2 - Other" --account 123456789012
|
|
141
179
|
```
|
|
142
180
|
|
|
181
|
+
## Trends Report
|
|
182
|
+
|
|
183
|
+
The `trends` command generates a markdown report with **two types of analysis**:
|
|
184
|
+
|
|
185
|
+
### 1. Week-over-Week (WoW)
|
|
186
|
+
Compares each week to the previous week - good for catching immediate spikes and changes.
|
|
187
|
+
|
|
188
|
+
### 2. Trailing 30-Day (T-30)
|
|
189
|
+
Compares each week to the same week 4 weeks ago - filters out noise and shows sustained trends.
|
|
190
|
+
|
|
191
|
+
**Features:**
|
|
192
|
+
- **Service-level aggregation**: Shows total cost per service (not individual usage types)
|
|
193
|
+
- **Top 10 Increases/Decreases**: For each comparison period
|
|
194
|
+
- **Total rows**: Sum of top 10 changes for quick assessment
|
|
195
|
+
- **Filters**: Only shows changes >$10 and >5%
|
|
196
|
+
|
|
197
|
+
Example output:
|
|
198
|
+
```
|
|
199
|
+
Week of Oct 19 → Week of Oct 26 (WoW)
|
|
200
|
+
Increases: 4, Decreases: 10
|
|
201
|
+
Top: EC2 - Other (+$949.12)
|
|
202
|
+
|
|
203
|
+
Week of Oct 26 vs Week of Sep 28 (T-30)
|
|
204
|
+
Increases: 10, Decreases: 10
|
|
205
|
+
Top: EC2 - Other (+$886.39)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The report is saved to `cost_trends.md` by default and includes:
|
|
209
|
+
- Service name
|
|
210
|
+
- Previous/baseline cost
|
|
211
|
+
- Current cost
|
|
212
|
+
- Change amount and percentage
|
|
213
|
+
- Total of top 10 changes
|
|
214
|
+
|
|
215
|
+
## Monthly Report
|
|
216
|
+
|
|
217
|
+
The `monthly` command generates month-over-month cost comparisons:
|
|
218
|
+
|
|
219
|
+
**Features:**
|
|
220
|
+
- **Service-level aggregation**: Shows total cost per service
|
|
221
|
+
- **Calendar month comparisons**: October vs September, September vs August, etc.
|
|
222
|
+
- **Top 10 Increases/Decreases**: For each month comparison
|
|
223
|
+
- **Total rows**: Sum of top 10 changes
|
|
224
|
+
- **Filters**: Only shows changes >$50 and >5%
|
|
225
|
+
|
|
226
|
+
Example output:
|
|
227
|
+
```
|
|
228
|
+
October 2025 → November 2025
|
|
229
|
+
Increases: 1, Decreases: 10
|
|
230
|
+
Top: Savings Plans for AWS Compute usage (+$231,161.46)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The report is saved to `monthly_trends.md` by default.
|
|
234
|
+
|
|
235
|
+
## Drill-Down Analysis
|
|
236
|
+
|
|
237
|
+
The `drill` command allows you to investigate cost changes at different levels of detail:
|
|
238
|
+
|
|
239
|
+
**Funnel Approach:**
|
|
240
|
+
1. **Start broad:** `cc trends` → See EC2 costs up $1000
|
|
241
|
+
2. **Drill by service:** `cc drill --service "EC2 - Other"` → See which accounts
|
|
242
|
+
3. **Drill deeper:** `cc drill --service "EC2 - Other" --account 123` → See usage types
|
|
243
|
+
|
|
244
|
+
**Features:**
|
|
245
|
+
- **Automatic grouping**: Shows the next level of detail based on your filters
|
|
246
|
+
- No filters → Shows services
|
|
247
|
+
- Service only → Shows accounts using that service
|
|
248
|
+
- Account only → Shows services in that account
|
|
249
|
+
- Service + Account → Shows usage types
|
|
250
|
+
- **Top 10 Increases/Decreases**: For each week comparison
|
|
251
|
+
- **Total rows**: Sum of top 10 changes
|
|
252
|
+
- **Filters**: Only shows changes >$10 and >5%
|
|
253
|
+
|
|
254
|
+
Example output:
|
|
255
|
+
```
|
|
256
|
+
# Drill by service
|
|
257
|
+
cc drill --profile myprofile --service "EC2 - Other"
|
|
258
|
+
|
|
259
|
+
Showing top accounts:
|
|
260
|
+
Week of Oct 19 → Week of Oct 26
|
|
261
|
+
Increases: 3, Decreases: 2
|
|
262
|
+
Top: 887649220066 (+$450.23)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The report is saved to `drill_down.md` by default.
|
|
266
|
+
|
|
143
267
|
## Output
|
|
144
268
|
|
|
145
269
|
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
aws_cost_calculator_cli-1.4.0.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
+
cost_calculator/api_client.py,sha256=pSH2U0tOghDd3fisPcKqEJG3TghQYZi18HZmianHd6Y,2932
|
|
4
|
+
cost_calculator/cli.py,sha256=qK6WQcAM5W15NciGOUtPpqcEvgyua5n5GRRysB4NPWw,27631
|
|
5
|
+
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
6
|
+
cost_calculator/executor.py,sha256=RZ45GuA8tzKqj_pJaZ-BVSc8xbxxWTi4yCGcKqvNsUg,5601
|
|
7
|
+
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
8
|
+
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
9
|
+
aws_cost_calculator_cli-1.4.0.dist-info/METADATA,sha256=LJkphxt8op0vOE_UJv-rGYRI4obstONupdOS8tYHGKg,7793
|
|
10
|
+
aws_cost_calculator_cli-1.4.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
11
|
+
aws_cost_calculator_cli-1.4.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
12
|
+
aws_cost_calculator_cli-1.4.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
13
|
+
aws_cost_calculator_cli-1.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API client for calling Lambda backend.
|
|
3
|
+
Falls back to local execution if API is not configured.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
import requests
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_api_config():
|
|
12
|
+
"""
|
|
13
|
+
Get API configuration from environment or config file.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
dict with 'base_url' and 'api_secret', or None if not configured
|
|
17
|
+
"""
|
|
18
|
+
# Try environment variables first
|
|
19
|
+
base_url = os.environ.get('COST_API_URL')
|
|
20
|
+
api_secret = os.environ.get('COST_API_SECRET')
|
|
21
|
+
|
|
22
|
+
if base_url and api_secret:
|
|
23
|
+
return {
|
|
24
|
+
'base_url': base_url.rstrip('/'),
|
|
25
|
+
'api_secret': api_secret
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Try config file
|
|
29
|
+
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
30
|
+
api_config_file = config_dir / 'api_config.json'
|
|
31
|
+
|
|
32
|
+
if api_config_file.exists():
|
|
33
|
+
with open(api_config_file, 'r') as f:
|
|
34
|
+
config = json.load(f)
|
|
35
|
+
if 'base_url' in config and 'api_secret' in config:
|
|
36
|
+
return {
|
|
37
|
+
'base_url': config['base_url'].rstrip('/'),
|
|
38
|
+
'api_secret': config['api_secret']
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def call_lambda_api(endpoint, credentials, accounts, **kwargs):
|
|
45
|
+
"""
|
|
46
|
+
Call Lambda API endpoint.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
endpoint: API endpoint name ('trends', 'monthly', 'drill')
|
|
50
|
+
credentials: dict with AWS credentials
|
|
51
|
+
accounts: list of account IDs
|
|
52
|
+
**kwargs: additional parameters for the specific endpoint
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict: API response data
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
Exception: if API call fails
|
|
59
|
+
"""
|
|
60
|
+
api_config = get_api_config()
|
|
61
|
+
|
|
62
|
+
if not api_config:
|
|
63
|
+
raise Exception("API not configured. Set COST_API_URL and COST_API_SECRET environment variables.")
|
|
64
|
+
|
|
65
|
+
# Map endpoint names to URLs
|
|
66
|
+
endpoint_urls = {
|
|
67
|
+
'trends': f"{api_config['base_url']}/trends",
|
|
68
|
+
'monthly': f"{api_config['base_url']}/monthly",
|
|
69
|
+
'drill': f"{api_config['base_url']}/drill"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# For the actual Lambda URLs (no path)
|
|
73
|
+
if '/trends' not in api_config['base_url']:
|
|
74
|
+
# Base URL is the function URL itself
|
|
75
|
+
url = api_config['base_url']
|
|
76
|
+
else:
|
|
77
|
+
url = endpoint_urls.get(endpoint)
|
|
78
|
+
|
|
79
|
+
if not url:
|
|
80
|
+
raise Exception(f"Unknown endpoint: {endpoint}")
|
|
81
|
+
|
|
82
|
+
# Build request payload
|
|
83
|
+
payload = {
|
|
84
|
+
'credentials': credentials,
|
|
85
|
+
'accounts': accounts
|
|
86
|
+
}
|
|
87
|
+
payload.update(kwargs)
|
|
88
|
+
|
|
89
|
+
# Make API call
|
|
90
|
+
headers = {
|
|
91
|
+
'X-API-Secret': api_config['api_secret'],
|
|
92
|
+
'Content-Type': 'application/json'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
|
96
|
+
|
|
97
|
+
if response.status_code != 200:
|
|
98
|
+
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
99
|
+
|
|
100
|
+
return response.json()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def is_api_configured():
|
|
104
|
+
"""Check if API is configured."""
|
|
105
|
+
return get_api_config() is not None
|
cost_calculator/cli.py
CHANGED
|
@@ -13,6 +13,10 @@ import boto3
|
|
|
13
13
|
import json
|
|
14
14
|
from datetime import datetime, timedelta
|
|
15
15
|
from pathlib import Path
|
|
16
|
+
from cost_calculator.trends import format_trends_markdown
|
|
17
|
+
from cost_calculator.monthly import format_monthly_markdown
|
|
18
|
+
from cost_calculator.drill import format_drill_down_markdown
|
|
19
|
+
from cost_calculator.executor import execute_trends, execute_monthly, execute_drill
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
def load_profile(profile_name):
|
|
@@ -539,5 +543,219 @@ def configure(profile, access_key_id, secret_access_key, session_token, region):
|
|
|
539
543
|
click.echo(" you'll need to reconfigure when they expire.")
|
|
540
544
|
|
|
541
545
|
|
|
546
|
+
@cli.command()
|
|
547
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
548
|
+
@click.option('--weeks', default=3, help='Number of weeks to analyze (default: 3)')
|
|
549
|
+
@click.option('--output', default='cost_trends.md', help='Output markdown file (default: cost_trends.md)')
|
|
550
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
|
|
551
|
+
def trends(profile, weeks, output, json_output):
|
|
552
|
+
"""Analyze cost trends with Week-over-Week and Trailing 30-Day comparisons"""
|
|
553
|
+
|
|
554
|
+
# Load profile configuration
|
|
555
|
+
config = load_profile(profile)
|
|
556
|
+
|
|
557
|
+
click.echo(f"Analyzing last {weeks} weeks...")
|
|
558
|
+
click.echo("")
|
|
559
|
+
|
|
560
|
+
# Execute via API or locally
|
|
561
|
+
trends_data = execute_trends(config, weeks)
|
|
562
|
+
|
|
563
|
+
if json_output:
|
|
564
|
+
# Output as JSON
|
|
565
|
+
import json
|
|
566
|
+
click.echo(json.dumps(trends_data, indent=2, default=str))
|
|
567
|
+
else:
|
|
568
|
+
# Generate markdown report
|
|
569
|
+
markdown = format_trends_markdown(trends_data)
|
|
570
|
+
|
|
571
|
+
# Save to file
|
|
572
|
+
with open(output, 'w') as f:
|
|
573
|
+
f.write(markdown)
|
|
574
|
+
|
|
575
|
+
click.echo(f"✓ Trends report saved to {output}")
|
|
576
|
+
click.echo("")
|
|
577
|
+
|
|
578
|
+
# Show summary
|
|
579
|
+
click.echo("WEEK-OVER-WEEK:")
|
|
580
|
+
for comparison in trends_data['wow_comparisons']:
|
|
581
|
+
prev_week = comparison['prev_week']['label']
|
|
582
|
+
curr_week = comparison['curr_week']['label']
|
|
583
|
+
num_increases = len(comparison['increases'])
|
|
584
|
+
num_decreases = len(comparison['decreases'])
|
|
585
|
+
|
|
586
|
+
click.echo(f" {prev_week} → {curr_week}")
|
|
587
|
+
click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
|
|
588
|
+
|
|
589
|
+
if comparison['increases']:
|
|
590
|
+
top = comparison['increases'][0]
|
|
591
|
+
click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
|
|
592
|
+
|
|
593
|
+
click.echo("")
|
|
594
|
+
|
|
595
|
+
click.echo("TRAILING 30-DAY (T-30):")
|
|
596
|
+
for comparison in trends_data['t30_comparisons']:
|
|
597
|
+
baseline_week = comparison['baseline_week']['label']
|
|
598
|
+
curr_week = comparison['curr_week']['label']
|
|
599
|
+
num_increases = len(comparison['increases'])
|
|
600
|
+
num_decreases = len(comparison['decreases'])
|
|
601
|
+
|
|
602
|
+
click.echo(f" {curr_week} vs {baseline_week}")
|
|
603
|
+
click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
|
|
604
|
+
|
|
605
|
+
if comparison['increases']:
|
|
606
|
+
top = comparison['increases'][0]
|
|
607
|
+
click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
|
|
608
|
+
|
|
609
|
+
click.echo("")
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@cli.command()
|
|
613
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
614
|
+
@click.option('--months', default=6, help='Number of months to analyze (default: 6)')
|
|
615
|
+
@click.option('--output', default='monthly_trends.md', help='Output markdown file (default: monthly_trends.md)')
|
|
616
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
|
|
617
|
+
def monthly(profile, months, output, json_output):
|
|
618
|
+
"""Analyze month-over-month cost trends at service level"""
|
|
619
|
+
|
|
620
|
+
# Load profile configuration
|
|
621
|
+
config = load_profile(profile)
|
|
622
|
+
|
|
623
|
+
click.echo(f"Analyzing last {months} months...")
|
|
624
|
+
click.echo("")
|
|
625
|
+
|
|
626
|
+
# Execute via API or locally
|
|
627
|
+
monthly_data = execute_monthly(config, months)
|
|
628
|
+
|
|
629
|
+
if json_output:
|
|
630
|
+
# Output as JSON
|
|
631
|
+
output_data = {
|
|
632
|
+
'generated': datetime.now().isoformat(),
|
|
633
|
+
'months': months,
|
|
634
|
+
'comparisons': []
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
for comparison in monthly_data['comparisons']:
|
|
638
|
+
output_data['comparisons'].append({
|
|
639
|
+
'prev_month': comparison['prev_month']['label'],
|
|
640
|
+
'curr_month': comparison['curr_month']['label'],
|
|
641
|
+
'increases': comparison['increases'],
|
|
642
|
+
'decreases': comparison['decreases'],
|
|
643
|
+
'total_increase': comparison['total_increase'],
|
|
644
|
+
'total_decrease': comparison['total_decrease']
|
|
645
|
+
})
|
|
646
|
+
|
|
647
|
+
click.echo(json.dumps(output_data, indent=2))
|
|
648
|
+
else:
|
|
649
|
+
# Generate markdown report
|
|
650
|
+
markdown = format_monthly_markdown(monthly_data)
|
|
651
|
+
|
|
652
|
+
# Save to file
|
|
653
|
+
with open(output, 'w') as f:
|
|
654
|
+
f.write(markdown)
|
|
655
|
+
|
|
656
|
+
click.echo(f"✓ Monthly trends report saved to {output}")
|
|
657
|
+
click.echo("")
|
|
658
|
+
|
|
659
|
+
# Show summary
|
|
660
|
+
for comparison in monthly_data['comparisons']:
|
|
661
|
+
prev_month = comparison['prev_month']['label']
|
|
662
|
+
curr_month = comparison['curr_month']['label']
|
|
663
|
+
num_increases = len(comparison['increases'])
|
|
664
|
+
num_decreases = len(comparison['decreases'])
|
|
665
|
+
|
|
666
|
+
click.echo(f"{prev_month} → {curr_month}")
|
|
667
|
+
click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
|
|
668
|
+
|
|
669
|
+
if comparison['increases']:
|
|
670
|
+
top = comparison['increases'][0]
|
|
671
|
+
click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
|
|
672
|
+
|
|
673
|
+
click.echo("")
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
@cli.command()
|
|
677
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
678
|
+
@click.option('--weeks', default=4, help='Number of weeks to analyze (default: 4)')
|
|
679
|
+
@click.option('--service', help='Filter by service name (e.g., "EC2 - Other")')
|
|
680
|
+
@click.option('--account', help='Filter by account ID')
|
|
681
|
+
@click.option('--usage-type', help='Filter by usage type')
|
|
682
|
+
@click.option('--output', default='drill_down.md', help='Output markdown file (default: drill_down.md)')
|
|
683
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
|
|
684
|
+
def drill(profile, weeks, service, account, usage_type, output, json_output):
|
|
685
|
+
"""Drill down into cost changes by service, account, or usage type"""
|
|
686
|
+
|
|
687
|
+
# Load profile configuration
|
|
688
|
+
config = load_profile(profile)
|
|
689
|
+
|
|
690
|
+
# Show filters
|
|
691
|
+
click.echo(f"Analyzing last {weeks} weeks...")
|
|
692
|
+
if service:
|
|
693
|
+
click.echo(f" Service filter: {service}")
|
|
694
|
+
if account:
|
|
695
|
+
click.echo(f" Account filter: {account}")
|
|
696
|
+
if usage_type:
|
|
697
|
+
click.echo(f" Usage type filter: {usage_type}")
|
|
698
|
+
click.echo("")
|
|
699
|
+
|
|
700
|
+
# Execute via API or locally
|
|
701
|
+
drill_data = execute_drill(config, weeks, service, account, usage_type)
|
|
702
|
+
|
|
703
|
+
if json_output:
|
|
704
|
+
# Output as JSON
|
|
705
|
+
output_data = {
|
|
706
|
+
'generated': datetime.now().isoformat(),
|
|
707
|
+
'weeks': weeks,
|
|
708
|
+
'filters': drill_data['filters'],
|
|
709
|
+
'group_by': drill_data['group_by'],
|
|
710
|
+
'comparisons': []
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
for comparison in drill_data['comparisons']:
|
|
714
|
+
output_data['comparisons'].append({
|
|
715
|
+
'prev_week': comparison['prev_week']['label'],
|
|
716
|
+
'curr_week': comparison['curr_week']['label'],
|
|
717
|
+
'increases': comparison['increases'],
|
|
718
|
+
'decreases': comparison['decreases'],
|
|
719
|
+
'total_increase': comparison['total_increase'],
|
|
720
|
+
'total_decrease': comparison['total_decrease']
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
click.echo(json.dumps(output_data, indent=2))
|
|
724
|
+
else:
|
|
725
|
+
# Generate markdown report
|
|
726
|
+
markdown = format_drill_down_markdown(drill_data)
|
|
727
|
+
|
|
728
|
+
# Save to file
|
|
729
|
+
with open(output, 'w') as f:
|
|
730
|
+
f.write(markdown)
|
|
731
|
+
|
|
732
|
+
click.echo(f"✓ Drill-down report saved to {output}")
|
|
733
|
+
click.echo("")
|
|
734
|
+
|
|
735
|
+
# Show summary
|
|
736
|
+
group_by_label = {
|
|
737
|
+
'SERVICE': 'services',
|
|
738
|
+
'LINKED_ACCOUNT': 'accounts',
|
|
739
|
+
'USAGE_TYPE': 'usage types',
|
|
740
|
+
'REGION': 'regions'
|
|
741
|
+
}.get(drill_data['group_by'], 'items')
|
|
742
|
+
|
|
743
|
+
click.echo(f"Showing top {group_by_label}:")
|
|
744
|
+
for comparison in drill_data['comparisons']:
|
|
745
|
+
prev_week = comparison['prev_week']['label']
|
|
746
|
+
curr_week = comparison['curr_week']['label']
|
|
747
|
+
num_increases = len(comparison['increases'])
|
|
748
|
+
num_decreases = len(comparison['decreases'])
|
|
749
|
+
|
|
750
|
+
click.echo(f"{prev_week} → {curr_week}")
|
|
751
|
+
click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
|
|
752
|
+
|
|
753
|
+
if comparison['increases']:
|
|
754
|
+
top = comparison['increases'][0]
|
|
755
|
+
click.echo(f" Top: {top['dimension'][:50]} (+${top['change']:,.2f})")
|
|
756
|
+
|
|
757
|
+
click.echo("")
|
|
758
|
+
|
|
759
|
+
|
|
542
760
|
if __name__ == '__main__':
|
|
543
761
|
cli()
|