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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 1.11.1
3
+ Version: 2.0.1
4
4
  Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
5
5
  Home-page: https://github.com/trilogy-group/aws-cost-calculator
6
6
  Author: Cost Optimization Team
@@ -0,0 +1,15 @@
1
+ aws_cost_calculator_cli-2.0.1.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
2
+ cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
3
+ cost_calculator/api_client.py,sha256=4ZI2XcGIN3FBeQqb7xOxQ91kCoeM43-rExiOELXoKBQ,2485
4
+ cost_calculator/cli.py,sha256=7j_jK2PKOchF9oereH6GtDtTjDnlhiTaVuS5FFdeL6U,76371
5
+ cost_calculator/cur.py,sha256=QaZ_nyDSw5_cti-h5Ho6eYLbqzY5TWoub24DpyzIiSs,9502
6
+ cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
7
+ cost_calculator/executor.py,sha256=yZTCUgJc1OpB892O3mq9ZA0Yekc7N-HvaW8xLFyrXjo,8681
8
+ cost_calculator/forensics.py,sha256=uhRo3I_zOeMEaBENHfgq65URga31W0Z4vzS2UN6VmTY,12819
9
+ cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
10
+ cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
11
+ aws_cost_calculator_cli-2.0.1.dist-info/METADATA,sha256=vC_DLqKCR82e1D9uideemfOE6AihjB57fR4wS1F-WWc,11978
12
+ aws_cost_calculator_cli-2.0.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
13
+ aws_cost_calculator_cli-2.0.1.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
14
+ aws_cost_calculator_cli-2.0.1.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
15
+ aws_cost_calculator_cli-2.0.1.dist-info/RECORD,,
cost_calculator/cli.py CHANGED
@@ -67,127 +67,107 @@ def apply_auth_options(config, sso=None, access_key_id=None, secret_access_key=N
67
67
  return config
68
68
 
69
69
 
70
- def load_profile(profile_name):
71
- """Load profile configuration from DynamoDB API or local file as fallback"""
70
+ def get_api_secret():
71
+ """Get API secret from config file or environment variable"""
72
72
  import os
73
- import requests
74
-
75
- config_dir = Path.home() / '.config' / 'cost-calculator'
76
- config_file = config_dir / 'profiles.json'
77
- creds_file = config_dir / 'credentials.json'
78
73
 
79
- # Try DynamoDB API first if COST_API_SECRET is set
74
+ # Check environment variable first
80
75
  api_secret = os.environ.get('COST_API_SECRET')
81
76
  if api_secret:
82
- try:
83
- response = requests.post(
84
- 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
85
- headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
86
- json={'operation': 'get', 'profile_name': profile_name},
87
- timeout=10
88
- )
89
-
90
- if response.status_code == 200:
91
- response_data = response.json()
92
- # API returns {"profile": {...}} wrapper
93
- profile_data = response_data.get('profile', response_data)
94
- profile = {'accounts': profile_data['accounts']}
95
-
96
- # If profile has aws_profile field, use it
97
- if 'aws_profile' in profile_data:
98
- profile['aws_profile'] = profile_data['aws_profile']
99
- # Check for AWS_PROFILE environment variable (SSO support)
100
- elif os.environ.get('AWS_PROFILE'):
101
- profile['aws_profile'] = os.environ['AWS_PROFILE']
102
- # Use environment credentials
103
- elif os.environ.get('AWS_ACCESS_KEY_ID'):
104
- profile['credentials'] = {
105
- 'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
106
- 'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
107
- 'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
108
- }
109
- else:
110
- # Try to find a matching AWS profile by name
111
- # This allows "khoros" profile to work with "khoros_umbrella" AWS profile
112
- import subprocess
113
- try:
114
- result = subprocess.run(
115
- ['aws', 'configure', 'list-profiles'],
116
- capture_output=True,
117
- text=True,
118
- timeout=5
119
- )
120
- if result.returncode == 0:
121
- available_profiles = result.stdout.strip().split('\n')
122
- # Try exact match first
123
- if profile_name in available_profiles:
124
- profile['aws_profile'] = profile_name
125
- # Try with common suffixes
126
- elif f"{profile_name}_umbrella" in available_profiles:
127
- profile['aws_profile'] = f"{profile_name}_umbrella"
128
- elif f"{profile_name}-umbrella" in available_profiles:
129
- profile['aws_profile'] = f"{profile_name}-umbrella"
130
- elif f"{profile_name}_prod" in available_profiles:
131
- profile['aws_profile'] = f"{profile_name}_prod"
132
- # If no match found, leave it unset - user must provide --sso
133
- except:
134
- # If we can't list profiles, leave it unset - user must provide --sso
135
- pass
136
-
137
- return profile
138
- else:
139
- raise click.ClickException(
140
- f"Profile '{profile_name}' not found in DynamoDB.\n"
141
- f"Run: cc profile create --name {profile_name} --accounts \"...\""
142
- )
143
- except requests.exceptions.RequestException as e:
144
- raise click.ClickException(
145
- f"Failed to fetch profile from API: {e}\n"
146
- )
77
+ return api_secret
78
+
79
+ # Check config file
80
+ config_dir = Path.home() / '.config' / 'cost-calculator'
81
+ config_file = config_dir / 'config.json'
147
82
 
148
- # Fallback to local file if no API secret
149
83
  if config_file.exists():
150
84
  with open(config_file) as f:
151
- profiles = json.load(f)
85
+ config = json.load(f)
86
+ return config.get('api_secret')
87
+
88
+ return None
89
+
90
+
91
+ def load_profile(profile_name):
92
+ """Load profile configuration from DynamoDB API (API-only, no local files)"""
93
+ import requests
94
+
95
+ # Get API secret
96
+ api_secret = get_api_secret()
97
+
98
+ if not api_secret:
99
+ raise click.ClickException(
100
+ "No API secret configured.\n"
101
+ "Run: cc configure --api-secret YOUR_SECRET\n"
102
+ "Or set environment variable: export COST_API_SECRET=YOUR_SECRET"
103
+ )
104
+
105
+ try:
106
+ response = requests.post(
107
+ 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
108
+ headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
109
+ json={'operation': 'get', 'profile_name': profile_name},
110
+ timeout=10
111
+ )
152
112
 
153
- if profile_name in profiles:
154
- profile = profiles[profile_name]
113
+ if response.status_code == 200:
114
+ response_data = response.json()
115
+ # API returns {"profile": {...}} wrapper
116
+ profile_data = response_data.get('profile', response_data)
117
+ profile = {'accounts': profile_data['accounts']}
155
118
 
156
- # Load credentials if using static credentials (not SSO)
157
- if 'aws_profile' not in profile:
158
- if not creds_file.exists():
159
- # Try environment variables
160
- if os.environ.get('AWS_ACCESS_KEY_ID'):
161
- profile['credentials'] = {
162
- 'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
163
- 'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
164
- 'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
165
- }
166
- return profile
167
-
168
- raise click.ClickException(
169
- f"No credentials found for profile '{profile_name}'.\n"
170
- f"Run: cc configure --profile {profile_name}"
171
- )
172
-
173
- with open(creds_file) as f:
174
- creds = json.load(f)
175
-
176
- if profile_name not in creds:
177
- raise click.ClickException(
178
- f"No credentials found for profile '{profile_name}'.\n"
179
- f"Run: cc configure --profile {profile_name}"
119
+ # If profile has aws_profile field, use it
120
+ if 'aws_profile' in profile_data:
121
+ profile['aws_profile'] = profile_data['aws_profile']
122
+ # Check for AWS_PROFILE environment variable (SSO support)
123
+ elif os.environ.get('AWS_PROFILE'):
124
+ profile['aws_profile'] = os.environ['AWS_PROFILE']
125
+ # Use environment credentials
126
+ elif os.environ.get('AWS_ACCESS_KEY_ID'):
127
+ profile['credentials'] = {
128
+ 'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
129
+ 'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
130
+ 'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
131
+ }
132
+ else:
133
+ # Try to find a matching AWS profile by name
134
+ # This allows "khoros" profile to work with "khoros_umbrella" AWS profile
135
+ import subprocess
136
+ try:
137
+ result = subprocess.run(
138
+ ['aws', 'configure', 'list-profiles'],
139
+ capture_output=True,
140
+ text=True,
141
+ timeout=5
180
142
  )
181
-
182
- profile['credentials'] = creds[profile_name]
143
+ if result.returncode == 0:
144
+ available_profiles = result.stdout.strip().split('\n')
145
+ # Try exact match first
146
+ if profile_name in available_profiles:
147
+ profile['aws_profile'] = profile_name
148
+ # Try with common suffixes
149
+ elif f"{profile_name}_umbrella" in available_profiles:
150
+ profile['aws_profile'] = f"{profile_name}_umbrella"
151
+ elif f"{profile_name}-umbrella" in available_profiles:
152
+ profile['aws_profile'] = f"{profile_name}-umbrella"
153
+ elif f"{profile_name}_prod" in available_profiles:
154
+ profile['aws_profile'] = f"{profile_name}_prod"
155
+ # If no match found, leave it unset - user must provide --sso
156
+ except:
157
+ # If we can't list profiles, leave it unset - user must provide --sso
158
+ pass
183
159
 
184
160
  return profile
185
-
186
- # Profile not found anywhere
187
- raise click.ClickException(
188
- f"Profile '{profile_name}' not found.\n"
189
- f"Run: cc profile create --name {profile_name} --accounts \"...\""
190
- )
161
+ else:
162
+ raise click.ClickException(
163
+ f"Profile '{profile_name}' not found in DynamoDB.\n"
164
+ f"Run: cc profile create --name {profile_name} --accounts \"...\""
165
+ )
166
+ except requests.exceptions.RequestException as e:
167
+ raise click.ClickException(
168
+ f"Failed to fetch profile from API: {e}\n"
169
+ "Check your API secret and network connection."
170
+ )
191
171
 
192
172
 
193
173
  def calculate_costs(profile_config, accounts, start_date, offset, window):
@@ -737,77 +717,81 @@ def setup():
737
717
 
738
718
 
739
719
  @cli.command()
740
- @click.option('--profile', required=True, help='Profile name to configure')
741
- @click.option('--access-key-id', prompt=True, hide_input=False, help='AWS Access Key ID')
742
- @click.option('--secret-access-key', prompt=True, hide_input=True, help='AWS Secret Access Key')
743
- @click.option('--session-token', default='', help='AWS Session Token (optional, for temporary credentials)')
744
- @click.option('--region', default='us-east-1', help='AWS Region (default: us-east-1)')
745
- def configure(profile, access_key_id, secret_access_key, session_token, region):
746
- """Configure AWS credentials for a profile (alternative to SSO)"""
720
+ @click.option('--api-secret', help='API secret for DynamoDB profile access')
721
+ @click.option('--show', is_flag=True, help='Show current configuration')
722
+ def configure(api_secret, show):
723
+ """
724
+ Configure Cost Calculator CLI settings.
747
725
 
748
- config_dir = Path.home() / '.config' / 'cost-calculator'
749
- config_file = config_dir / 'profiles.json'
750
- creds_file = config_dir / 'credentials.json'
726
+ This tool requires an API secret to access profiles stored in DynamoDB.
727
+ The secret can be configured here or set via COST_API_SECRET environment variable.
751
728
 
752
- # Create config directory if it doesn't exist
729
+ Examples:
730
+ # Configure API secret
731
+ cc configure --api-secret YOUR_SECRET_KEY
732
+
733
+ # Show current configuration
734
+ cc configure --show
735
+
736
+ # Use environment variable instead (no configuration needed)
737
+ export COST_API_SECRET=YOUR_SECRET_KEY
738
+ """
739
+ import os
740
+
741
+ config_dir = Path.home() / '.config' / 'cost-calculator'
753
742
  config_dir.mkdir(parents=True, exist_ok=True)
743
+ config_file = config_dir / 'config.json'
754
744
 
755
- # Load existing profiles
756
- if config_file.exists() and config_file.stat().st_size > 0:
757
- try:
745
+ if show:
746
+ # Show current configuration
747
+ if config_file.exists():
758
748
  with open(config_file) as f:
759
- profiles = json.load(f)
760
- except json.JSONDecodeError:
761
- profiles = {}
762
- else:
763
- profiles = {}
764
-
765
- # Check if profile exists
766
- if profile not in profiles:
767
- click.echo(f"Error: Profile '{profile}' not found. Create it first with: cc init --profile {profile}")
749
+ config = json.load(f)
750
+ if 'api_secret' in config:
751
+ masked_secret = config['api_secret'][:8] + '...' + config['api_secret'][-4:]
752
+ click.echo(f"API Secret: {masked_secret} (configured)")
753
+ else:
754
+ click.echo("API Secret: Not configured")
755
+ else:
756
+ click.echo("No configuration file found")
757
+
758
+ # Check environment variable
759
+ import os
760
+ if os.environ.get('COST_API_SECRET'):
761
+ click.echo("Environment: COST_API_SECRET is set")
762
+ else:
763
+ click.echo("Environment: COST_API_SECRET is not set")
764
+
768
765
  return
769
766
 
770
- # Remove aws_profile if it exists (switching from SSO to static creds)
771
- if 'aws_profile' in profiles[profile]:
772
- del profiles[profile]['aws_profile']
773
-
774
- # Save updated profile
775
- with open(config_file, 'w') as f:
776
- json.dump(profiles, f, indent=2)
777
-
778
- # Load or create credentials file
779
- if creds_file.exists() and creds_file.stat().st_size > 0:
780
- try:
781
- with open(creds_file) as f:
782
- creds = json.load(f)
783
- except json.JSONDecodeError:
784
- creds = {}
785
- else:
786
- creds = {}
767
+ if not api_secret:
768
+ raise click.ClickException(
769
+ "Please provide --api-secret or use --show to view current configuration\n"
770
+ "Example: cc configure --api-secret YOUR_SECRET_KEY"
771
+ )
787
772
 
788
- # Store credentials (encrypted would be better, but for now just file permissions)
789
- creds[profile] = {
790
- 'aws_access_key_id': access_key_id,
791
- 'aws_secret_access_key': secret_access_key,
792
- 'region': region
793
- }
773
+ # Load existing config
774
+ config = {}
775
+ if config_file.exists():
776
+ with open(config_file) as f:
777
+ config = json.load(f)
794
778
 
795
- if session_token:
796
- creds[profile]['aws_session_token'] = session_token
779
+ # Update API secret
780
+ config['api_secret'] = api_secret
797
781
 
798
- # Save credentials with restricted permissions
799
- with open(creds_file, 'w') as f:
800
- json.dump(creds, f, indent=2)
782
+ # Save config
783
+ with open(config_file, 'w') as f:
784
+ json.dump(config, f, indent=2)
801
785
 
802
- # Set file permissions to 600 (owner read/write only)
803
- creds_file.chmod(0o600)
786
+ # Set restrictive permissions (Unix/Mac only - Windows uses different permission model)
787
+ import platform
788
+ if platform.system() != 'Windows':
789
+ os.chmod(config_file, 0o600)
804
790
 
805
- click.echo(f"✓ AWS credentials configured for profile '{profile}'")
806
- click.echo(f"✓ Credentials saved to {creds_file} (permissions: 600)")
807
- click.echo(f"\nUsage: cc calculate --profile {profile}")
808
- click.echo("\nNote: Credentials are stored locally. For temporary credentials,")
809
- click.echo(" you'll need to reconfigure when they expire.")
810
-
791
+ masked_secret = api_secret[:8] + '...' + api_secret[-4:]
792
+ click.echo(f"✓ API secret configured: {masked_secret}")
793
+ click.echo(f"\nYou can now run: cc calculate --profile PROFILE_NAME")
794
+ click.echo(f"\nNote: Profiles are stored in DynamoDB and accessed via the API.")
811
795
 
812
796
  @cli.command()
813
797
  @click.option('--profile', required=True, help='Profile name')
@@ -1,15 +0,0 @@
1
- aws_cost_calculator_cli-1.11.1.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
2
- cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
3
- cost_calculator/api_client.py,sha256=4ZI2XcGIN3FBeQqb7xOxQ91kCoeM43-rExiOELXoKBQ,2485
4
- cost_calculator/cli.py,sha256=OudMcAitmJfWgZKxjlHtfXnx2SlKrYh6FzPTkHabJlI,77975
5
- cost_calculator/cur.py,sha256=QaZ_nyDSw5_cti-h5Ho6eYLbqzY5TWoub24DpyzIiSs,9502
6
- cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
7
- cost_calculator/executor.py,sha256=yZTCUgJc1OpB892O3mq9ZA0Yekc7N-HvaW8xLFyrXjo,8681
8
- cost_calculator/forensics.py,sha256=uhRo3I_zOeMEaBENHfgq65URga31W0Z4vzS2UN6VmTY,12819
9
- cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
10
- cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
11
- aws_cost_calculator_cli-1.11.1.dist-info/METADATA,sha256=d-AHFprwq0-lD-D3ilWa7IGBxOszOdMqP7nJl5J0gyA,11979
12
- aws_cost_calculator_cli-1.11.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
13
- aws_cost_calculator_cli-1.11.1.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
14
- aws_cost_calculator_cli-1.11.1.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
15
- aws_cost_calculator_cli-1.11.1.dist-info/RECORD,,