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.
@@ -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
+ }
@@ -10,33 +10,15 @@ from pathlib import Path
10
10
 
11
11
  def get_api_config():
12
12
  """
13
- Get API configuration from environment or config file.
13
+ Get API configuration.
14
14
 
15
15
  Returns:
16
- dict with 'base_url' and 'api_secret', or None if not configured
16
+ dict: API configuration with api_secret, or None if not configured
17
17
  """
18
- # Try environment variables first
19
- base_url = os.environ.get('COST_API_URL')
20
- api_secret = os.environ.get('COST_API_SECRET')
18
+ api_secret = os.environ.get('COST_API_SECRET', '')
21
19
 
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
- }
20
+ if api_secret:
21
+ return {'api_secret': api_secret}
40
22
 
41
23
  return None
42
24
 
@@ -60,21 +42,18 @@ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
60
42
  api_config = get_api_config()
61
43
 
62
44
  if not api_config:
63
- raise Exception("API not configured. Set COST_API_URL and COST_API_SECRET environment variables.")
45
+ raise Exception("API not configured. Set COST_API_SECRET environment variable.")
64
46
 
65
- # Map endpoint names to URLs
47
+ # Map endpoint names to Lambda URLs
66
48
  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"
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/'
70
54
  }
71
55
 
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)
56
+ url = endpoint_urls.get(endpoint)
78
57
 
79
58
  if not url:
80
59
  raise Exception(f"Unknown endpoint: {endpoint}")