aws-cost-calculator-cli 1.6.2__py3-none-any.whl → 2.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.
- {aws_cost_calculator_cli-1.6.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/METADATA +31 -12
- aws_cost_calculator_cli-2.4.0.dist-info/RECORD +16 -0
- {aws_cost_calculator_cli-1.6.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/top_level.txt +0 -1
- cost_calculator/api_client.py +18 -20
- cost_calculator/cli.py +1750 -249
- cost_calculator/cur.py +244 -0
- cost_calculator/dimensions.py +141 -0
- cost_calculator/executor.py +124 -101
- cost_calculator/forensics.py +323 -0
- aws_cost_calculator_cli-1.6.2.dist-info/RECORD +0 -25
- backend/__init__.py +0 -1
- backend/algorithms/__init__.py +0 -1
- backend/algorithms/analyze.py +0 -272
- backend/algorithms/drill.py +0 -323
- backend/algorithms/monthly.py +0 -242
- backend/algorithms/trends.py +0 -353
- backend/handlers/__init__.py +0 -1
- backend/handlers/analyze.py +0 -112
- backend/handlers/drill.py +0 -117
- backend/handlers/monthly.py +0 -106
- backend/handlers/profiles.py +0 -148
- backend/handlers/trends.py +0 -106
- {aws_cost_calculator_cli-1.6.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.6.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.6.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/licenses/LICENSE +0 -0
backend/handlers/profiles.py
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
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
|
-
}
|
backend/handlers/trends.py
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
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
|
-
}
|
|
File without changes
|
{aws_cost_calculator_cli-1.6.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.6.2.dist-info → aws_cost_calculator_cli-2.4.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|