aws-cost-calculator-cli 1.4.0__py3-none-any.whl → 1.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aws_cost_calculator_cli-1.4.0.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/METADATA +161 -32
- aws_cost_calculator_cli-1.6.2.dist-info/RECORD +25 -0
- {aws_cost_calculator_cli-1.4.0.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/WHEEL +1 -1
- {aws_cost_calculator_cli-1.4.0.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/top_level.txt +1 -0
- backend/__init__.py +1 -0
- backend/algorithms/__init__.py +1 -0
- backend/algorithms/analyze.py +272 -0
- backend/algorithms/drill.py +323 -0
- backend/algorithms/monthly.py +242 -0
- backend/algorithms/trends.py +353 -0
- backend/handlers/__init__.py +1 -0
- backend/handlers/analyze.py +112 -0
- backend/handlers/drill.py +117 -0
- backend/handlers/monthly.py +106 -0
- backend/handlers/profiles.py +148 -0
- backend/handlers/trends.py +106 -0
- cost_calculator/api_client.py +13 -34
- cost_calculator/cli.py +283 -41
- cost_calculator/executor.py +93 -11
- aws_cost_calculator_cli-1.4.0.dist-info/RECORD +0 -13
- {aws_cost_calculator_cli-1.4.0.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.4.0.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/licenses/LICENSE +0 -0
cost_calculator/cli.py
CHANGED
|
@@ -19,49 +19,143 @@ from cost_calculator.drill import format_drill_down_markdown
|
|
|
19
19
|
from cost_calculator.executor import execute_trends, execute_monthly, execute_drill
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def apply_auth_options(config, sso=None, access_key_id=None, secret_access_key=None, session_token=None):
|
|
23
|
+
"""Apply authentication options to profile config
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Profile configuration dict
|
|
27
|
+
sso: AWS SSO profile name
|
|
28
|
+
access_key_id: AWS Access Key ID
|
|
29
|
+
secret_access_key: AWS Secret Access Key
|
|
30
|
+
session_token: AWS Session Token
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Updated config dict
|
|
34
|
+
"""
|
|
35
|
+
import subprocess
|
|
36
|
+
|
|
37
|
+
if sso:
|
|
38
|
+
# SSO authentication - trigger login if needed
|
|
39
|
+
try:
|
|
40
|
+
# Test if SSO session is valid
|
|
41
|
+
result = subprocess.run(
|
|
42
|
+
['aws', 'sts', 'get-caller-identity', '--profile', sso],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
timeout=5
|
|
46
|
+
)
|
|
47
|
+
if result.returncode != 0:
|
|
48
|
+
if 'expired' in result.stderr.lower() or 'token' in result.stderr.lower():
|
|
49
|
+
click.echo(f"SSO session expired or not initialized. Logging in...")
|
|
50
|
+
subprocess.run(['aws', 'sso', 'login', '--profile', sso], check=True)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
click.echo(f"Warning: Could not verify SSO session: {e}")
|
|
53
|
+
|
|
54
|
+
config['aws_profile'] = sso
|
|
55
|
+
elif access_key_id and secret_access_key:
|
|
56
|
+
# Static credentials provided via CLI
|
|
57
|
+
config['credentials'] = {
|
|
58
|
+
'aws_access_key_id': access_key_id,
|
|
59
|
+
'aws_secret_access_key': secret_access_key,
|
|
60
|
+
'region': 'us-east-1'
|
|
61
|
+
}
|
|
62
|
+
if session_token:
|
|
63
|
+
config['credentials']['aws_session_token'] = session_token
|
|
64
|
+
|
|
65
|
+
return config
|
|
66
|
+
|
|
67
|
+
|
|
22
68
|
def load_profile(profile_name):
|
|
23
|
-
"""Load profile configuration from
|
|
69
|
+
"""Load profile configuration from local file or DynamoDB API"""
|
|
70
|
+
import os
|
|
71
|
+
import requests
|
|
72
|
+
|
|
24
73
|
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
25
74
|
config_file = config_dir / 'profiles.json'
|
|
26
75
|
creds_file = config_dir / 'credentials.json'
|
|
27
76
|
|
|
28
|
-
|
|
77
|
+
# Try local file first
|
|
78
|
+
if config_file.exists():
|
|
79
|
+
with open(config_file) as f:
|
|
80
|
+
profiles = json.load(f)
|
|
81
|
+
|
|
82
|
+
if profile_name in profiles:
|
|
83
|
+
profile = profiles[profile_name]
|
|
84
|
+
|
|
85
|
+
# Load credentials if using static credentials (not SSO)
|
|
86
|
+
if 'aws_profile' not in profile:
|
|
87
|
+
if not creds_file.exists():
|
|
88
|
+
# Try environment variables
|
|
89
|
+
if os.environ.get('AWS_ACCESS_KEY_ID'):
|
|
90
|
+
profile['credentials'] = {
|
|
91
|
+
'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
|
|
92
|
+
'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
93
|
+
'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
|
|
94
|
+
}
|
|
95
|
+
return profile
|
|
96
|
+
|
|
97
|
+
raise click.ClickException(
|
|
98
|
+
f"No credentials found for profile '{profile_name}'.\n"
|
|
99
|
+
f"Run: cc configure --profile {profile_name}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
with open(creds_file) as f:
|
|
103
|
+
creds = json.load(f)
|
|
104
|
+
|
|
105
|
+
if profile_name not in creds:
|
|
106
|
+
raise click.ClickException(
|
|
107
|
+
f"No credentials found for profile '{profile_name}'.\n"
|
|
108
|
+
f"Run: cc configure --profile {profile_name}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
profile['credentials'] = creds[profile_name]
|
|
112
|
+
|
|
113
|
+
return profile
|
|
114
|
+
|
|
115
|
+
# Profile not found locally - try DynamoDB API
|
|
116
|
+
api_secret = os.environ.get('COST_API_SECRET')
|
|
117
|
+
if not api_secret:
|
|
29
118
|
raise click.ClickException(
|
|
30
|
-
f"Profile
|
|
119
|
+
f"Profile '{profile_name}' not found locally and COST_API_SECRET not set.\n"
|
|
31
120
|
f"Run: cc init --profile {profile_name}"
|
|
32
121
|
)
|
|
33
122
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
f"Available profiles: {', '.join(profiles.keys())}"
|
|
123
|
+
try:
|
|
124
|
+
response = requests.post(
|
|
125
|
+
'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
|
|
126
|
+
headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
|
|
127
|
+
json={'operation': 'get', 'profile_name': profile_name},
|
|
128
|
+
timeout=10
|
|
41
129
|
)
|
|
42
|
-
|
|
43
|
-
profile = profiles[profile_name]
|
|
44
|
-
|
|
45
|
-
# Load credentials if using static credentials (not SSO)
|
|
46
|
-
if 'aws_profile' not in profile:
|
|
47
|
-
if not creds_file.exists():
|
|
48
|
-
raise click.ClickException(
|
|
49
|
-
f"No credentials found for profile '{profile_name}'.\n"
|
|
50
|
-
f"Run: cc configure --profile {profile_name}"
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
with open(creds_file) as f:
|
|
54
|
-
creds = json.load(f)
|
|
55
130
|
|
|
56
|
-
if
|
|
131
|
+
if response.status_code == 200:
|
|
132
|
+
response_data = response.json()
|
|
133
|
+
# API returns {"profile": {...}} wrapper
|
|
134
|
+
profile_data = response_data.get('profile', response_data)
|
|
135
|
+
profile = {'accounts': profile_data['accounts']}
|
|
136
|
+
|
|
137
|
+
# Check for AWS_PROFILE environment variable (SSO support)
|
|
138
|
+
if os.environ.get('AWS_PROFILE'):
|
|
139
|
+
profile['aws_profile'] = os.environ['AWS_PROFILE']
|
|
140
|
+
# Use environment credentials
|
|
141
|
+
elif os.environ.get('AWS_ACCESS_KEY_ID'):
|
|
142
|
+
profile['credentials'] = {
|
|
143
|
+
'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
|
|
144
|
+
'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
145
|
+
'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return profile
|
|
149
|
+
else:
|
|
57
150
|
raise click.ClickException(
|
|
58
|
-
f"
|
|
59
|
-
f"Run: cc
|
|
151
|
+
f"Profile '{profile_name}' not found in DynamoDB.\n"
|
|
152
|
+
f"Run: cc profile create --name {profile_name} --accounts \"...\""
|
|
60
153
|
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
154
|
+
except requests.exceptions.RequestException as e:
|
|
155
|
+
raise click.ClickException(
|
|
156
|
+
f"Failed to fetch profile from API: {e}\n"
|
|
157
|
+
f"Run: cc init --profile {profile_name}"
|
|
158
|
+
)
|
|
65
159
|
|
|
66
160
|
|
|
67
161
|
def calculate_costs(profile_config, accounts, start_date, offset, window):
|
|
@@ -231,7 +325,7 @@ def calculate_costs(profile_config, accounts, start_date, offset, window):
|
|
|
231
325
|
support_month = support_month_date - timedelta(days=1) # Go back to previous month
|
|
232
326
|
days_in_support_month = support_month.day # This gives us the last day of the month
|
|
233
327
|
|
|
234
|
-
# Support allocation: divide by 2 (
|
|
328
|
+
# Support allocation: divide by 2 (50% allocation), then by days in month
|
|
235
329
|
support_per_day = (support_cost / 2) / days_in_support_month
|
|
236
330
|
|
|
237
331
|
# Calculate daily rate
|
|
@@ -296,12 +390,31 @@ def cli():
|
|
|
296
390
|
@click.option('--offset', default=2, help='Days to go back from start date (default: 2)')
|
|
297
391
|
@click.option('--window', default=30, help='Number of days to analyze (default: 30)')
|
|
298
392
|
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
299
|
-
|
|
300
|
-
|
|
393
|
+
@click.option('--sso', help='AWS SSO profile name (e.g., my_sso_profile)')
|
|
394
|
+
@click.option('--access-key-id', help='AWS Access Key ID (for static credentials)')
|
|
395
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key (for static credentials)')
|
|
396
|
+
@click.option('--session-token', help='AWS Session Token (for static credentials)')
|
|
397
|
+
def calculate(profile, start_date, offset, window, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
398
|
+
"""
|
|
399
|
+
Calculate AWS costs for the specified period
|
|
400
|
+
|
|
401
|
+
\b
|
|
402
|
+
Authentication Options:
|
|
403
|
+
1. SSO: --sso <profile_name>
|
|
404
|
+
Example: cc calculate --profile myprofile --sso my_sso_profile
|
|
405
|
+
|
|
406
|
+
2. Static Credentials: --access-key-id, --secret-access-key, --session-token
|
|
407
|
+
Example: cc calculate --profile myprofile --access-key-id ASIA... --secret-access-key ... --session-token ...
|
|
408
|
+
|
|
409
|
+
3. Environment Variables: AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
|
|
410
|
+
"""
|
|
301
411
|
|
|
302
412
|
# Load profile configuration
|
|
303
413
|
config = load_profile(profile)
|
|
304
414
|
|
|
415
|
+
# Apply authentication options
|
|
416
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
417
|
+
|
|
305
418
|
# Calculate costs
|
|
306
419
|
result = calculate_costs(
|
|
307
420
|
profile_config=config,
|
|
@@ -547,12 +660,17 @@ def configure(profile, access_key_id, secret_access_key, session_token, region):
|
|
|
547
660
|
@click.option('--profile', required=True, help='Profile name')
|
|
548
661
|
@click.option('--weeks', default=3, help='Number of weeks to analyze (default: 3)')
|
|
549
662
|
@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
|
|
551
|
-
|
|
663
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
664
|
+
@click.option('--sso', help='AWS SSO profile name')
|
|
665
|
+
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
666
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
667
|
+
@click.option('--session-token', help='AWS Session Token')
|
|
668
|
+
def trends(profile, weeks, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
552
669
|
"""Analyze cost trends with Week-over-Week and Trailing 30-Day comparisons"""
|
|
553
670
|
|
|
554
671
|
# Load profile configuration
|
|
555
672
|
config = load_profile(profile)
|
|
673
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
556
674
|
|
|
557
675
|
click.echo(f"Analyzing last {weeks} weeks...")
|
|
558
676
|
click.echo("")
|
|
@@ -613,12 +731,17 @@ def trends(profile, weeks, output, json_output):
|
|
|
613
731
|
@click.option('--profile', required=True, help='Profile name')
|
|
614
732
|
@click.option('--months', default=6, help='Number of months to analyze (default: 6)')
|
|
615
733
|
@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
|
|
617
|
-
|
|
734
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
735
|
+
@click.option('--sso', help='AWS SSO profile name')
|
|
736
|
+
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
737
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
738
|
+
@click.option('--session-token', help='AWS Session Token')
|
|
739
|
+
def monthly(profile, months, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
618
740
|
"""Analyze month-over-month cost trends at service level"""
|
|
619
741
|
|
|
620
|
-
# Load profile
|
|
742
|
+
# Load profile
|
|
621
743
|
config = load_profile(profile)
|
|
744
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
622
745
|
|
|
623
746
|
click.echo(f"Analyzing last {months} months...")
|
|
624
747
|
click.echo("")
|
|
@@ -680,12 +803,17 @@ def monthly(profile, months, output, json_output):
|
|
|
680
803
|
@click.option('--account', help='Filter by account ID')
|
|
681
804
|
@click.option('--usage-type', help='Filter by usage type')
|
|
682
805
|
@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
|
|
684
|
-
|
|
806
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
807
|
+
@click.option('--sso', help='AWS SSO profile name')
|
|
808
|
+
@click.option('--access-key-id', help='AWS Access Key ID')
|
|
809
|
+
@click.option('--secret-access-key', help='AWS Secret Access Key')
|
|
810
|
+
@click.option('--session-token', help='AWS Session Token')
|
|
811
|
+
def drill(profile, weeks, service, account, usage_type, output, json_output, sso, access_key_id, secret_access_key, session_token):
|
|
685
812
|
"""Drill down into cost changes by service, account, or usage type"""
|
|
686
813
|
|
|
687
|
-
# Load profile
|
|
814
|
+
# Load profile
|
|
688
815
|
config = load_profile(profile)
|
|
816
|
+
config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
|
|
689
817
|
|
|
690
818
|
# Show filters
|
|
691
819
|
click.echo(f"Analyzing last {weeks} weeks...")
|
|
@@ -757,5 +885,119 @@ def drill(profile, weeks, service, account, usage_type, output, json_output):
|
|
|
757
885
|
click.echo("")
|
|
758
886
|
|
|
759
887
|
|
|
888
|
+
@cli.command()
|
|
889
|
+
@click.option('--profile', required=True, help='Profile name')
|
|
890
|
+
@click.option('--type', 'analysis_type', default='summary',
|
|
891
|
+
type=click.Choice(['summary', 'volatility', 'trends', 'search']),
|
|
892
|
+
help='Analysis type')
|
|
893
|
+
@click.option('--weeks', default=12, help='Number of weeks (default: 12)')
|
|
894
|
+
@click.option('--pattern', help='Service search pattern (for search type)')
|
|
895
|
+
@click.option('--min-cost', type=float, help='Minimum cost filter (for search type)')
|
|
896
|
+
@click.option('--json-output', is_flag=True, help='Output as JSON')
|
|
897
|
+
def analyze(profile, analysis_type, weeks, pattern, min_cost, json_output):
|
|
898
|
+
"""Perform pandas-based analysis (aggregations, volatility, trends, search)"""
|
|
899
|
+
|
|
900
|
+
config = load_profile(profile)
|
|
901
|
+
|
|
902
|
+
if not json_output:
|
|
903
|
+
click.echo(f"Running {analysis_type} analysis for {weeks} weeks...")
|
|
904
|
+
|
|
905
|
+
from cost_calculator.executor import execute_analyze
|
|
906
|
+
result = execute_analyze(config, weeks, analysis_type, pattern, min_cost)
|
|
907
|
+
|
|
908
|
+
if json_output:
|
|
909
|
+
import json
|
|
910
|
+
click.echo(json.dumps(result, indent=2, default=str))
|
|
911
|
+
else:
|
|
912
|
+
# Format output based on type
|
|
913
|
+
if analysis_type == 'summary':
|
|
914
|
+
click.echo(f"\n📊 Summary ({result.get('total_services', 0)} services)")
|
|
915
|
+
click.echo(f"Weeks analyzed: {result.get('weeks_analyzed', 0)}")
|
|
916
|
+
click.echo(f"\nTop 10 Services (by total change):")
|
|
917
|
+
for svc in result.get('services', [])[:10]:
|
|
918
|
+
click.echo(f" {svc['service']}")
|
|
919
|
+
click.echo(f" Total: ${svc['change_sum']:,.2f}")
|
|
920
|
+
click.echo(f" Average: ${svc['change_mean']:,.2f}")
|
|
921
|
+
click.echo(f" Volatility: {svc['volatility']:.3f}")
|
|
922
|
+
|
|
923
|
+
elif analysis_type == 'volatility':
|
|
924
|
+
click.echo(f"\n📈 High Volatility Services:")
|
|
925
|
+
for svc in result.get('high_volatility_services', [])[:10]:
|
|
926
|
+
click.echo(f" {svc['service']}: CV={svc['coefficient_of_variation']:.3f}")
|
|
927
|
+
|
|
928
|
+
outliers = result.get('outliers', [])
|
|
929
|
+
if outliers:
|
|
930
|
+
click.echo(f"\n⚠️ Outliers ({len(outliers)}):")
|
|
931
|
+
for o in outliers[:5]:
|
|
932
|
+
click.echo(f" {o['service']} ({o['week']}): ${o['change']:,.2f} (z={o['z_score']:.2f})")
|
|
933
|
+
|
|
934
|
+
elif analysis_type == 'trends':
|
|
935
|
+
inc = result.get('increasing_trends', [])
|
|
936
|
+
dec = result.get('decreasing_trends', [])
|
|
937
|
+
|
|
938
|
+
click.echo(f"\n📈 Increasing Trends ({len(inc)}):")
|
|
939
|
+
for t in inc[:5]:
|
|
940
|
+
click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
|
|
941
|
+
|
|
942
|
+
click.echo(f"\n📉 Decreasing Trends ({len(dec)}):")
|
|
943
|
+
for t in dec[:5]:
|
|
944
|
+
click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
|
|
945
|
+
|
|
946
|
+
elif analysis_type == 'search':
|
|
947
|
+
matches = result.get('matches', [])
|
|
948
|
+
click.echo(f"\n🔍 Search Results ({len(matches)} matches)")
|
|
949
|
+
if pattern:
|
|
950
|
+
click.echo(f"Pattern: {pattern}")
|
|
951
|
+
if min_cost:
|
|
952
|
+
click.echo(f"Min cost: ${min_cost:,.2f}")
|
|
953
|
+
|
|
954
|
+
for m in matches[:20]:
|
|
955
|
+
click.echo(f" {m['service']}: ${m['curr_cost']:,.2f}")
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
@cli.command()
|
|
959
|
+
@click.argument('operation', type=click.Choice(['list', 'get', 'create', 'update', 'delete']))
|
|
960
|
+
@click.option('--name', help='Profile name')
|
|
961
|
+
@click.option('--accounts', help='Comma-separated account IDs')
|
|
962
|
+
@click.option('--description', help='Profile description')
|
|
963
|
+
def profile(operation, name, accounts, description):
|
|
964
|
+
"""Manage profiles (CRUD operations)"""
|
|
965
|
+
|
|
966
|
+
from cost_calculator.executor import execute_profile_operation
|
|
967
|
+
|
|
968
|
+
# Parse accounts if provided
|
|
969
|
+
account_list = None
|
|
970
|
+
if accounts:
|
|
971
|
+
account_list = [a.strip() for a in accounts.split(',')]
|
|
972
|
+
|
|
973
|
+
result = execute_profile_operation(
|
|
974
|
+
operation=operation,
|
|
975
|
+
profile_name=name,
|
|
976
|
+
accounts=account_list,
|
|
977
|
+
description=description
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
if operation == 'list':
|
|
981
|
+
profiles = result.get('profiles', [])
|
|
982
|
+
click.echo(f"\n📋 Profiles ({len(profiles)}):")
|
|
983
|
+
for p in profiles:
|
|
984
|
+
click.echo(f" {p['profile_name']}: {len(p.get('accounts', []))} accounts")
|
|
985
|
+
if p.get('description'):
|
|
986
|
+
click.echo(f" {p['description']}")
|
|
987
|
+
|
|
988
|
+
elif operation == 'get':
|
|
989
|
+
profile_data = result.get('profile', {})
|
|
990
|
+
click.echo(f"\n📋 Profile: {profile_data.get('profile_name')}")
|
|
991
|
+
click.echo(f"Accounts: {len(profile_data.get('accounts', []))}")
|
|
992
|
+
if profile_data.get('description'):
|
|
993
|
+
click.echo(f"Description: {profile_data['description']}")
|
|
994
|
+
click.echo(f"\nAccounts:")
|
|
995
|
+
for acc in profile_data.get('accounts', []):
|
|
996
|
+
click.echo(f" {acc}")
|
|
997
|
+
|
|
998
|
+
else:
|
|
999
|
+
click.echo(result.get('message', 'Operation completed'))
|
|
1000
|
+
|
|
1001
|
+
|
|
760
1002
|
if __name__ == '__main__':
|
|
761
1003
|
cli()
|
cost_calculator/executor.py
CHANGED
|
@@ -11,22 +11,33 @@ def get_credentials_dict(config):
|
|
|
11
11
|
Extract credentials from config in format needed for API.
|
|
12
12
|
|
|
13
13
|
Returns:
|
|
14
|
-
dict with access_key, secret_key, session_token
|
|
14
|
+
dict with access_key, secret_key, session_token, or None if profile is 'dummy'
|
|
15
15
|
"""
|
|
16
16
|
if 'aws_profile' in config:
|
|
17
|
-
#
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
frozen_creds = credentials.get_frozen_credentials()
|
|
17
|
+
# Skip credential loading for dummy profile (API-only mode)
|
|
18
|
+
if config['aws_profile'] == 'dummy':
|
|
19
|
+
return None
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
# Get temporary credentials from SSO session
|
|
22
|
+
try:
|
|
23
|
+
session = boto3.Session(profile_name=config['aws_profile'])
|
|
24
|
+
credentials = session.get_credentials()
|
|
25
|
+
frozen_creds = credentials.get_frozen_credentials()
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
'access_key': frozen_creds.access_key,
|
|
29
|
+
'secret_key': frozen_creds.secret_key,
|
|
30
|
+
'session_token': frozen_creds.token
|
|
31
|
+
}
|
|
32
|
+
except Exception:
|
|
33
|
+
# If profile not found, return None (API will handle)
|
|
34
|
+
return None
|
|
27
35
|
else:
|
|
28
36
|
# Use static credentials
|
|
29
|
-
creds = config
|
|
37
|
+
creds = config.get('credentials', {})
|
|
38
|
+
if not creds:
|
|
39
|
+
return None
|
|
40
|
+
|
|
30
41
|
result = {
|
|
31
42
|
'access_key': creds['aws_access_key_id'],
|
|
32
43
|
'secret_key': creds['aws_secret_access_key']
|
|
@@ -157,3 +168,74 @@ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage
|
|
|
157
168
|
account_filter=account_filter,
|
|
158
169
|
usage_type_filter=usage_type_filter
|
|
159
170
|
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
|
|
174
|
+
"""
|
|
175
|
+
Execute pandas-based analysis via API.
|
|
176
|
+
Note: This only works via API (requires pandas layer).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
dict: analysis results
|
|
180
|
+
"""
|
|
181
|
+
accounts = config['accounts']
|
|
182
|
+
|
|
183
|
+
if not is_api_configured():
|
|
184
|
+
raise click.ClickException(
|
|
185
|
+
"Analyze command requires API configuration.\n"
|
|
186
|
+
"Set COST_API_SECRET environment variable."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
credentials = get_credentials_dict(config)
|
|
190
|
+
kwargs = {'weeks': weeks, 'type': analysis_type}
|
|
191
|
+
|
|
192
|
+
if pattern:
|
|
193
|
+
kwargs['pattern'] = pattern
|
|
194
|
+
if min_cost:
|
|
195
|
+
kwargs['min_cost'] = min_cost
|
|
196
|
+
|
|
197
|
+
return call_lambda_api('analyze', credentials, accounts, **kwargs)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def execute_profile_operation(operation, profile_name=None, accounts=None, description=None):
|
|
201
|
+
"""
|
|
202
|
+
Execute profile CRUD operations via API.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
dict: operation result
|
|
206
|
+
"""
|
|
207
|
+
if not is_api_configured():
|
|
208
|
+
raise click.ClickException(
|
|
209
|
+
"Profile commands require API configuration.\n"
|
|
210
|
+
"Set COST_API_SECRET environment variable."
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Profile operations don't need AWS credentials, just API secret
|
|
214
|
+
import os
|
|
215
|
+
import requests
|
|
216
|
+
import json
|
|
217
|
+
|
|
218
|
+
api_secret = os.environ.get('COST_API_SECRET', '')
|
|
219
|
+
|
|
220
|
+
# Use profiles endpoint (hardcoded URL)
|
|
221
|
+
url = 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
|
|
222
|
+
|
|
223
|
+
payload = {'operation': operation}
|
|
224
|
+
if profile_name:
|
|
225
|
+
payload['profile_name'] = profile_name
|
|
226
|
+
if accounts:
|
|
227
|
+
payload['accounts'] = accounts
|
|
228
|
+
if description:
|
|
229
|
+
payload['description'] = description
|
|
230
|
+
|
|
231
|
+
headers = {
|
|
232
|
+
'X-API-Secret': api_secret,
|
|
233
|
+
'Content-Type': 'application/json'
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
|
237
|
+
|
|
238
|
+
if response.status_code != 200:
|
|
239
|
+
raise Exception(f"API call failed: {response.status_code} - {response.text}")
|
|
240
|
+
|
|
241
|
+
return response.json()
|
|
@@ -1,13 +0,0 @@
|
|
|
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,,
|
{aws_cost_calculator_cli-1.4.0.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.4.0.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|