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