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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 1.0.2
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. List profiles
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,,