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.
- {aws_cost_calculator_cli-1.6.0.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/METADATA +142 -23
- aws_cost_calculator_cli-1.6.3.dist-info/RECORD +25 -0
- {aws_cost_calculator_cli-1.6.0.dist-info → aws_cost_calculator_cli-1.6.3.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 +91 -5
- aws_cost_calculator_cli-1.6.0.dist-info/RECORD +0 -13
- {aws_cost_calculator_cli-1.6.0.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.6.0.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.6.0.dist-info → aws_cost_calculator_cli-1.6.3.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
|
@@ -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 (
|
|
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.,
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
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,,
|
|
File without changes
|
{aws_cost_calculator_cli-1.6.0.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{aws_cost_calculator_cli-1.6.0.dist-info → aws_cost_calculator_cli-1.6.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|