aws-cost-calculator-cli 1.6.0__py3-none-any.whl → 1.6.3__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.

@@ -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
@@ -11,6 +11,8 @@ Usage:
11
11
  import click
12
12
  import boto3
13
13
  import json
14
+ import os
15
+ import platform
14
16
  from datetime import datetime, timedelta
15
17
  from pathlib import Path
16
18
  from cost_calculator.trends import format_trends_markdown
@@ -325,7 +327,7 @@ def calculate_costs(profile_config, accounts, start_date, offset, window):
325
327
  support_month = support_month_date - timedelta(days=1) # Go back to previous month
326
328
  days_in_support_month = support_month.day # This gives us the last day of the month
327
329
 
328
- # Support allocation: divide by 2 (half to Khoros), then by days in month
330
+ # Support allocation: divide by 2 (50% allocation), then by days in month
329
331
  support_per_day = (support_cost / 2) / days_in_support_month
330
332
 
331
333
  # Calculate daily rate
@@ -384,26 +386,110 @@ def cli():
384
386
  pass
385
387
 
386
388
 
389
+ @cli.command('setup-api')
390
+ @click.option('--api-secret', required=True, prompt=True, hide_input=True, help='COST_API_SECRET value')
391
+ def setup_api(api_secret):
392
+ """
393
+ Configure COST_API_SECRET for backend API access
394
+
395
+ Saves the API secret to the appropriate location based on your OS:
396
+ - Mac/Linux: ~/.zshrc or ~/.bashrc
397
+ - Windows: User environment variables
398
+
399
+ Example:
400
+ cc setup-api --api-secret your-secret-here
401
+
402
+ Or let it prompt you (input will be hidden):
403
+ cc setup-api
404
+ """
405
+ system = platform.system()
406
+
407
+ if system == "Windows":
408
+ # Windows: Set user environment variable
409
+ try:
410
+ import winreg
411
+ key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_SET_VALUE)
412
+ winreg.SetValueEx(key, 'COST_API_SECRET', 0, winreg.REG_SZ, api_secret)
413
+ winreg.CloseKey(key)
414
+ click.echo("✓ COST_API_SECRET saved to Windows user environment variables")
415
+ click.echo(" Please restart your terminal for changes to take effect")
416
+ except Exception as e:
417
+ click.echo(f"✗ Error setting Windows environment variable: {e}", err=True)
418
+ click.echo("\nManual setup:")
419
+ click.echo("1. Open System Properties > Environment Variables")
420
+ click.echo("2. Add new User variable:")
421
+ click.echo(" Name: COST_API_SECRET")
422
+ click.echo(f" Value: {api_secret}")
423
+ return
424
+ else:
425
+ # Mac/Linux: Add to shell profile
426
+ shell = os.environ.get('SHELL', '/bin/bash')
427
+
428
+ if 'zsh' in shell:
429
+ profile_file = Path.home() / '.zshrc'
430
+ else:
431
+ profile_file = Path.home() / '.bashrc'
432
+
433
+ # Check if already exists
434
+ export_line = f'export COST_API_SECRET="{api_secret}"'
435
+
436
+ try:
437
+ if profile_file.exists():
438
+ content = profile_file.read_text()
439
+ if 'COST_API_SECRET' in content:
440
+ # Replace existing
441
+ lines = content.split('\n')
442
+ new_lines = []
443
+ for line in lines:
444
+ if 'COST_API_SECRET' in line and line.strip().startswith('export'):
445
+ new_lines.append(export_line)
446
+ else:
447
+ new_lines.append(line)
448
+ profile_file.write_text('\n'.join(new_lines))
449
+ click.echo(f"✓ Updated COST_API_SECRET in {profile_file}")
450
+ else:
451
+ # Append
452
+ with profile_file.open('a') as f:
453
+ f.write(f'\n# AWS Cost Calculator API Secret\n{export_line}\n')
454
+ click.echo(f"✓ Added COST_API_SECRET to {profile_file}")
455
+ else:
456
+ # Create new file
457
+ profile_file.write_text(f'# AWS Cost Calculator API Secret\n{export_line}\n')
458
+ click.echo(f"✓ Created {profile_file} with COST_API_SECRET")
459
+
460
+ # Also set for current session
461
+ os.environ['COST_API_SECRET'] = api_secret
462
+ click.echo(f"✓ Set COST_API_SECRET for current session")
463
+ click.echo(f"\nTo use in new terminals, run: source {profile_file}")
464
+
465
+ except Exception as e:
466
+ click.echo(f"✗ Error writing to {profile_file}: {e}", err=True)
467
+ click.echo(f"\nManual setup: Add this line to {profile_file}:")
468
+ click.echo(f" {export_line}")
469
+ return
470
+
471
+
387
472
  @cli.command()
388
473
  @click.option('--profile', required=True, help='Profile name (e.g., myprofile)')
389
474
  @click.option('--start-date', help='Start date (YYYY-MM-DD, default: today)')
390
475
  @click.option('--offset', default=2, help='Days to go back from start date (default: 2)')
391
476
  @click.option('--window', default=30, help='Number of days to analyze (default: 30)')
392
477
  @click.option('--json-output', is_flag=True, help='Output as JSON')
393
- @click.option('--sso', help='AWS SSO profile name (e.g., khoros_umbrella)')
478
+ @click.option('--sso', help='AWS SSO profile name (e.g., my_sso_profile)')
394
479
  @click.option('--access-key-id', help='AWS Access Key ID (for static credentials)')
395
480
  @click.option('--secret-access-key', help='AWS Secret Access Key (for static credentials)')
396
481
  @click.option('--session-token', help='AWS Session Token (for static credentials)')
397
482
  def calculate(profile, start_date, offset, window, json_output, sso, access_key_id, secret_access_key, session_token):
398
- """Calculate AWS costs for the specified period
483
+ """
484
+ Calculate AWS costs for the specified period
399
485
 
400
486
  \b
401
487
  Authentication Options:
402
488
  1. SSO: --sso <profile_name>
403
- Example: cc calculate --profile khoros --sso khoros_umbrella
489
+ Example: cc calculate --profile myprofile --sso my_sso_profile
404
490
 
405
491
  2. Static Credentials: --access-key-id, --secret-access-key, --session-token
406
- Example: cc calculate --profile khoros --access-key-id ASIA... --secret-access-key ... --session-token ...
492
+ Example: cc calculate --profile myprofile --access-key-id ASIA... --secret-access-key ... --session-token ...
407
493
 
408
494
  3. Environment Variables: AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
409
495
  """
@@ -1,13 +0,0 @@
1
- aws_cost_calculator_cli-1.6.0.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=sJYvzbdHCxOEcCgOjZs4o9MOogV1Yh8r7x0hJtd__K0,38639
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.6.0.dist-info/METADATA,sha256=AaLbUH-anBc1lv2o2DpdJR3v0tSavhPtDo5Sjb7VsHA,8612
10
- aws_cost_calculator_cli-1.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- aws_cost_calculator_cli-1.6.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
12
- aws_cost_calculator_cli-1.6.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
13
- aws_cost_calculator_cli-1.6.0.dist-info/RECORD,,