aws-cost-calculator-cli 1.3.0__py3-none-any.whl → 1.5.1__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.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/METADATA +13 -9
- aws_cost_calculator_cli-1.5.1.dist-info/RECORD +13 -0
- cost_calculator/api_client.py +84 -0
- cost_calculator/cli.py +124 -97
- cost_calculator/executor.py +230 -0
- aws_cost_calculator_cli-1.3.0.dist-info/RECORD +0 -11
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/top_level.txt +0 -0
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
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
|
|
@@ -241,14 +242,17 @@ The `drill` command allows you to investigate cost changes at different levels o
|
|
|
241
242
|
3. **Drill deeper:** `cc drill --service "EC2 - Other" --account 123` → See usage types
|
|
242
243
|
|
|
243
244
|
**Features:**
|
|
244
|
-
- **
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
- **
|
|
250
|
-
- **
|
|
251
|
-
- **
|
|
245
|
+
- **Week-over-week cost analysis**: Compare costs between consecutive weeks
|
|
246
|
+
- **Month-over-month cost analysis**: Compare costs between consecutive months
|
|
247
|
+
- **Drill-down analysis**: Analyze costs by service, account, or usage type
|
|
248
|
+
- **Pandas aggregations**: Time series analysis with sum, avg, std across all weeks
|
|
249
|
+
- **Volatility detection**: Identify services with high cost variability and outliers
|
|
250
|
+
- **Trend detection**: Auto-detect increasing/decreasing cost patterns
|
|
251
|
+
- **Search & filter**: Find services by pattern or cost threshold
|
|
252
|
+
- **Profile management**: CRUD operations for account profiles in DynamoDB
|
|
253
|
+
- **Markdown reports**: Generate formatted reports
|
|
254
|
+
- **JSON output**: Machine-readable output for automation
|
|
255
|
+
- **Lambda API backend**: Serverless backend with pandas/numpy support
|
|
252
256
|
|
|
253
257
|
Example output:
|
|
254
258
|
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
aws_cost_calculator_cli-1.5.1.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
+
cost_calculator/api_client.py,sha256=LUzQmveDF0X9MqAyThp9mbSzJzkOO73Pk4F7IEJjASU,2353
|
|
4
|
+
cost_calculator/cli.py,sha256=ufK28divdvrceEryWd8cCWjvG5pT2owaqprskX2epeQ,32589
|
|
5
|
+
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
6
|
+
cost_calculator/executor.py,sha256=aPYEm8KdQl7xTpG1gvJ-2adAIJ2PW1_ly27xggQxNNE,7671
|
|
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.5.1.dist-info/METADATA,sha256=g9xQIorywmJ-Xq3zQO0IYdW7otVkkDkZhJwbXgN5PjU,8176
|
|
10
|
+
aws_cost_calculator_cli-1.5.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
11
|
+
aws_cost_calculator_cli-1.5.1.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
12
|
+
aws_cost_calculator_cli-1.5.1.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
13
|
+
aws_cost_calculator_cli-1.5.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,84 @@
|
|
|
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.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
dict: API configuration with api_secret, or None if not configured
|
|
17
|
+
"""
|
|
18
|
+
api_secret = os.environ.get('COST_API_SECRET', '')
|
|
19
|
+
|
|
20
|
+
if api_secret:
|
|
21
|
+
return {'api_secret': api_secret}
|
|
22
|
+
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def call_lambda_api(endpoint, credentials, accounts, **kwargs):
|
|
27
|
+
"""
|
|
28
|
+
Call Lambda API endpoint.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
endpoint: API endpoint name ('trends', 'monthly', 'drill')
|
|
32
|
+
credentials: dict with AWS credentials
|
|
33
|
+
accounts: list of account IDs
|
|
34
|
+
**kwargs: additional parameters for the specific endpoint
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: API response data
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
Exception: if API call fails
|
|
41
|
+
"""
|
|
42
|
+
api_config = get_api_config()
|
|
43
|
+
|
|
44
|
+
if not api_config:
|
|
45
|
+
raise Exception("API not configured. Set COST_API_SECRET environment variable.")
|
|
46
|
+
|
|
47
|
+
# Map endpoint names to Lambda URLs
|
|
48
|
+
endpoint_urls = {
|
|
49
|
+
'trends': 'https://pq3mqntc6vuwi4zw5flulsoleq0yiqtl.lambda-url.us-east-1.on.aws/',
|
|
50
|
+
'monthly': 'https://6aueebodw6q4zdeu3aaexb6tle0fqhhr.lambda-url.us-east-1.on.aws/',
|
|
51
|
+
'drill': 'https://3ncm2gzxrsyptrhud3ua3x5lju0akvsr.lambda-url.us-east-1.on.aws/',
|
|
52
|
+
'analyze': 'https://y6npmidtxwzg62nrqzkbacfs5q0edwgs.lambda-url.us-east-1.on.aws/',
|
|
53
|
+
'profiles': 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
url = endpoint_urls.get(endpoint)
|
|
57
|
+
|
|
58
|
+
if not url:
|
|
59
|
+
raise Exception(f"Unknown endpoint: {endpoint}")
|
|
60
|
+
|
|
61
|
+
# Build request payload
|
|
62
|
+
payload = {
|
|
63
|
+
'credentials': credentials,
|
|
64
|
+
'accounts': accounts
|
|
65
|
+
}
|
|
66
|
+
payload.update(kwargs)
|
|
67
|
+
|
|
68
|
+
# Make API call
|
|
69
|
+
headers = {
|
|
70
|
+
'X-API-Secret': api_config['api_secret'],
|
|
71
|
+
'Content-Type': 'application/json'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
response = requests.post(url, headers=headers, json=payload, timeout=300)
|
|
75
|
+
|
|
76
|
+
if response.status_code != 200:
|
|
77
|
+
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
78
|
+
|
|
79
|
+
return response.json()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_api_configured():
|
|
83
|
+
"""Check if API is configured."""
|
|
84
|
+
return get_api_config() is not None
|
cost_calculator/cli.py
CHANGED
|
@@ -13,9 +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
|
|
17
|
-
from cost_calculator.monthly import
|
|
18
|
-
from cost_calculator.drill import
|
|
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
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def load_profile(profile_name):
|
|
@@ -553,48 +554,11 @@ def trends(profile, weeks, output, json_output):
|
|
|
553
554
|
# Load profile configuration
|
|
554
555
|
config = load_profile(profile)
|
|
555
556
|
|
|
556
|
-
# Initialize boto3 client
|
|
557
|
-
try:
|
|
558
|
-
if 'aws_profile' in config:
|
|
559
|
-
aws_profile = config['aws_profile']
|
|
560
|
-
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
561
|
-
session = boto3.Session(profile_name=aws_profile)
|
|
562
|
-
ce_client = session.client('ce', region_name='us-east-1')
|
|
563
|
-
else:
|
|
564
|
-
creds = config['credentials']
|
|
565
|
-
click.echo(f"AWS Credentials: Static")
|
|
566
|
-
|
|
567
|
-
session_kwargs = {
|
|
568
|
-
'aws_access_key_id': creds['aws_access_key_id'],
|
|
569
|
-
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
570
|
-
'region_name': creds.get('region', 'us-east-1')
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
if 'aws_session_token' in creds:
|
|
574
|
-
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
575
|
-
|
|
576
|
-
session = boto3.Session(**session_kwargs)
|
|
577
|
-
ce_client = session.client('ce')
|
|
578
|
-
|
|
579
|
-
except Exception as e:
|
|
580
|
-
if 'Token has expired' in str(e) or 'sso' in str(e).lower():
|
|
581
|
-
if 'aws_profile' in config:
|
|
582
|
-
raise click.ClickException(
|
|
583
|
-
f"AWS SSO session expired or not initialized.\n"
|
|
584
|
-
f"Run: aws sso login --profile {config['aws_profile']}"
|
|
585
|
-
)
|
|
586
|
-
else:
|
|
587
|
-
raise click.ClickException(
|
|
588
|
-
f"AWS credentials expired.\n"
|
|
589
|
-
f"Run: cc configure --profile {profile}"
|
|
590
|
-
)
|
|
591
|
-
raise
|
|
592
|
-
|
|
593
557
|
click.echo(f"Analyzing last {weeks} weeks...")
|
|
594
558
|
click.echo("")
|
|
595
559
|
|
|
596
|
-
#
|
|
597
|
-
trends_data =
|
|
560
|
+
# Execute via API or locally
|
|
561
|
+
trends_data = execute_trends(config, weeks)
|
|
598
562
|
|
|
599
563
|
if json_output:
|
|
600
564
|
# Output as JSON
|
|
@@ -656,34 +620,11 @@ def monthly(profile, months, output, json_output):
|
|
|
656
620
|
# Load profile configuration
|
|
657
621
|
config = load_profile(profile)
|
|
658
622
|
|
|
659
|
-
# Initialize boto3 client
|
|
660
|
-
try:
|
|
661
|
-
if 'aws_profile' in config:
|
|
662
|
-
aws_profile = config['aws_profile']
|
|
663
|
-
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
664
|
-
session = boto3.Session(profile_name=aws_profile)
|
|
665
|
-
else:
|
|
666
|
-
# Use static credentials
|
|
667
|
-
creds = config['credentials']
|
|
668
|
-
click.echo("AWS Credentials: Static")
|
|
669
|
-
session = boto3.Session(
|
|
670
|
-
aws_access_key_id=creds['aws_access_key_id'],
|
|
671
|
-
aws_secret_access_key=creds['aws_secret_access_key'],
|
|
672
|
-
aws_session_token=creds.get('aws_session_token')
|
|
673
|
-
)
|
|
674
|
-
|
|
675
|
-
ce_client = session.client('ce', region_name='us-east-1')
|
|
676
|
-
except Exception as e:
|
|
677
|
-
raise click.ClickException(f"Failed to initialize AWS session: {str(e)}")
|
|
678
|
-
|
|
679
|
-
# Get account list
|
|
680
|
-
accounts = config['accounts']
|
|
681
|
-
|
|
682
623
|
click.echo(f"Analyzing last {months} months...")
|
|
683
624
|
click.echo("")
|
|
684
625
|
|
|
685
|
-
#
|
|
686
|
-
monthly_data =
|
|
626
|
+
# Execute via API or locally
|
|
627
|
+
monthly_data = execute_monthly(config, months)
|
|
687
628
|
|
|
688
629
|
if json_output:
|
|
689
630
|
# Output as JSON
|
|
@@ -746,29 +687,6 @@ def drill(profile, weeks, service, account, usage_type, output, json_output):
|
|
|
746
687
|
# Load profile configuration
|
|
747
688
|
config = load_profile(profile)
|
|
748
689
|
|
|
749
|
-
# Initialize boto3 client
|
|
750
|
-
try:
|
|
751
|
-
if 'aws_profile' in config:
|
|
752
|
-
aws_profile = config['aws_profile']
|
|
753
|
-
click.echo(f"AWS Profile: {aws_profile} (SSO)")
|
|
754
|
-
session = boto3.Session(profile_name=aws_profile)
|
|
755
|
-
else:
|
|
756
|
-
# Use static credentials
|
|
757
|
-
creds = config['credentials']
|
|
758
|
-
click.echo("AWS Credentials: Static")
|
|
759
|
-
session = boto3.Session(
|
|
760
|
-
aws_access_key_id=creds['aws_access_key_id'],
|
|
761
|
-
aws_secret_access_key=creds['aws_secret_access_key'],
|
|
762
|
-
aws_session_token=creds.get('aws_session_token')
|
|
763
|
-
)
|
|
764
|
-
|
|
765
|
-
ce_client = session.client('ce', region_name='us-east-1')
|
|
766
|
-
except Exception as e:
|
|
767
|
-
raise click.ClickException(f"Failed to initialize AWS session: {str(e)}")
|
|
768
|
-
|
|
769
|
-
# Get account list
|
|
770
|
-
accounts = config['accounts']
|
|
771
|
-
|
|
772
690
|
# Show filters
|
|
773
691
|
click.echo(f"Analyzing last {weeks} weeks...")
|
|
774
692
|
if service:
|
|
@@ -779,13 +697,8 @@ def drill(profile, weeks, service, account, usage_type, output, json_output):
|
|
|
779
697
|
click.echo(f" Usage type filter: {usage_type}")
|
|
780
698
|
click.echo("")
|
|
781
699
|
|
|
782
|
-
#
|
|
783
|
-
drill_data =
|
|
784
|
-
ce_client, accounts, weeks,
|
|
785
|
-
service_filter=service,
|
|
786
|
-
account_filter=account,
|
|
787
|
-
usage_type_filter=usage_type
|
|
788
|
-
)
|
|
700
|
+
# Execute via API or locally
|
|
701
|
+
drill_data = execute_drill(config, weeks, service, account, usage_type)
|
|
789
702
|
|
|
790
703
|
if json_output:
|
|
791
704
|
# Output as JSON
|
|
@@ -844,5 +757,119 @@ def drill(profile, weeks, service, account, usage_type, output, json_output):
|
|
|
844
757
|
click.echo("")
|
|
845
758
|
|
|
846
759
|
|
|
760
|
+
@cli.command()
|
|
761
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
762
|
+
@click.option('--type', 'analysis_type', default='summary',
|
|
763
|
+
type=click.Choice(['summary', 'volatility', 'trends', 'search']),
|
|
764
|
+
help='Analysis type')
|
|
765
|
+
@click.option('--weeks', default=12, help='Number of weeks (default: 12)')
|
|
766
|
+
@click.option('--pattern', help='Service search pattern (for search type)')
|
|
767
|
+
@click.option('--min-cost', type=float, help='Minimum cost filter (for search type)')
|
|
768
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
769
|
+
def analyze(profile, analysis_type, weeks, pattern, min_cost, json_output):
|
|
770
|
+
"""Perform pandas-based analysis (aggregations, volatility, trends, search)"""
|
|
771
|
+
|
|
772
|
+
config = load_profile(profile)
|
|
773
|
+
|
|
774
|
+
if not json_output:
|
|
775
|
+
click.echo(f"Running {analysis_type} analysis for {weeks} weeks...")
|
|
776
|
+
|
|
777
|
+
from cost_calculator.executor import execute_analyze
|
|
778
|
+
result = execute_analyze(config, weeks, analysis_type, pattern, min_cost)
|
|
779
|
+
|
|
780
|
+
if json_output:
|
|
781
|
+
import json
|
|
782
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
783
|
+
else:
|
|
784
|
+
# Format output based on type
|
|
785
|
+
if analysis_type == 'summary':
|
|
786
|
+
click.echo(f"\n📊 Summary ({result.get('total_services', 0)} services)")
|
|
787
|
+
click.echo(f"Weeks analyzed: {result.get('weeks_analyzed', 0)}")
|
|
788
|
+
click.echo(f"\nTop 10 Services (by total change):")
|
|
789
|
+
for svc in result.get('services', [])[:10]:
|
|
790
|
+
click.echo(f" {svc['service']}")
|
|
791
|
+
click.echo(f" Total: ${svc['change_sum']:,.2f}")
|
|
792
|
+
click.echo(f" Average: ${svc['change_mean']:,.2f}")
|
|
793
|
+
click.echo(f" Volatility: {svc['volatility']:.3f}")
|
|
794
|
+
|
|
795
|
+
elif analysis_type == 'volatility':
|
|
796
|
+
click.echo(f"\n📈 High Volatility Services:")
|
|
797
|
+
for svc in result.get('high_volatility_services', [])[:10]:
|
|
798
|
+
click.echo(f" {svc['service']}: CV={svc['coefficient_of_variation']:.3f}")
|
|
799
|
+
|
|
800
|
+
outliers = result.get('outliers', [])
|
|
801
|
+
if outliers:
|
|
802
|
+
click.echo(f"\n⚠️ Outliers ({len(outliers)}):")
|
|
803
|
+
for o in outliers[:5]:
|
|
804
|
+
click.echo(f" {o['service']} ({o['week']}): ${o['change']:,.2f} (z={o['z_score']:.2f})")
|
|
805
|
+
|
|
806
|
+
elif analysis_type == 'trends':
|
|
807
|
+
inc = result.get('increasing_trends', [])
|
|
808
|
+
dec = result.get('decreasing_trends', [])
|
|
809
|
+
|
|
810
|
+
click.echo(f"\n📈 Increasing Trends ({len(inc)}):")
|
|
811
|
+
for t in inc[:5]:
|
|
812
|
+
click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
|
|
813
|
+
|
|
814
|
+
click.echo(f"\n📉 Decreasing Trends ({len(dec)}):")
|
|
815
|
+
for t in dec[:5]:
|
|
816
|
+
click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
|
|
817
|
+
|
|
818
|
+
elif analysis_type == 'search':
|
|
819
|
+
matches = result.get('matches', [])
|
|
820
|
+
click.echo(f"\n🔍 Search Results ({len(matches)} matches)")
|
|
821
|
+
if pattern:
|
|
822
|
+
click.echo(f"Pattern: {pattern}")
|
|
823
|
+
if min_cost:
|
|
824
|
+
click.echo(f"Min cost: ${min_cost:,.2f}")
|
|
825
|
+
|
|
826
|
+
for m in matches[:20]:
|
|
827
|
+
click.echo(f" {m['service']}: ${m['curr_cost']:,.2f}")
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
@cli.command()
|
|
831
|
+
@click.argument('operation', type=click.Choice(['list', 'get', 'create', 'update', 'delete']))
|
|
832
|
+
@click.option('--name', help='Profile name')
|
|
833
|
+
@click.option('--accounts', help='Comma-separated account IDs')
|
|
834
|
+
@click.option('--description', help='Profile description')
|
|
835
|
+
def profile(operation, name, accounts, description):
|
|
836
|
+
"""Manage profiles (CRUD operations)"""
|
|
837
|
+
|
|
838
|
+
from cost_calculator.executor import execute_profile_operation
|
|
839
|
+
|
|
840
|
+
# Parse accounts if provided
|
|
841
|
+
account_list = None
|
|
842
|
+
if accounts:
|
|
843
|
+
account_list = [a.strip() for a in accounts.split(',')]
|
|
844
|
+
|
|
845
|
+
result = execute_profile_operation(
|
|
846
|
+
operation=operation,
|
|
847
|
+
profile_name=name,
|
|
848
|
+
accounts=account_list,
|
|
849
|
+
description=description
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
if operation == 'list':
|
|
853
|
+
profiles = result.get('profiles', [])
|
|
854
|
+
click.echo(f"\n📋 Profiles ({len(profiles)}):")
|
|
855
|
+
for p in profiles:
|
|
856
|
+
click.echo(f" {p['profile_name']}: {len(p.get('accounts', []))} accounts")
|
|
857
|
+
if p.get('description'):
|
|
858
|
+
click.echo(f" {p['description']}")
|
|
859
|
+
|
|
860
|
+
elif operation == 'get':
|
|
861
|
+
profile_data = result.get('profile', {})
|
|
862
|
+
click.echo(f"\n📋 Profile: {profile_data.get('profile_name')}")
|
|
863
|
+
click.echo(f"Accounts: {len(profile_data.get('accounts', []))}")
|
|
864
|
+
if profile_data.get('description'):
|
|
865
|
+
click.echo(f"Description: {profile_data['description']}")
|
|
866
|
+
click.echo(f"\nAccounts:")
|
|
867
|
+
for acc in profile_data.get('accounts', []):
|
|
868
|
+
click.echo(f" {acc}")
|
|
869
|
+
|
|
870
|
+
else:
|
|
871
|
+
click.echo(result.get('message', 'Operation completed'))
|
|
872
|
+
|
|
873
|
+
|
|
847
874
|
if __name__ == '__main__':
|
|
848
875
|
cli()
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Executor that routes to either API or local execution.
|
|
3
|
+
"""
|
|
4
|
+
import boto3
|
|
5
|
+
import click
|
|
6
|
+
from cost_calculator.api_client import is_api_configured, call_lambda_api
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_credentials_dict(config):
|
|
10
|
+
"""
|
|
11
|
+
Extract credentials from config in format needed for API.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
dict with access_key, secret_key, session_token
|
|
15
|
+
"""
|
|
16
|
+
if 'aws_profile' in config:
|
|
17
|
+
# Get temporary credentials from SSO session
|
|
18
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
19
|
+
credentials = session.get_credentials()
|
|
20
|
+
frozen_creds = credentials.get_frozen_credentials()
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
'access_key': frozen_creds.access_key,
|
|
24
|
+
'secret_key': frozen_creds.secret_key,
|
|
25
|
+
'session_token': frozen_creds.token
|
|
26
|
+
}
|
|
27
|
+
else:
|
|
28
|
+
# Use static credentials
|
|
29
|
+
creds = config['credentials']
|
|
30
|
+
result = {
|
|
31
|
+
'access_key': creds['aws_access_key_id'],
|
|
32
|
+
'secret_key': creds['aws_secret_access_key']
|
|
33
|
+
}
|
|
34
|
+
if 'aws_session_token' in creds:
|
|
35
|
+
result['session_token'] = creds['aws_session_token']
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def execute_trends(config, weeks):
|
|
40
|
+
"""
|
|
41
|
+
Execute trends analysis via API or locally.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
dict: trends data
|
|
45
|
+
"""
|
|
46
|
+
accounts = config['accounts']
|
|
47
|
+
|
|
48
|
+
if is_api_configured():
|
|
49
|
+
# Use API
|
|
50
|
+
click.echo("Using Lambda API...")
|
|
51
|
+
credentials = get_credentials_dict(config)
|
|
52
|
+
return call_lambda_api('trends', credentials, accounts, weeks=weeks)
|
|
53
|
+
else:
|
|
54
|
+
# Use local execution
|
|
55
|
+
click.echo("Using local execution...")
|
|
56
|
+
from cost_calculator.trends import analyze_trends
|
|
57
|
+
|
|
58
|
+
# Initialize boto3 client
|
|
59
|
+
if 'aws_profile' in config:
|
|
60
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
61
|
+
else:
|
|
62
|
+
creds = config['credentials']
|
|
63
|
+
session_kwargs = {
|
|
64
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
65
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
66
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
67
|
+
}
|
|
68
|
+
if 'aws_session_token' in creds:
|
|
69
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
70
|
+
session = boto3.Session(**session_kwargs)
|
|
71
|
+
|
|
72
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
73
|
+
return analyze_trends(ce_client, accounts, weeks)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def execute_monthly(config, months):
|
|
77
|
+
"""
|
|
78
|
+
Execute monthly analysis via API or locally.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
dict: monthly data
|
|
82
|
+
"""
|
|
83
|
+
accounts = config['accounts']
|
|
84
|
+
|
|
85
|
+
if is_api_configured():
|
|
86
|
+
# Use API
|
|
87
|
+
click.echo("Using Lambda API...")
|
|
88
|
+
credentials = get_credentials_dict(config)
|
|
89
|
+
return call_lambda_api('monthly', credentials, accounts, months=months)
|
|
90
|
+
else:
|
|
91
|
+
# Use local execution
|
|
92
|
+
click.echo("Using local execution...")
|
|
93
|
+
from cost_calculator.monthly import analyze_monthly_trends
|
|
94
|
+
|
|
95
|
+
# Initialize boto3 client
|
|
96
|
+
if 'aws_profile' in config:
|
|
97
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
98
|
+
else:
|
|
99
|
+
creds = config['credentials']
|
|
100
|
+
session_kwargs = {
|
|
101
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
102
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
103
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
104
|
+
}
|
|
105
|
+
if 'aws_session_token' in creds:
|
|
106
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
107
|
+
session = boto3.Session(**session_kwargs)
|
|
108
|
+
|
|
109
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
110
|
+
return analyze_monthly_trends(ce_client, accounts, months)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None):
|
|
114
|
+
"""
|
|
115
|
+
Execute drill-down analysis via API or locally.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
dict: drill data
|
|
119
|
+
"""
|
|
120
|
+
accounts = config['accounts']
|
|
121
|
+
|
|
122
|
+
if is_api_configured():
|
|
123
|
+
# Use API
|
|
124
|
+
click.echo("Using Lambda API...")
|
|
125
|
+
credentials = get_credentials_dict(config)
|
|
126
|
+
kwargs = {'weeks': weeks}
|
|
127
|
+
if service_filter:
|
|
128
|
+
kwargs['service'] = service_filter
|
|
129
|
+
if account_filter:
|
|
130
|
+
kwargs['account'] = account_filter
|
|
131
|
+
if usage_type_filter:
|
|
132
|
+
kwargs['usage_type'] = usage_type_filter
|
|
133
|
+
return call_lambda_api('drill', credentials, accounts, **kwargs)
|
|
134
|
+
else:
|
|
135
|
+
# Use local execution
|
|
136
|
+
click.echo("Using local execution...")
|
|
137
|
+
from cost_calculator.drill import analyze_drill_down
|
|
138
|
+
|
|
139
|
+
# Initialize boto3 client
|
|
140
|
+
if 'aws_profile' in config:
|
|
141
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
142
|
+
else:
|
|
143
|
+
creds = config['credentials']
|
|
144
|
+
session_kwargs = {
|
|
145
|
+
'aws_access_key_id': creds['aws_access_key_id'],
|
|
146
|
+
'aws_secret_access_key': creds['aws_secret_access_key'],
|
|
147
|
+
'region_name': creds.get('region', 'us-east-1')
|
|
148
|
+
}
|
|
149
|
+
if 'aws_session_token' in creds:
|
|
150
|
+
session_kwargs['aws_session_token'] = creds['aws_session_token']
|
|
151
|
+
session = boto3.Session(**session_kwargs)
|
|
152
|
+
|
|
153
|
+
ce_client = session.client('ce', region_name='us-east-1')
|
|
154
|
+
return analyze_drill_down(
|
|
155
|
+
ce_client, accounts, weeks,
|
|
156
|
+
service_filter=service_filter,
|
|
157
|
+
account_filter=account_filter,
|
|
158
|
+
usage_type_filter=usage_type_filter
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
|
|
163
|
+
"""
|
|
164
|
+
Execute pandas-based analysis via API.
|
|
165
|
+
Note: This only works via API (requires pandas layer).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
dict: analysis results
|
|
169
|
+
"""
|
|
170
|
+
accounts = config['accounts']
|
|
171
|
+
|
|
172
|
+
if not is_api_configured():
|
|
173
|
+
raise click.ClickException(
|
|
174
|
+
"Analyze command requires API configuration.\n"
|
|
175
|
+
"Set COST_API_SECRET environment variable."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
credentials = get_credentials_dict(config)
|
|
179
|
+
kwargs = {'weeks': weeks, 'type': analysis_type}
|
|
180
|
+
|
|
181
|
+
if pattern:
|
|
182
|
+
kwargs['pattern'] = pattern
|
|
183
|
+
if min_cost:
|
|
184
|
+
kwargs['min_cost'] = min_cost
|
|
185
|
+
|
|
186
|
+
return call_lambda_api('analyze', credentials, accounts, **kwargs)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def execute_profile_operation(operation, profile_name=None, accounts=None, description=None):
|
|
190
|
+
"""
|
|
191
|
+
Execute profile CRUD operations via API.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
dict: operation result
|
|
195
|
+
"""
|
|
196
|
+
if not is_api_configured():
|
|
197
|
+
raise click.ClickException(
|
|
198
|
+
"Profile commands require API configuration.\n"
|
|
199
|
+
"Set COST_API_SECRET environment variable."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Profile operations don't need AWS credentials, just API secret
|
|
203
|
+
import os
|
|
204
|
+
import requests
|
|
205
|
+
import json
|
|
206
|
+
|
|
207
|
+
api_secret = os.environ.get('COST_API_SECRET', '')
|
|
208
|
+
|
|
209
|
+
# Use profiles endpoint (hardcoded URL)
|
|
210
|
+
url = 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
|
|
211
|
+
|
|
212
|
+
payload = {'operation': operation}
|
|
213
|
+
if profile_name:
|
|
214
|
+
payload['profile_name'] = profile_name
|
|
215
|
+
if accounts:
|
|
216
|
+
payload['accounts'] = accounts
|
|
217
|
+
if description:
|
|
218
|
+
payload['description'] = description
|
|
219
|
+
|
|
220
|
+
headers = {
|
|
221
|
+
'X-API-Secret': api_secret,
|
|
222
|
+
'Content-Type': 'application/json'
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
|
226
|
+
|
|
227
|
+
if response.status_code != 200:
|
|
228
|
+
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
229
|
+
|
|
230
|
+
return response.json()
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
aws_cost_calculator_cli-1.3.0.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
-
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
-
cost_calculator/cli.py,sha256=YJOWA-uWSMEZc4QHof8DF_An6VpKmTsxh0X_G31y1SM,31018
|
|
4
|
-
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
5
|
-
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
6
|
-
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
7
|
-
aws_cost_calculator_cli-1.3.0.dist-info/METADATA,sha256=jZYbRF46p0qvjkw4Dqwja-4cnOJv59i7zTzmFXxfCsw,7761
|
|
8
|
-
aws_cost_calculator_cli-1.3.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
9
|
-
aws_cost_calculator_cli-1.3.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
10
|
-
aws_cost_calculator_cli-1.3.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
11
|
-
aws_cost_calculator_cli-1.3.0.dist-info/RECORD,,
|
|
File without changes
|
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.3.0.dist-info → aws_cost_calculator_cli-1.5.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|