aws-cost-calculator-cli 1.0.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.

Potentially problematic release.


This version of aws-cost-calculator-cli might be problematic. Click here for more details.

@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: aws-cost-calculator-cli
3
+ Version: 1.0.2
4
+ Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
5
+ Home-page: https://github.com/yourusername/cost-calculator
6
+ Author: Cost Optimization Team
7
+ Author-email:
8
+ Project-URL: Documentation, https://github.com/yourusername/cost-calculator/blob/main/README.md
9
+ Keywords: aws cost calculator billing optimization cloud
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Topic :: System :: Monitoring
14
+ Classifier: Topic :: Utilities
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: click>=8.0.0
25
+ Requires-Dist: boto3>=1.26.0
26
+ Dynamic: author
27
+ Dynamic: classifier
28
+ Dynamic: description
29
+ Dynamic: description-content-type
30
+ Dynamic: home-page
31
+ Dynamic: keywords
32
+ Dynamic: license-file
33
+ Dynamic: project-url
34
+ Dynamic: requires-dist
35
+ Dynamic: requires-python
36
+ Dynamic: summary
37
+
38
+ # AWS Cost Calculator (cc)
39
+
40
+ A CLI tool to quickly calculate AWS costs across multiple accounts.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ cd ~/cost-calculator
46
+ pip install -e .
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ### 1. Login to AWS SSO
52
+
53
+ ```bash
54
+ aws sso login --profile my_aws_profile
55
+ ```
56
+
57
+ **Note:** You need to do this before running cost calculations. The SSO session typically lasts 8-12 hours.
58
+
59
+ ### 2. Initialize a profile
60
+
61
+ ```bash
62
+ cc init --profile myprofile \
63
+ --aws-profile my_aws_profile \
64
+ --accounts "123456789012,234567890123,345678901234"
65
+ ```
66
+
67
+ ### 3. Calculate costs
68
+
69
+ ```bash
70
+ # Default: Today minus 2 days, going back 30 days
71
+ cc calculate --profile myprofile
72
+
73
+ # Specific start date
74
+ cc calculate --profile myprofile --start-date 2025-11-04
75
+
76
+ # Custom offset and window
77
+ cc calculate --profile myprofile --offset 2 --window 30
78
+
79
+ # JSON output
80
+ cc calculate --profile myprofile --json-output
81
+ ```
82
+
83
+ ### 4. List profiles
84
+
85
+ ```bash
86
+ cc list-profiles
87
+ ```
88
+
89
+ ## How It Works
90
+
91
+ ### Date Calculation
92
+ - **Start Date**: Defaults to today, or specify with `--start-date`
93
+ - **Offset**: Days to go back from start date (default: 2)
94
+ - **Window**: Number of days to analyze (default: 30)
95
+
96
+ Example: If today is Nov 4, 2025:
97
+ - With offset=2, window=30: Analyzes Oct 3 - Nov 2 (30 days)
98
+
99
+ ### Cost Calculation
100
+ 1. **Operational Costs**: Sum of daily costs ÷ window days
101
+ 2. **Support Allocation**:
102
+ - Gets support cost from the analysis month
103
+ - Divides by 2 (50% allocation)
104
+ - Divides by days in that month
105
+ 3. **Daily Rate**: Operational + Support per day
106
+ 4. **Annual Projection**: Daily rate × 365
107
+
108
+ ### Filters Applied
109
+ - **Billing Entity**: AWS only (excludes marketplace)
110
+ - **Excluded**: Tax, Support (calculated separately)
111
+ - **Metric**: Net Amortized Cost
112
+
113
+ ## Configuration
114
+
115
+ Profiles are stored in: `~/.config/cost-calculator/profiles.json`
116
+
117
+ Example:
118
+ ```json
119
+ {
120
+ "myprofile": {
121
+ "aws_profile": "my_aws_profile",
122
+ "accounts": ["123456789012", "234567890123", "345678901234"]
123
+ }
124
+ }
125
+ ```
126
+
127
+ ## Examples
128
+
129
+ ```bash
130
+ # Quick daily check
131
+ cc calculate --profile myprofile
132
+
133
+ # Historical analysis
134
+ cc calculate --profile myprofile --start-date 2025-10-01
135
+
136
+ # Export to JSON for processing
137
+ cc calculate --profile myprofile --json-output > costs.json
138
+
139
+ # Different window size
140
+ cc calculate --profile myprofile --window 60
141
+ ```
142
+
143
+ ## Output
144
+
145
+ ```
146
+ Analyzing: 2025-10-03 to 2025-11-02 (30 days)
147
+ AWS Profile: my_aws_profile
148
+ Accounts: 3
149
+
150
+ Fetching cost data...
151
+ Fetching support costs...
152
+ ============================================================
153
+ Period: 2025-10-03 to 2025-11-02
154
+ Days analyzed: 30
155
+ ============================================================
156
+ Total operational cost: $450,000.00
157
+ Daily operational: $14,516.13
158
+ Support (month): $15,000.00
159
+ Support per day (÷2÷days): $241.94
160
+ ============================================================
161
+ DAILY RATE: $14,758.07
162
+ ANNUAL PROJECTION: $5,386,695
163
+ ============================================================
164
+ ```
@@ -0,0 +1,8 @@
1
+ aws_cost_calculator_cli-1.0.2.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
2
+ cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
3
+ cost_calculator/cli.py,sha256=HsVEZhasXTK1fyeStdJFEng7Dj5l5JK9aMfQxOL6N5I,16320
4
+ aws_cost_calculator_cli-1.0.2.dist-info/METADATA,sha256=ZF9eEr4PJEPlKOeV3wMV56nnZnBAGIdeJpaL821X5QE,4227
5
+ aws_cost_calculator_cli-1.0.2.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
6
+ aws_cost_calculator_cli-1.0.2.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
7
+ aws_cost_calculator_cli-1.0.2.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
8
+ aws_cost_calculator_cli-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.7.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cc = cost_calculator.cli:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Cost Optimization Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cost_calculator
@@ -0,0 +1,2 @@
1
+ """AWS Cost Calculator CLI"""
2
+ __version__ = '1.0.0'
cost_calculator/cli.py ADDED
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AWS Cost Calculator CLI
4
+
5
+ Usage:
6
+ cc --profile myprofile
7
+ cc --profile myprofile --start-date 2025-11-04
8
+ cc --profile myprofile --offset 2 --window 30
9
+ """
10
+
11
+ import click
12
+ import boto3
13
+ import json
14
+ from datetime import datetime, timedelta
15
+ from pathlib import Path
16
+
17
+
18
+ def load_profile(profile_name):
19
+ """Load profile configuration from ~/.config/cost-calculator/profiles.json"""
20
+ config_dir = Path.home() / '.config' / 'cost-calculator'
21
+ config_file = config_dir / 'profiles.json'
22
+ creds_file = config_dir / 'credentials.json'
23
+
24
+ if not config_file.exists():
25
+ raise click.ClickException(
26
+ f"Profile configuration not found at {config_file}\n"
27
+ f"Run: cc init --profile {profile_name}"
28
+ )
29
+
30
+ with open(config_file) as f:
31
+ profiles = json.load(f)
32
+
33
+ if profile_name not in profiles:
34
+ raise click.ClickException(
35
+ f"Profile '{profile_name}' not found in {config_file}\n"
36
+ f"Available profiles: {', '.join(profiles.keys())}"
37
+ )
38
+
39
+ profile = profiles[profile_name]
40
+
41
+ # Load credentials if using static credentials (not SSO)
42
+ if 'aws_profile' not in profile:
43
+ if not creds_file.exists():
44
+ raise click.ClickException(
45
+ f"No credentials found for profile '{profile_name}'.\n"
46
+ f"Run: cc configure --profile {profile_name}"
47
+ )
48
+
49
+ with open(creds_file) as f:
50
+ creds = json.load(f)
51
+
52
+ if profile_name not in creds:
53
+ raise click.ClickException(
54
+ f"No credentials found for profile '{profile_name}'.\n"
55
+ f"Run: cc configure --profile {profile_name}"
56
+ )
57
+
58
+ profile['credentials'] = creds[profile_name]
59
+
60
+ return profile
61
+
62
+
63
+ def calculate_costs(profile_config, accounts, start_date, offset, window):
64
+ """
65
+ Calculate AWS costs for the specified period.
66
+
67
+ Args:
68
+ profile_config: Profile configuration (with aws_profile or credentials)
69
+ accounts: List of AWS account IDs
70
+ start_date: Start date (defaults to today)
71
+ offset: Days to go back from start_date (default: 2)
72
+ window: Number of days to analyze (default: 30)
73
+
74
+ Returns:
75
+ dict with cost breakdown
76
+ """
77
+ # Calculate date range
78
+ if start_date:
79
+ end_date = datetime.strptime(start_date, '%Y-%m-%d')
80
+ else:
81
+ end_date = datetime.now()
82
+
83
+ # Go back by offset days
84
+ end_date = end_date - timedelta(days=offset)
85
+
86
+ # Start date is window days before end_date
87
+ start_date_calc = end_date - timedelta(days=window)
88
+
89
+ # Format for API (end date is exclusive, so add 1 day)
90
+ api_start = start_date_calc.strftime('%Y-%m-%d')
91
+ api_end = (end_date + timedelta(days=1)).strftime('%Y-%m-%d')
92
+
93
+ click.echo(f"Analyzing: {api_start} to {end_date.strftime('%Y-%m-%d')} ({window} days)")
94
+
95
+ # Initialize boto3 client
96
+ try:
97
+ if 'aws_profile' in profile_config:
98
+ # SSO-based authentication
99
+ aws_profile = profile_config['aws_profile']
100
+ click.echo(f"AWS Profile: {aws_profile} (SSO)")
101
+ click.echo(f"Accounts: {len(accounts)}")
102
+ click.echo("")
103
+ session = boto3.Session(profile_name=aws_profile)
104
+ ce_client = session.client('ce', region_name='us-east-1')
105
+ else:
106
+ # Static credentials
107
+ creds = profile_config['credentials']
108
+ click.echo(f"AWS Credentials: Static")
109
+ click.echo(f"Accounts: {len(accounts)}")
110
+ click.echo("")
111
+
112
+ session_kwargs = {
113
+ 'aws_access_key_id': creds['aws_access_key_id'],
114
+ 'aws_secret_access_key': creds['aws_secret_access_key'],
115
+ 'region_name': creds.get('region', 'us-east-1')
116
+ }
117
+
118
+ if 'aws_session_token' in creds:
119
+ session_kwargs['aws_session_token'] = creds['aws_session_token']
120
+
121
+ session = boto3.Session(**session_kwargs)
122
+ ce_client = session.client('ce')
123
+
124
+ except Exception as e:
125
+ if 'Token has expired' in str(e) or 'sso' in str(e).lower():
126
+ if 'aws_profile' in profile_config:
127
+ raise click.ClickException(
128
+ f"AWS SSO session expired or not initialized.\n"
129
+ f"Run: aws sso login --profile {profile_config['aws_profile']}"
130
+ )
131
+ else:
132
+ raise click.ClickException(
133
+ f"AWS credentials expired.\n"
134
+ f"Run: cc configure --profile <profile_name>"
135
+ )
136
+ raise
137
+
138
+ # Build filter
139
+ cost_filter = {
140
+ "And": [
141
+ {
142
+ "Dimensions": {
143
+ "Key": "LINKED_ACCOUNT",
144
+ "Values": accounts
145
+ }
146
+ },
147
+ {
148
+ "Dimensions": {
149
+ "Key": "BILLING_ENTITY",
150
+ "Values": ["AWS"]
151
+ }
152
+ },
153
+ {
154
+ "Not": {
155
+ "Dimensions": {
156
+ "Key": "RECORD_TYPE",
157
+ "Values": ["Tax", "Support"]
158
+ }
159
+ }
160
+ }
161
+ ]
162
+ }
163
+
164
+ # Get daily costs
165
+ click.echo("Fetching cost data...")
166
+ try:
167
+ response = ce_client.get_cost_and_usage(
168
+ TimePeriod={
169
+ 'Start': api_start,
170
+ 'End': api_end
171
+ },
172
+ Granularity='DAILY',
173
+ Metrics=['NetAmortizedCost'],
174
+ Filter=cost_filter
175
+ )
176
+ except Exception as e:
177
+ if 'Token has expired' in str(e) or 'expired' in str(e).lower():
178
+ raise click.ClickException(
179
+ f"AWS SSO session expired.\n"
180
+ f"Run: aws sso login --profile {aws_profile}"
181
+ )
182
+ raise
183
+
184
+ # Calculate total
185
+ total_cost = sum(
186
+ float(day['Total']['NetAmortizedCost']['Amount'])
187
+ for day in response['ResultsByTime']
188
+ )
189
+
190
+ # Get support cost from the 1st of the month containing the end date
191
+ # Support is charged on the 1st of each month for the previous month's usage
192
+ # For Oct 3-Nov 2 analysis, we get support from Nov 1 (which is October's support)
193
+ support_month_date = end_date.replace(day=1)
194
+ support_date_str = support_month_date.strftime('%Y-%m-%d')
195
+ support_date_end = (support_month_date + timedelta(days=1)).strftime('%Y-%m-%d')
196
+
197
+ click.echo("Fetching support costs...")
198
+ support_response = ce_client.get_cost_and_usage(
199
+ TimePeriod={
200
+ 'Start': support_date_str,
201
+ 'End': support_date_end
202
+ },
203
+ Granularity='DAILY',
204
+ Metrics=['NetAmortizedCost'],
205
+ Filter={
206
+ "And": [
207
+ {
208
+ "Dimensions": {
209
+ "Key": "LINKED_ACCOUNT",
210
+ "Values": accounts
211
+ }
212
+ },
213
+ {
214
+ "Dimensions": {
215
+ "Key": "RECORD_TYPE",
216
+ "Values": ["Support"]
217
+ }
218
+ }
219
+ ]
220
+ }
221
+ )
222
+
223
+ support_cost = float(support_response['ResultsByTime'][0]['Total']['NetAmortizedCost']['Amount'])
224
+
225
+ # Calculate days in the month that the support covers
226
+ # Support on Nov 1 covers October (31 days)
227
+ support_month = support_month_date - timedelta(days=1) # Go back to previous month
228
+ days_in_support_month = support_month.day # This gives us the last day of the month
229
+
230
+ # Support allocation: divide by 2 (half to Khoros), then by days in month
231
+ support_per_day = (support_cost / 2) / days_in_support_month
232
+
233
+ # Calculate daily rate
234
+ # NOTE: We divide operational by window, but support by days_in_support_month
235
+ # This matches the console's calculation method
236
+ daily_operational = total_cost / days_in_support_month # Use 31 for October, not 30
237
+ daily_total = daily_operational + support_per_day
238
+
239
+ # Annual projection
240
+ annual = daily_total * 365
241
+
242
+ return {
243
+ 'period': {
244
+ 'start': api_start,
245
+ 'end': end_date.strftime('%Y-%m-%d'),
246
+ 'days': window
247
+ },
248
+ 'costs': {
249
+ 'total_operational': total_cost,
250
+ 'daily_operational': daily_operational,
251
+ 'support_month': support_cost,
252
+ 'support_per_day': support_per_day,
253
+ 'daily_total': daily_total,
254
+ 'annual_projection': annual
255
+ }
256
+ }
257
+
258
+
259
+ @click.group()
260
+ def cli():
261
+ """
262
+ AWS Cost Calculator - Calculate daily and annual AWS costs
263
+
264
+ \b
265
+ Two authentication methods:
266
+ 1. AWS SSO (recommended for interactive use)
267
+ 2. Static credentials (for automation/CI)
268
+
269
+ \b
270
+ Quick Start:
271
+ # SSO Method
272
+ aws sso login --profile my_aws_profile
273
+ cc init --profile myprofile --aws-profile my_aws_profile --accounts "123,456,789"
274
+ cc calculate --profile myprofile
275
+
276
+ # Static Credentials Method
277
+ cc init --profile myprofile --aws-profile dummy --accounts "123,456,789"
278
+ cc configure --profile myprofile
279
+ cc calculate --profile myprofile
280
+
281
+ \b
282
+ For detailed documentation, see:
283
+ - COST_CALCULATION_METHODOLOGY.md
284
+ - README.md
285
+ """
286
+ pass
287
+
288
+
289
+ @cli.command()
290
+ @click.option('--profile', required=True, help='Profile name (e.g., myprofile)')
291
+ @click.option('--start-date', help='Start date (YYYY-MM-DD, default: today)')
292
+ @click.option('--offset', default=2, help='Days to go back from start date (default: 2)')
293
+ @click.option('--window', default=30, help='Number of days to analyze (default: 30)')
294
+ @click.option('--json-output', is_flag=True, help='Output as JSON')
295
+ def calculate(profile, start_date, offset, window, json_output):
296
+ """Calculate AWS costs for the specified period"""
297
+
298
+ # Load profile configuration
299
+ config = load_profile(profile)
300
+
301
+ # Calculate costs
302
+ result = calculate_costs(
303
+ profile_config=config,
304
+ accounts=config['accounts'],
305
+ start_date=start_date,
306
+ offset=offset,
307
+ window=window
308
+ )
309
+
310
+ if json_output:
311
+ click.echo(json.dumps(result, indent=2))
312
+ else:
313
+ # Pretty print results
314
+ click.echo("=" * 60)
315
+ click.echo(f"Period: {result['period']['start']} to {result['period']['end']}")
316
+ click.echo(f"Days analyzed: {result['period']['days']}")
317
+ click.echo("=" * 60)
318
+ click.echo(f"Total operational cost: ${result['costs']['total_operational']:,.2f}")
319
+ click.echo(f"Daily operational: ${result['costs']['daily_operational']:,.2f}")
320
+ click.echo(f"Support (month): ${result['costs']['support_month']:,.2f}")
321
+ click.echo(f"Support per day (÷2÷days): ${result['costs']['support_per_day']:,.2f}")
322
+ click.echo("=" * 60)
323
+ click.echo(f"DAILY RATE: ${result['costs']['daily_total']:,.2f}")
324
+ click.echo(f"ANNUAL PROJECTION: ${result['costs']['annual_projection']:,.0f}")
325
+ click.echo("=" * 60)
326
+
327
+
328
+ @cli.command()
329
+ @click.option('--profile', required=True, help='Profile name to create')
330
+ @click.option('--aws-profile', required=True, help='AWS CLI profile name')
331
+ @click.option('--accounts', required=True, help='Comma-separated list of account IDs')
332
+ def init(profile, aws_profile, accounts):
333
+ """Initialize a new profile configuration"""
334
+
335
+ config_dir = Path.home() / '.config' / 'cost-calculator'
336
+ config_file = config_dir / 'profiles.json'
337
+
338
+ # Create config directory if it doesn't exist
339
+ config_dir.mkdir(parents=True, exist_ok=True)
340
+
341
+ # Load existing profiles or create new
342
+ if config_file.exists() and config_file.stat().st_size > 0:
343
+ try:
344
+ with open(config_file) as f:
345
+ profiles = json.load(f)
346
+ except json.JSONDecodeError:
347
+ profiles = {}
348
+ else:
349
+ profiles = {}
350
+
351
+ # Parse accounts
352
+ account_list = [acc.strip() for acc in accounts.split(',')]
353
+
354
+ # Add new profile
355
+ profiles[profile] = {
356
+ 'aws_profile': aws_profile,
357
+ 'accounts': account_list
358
+ }
359
+
360
+ # Save
361
+ with open(config_file, 'w') as f:
362
+ json.dump(profiles, f, indent=2)
363
+
364
+ click.echo(f"✓ Profile '{profile}' created with {len(account_list)} accounts")
365
+ click.echo(f"✓ Configuration saved to {config_file}")
366
+ click.echo(f"\nUsage: cc calculate --profile {profile}")
367
+
368
+
369
+ @cli.command()
370
+ def list_profiles():
371
+ """List all configured profiles"""
372
+
373
+ config_file = Path.home() / '.config' / 'cost-calculator' / 'profiles.json'
374
+
375
+ if not config_file.exists():
376
+ click.echo("No profiles configured. Run: cc init --profile <name>")
377
+ return
378
+
379
+ with open(config_file) as f:
380
+ profiles = json.load(f)
381
+
382
+ if not profiles:
383
+ click.echo("No profiles configured.")
384
+ return
385
+
386
+ click.echo("Configured profiles:")
387
+ click.echo("")
388
+ for name, config in profiles.items():
389
+ click.echo(f" {name}")
390
+ if 'aws_profile' in config:
391
+ click.echo(f" AWS Profile: {config['aws_profile']} (SSO)")
392
+ else:
393
+ click.echo(f" AWS Credentials: Configured (Static)")
394
+ click.echo(f" Accounts: {len(config['accounts'])}")
395
+ click.echo("")
396
+
397
+
398
+ @cli.command()
399
+ @click.option('--profile', required=True, help='Profile name to configure')
400
+ @click.option('--access-key-id', prompt=True, hide_input=False, help='AWS Access Key ID')
401
+ @click.option('--secret-access-key', prompt=True, hide_input=True, help='AWS Secret Access Key')
402
+ @click.option('--session-token', default='', help='AWS Session Token (optional, for temporary credentials)')
403
+ @click.option('--region', default='us-east-1', help='AWS Region (default: us-east-1)')
404
+ def configure(profile, access_key_id, secret_access_key, session_token, region):
405
+ """Configure AWS credentials for a profile (alternative to SSO)"""
406
+
407
+ config_dir = Path.home() / '.config' / 'cost-calculator'
408
+ config_file = config_dir / 'profiles.json'
409
+ creds_file = config_dir / 'credentials.json'
410
+
411
+ # Create config directory if it doesn't exist
412
+ config_dir.mkdir(parents=True, exist_ok=True)
413
+
414
+ # Load existing profiles
415
+ if config_file.exists() and config_file.stat().st_size > 0:
416
+ try:
417
+ with open(config_file) as f:
418
+ profiles = json.load(f)
419
+ except json.JSONDecodeError:
420
+ profiles = {}
421
+ else:
422
+ profiles = {}
423
+
424
+ # Check if profile exists
425
+ if profile not in profiles:
426
+ click.echo(f"Error: Profile '{profile}' not found. Create it first with: cc init --profile {profile}")
427
+ return
428
+
429
+ # Remove aws_profile if it exists (switching from SSO to static creds)
430
+ if 'aws_profile' in profiles[profile]:
431
+ del profiles[profile]['aws_profile']
432
+
433
+ # Save updated profile
434
+ with open(config_file, 'w') as f:
435
+ json.dump(profiles, f, indent=2)
436
+
437
+ # Load or create credentials file
438
+ if creds_file.exists() and creds_file.stat().st_size > 0:
439
+ try:
440
+ with open(creds_file) as f:
441
+ creds = json.load(f)
442
+ except json.JSONDecodeError:
443
+ creds = {}
444
+ else:
445
+ creds = {}
446
+
447
+ # Store credentials (encrypted would be better, but for now just file permissions)
448
+ creds[profile] = {
449
+ 'aws_access_key_id': access_key_id,
450
+ 'aws_secret_access_key': secret_access_key,
451
+ 'region': region
452
+ }
453
+
454
+ if session_token:
455
+ creds[profile]['aws_session_token'] = session_token
456
+
457
+ # Save credentials with restricted permissions
458
+ with open(creds_file, 'w') as f:
459
+ json.dump(creds, f, indent=2)
460
+
461
+ # Set file permissions to 600 (owner read/write only)
462
+ creds_file.chmod(0o600)
463
+
464
+ click.echo(f"✓ AWS credentials configured for profile '{profile}'")
465
+ click.echo(f"✓ Credentials saved to {creds_file} (permissions: 600)")
466
+ click.echo(f"\nUsage: cc calculate --profile {profile}")
467
+ click.echo("\nNote: Credentials are stored locally. For temporary credentials,")
468
+ click.echo(" you'll need to reconfigure when they expire.")
469
+
470
+
471
+ if __name__ == '__main__':
472
+ cli()