aws-cost-calculator-cli 1.0.2__py3-none-any.whl → 1.2.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.0.2.dist-info → aws_cost_calculator_cli-1.2.0.dist-info}/METADATA +84 -2
- aws_cost_calculator_cli-1.2.0.dist-info/RECORD +10 -0
- cost_calculator/cli.py +263 -0
- cost_calculator/monthly.py +242 -0
- cost_calculator/trends.py +353 -0
- 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.2.0.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.2.0.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.2.0.dist-info}/top_level.txt +0 -0
{aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.2.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.2.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
|
|
@@ -80,7 +80,23 @@ cc calculate --profile myprofile --offset 2 --window 30
|
|
|
80
80
|
cc calculate --profile myprofile --json-output
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
### 4.
|
|
83
|
+
### 4. Analyze cost trends
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Analyze last 3 weeks (default)
|
|
87
|
+
cc trends --profile myprofile
|
|
88
|
+
|
|
89
|
+
# Analyze more weeks
|
|
90
|
+
cc trends --profile myprofile --weeks 5
|
|
91
|
+
|
|
92
|
+
# Custom output file
|
|
93
|
+
cc trends --profile myprofile --output weekly_trends.md
|
|
94
|
+
|
|
95
|
+
# JSON output
|
|
96
|
+
cc trends --profile myprofile --json-output
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 5. List profiles
|
|
84
100
|
|
|
85
101
|
```bash
|
|
86
102
|
cc list-profiles
|
|
@@ -138,8 +154,74 @@ cc calculate --profile myprofile --json-output > costs.json
|
|
|
138
154
|
|
|
139
155
|
# Different window size
|
|
140
156
|
cc calculate --profile myprofile --window 60
|
|
157
|
+
|
|
158
|
+
# Weekly cost trends analysis
|
|
159
|
+
cc trends --profile myprofile
|
|
160
|
+
|
|
161
|
+
# Analyze last 8 weeks
|
|
162
|
+
cc trends --profile myprofile --weeks 8
|
|
163
|
+
|
|
164
|
+
# Monthly cost trends analysis
|
|
165
|
+
cc monthly --profile myprofile
|
|
166
|
+
|
|
167
|
+
# Analyze last 12 months
|
|
168
|
+
cc monthly --profile myprofile --months 12
|
|
141
169
|
```
|
|
142
170
|
|
|
171
|
+
## Trends Report
|
|
172
|
+
|
|
173
|
+
The `trends` command generates a markdown report with **two types of analysis**:
|
|
174
|
+
|
|
175
|
+
### 1. Week-over-Week (WoW)
|
|
176
|
+
Compares each week to the previous week - good for catching immediate spikes and changes.
|
|
177
|
+
|
|
178
|
+
### 2. Trailing 30-Day (T-30)
|
|
179
|
+
Compares each week to the same week 4 weeks ago - filters out noise and shows sustained trends.
|
|
180
|
+
|
|
181
|
+
**Features:**
|
|
182
|
+
- **Service-level aggregation**: Shows total cost per service (not individual usage types)
|
|
183
|
+
- **Top 10 Increases/Decreases**: For each comparison period
|
|
184
|
+
- **Total rows**: Sum of top 10 changes for quick assessment
|
|
185
|
+
- **Filters**: Only shows changes >$10 and >5%
|
|
186
|
+
|
|
187
|
+
Example output:
|
|
188
|
+
```
|
|
189
|
+
Week of Oct 19 → Week of Oct 26 (WoW)
|
|
190
|
+
Increases: 4, Decreases: 10
|
|
191
|
+
Top: EC2 - Other (+$949.12)
|
|
192
|
+
|
|
193
|
+
Week of Oct 26 vs Week of Sep 28 (T-30)
|
|
194
|
+
Increases: 10, Decreases: 10
|
|
195
|
+
Top: EC2 - Other (+$886.39)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The report is saved to `cost_trends.md` by default and includes:
|
|
199
|
+
- Service name
|
|
200
|
+
- Previous/baseline cost
|
|
201
|
+
- Current cost
|
|
202
|
+
- Change amount and percentage
|
|
203
|
+
- Total of top 10 changes
|
|
204
|
+
|
|
205
|
+
## Monthly Report
|
|
206
|
+
|
|
207
|
+
The `monthly` command generates month-over-month cost comparisons:
|
|
208
|
+
|
|
209
|
+
**Features:**
|
|
210
|
+
- **Service-level aggregation**: Shows total cost per service
|
|
211
|
+
- **Calendar month comparisons**: October vs September, September vs August, etc.
|
|
212
|
+
- **Top 10 Increases/Decreases**: For each month comparison
|
|
213
|
+
- **Total rows**: Sum of top 10 changes
|
|
214
|
+
- **Filters**: Only shows changes >$50 and >5%
|
|
215
|
+
|
|
216
|
+
Example output:
|
|
217
|
+
```
|
|
218
|
+
October 2025 → November 2025
|
|
219
|
+
Increases: 1, Decreases: 10
|
|
220
|
+
Top: Savings Plans for AWS Compute usage (+$231,161.46)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The report is saved to `monthly_trends.md` by default.
|
|
224
|
+
|
|
143
225
|
## Output
|
|
144
226
|
|
|
145
227
|
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
aws_cost_calculator_cli-1.2.0.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
+
cost_calculator/cli.py,sha256=fQPSDb-vp6qEpCgNGiyEt2FXdFJE6wtjRg5r67hLYDU,26639
|
|
4
|
+
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
5
|
+
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
6
|
+
aws_cost_calculator_cli-1.2.0.dist-info/METADATA,sha256=dEgROlV7F3TkZkQEFAE_Ppw_0htCkHQ2vhk6Zh31BUA,6406
|
|
7
|
+
aws_cost_calculator_cli-1.2.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
8
|
+
aws_cost_calculator_cli-1.2.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
9
|
+
aws_cost_calculator_cli-1.2.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
10
|
+
aws_cost_calculator_cli-1.2.0.dist-info/RECORD,,
|
cost_calculator/cli.py
CHANGED
|
@@ -13,6 +13,8 @@ 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 analyze_trends, format_trends_markdown
|
|
17
|
+
from cost_calculator.monthly import analyze_monthly_trends, format_monthly_markdown
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def load_profile(profile_name):
|
|
@@ -395,6 +397,77 @@ def list_profiles():
|
|
|
395
397
|
click.echo("")
|
|
396
398
|
|
|
397
399
|
|
|
400
|
+
@cli.command()
|
|
401
|
+
def setup():
|
|
402
|
+
"""Show setup instructions for manual profile configuration"""
|
|
403
|
+
import platform
|
|
404
|
+
|
|
405
|
+
system = platform.system()
|
|
406
|
+
|
|
407
|
+
if system == "Windows":
|
|
408
|
+
config_path = "%USERPROFILE%\\.config\\cost-calculator\\profiles.json"
|
|
409
|
+
config_path_example = "C:\\Users\\YourName\\.config\\cost-calculator\\profiles.json"
|
|
410
|
+
mkdir_cmd = "mkdir %USERPROFILE%\\.config\\cost-calculator"
|
|
411
|
+
edit_cmd = "notepad %USERPROFILE%\\.config\\cost-calculator\\profiles.json"
|
|
412
|
+
else: # macOS/Linux
|
|
413
|
+
config_path = "~/.config/cost-calculator/profiles.json"
|
|
414
|
+
config_path_example = "/Users/yourname/.config/cost-calculator/profiles.json"
|
|
415
|
+
mkdir_cmd = "mkdir -p ~/.config/cost-calculator"
|
|
416
|
+
edit_cmd = "nano ~/.config/cost-calculator/profiles.json"
|
|
417
|
+
|
|
418
|
+
click.echo("=" * 70)
|
|
419
|
+
click.echo("AWS Cost Calculator - Manual Profile Setup")
|
|
420
|
+
click.echo("=" * 70)
|
|
421
|
+
click.echo("")
|
|
422
|
+
click.echo(f"Platform: {system}")
|
|
423
|
+
click.echo(f"Config location: {config_path}")
|
|
424
|
+
click.echo("")
|
|
425
|
+
click.echo("Step 1: Create the config directory")
|
|
426
|
+
click.echo(f" {mkdir_cmd}")
|
|
427
|
+
click.echo("")
|
|
428
|
+
click.echo("Step 2: Create the profiles.json file")
|
|
429
|
+
click.echo(f" {edit_cmd}")
|
|
430
|
+
click.echo("")
|
|
431
|
+
click.echo("Step 3: Add your profile configuration (JSON format):")
|
|
432
|
+
click.echo("")
|
|
433
|
+
click.echo(' {')
|
|
434
|
+
click.echo(' "myprofile": {')
|
|
435
|
+
click.echo(' "aws_profile": "my_aws_profile",')
|
|
436
|
+
click.echo(' "accounts": [')
|
|
437
|
+
click.echo(' "123456789012",')
|
|
438
|
+
click.echo(' "234567890123",')
|
|
439
|
+
click.echo(' "345678901234"')
|
|
440
|
+
click.echo(' ]')
|
|
441
|
+
click.echo(' }')
|
|
442
|
+
click.echo(' }')
|
|
443
|
+
click.echo("")
|
|
444
|
+
click.echo("Step 4: Save the file")
|
|
445
|
+
click.echo("")
|
|
446
|
+
click.echo("Step 5: Verify it works")
|
|
447
|
+
click.echo(" cc list-profiles")
|
|
448
|
+
click.echo("")
|
|
449
|
+
click.echo("Step 6: Configure AWS credentials")
|
|
450
|
+
click.echo(" Option A (SSO):")
|
|
451
|
+
click.echo(" aws sso login --profile my_aws_profile")
|
|
452
|
+
click.echo(" cc calculate --profile myprofile")
|
|
453
|
+
click.echo("")
|
|
454
|
+
click.echo(" Option B (Static credentials):")
|
|
455
|
+
click.echo(" cc configure --profile myprofile")
|
|
456
|
+
click.echo(" cc calculate --profile myprofile")
|
|
457
|
+
click.echo("")
|
|
458
|
+
click.echo("=" * 70)
|
|
459
|
+
click.echo("")
|
|
460
|
+
click.echo("For multiple profiles, add more entries to the JSON:")
|
|
461
|
+
click.echo("")
|
|
462
|
+
click.echo(' {')
|
|
463
|
+
click.echo(' "profile1": { ... },')
|
|
464
|
+
click.echo(' "profile2": { ... }')
|
|
465
|
+
click.echo(' }')
|
|
466
|
+
click.echo("")
|
|
467
|
+
click.echo(f"Full path example: {config_path_example}")
|
|
468
|
+
click.echo("=" * 70)
|
|
469
|
+
|
|
470
|
+
|
|
398
471
|
@cli.command()
|
|
399
472
|
@click.option('--profile', required=True, help='Profile name to configure')
|
|
400
473
|
@click.option('--access-key-id', prompt=True, hide_input=False, help='AWS Access Key ID')
|
|
@@ -468,5 +541,195 @@ def configure(profile, access_key_id, secret_access_key, session_token, region):
|
|
|
468
541
|
click.echo(" you'll need to reconfigure when they expire.")
|
|
469
542
|
|
|
470
543
|
|
|
544
|
+
@cli.command()
|
|
545
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
546
|
+
@click.option('--weeks', default=3, help='Number of weeks to analyze (default: 3)')
|
|
547
|
+
@click.option('--output', default='cost_trends.md', help='Output markdown file (default: cost_trends.md)')
|
|
548
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
|
|
549
|
+
def trends(profile, weeks, output, json_output):
|
|
550
|
+
"""Analyze cost trends with Week-over-Week and Trailing 30-Day comparisons"""
|
|
551
|
+
|
|
552
|
+
# Load profile configuration
|
|
553
|
+
config = load_profile(profile)
|
|
554
|
+
|
|
555
|
+
# Initialize boto3 client
|
|
556
|
+
try:
|
|
557
|
+
if 'aws_profile' in config:
|
|
558
|
+
aws_profile = config['aws_profile']
|
|
559
|
+
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
560
|
+
session = boto3.Session(profile_name=aws_profile)
|
|
561
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
562
|
+
else:
|
|
563
|
+
creds = config['credentials']
|
|
564
|
+
click.echo(f"AWS Credentials: Static")
|
|
565
|
+
|
|
566
|
+
session_kwargs = {
|
|
567
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
568
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
569
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if 'aws_session_token' in creds:
|
|
573
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
574
|
+
|
|
575
|
+
session = boto3.Session(**session_kwargs)
|
|
576
|
+
ce_client = session.client('ce')
|
|
577
|
+
|
|
578
|
+
except Exception as e:
|
|
579
|
+
if 'Token has expired' in str(e) or 'sso' in str(e).lower():
|
|
580
|
+
if 'aws_profile' in config:
|
|
581
|
+
raise click.ClickException(
|
|
582
|
+
f"AWS SSO session expired or not initialized.\n"
|
|
583
|
+
f"Run: aws sso login --profile {config['aws_profile']}"
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
raise click.ClickException(
|
|
587
|
+
f"AWS credentials expired.\n"
|
|
588
|
+
f"Run: cc configure --profile {profile}"
|
|
589
|
+
)
|
|
590
|
+
raise
|
|
591
|
+
|
|
592
|
+
click.echo(f"Analyzing last {weeks} weeks...")
|
|
593
|
+
click.echo("")
|
|
594
|
+
|
|
595
|
+
# Analyze trends
|
|
596
|
+
trends_data = analyze_trends(ce_client, config['accounts'], num_weeks=weeks)
|
|
597
|
+
|
|
598
|
+
if json_output:
|
|
599
|
+
# Output as JSON
|
|
600
|
+
import json
|
|
601
|
+
click.echo(json.dumps(trends_data, indent=2, default=str))
|
|
602
|
+
else:
|
|
603
|
+
# Generate markdown report
|
|
604
|
+
markdown = format_trends_markdown(trends_data)
|
|
605
|
+
|
|
606
|
+
# Save to file
|
|
607
|
+
with open(output, 'w') as f:
|
|
608
|
+
f.write(markdown)
|
|
609
|
+
|
|
610
|
+
click.echo(f"✓ Trends report saved to {output}")
|
|
611
|
+
click.echo("")
|
|
612
|
+
|
|
613
|
+
# Show summary
|
|
614
|
+
click.echo("WEEK-OVER-WEEK:")
|
|
615
|
+
for comparison in trends_data['wow_comparisons']:
|
|
616
|
+
prev_week = comparison['prev_week']['label']
|
|
617
|
+
curr_week = comparison['curr_week']['label']
|
|
618
|
+
num_increases = len(comparison['increases'])
|
|
619
|
+
num_decreases = len(comparison['decreases'])
|
|
620
|
+
|
|
621
|
+
click.echo(f" {prev_week} → {curr_week}")
|
|
622
|
+
click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
|
|
623
|
+
|
|
624
|
+
if comparison['increases']:
|
|
625
|
+
top = comparison['increases'][0]
|
|
626
|
+
click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
|
|
627
|
+
|
|
628
|
+
click.echo("")
|
|
629
|
+
|
|
630
|
+
click.echo("TRAILING 30-DAY (T-30):")
|
|
631
|
+
for comparison in trends_data['t30_comparisons']:
|
|
632
|
+
baseline_week = comparison['baseline_week']['label']
|
|
633
|
+
curr_week = comparison['curr_week']['label']
|
|
634
|
+
num_increases = len(comparison['increases'])
|
|
635
|
+
num_decreases = len(comparison['decreases'])
|
|
636
|
+
|
|
637
|
+
click.echo(f" {curr_week} vs {baseline_week}")
|
|
638
|
+
click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
|
|
639
|
+
|
|
640
|
+
if comparison['increases']:
|
|
641
|
+
top = comparison['increases'][0]
|
|
642
|
+
click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
|
|
643
|
+
|
|
644
|
+
click.echo("")
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@cli.command()
|
|
648
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
649
|
+
@click.option('--months', default=6, help='Number of months to analyze (default: 6)')
|
|
650
|
+
@click.option('--output', default='monthly_trends.md', help='Output markdown file (default: monthly_trends.md)')
|
|
651
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
|
|
652
|
+
def monthly(profile, months, output, json_output):
|
|
653
|
+
"""Analyze month-over-month cost trends at service level"""
|
|
654
|
+
|
|
655
|
+
# Load profile configuration
|
|
656
|
+
config = load_profile(profile)
|
|
657
|
+
|
|
658
|
+
# Initialize boto3 client
|
|
659
|
+
try:
|
|
660
|
+
if 'aws_profile' in config:
|
|
661
|
+
aws_profile = config['aws_profile']
|
|
662
|
+
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
663
|
+
session = boto3.Session(profile_name=aws_profile)
|
|
664
|
+
else:
|
|
665
|
+
# Use static credentials
|
|
666
|
+
creds = config['credentials']
|
|
667
|
+
click.echo("AWS Credentials: Static")
|
|
668
|
+
session = boto3.Session(
|
|
669
|
+
aws_access_key_id=creds['aws_access_key_id'],
|
|
670
|
+
aws_secret_access_key=creds['aws_secret_access_key'],
|
|
671
|
+
aws_session_token=creds.get('aws_session_token')
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
675
|
+
except Exception as e:
|
|
676
|
+
raise click.ClickException(f"Failed to initialize AWS session: {str(e)}")
|
|
677
|
+
|
|
678
|
+
# Get account list
|
|
679
|
+
accounts = config['accounts']
|
|
680
|
+
|
|
681
|
+
click.echo(f"Analyzing last {months} months...")
|
|
682
|
+
click.echo("")
|
|
683
|
+
|
|
684
|
+
# Analyze monthly trends
|
|
685
|
+
monthly_data = analyze_monthly_trends(ce_client, accounts, months)
|
|
686
|
+
|
|
687
|
+
if json_output:
|
|
688
|
+
# Output as JSON
|
|
689
|
+
output_data = {
|
|
690
|
+
'generated': datetime.now().isoformat(),
|
|
691
|
+
'months': months,
|
|
692
|
+
'comparisons': []
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
for comparison in monthly_data['comparisons']:
|
|
696
|
+
output_data['comparisons'].append({
|
|
697
|
+
'prev_month': comparison['prev_month']['label'],
|
|
698
|
+
'curr_month': comparison['curr_month']['label'],
|
|
699
|
+
'increases': comparison['increases'],
|
|
700
|
+
'decreases': comparison['decreases'],
|
|
701
|
+
'total_increase': comparison['total_increase'],
|
|
702
|
+
'total_decrease': comparison['total_decrease']
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
click.echo(json.dumps(output_data, indent=2))
|
|
706
|
+
else:
|
|
707
|
+
# Generate markdown report
|
|
708
|
+
markdown = format_monthly_markdown(monthly_data)
|
|
709
|
+
|
|
710
|
+
# Save to file
|
|
711
|
+
with open(output, 'w') as f:
|
|
712
|
+
f.write(markdown)
|
|
713
|
+
|
|
714
|
+
click.echo(f"✓ Monthly trends report saved to {output}")
|
|
715
|
+
click.echo("")
|
|
716
|
+
|
|
717
|
+
# Show summary
|
|
718
|
+
for comparison in monthly_data['comparisons']:
|
|
719
|
+
prev_month = comparison['prev_month']['label']
|
|
720
|
+
curr_month = comparison['curr_month']['label']
|
|
721
|
+
num_increases = len(comparison['increases'])
|
|
722
|
+
num_decreases = len(comparison['decreases'])
|
|
723
|
+
|
|
724
|
+
click.echo(f"{prev_month} → {curr_month}")
|
|
725
|
+
click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
|
|
726
|
+
|
|
727
|
+
if comparison['increases']:
|
|
728
|
+
top = comparison['increases'][0]
|
|
729
|
+
click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
|
|
730
|
+
|
|
731
|
+
click.echo("")
|
|
732
|
+
|
|
733
|
+
|
|
471
734
|
if __name__ == '__main__':
|
|
472
735
|
cli()
|
|
@@ -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)
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
aws_cost_calculator_cli-1.0.2.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
-
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
-
cost_calculator/cli.py,sha256=HsVEZhasXTK1fyeStdJFEng7Dj5l5JK9aMfQxOL6N5I,16320
|
|
4
|
-
aws_cost_calculator_cli-1.0.2.dist-info/METADATA,sha256=ZF9eEr4PJEPlKOeV3wMV56nnZnBAGIdeJpaL821X5QE,4227
|
|
5
|
-
aws_cost_calculator_cli-1.0.2.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
6
|
-
aws_cost_calculator_cli-1.0.2.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
7
|
-
aws_cost_calculator_cli-1.0.2.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
8
|
-
aws_cost_calculator_cli-1.0.2.dist-info/RECORD,,
|
|
File without changes
|
{aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.2.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.2.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.0.2.dist-info → aws_cost_calculator_cli-1.2.0.dist-info}/top_level.txt
RENAMED
|
File without changes
|