aws-cost-calculator-cli 1.5.2__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.5.2.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/METADATA +150 -24
- aws_cost_calculator_cli-1.6.2.dist-info/RECORD +25 -0
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/WHEEL +1 -1
- {aws_cost_calculator_cli-1.5.2.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/cli.py +169 -41
- aws_cost_calculator_cli-1.5.2.dist-info/RECORD +0 -13
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for profile CRUD operations.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import boto3
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handler(event, context):
|
|
11
|
+
"""Handle profile CRUD operations."""
|
|
12
|
+
|
|
13
|
+
# Parse request
|
|
14
|
+
try:
|
|
15
|
+
if isinstance(event.get('body'), str):
|
|
16
|
+
body = json.loads(event['body'])
|
|
17
|
+
else:
|
|
18
|
+
body = event.get('body', {})
|
|
19
|
+
except:
|
|
20
|
+
body = event
|
|
21
|
+
|
|
22
|
+
# Validate API secret
|
|
23
|
+
headers = event.get('headers', {})
|
|
24
|
+
api_secret = headers.get('X-API-Secret') or headers.get('x-api-secret')
|
|
25
|
+
|
|
26
|
+
secret_name = os.environ.get('SECRET_NAME', 'cost-calculator-api-secret')
|
|
27
|
+
secrets_client = boto3.client('secretsmanager')
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
secret_response = secrets_client.get_secret_value(SecretId=secret_name)
|
|
31
|
+
expected_secret = secret_response['SecretString']
|
|
32
|
+
|
|
33
|
+
if api_secret != expected_secret:
|
|
34
|
+
return {
|
|
35
|
+
'statusCode': 401,
|
|
36
|
+
'headers': {'Content-Type': 'application/json'},
|
|
37
|
+
'body': json.dumps({'error': 'Unauthorized'})
|
|
38
|
+
}
|
|
39
|
+
except Exception as e:
|
|
40
|
+
return {
|
|
41
|
+
'statusCode': 500,
|
|
42
|
+
'headers': {'Content-Type': 'application/json'},
|
|
43
|
+
'body': json.dumps({'error': f'Secret validation failed: {str(e)}'})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Get operation
|
|
47
|
+
operation = body.get('operation') # list, get, create, update, delete
|
|
48
|
+
profile_name = body.get('profile_name')
|
|
49
|
+
|
|
50
|
+
# DynamoDB table
|
|
51
|
+
table_name = os.environ.get('PROFILES_TABLE', 'cost-calculator-profiles')
|
|
52
|
+
dynamodb = boto3.resource('dynamodb')
|
|
53
|
+
table = dynamodb.Table(table_name)
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
if operation == 'list':
|
|
57
|
+
# List all profiles
|
|
58
|
+
response = table.scan()
|
|
59
|
+
profiles = response.get('Items', [])
|
|
60
|
+
return {
|
|
61
|
+
'statusCode': 200,
|
|
62
|
+
'headers': {'Content-Type': 'application/json'},
|
|
63
|
+
'body': json.dumps({'profiles': profiles})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
elif operation == 'get':
|
|
67
|
+
# Get specific profile
|
|
68
|
+
if not profile_name:
|
|
69
|
+
return {
|
|
70
|
+
'statusCode': 400,
|
|
71
|
+
'headers': {'Content-Type': 'application/json'},
|
|
72
|
+
'body': json.dumps({'error': 'profile_name required'})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
response = table.get_item(Key={'profile_name': profile_name})
|
|
76
|
+
if 'Item' not in response:
|
|
77
|
+
return {
|
|
78
|
+
'statusCode': 404,
|
|
79
|
+
'headers': {'Content-Type': 'application/json'},
|
|
80
|
+
'body': json.dumps({'error': 'Profile not found'})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
'statusCode': 200,
|
|
85
|
+
'headers': {'Content-Type': 'application/json'},
|
|
86
|
+
'body': json.dumps({'profile': response['Item']})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
elif operation == 'create' or operation == 'update':
|
|
90
|
+
# Create or update profile
|
|
91
|
+
if not profile_name:
|
|
92
|
+
return {
|
|
93
|
+
'statusCode': 400,
|
|
94
|
+
'headers': {'Content-Type': 'application/json'},
|
|
95
|
+
'body': json.dumps({'error': 'profile_name required'})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
accounts = body.get('accounts', [])
|
|
99
|
+
description = body.get('description', '')
|
|
100
|
+
|
|
101
|
+
item = {
|
|
102
|
+
'profile_name': profile_name,
|
|
103
|
+
'accounts': accounts,
|
|
104
|
+
'description': description,
|
|
105
|
+
'updated_at': datetime.utcnow().isoformat()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if operation == 'create':
|
|
109
|
+
item['created_at'] = datetime.utcnow().isoformat()
|
|
110
|
+
|
|
111
|
+
table.put_item(Item=item)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
'statusCode': 200,
|
|
115
|
+
'headers': {'Content-Type': 'application/json'},
|
|
116
|
+
'body': json.dumps({'message': 'Profile saved', 'profile': item})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
elif operation == 'delete':
|
|
120
|
+
# Delete profile
|
|
121
|
+
if not profile_name:
|
|
122
|
+
return {
|
|
123
|
+
'statusCode': 400,
|
|
124
|
+
'headers': {'Content-Type': 'application/json'},
|
|
125
|
+
'body': json.dumps({'error': 'profile_name required'})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
table.delete_item(Key={'profile_name': profile_name})
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
'statusCode': 200,
|
|
132
|
+
'headers': {'Content-Type': 'application/json'},
|
|
133
|
+
'body': json.dumps({'message': 'Profile deleted'})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
else:
|
|
137
|
+
return {
|
|
138
|
+
'statusCode': 400,
|
|
139
|
+
'headers': {'Content-Type': 'application/json'},
|
|
140
|
+
'body': json.dumps({'error': 'Invalid operation. Use: list, get, create, update, delete'})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return {
|
|
145
|
+
'statusCode': 500,
|
|
146
|
+
'headers': {'Content-Type': 'application/json'},
|
|
147
|
+
'body': json.dumps({'error': str(e)})
|
|
148
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lambda handler for trends analysis.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import boto3
|
|
6
|
+
import os
|
|
7
|
+
from algorithms.trends import analyze_trends
|
|
8
|
+
|
|
9
|
+
# Get API secret from Secrets Manager
|
|
10
|
+
secrets_client = boto3.client('secretsmanager')
|
|
11
|
+
api_secret_arn = os.environ['API_SECRET_ARN']
|
|
12
|
+
api_secret = secrets_client.get_secret_value(SecretId=api_secret_arn)['SecretString']
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handler(event, context):
|
|
16
|
+
"""
|
|
17
|
+
Lambda handler for trends analysis.
|
|
18
|
+
|
|
19
|
+
Expected event:
|
|
20
|
+
{
|
|
21
|
+
"credentials": {
|
|
22
|
+
"access_key": "AKIA...",
|
|
23
|
+
"secret_key": "...",
|
|
24
|
+
"session_token": "..." (optional)
|
|
25
|
+
},
|
|
26
|
+
"accounts": ["123456789012", "987654321098"],
|
|
27
|
+
"weeks": 4
|
|
28
|
+
}
|
|
29
|
+
"""
|
|
30
|
+
# Handle OPTIONS for CORS
|
|
31
|
+
if event.get('requestContext', {}).get('http', {}).get('method') == 'OPTIONS':
|
|
32
|
+
return {
|
|
33
|
+
'statusCode': 200,
|
|
34
|
+
'headers': {
|
|
35
|
+
'Access-Control-Allow-Origin': '*',
|
|
36
|
+
'Access-Control-Allow-Headers': '*',
|
|
37
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS'
|
|
38
|
+
},
|
|
39
|
+
'body': ''
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
# Validate API secret
|
|
44
|
+
headers = event.get('headers', {})
|
|
45
|
+
provided_secret = headers.get('x-api-secret') or headers.get('X-API-Secret')
|
|
46
|
+
|
|
47
|
+
if provided_secret != api_secret:
|
|
48
|
+
return {
|
|
49
|
+
'statusCode': 401,
|
|
50
|
+
'headers': {'Access-Control-Allow-Origin': '*'},
|
|
51
|
+
'body': json.dumps({'error': 'Unauthorized'})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Parse request body
|
|
55
|
+
body = json.loads(event.get('body', '{}'))
|
|
56
|
+
|
|
57
|
+
credentials = body.get('credentials', {})
|
|
58
|
+
accounts = body.get('accounts', [])
|
|
59
|
+
weeks = body.get('weeks', 3)
|
|
60
|
+
|
|
61
|
+
if not credentials or not accounts:
|
|
62
|
+
return {
|
|
63
|
+
'statusCode': 400,
|
|
64
|
+
'headers': {'Access-Control-Allow-Origin': '*'},
|
|
65
|
+
'body': json.dumps({'error': 'Missing credentials or accounts'})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Create Cost Explorer client with provided credentials
|
|
69
|
+
ce_client = boto3.client(
|
|
70
|
+
'ce',
|
|
71
|
+
region_name='us-east-1',
|
|
72
|
+
aws_access_key_id=credentials['access_key'],
|
|
73
|
+
aws_secret_access_key=credentials['secret_key'],
|
|
74
|
+
aws_session_token=credentials.get('session_token')
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Run analysis
|
|
78
|
+
trends_data = analyze_trends(ce_client, accounts, weeks)
|
|
79
|
+
|
|
80
|
+
# Convert datetime objects to strings for JSON serialization
|
|
81
|
+
def convert_dates(obj):
|
|
82
|
+
if isinstance(obj, dict):
|
|
83
|
+
return {k: convert_dates(v) for k, v in obj.items()}
|
|
84
|
+
elif isinstance(obj, list):
|
|
85
|
+
return [convert_dates(item) for item in obj]
|
|
86
|
+
elif hasattr(obj, 'isoformat'):
|
|
87
|
+
return obj.isoformat()
|
|
88
|
+
return obj
|
|
89
|
+
|
|
90
|
+
trends_data = convert_dates(trends_data)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
'statusCode': 200,
|
|
94
|
+
'headers': {
|
|
95
|
+
'Access-Control-Allow-Origin': '*',
|
|
96
|
+
'Content-Type': 'application/json'
|
|
97
|
+
},
|
|
98
|
+
'body': json.dumps(trends_data)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return {
|
|
103
|
+
'statusCode': 500,
|
|
104
|
+
'headers': {'Access-Control-Allow-Origin': '*'},
|
|
105
|
+
'body': json.dumps({'error': str(e)})
|
|
106
|
+
}
|
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...")
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
aws_cost_calculator_cli-1.5.2.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=tVyyBtXIj9OPyG-xQj8CUmyFjDhb9IVK639360dUZDc,8076
|
|
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.2.dist-info/METADATA,sha256=0wxy-jgVC-paubGHN87mDObETGr_u9qU4ZIP3xV49hM,8176
|
|
10
|
-
aws_cost_calculator_cli-1.5.2.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
11
|
-
aws_cost_calculator_cli-1.5.2.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
12
|
-
aws_cost_calculator_cli-1.5.2.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
13
|
-
aws_cost_calculator_cli-1.5.2.dist-info/RECORD,,
|
{aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.5.2.dist-info → aws_cost_calculator_cli-1.6.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|