aws-cost-calculator-cli 1.8.2__py3-none-any.whl → 1.11.0__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.8.2
3
+ Version: 1.11.0
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-1.11.0.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=UclTm6G5kZyk0Dw2Ot7IkVhKIJK96VSvKe8D4oCSk1Q,76053
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.0.dist-info/METADATA,sha256=M_I1zl12wIBb6fdmzxeAa7CNBc3gWOggYdzqpYLZNTw,11979
12
+ aws_cost_calculator_cli-1.11.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
13
+ aws_cost_calculator_cli-1.11.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
14
+ aws_cost_calculator_cli-1.11.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
15
+ aws_cost_calculator_cli-1.11.0.dist-info/RECORD,,
cost_calculator/cli.py CHANGED
@@ -68,7 +68,7 @@ def apply_auth_options(config, sso=None, access_key_id=None, secret_access_key=N
68
68
 
69
69
 
70
70
  def load_profile(profile_name):
71
- """Load profile configuration from local file or DynamoDB API"""
71
+ """Load profile configuration from DynamoDB API or local file as fallback"""
72
72
  import os
73
73
  import requests
74
74
 
@@ -76,7 +76,76 @@ def load_profile(profile_name):
76
76
  config_file = config_dir / 'profiles.json'
77
77
  creds_file = config_dir / 'credentials.json'
78
78
 
79
- # Try local file first
79
+ # Try DynamoDB API first if COST_API_SECRET is set
80
+ api_secret = os.environ.get('COST_API_SECRET')
81
+ 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
+ )
147
+
148
+ # Fallback to local file if no API secret
80
149
  if config_file.exists():
81
150
  with open(config_file) as f:
82
151
  profiles = json.load(f)
@@ -114,50 +183,11 @@ def load_profile(profile_name):
114
183
 
115
184
  return profile
116
185
 
117
- # Profile not found locally - try DynamoDB API
118
- api_secret = os.environ.get('COST_API_SECRET')
119
- if not api_secret:
120
- raise click.ClickException(
121
- f"Profile '{profile_name}' not found locally and COST_API_SECRET not set.\n"
122
- f"Run: cc init --profile {profile_name}"
123
- )
124
-
125
- try:
126
- response = requests.post(
127
- 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
128
- headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
129
- json={'operation': 'get', 'profile_name': profile_name},
130
- timeout=10
131
- )
132
-
133
- if response.status_code == 200:
134
- response_data = response.json()
135
- # API returns {"profile": {...}} wrapper
136
- profile_data = response_data.get('profile', response_data)
137
- profile = {'accounts': profile_data['accounts']}
138
-
139
- # Check for AWS_PROFILE environment variable (SSO support)
140
- if os.environ.get('AWS_PROFILE'):
141
- profile['aws_profile'] = os.environ['AWS_PROFILE']
142
- # Use environment credentials
143
- elif os.environ.get('AWS_ACCESS_KEY_ID'):
144
- profile['credentials'] = {
145
- 'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
146
- 'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
147
- 'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
148
- }
149
-
150
- return profile
151
- else:
152
- raise click.ClickException(
153
- f"Profile '{profile_name}' not found in DynamoDB.\n"
154
- f"Run: cc profile create --name {profile_name} --accounts \"...\""
155
- )
156
- except requests.exceptions.RequestException as e:
157
- raise click.ClickException(
158
- f"Failed to fetch profile from API: {e}\n"
159
- f"Run: cc init --profile {profile_name}"
160
- )
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
191
 
162
192
 
163
193
  def calculate_costs(profile_config, accounts, start_date, offset, window):
@@ -1175,6 +1205,27 @@ def investigate(profile, sso, weeks, account, service, no_cloudtrail, output):
1175
1205
  if sso:
1176
1206
  config['aws_profile'] = sso
1177
1207
 
1208
+ # Validate that we have a way to get credentials
1209
+ if 'aws_profile' not in config and 'credentials' not in config:
1210
+ import subprocess
1211
+ try:
1212
+ result = subprocess.run(
1213
+ ['aws', 'configure', 'list-profiles'],
1214
+ capture_output=True,
1215
+ text=True,
1216
+ timeout=5
1217
+ )
1218
+ available = result.stdout.strip().split('\n') if result.returncode == 0 else []
1219
+ suggestion = f"\nAvailable AWS profiles: {', '.join(available[:5])}" if available else ""
1220
+ except:
1221
+ suggestion = ""
1222
+
1223
+ raise click.ClickException(
1224
+ f"Profile '{profile}' has no AWS authentication configured.\n"
1225
+ f"Use --sso flag to specify your AWS SSO profile:\n"
1226
+ f" cc investigate --profile {profile} --sso YOUR_AWS_PROFILE{suggestion}"
1227
+ )
1228
+
1178
1229
  # Step 1: Cost Analysis
1179
1230
  click.echo("Step 1/3: Analyzing cost trends...")
1180
1231
  try:
@@ -1218,27 +1269,23 @@ def investigate(profile, sso, weeks, account, service, no_cloudtrail, output):
1218
1269
  click.echo(f" ⚠️ No credentials available for account")
1219
1270
  continue
1220
1271
 
1221
- # Inventory resources via backend API
1272
+ # Inventory resources via backend API only
1273
+ if not is_api_configured():
1274
+ click.echo(f" ✗ API not configured. Set COST_API_SECRET environment variable.")
1275
+ continue
1276
+
1222
1277
  try:
1223
1278
  regions = ['us-west-2', 'us-east-1', 'eu-west-1']
1224
1279
  for region in regions:
1225
1280
  try:
1226
- if is_api_configured():
1227
- inv = call_lambda_api(
1228
- 'forensics',
1229
- account_creds,
1230
- [], # accounts not needed for forensics
1231
- operation='inventory',
1232
- account_id=acc_id,
1233
- region=region
1234
- )
1235
- else:
1236
- # Fallback to local execution
1237
- from cost_calculator.forensics import inventory_resources
1238
- acc_profile = find_account_profile(acc_id)
1239
- if not acc_profile:
1240
- raise Exception("No SSO profile found and API not configured")
1241
- inv = inventory_resources(acc_id, acc_profile, region)
1281
+ inv = call_lambda_api(
1282
+ 'forensics',
1283
+ account_creds,
1284
+ [], # accounts not needed for forensics
1285
+ operation='inventory',
1286
+ account_id=acc_id,
1287
+ region=region
1288
+ )
1242
1289
 
1243
1290
  if not inv.get('error'):
1244
1291
  inventories.append(inv)
@@ -1252,13 +1299,15 @@ def investigate(profile, sso, weeks, account, service, no_cloudtrail, output):
1252
1299
  except Exception as e:
1253
1300
  click.echo(f" ✗ Inventory error: {str(e)}")
1254
1301
 
1255
- # CloudTrail analysis via backend API
1302
+ # CloudTrail analysis via backend API only
1256
1303
  if not no_cloudtrail:
1257
- try:
1258
- start_date = (datetime.now() - timedelta(days=weeks * 7)).isoformat() + 'Z'
1259
- end_date = datetime.now().isoformat() + 'Z'
1260
-
1261
- if is_api_configured():
1304
+ if not is_api_configured():
1305
+ click.echo(f" ✗ CloudTrail skipped: API not configured")
1306
+ else:
1307
+ try:
1308
+ start_date = (datetime.now() - timedelta(days=weeks * 7)).isoformat() + 'Z'
1309
+ end_date = datetime.now().isoformat() + 'Z'
1310
+
1262
1311
  ct_analysis = call_lambda_api(
1263
1312
  'forensics',
1264
1313
  account_creds,
@@ -1269,26 +1318,17 @@ def investigate(profile, sso, weeks, account, service, no_cloudtrail, output):
1269
1318
  end_date=end_date,
1270
1319
  region='us-west-2'
1271
1320
  )
1272
- else:
1273
- # Fallback to local execution
1274
- from cost_calculator.forensics import analyze_cloudtrail
1275
- acc_profile = find_account_profile(acc_id)
1276
- if not acc_profile:
1277
- raise Exception("No SSO profile found and API not configured")
1278
- start_dt = datetime.now() - timedelta(days=weeks * 7)
1279
- end_dt = datetime.now()
1280
- ct_analysis = analyze_cloudtrail(acc_id, acc_profile, start_dt, end_dt)
1281
-
1282
- cloudtrail_analyses.append(ct_analysis)
1283
-
1284
- if ct_analysis.get('error'):
1285
- click.echo(f" ⚠️ CloudTrail: {ct_analysis['error']}")
1286
- else:
1287
- click.echo(f" ✓ CloudTrail analysis complete")
1288
- click.echo(f" - {len(ct_analysis['event_summary'])} event types")
1289
- click.echo(f" - {len(ct_analysis['write_events'])} resource changes")
1290
- except Exception as e:
1291
- click.echo(f" ✗ CloudTrail error: {str(e)}")
1321
+
1322
+ cloudtrail_analyses.append(ct_analysis)
1323
+
1324
+ if ct_analysis.get('error'):
1325
+ click.echo(f" ⚠️ CloudTrail: {ct_analysis['error']}")
1326
+ else:
1327
+ click.echo(f" ✓ CloudTrail analysis complete")
1328
+ click.echo(f" - {len(ct_analysis['event_summary'])} event types")
1329
+ click.echo(f" - {len(ct_analysis['write_events'])} resource changes")
1330
+ except Exception as e:
1331
+ click.echo(f" ✗ CloudTrail error: {str(e)}")
1292
1332
 
1293
1333
  # Generate report
1294
1334
  click.echo(f"\nGenerating report...")
@@ -1340,5 +1380,596 @@ def find_account_profile(account_id):
1340
1380
  return None
1341
1381
 
1342
1382
 
1383
+ @cli.command()
1384
+ @click.option('--profile', required=True, help='Profile name')
1385
+ @click.option('--start-date', help='Start date (YYYY-MM-DD)')
1386
+ @click.option('--end-date', help='End date (YYYY-MM-DD)')
1387
+ @click.option('--days', type=int, default=10, help='Number of days to analyze (default: 10)')
1388
+ @click.option('--service', help='Filter by service name')
1389
+ @click.option('--account', help='Filter by account ID')
1390
+ @click.option('--sso', help='AWS SSO profile name')
1391
+ @click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
1392
+ def daily(profile, start_date, end_date, days, service, account, sso, output_json):
1393
+ """
1394
+ Get daily cost breakdown with granular detail
1395
+
1396
+ Example:
1397
+ cc daily --profile khoros --days 10 --service AmazonCloudWatch --account 820054669588
1398
+ """
1399
+ # Load profile
1400
+ config = load_profile(profile)
1401
+
1402
+ # Apply SSO if provided
1403
+ if sso:
1404
+ config['aws_profile'] = sso
1405
+
1406
+ # Calculate date range
1407
+ if end_date:
1408
+ end = datetime.strptime(end_date, '%Y-%m-%d')
1409
+ else:
1410
+ end = datetime.now()
1411
+
1412
+ if start_date:
1413
+ start = datetime.strptime(start_date, '%Y-%m-%d')
1414
+ else:
1415
+ start = end - timedelta(days=days)
1416
+
1417
+ click.echo(f"Daily breakdown: {start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}")
1418
+ if service:
1419
+ click.echo(f"Service filter: {service}")
1420
+ if account:
1421
+ click.echo(f"Account filter: {account}")
1422
+ click.echo("")
1423
+
1424
+ # Get credentials
1425
+ try:
1426
+ if 'aws_profile' in config:
1427
+ session = boto3.Session(profile_name=config['aws_profile'])
1428
+ else:
1429
+ creds = config['credentials']
1430
+ session = boto3.Session(
1431
+ aws_access_key_id=creds['aws_access_key_id'],
1432
+ aws_secret_access_key=creds['aws_secret_access_key'],
1433
+ aws_session_token=creds.get('aws_session_token')
1434
+ )
1435
+
1436
+ ce_client = session.client('ce', region_name='us-east-1')
1437
+
1438
+ # Build filter
1439
+ filter_parts = []
1440
+
1441
+ # Account filter
1442
+ if account:
1443
+ filter_parts.append({
1444
+ "Dimensions": {
1445
+ "Key": "LINKED_ACCOUNT",
1446
+ "Values": [account]
1447
+ }
1448
+ })
1449
+ else:
1450
+ filter_parts.append({
1451
+ "Dimensions": {
1452
+ "Key": "LINKED_ACCOUNT",
1453
+ "Values": config['accounts']
1454
+ }
1455
+ })
1456
+
1457
+ # Service filter
1458
+ if service:
1459
+ filter_parts.append({
1460
+ "Dimensions": {
1461
+ "Key": "SERVICE",
1462
+ "Values": [service]
1463
+ }
1464
+ })
1465
+
1466
+ # Exclude support and tax
1467
+ filter_parts.append({
1468
+ "Not": {
1469
+ "Dimensions": {
1470
+ "Key": "RECORD_TYPE",
1471
+ "Values": ["Tax", "Support"]
1472
+ }
1473
+ }
1474
+ })
1475
+
1476
+ cost_filter = {"And": filter_parts} if len(filter_parts) > 1 else filter_parts[0]
1477
+
1478
+ # Get daily costs
1479
+ response = ce_client.get_cost_and_usage(
1480
+ TimePeriod={
1481
+ 'Start': start.strftime('%Y-%m-%d'),
1482
+ 'End': (end + timedelta(days=1)).strftime('%Y-%m-%d')
1483
+ },
1484
+ Granularity='DAILY',
1485
+ Metrics=['UnblendedCost'],
1486
+ Filter=cost_filter
1487
+ )
1488
+
1489
+ # Collect results
1490
+ daily_costs = []
1491
+ total = 0
1492
+ for day in response['ResultsByTime']:
1493
+ date = day['TimePeriod']['Start']
1494
+ cost = float(day['Total']['UnblendedCost']['Amount'])
1495
+ total += cost
1496
+ daily_costs.append({'date': date, 'cost': cost})
1497
+
1498
+ num_days = len(response['ResultsByTime'])
1499
+ daily_avg = total / num_days if num_days > 0 else 0
1500
+ annual = daily_avg * 365
1501
+
1502
+ # Output results
1503
+ if output_json:
1504
+ import json
1505
+ result = {
1506
+ 'period': {
1507
+ 'start': start.strftime('%Y-%m-%d'),
1508
+ 'end': end.strftime('%Y-%m-%d'),
1509
+ 'days': num_days
1510
+ },
1511
+ 'filters': {
1512
+ 'service': service,
1513
+ 'account': account
1514
+ },
1515
+ 'daily_costs': daily_costs,
1516
+ 'summary': {
1517
+ 'total': total,
1518
+ 'daily_avg': daily_avg,
1519
+ 'annual_projection': annual
1520
+ }
1521
+ }
1522
+ click.echo(json.dumps(result, indent=2))
1523
+ else:
1524
+ click.echo("Date | Cost")
1525
+ click.echo("-----------|-----------")
1526
+ for item in daily_costs:
1527
+ click.echo(f"{item['date']} | ${item['cost']:,.2f}")
1528
+ click.echo("-----------|-----------")
1529
+ click.echo(f"Total | ${total:,.2f}")
1530
+ click.echo(f"Daily Avg | ${daily_avg:,.2f}")
1531
+ click.echo(f"Annual | ${annual:,.0f}")
1532
+
1533
+ except Exception as e:
1534
+ raise click.ClickException(f"Failed to get daily costs: {e}")
1535
+
1536
+
1537
+ @cli.command()
1538
+ @click.option('--profile', required=True, help='Profile name')
1539
+ @click.option('--account', help='Account ID to compare')
1540
+ @click.option('--service', help='Service to compare')
1541
+ @click.option('--before', required=True, help='Before period (YYYY-MM-DD:YYYY-MM-DD)')
1542
+ @click.option('--after', required=True, help='After period (YYYY-MM-DD:YYYY-MM-DD)')
1543
+ @click.option('--expected-reduction', type=float, help='Expected reduction percentage')
1544
+ @click.option('--sso', help='AWS SSO profile name')
1545
+ @click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
1546
+ def compare(profile, account, service, before, after, expected_reduction, sso, output_json):
1547
+ """
1548
+ Compare costs between two periods
1549
+
1550
+ Example:
1551
+ cc compare --profile khoros --account 180770971501 --service AmazonCloudWatch \
1552
+ --before "2025-10-28:2025-11-06" --after "2025-11-17:2025-11-26" --expected-reduction 50
1553
+ """
1554
+ # Load profile
1555
+ config = load_profile(profile)
1556
+
1557
+ # Apply SSO if provided
1558
+ if sso:
1559
+ config['aws_profile'] = sso
1560
+
1561
+ # Parse periods
1562
+ try:
1563
+ before_start, before_end = before.split(':')
1564
+ after_start, after_end = after.split(':')
1565
+ except ValueError:
1566
+ raise click.ClickException("Period format must be 'YYYY-MM-DD:YYYY-MM-DD'")
1567
+
1568
+ if not output_json:
1569
+ click.echo(f"Comparing periods:")
1570
+ click.echo(f" Before: {before_start} to {before_end}")
1571
+ click.echo(f" After: {after_start} to {after_end}")
1572
+ if service:
1573
+ click.echo(f" Service: {service}")
1574
+ if account:
1575
+ click.echo(f" Account: {account}")
1576
+ click.echo("")
1577
+
1578
+ # Get credentials
1579
+ try:
1580
+ if 'aws_profile' in config:
1581
+ session = boto3.Session(profile_name=config['aws_profile'])
1582
+ else:
1583
+ creds = config['credentials']
1584
+ session = boto3.Session(
1585
+ aws_access_key_id=creds['aws_access_key_id'],
1586
+ aws_secret_access_key=creds['aws_secret_access_key'],
1587
+ aws_session_token=creds.get('aws_session_token')
1588
+ )
1589
+
1590
+ ce_client = session.client('ce', region_name='us-east-1')
1591
+
1592
+ # Build filter
1593
+ def build_filter():
1594
+ filter_parts = []
1595
+
1596
+ if account:
1597
+ filter_parts.append({
1598
+ "Dimensions": {
1599
+ "Key": "LINKED_ACCOUNT",
1600
+ "Values": [account]
1601
+ }
1602
+ })
1603
+ else:
1604
+ filter_parts.append({
1605
+ "Dimensions": {
1606
+ "Key": "LINKED_ACCOUNT",
1607
+ "Values": config['accounts']
1608
+ }
1609
+ })
1610
+
1611
+ if service:
1612
+ filter_parts.append({
1613
+ "Dimensions": {
1614
+ "Key": "SERVICE",
1615
+ "Values": [service]
1616
+ }
1617
+ })
1618
+
1619
+ filter_parts.append({
1620
+ "Not": {
1621
+ "Dimensions": {
1622
+ "Key": "RECORD_TYPE",
1623
+ "Values": ["Tax", "Support"]
1624
+ }
1625
+ }
1626
+ })
1627
+
1628
+ return {"And": filter_parts} if len(filter_parts) > 1 else filter_parts[0]
1629
+
1630
+ cost_filter = build_filter()
1631
+
1632
+ # Get before period costs
1633
+ before_response = ce_client.get_cost_and_usage(
1634
+ TimePeriod={
1635
+ 'Start': before_start,
1636
+ 'End': (datetime.strptime(before_end, '%Y-%m-%d') + timedelta(days=1)).strftime('%Y-%m-%d')
1637
+ },
1638
+ Granularity='DAILY',
1639
+ Metrics=['UnblendedCost'],
1640
+ Filter=cost_filter
1641
+ )
1642
+
1643
+ # Get after period costs
1644
+ after_response = ce_client.get_cost_and_usage(
1645
+ TimePeriod={
1646
+ 'Start': after_start,
1647
+ 'End': (datetime.strptime(after_end, '%Y-%m-%d') + timedelta(days=1)).strftime('%Y-%m-%d')
1648
+ },
1649
+ Granularity='DAILY',
1650
+ Metrics=['UnblendedCost'],
1651
+ Filter=cost_filter
1652
+ )
1653
+
1654
+ # Calculate totals
1655
+ before_total = sum(float(day['Total']['UnblendedCost']['Amount']) for day in before_response['ResultsByTime'])
1656
+ after_total = sum(float(day['Total']['UnblendedCost']['Amount']) for day in after_response['ResultsByTime'])
1657
+
1658
+ before_days = len(before_response['ResultsByTime'])
1659
+ after_days = len(after_response['ResultsByTime'])
1660
+
1661
+ before_daily = before_total / before_days if before_days > 0 else 0
1662
+ after_daily = after_total / after_days if after_days > 0 else 0
1663
+
1664
+ reduction = before_daily - after_daily
1665
+ reduction_pct = (reduction / before_daily * 100) if before_daily > 0 else 0
1666
+ annual_savings = reduction * 365
1667
+
1668
+ # Output results
1669
+ if output_json:
1670
+ import json
1671
+ result = {
1672
+ 'before': {
1673
+ 'period': {'start': before_start, 'end': before_end},
1674
+ 'total': before_total,
1675
+ 'daily_avg': before_daily,
1676
+ 'days': before_days
1677
+ },
1678
+ 'after': {
1679
+ 'period': {'start': after_start, 'end': after_end},
1680
+ 'total': after_total,
1681
+ 'daily_avg': after_daily,
1682
+ 'days': after_days
1683
+ },
1684
+ 'comparison': {
1685
+ 'daily_reduction': reduction,
1686
+ 'reduction_pct': reduction_pct,
1687
+ 'annual_savings': annual_savings
1688
+ }
1689
+ }
1690
+
1691
+ if expected_reduction is not None:
1692
+ result['comparison']['expected_reduction_pct'] = expected_reduction
1693
+ result['comparison']['met_expectation'] = reduction_pct >= expected_reduction
1694
+
1695
+ click.echo(json.dumps(result, indent=2))
1696
+ else:
1697
+ click.echo("Before Period:")
1698
+ click.echo(f" Total: ${before_total:,.2f}")
1699
+ click.echo(f" Daily Avg: ${before_daily:,.2f}")
1700
+ click.echo(f" Days: {before_days}")
1701
+ click.echo("")
1702
+ click.echo("After Period:")
1703
+ click.echo(f" Total: ${after_total:,.2f}")
1704
+ click.echo(f" Daily Avg: ${after_daily:,.2f}")
1705
+ click.echo(f" Days: {after_days}")
1706
+ click.echo("")
1707
+ click.echo("Comparison:")
1708
+ click.echo(f" Daily Reduction: ${reduction:,.2f}")
1709
+ click.echo(f" Reduction %: {reduction_pct:.1f}%")
1710
+ click.echo(f" Annual Savings: ${annual_savings:,.0f}")
1711
+
1712
+ if expected_reduction is not None:
1713
+ click.echo("")
1714
+ if reduction_pct >= expected_reduction:
1715
+ click.echo(f"✅ Savings achieved: {reduction_pct:.1f}% (expected {expected_reduction}%)")
1716
+ else:
1717
+ click.echo(f"⚠️ Below target: {reduction_pct:.1f}% (expected {expected_reduction}%)")
1718
+
1719
+ except Exception as e:
1720
+ raise click.ClickException(f"Comparison failed: {e}")
1721
+
1722
+
1723
+ @cli.command()
1724
+ @click.option('--profile', required=True, help='Profile name')
1725
+ @click.option('--tag-key', required=True, help='Tag key to filter by')
1726
+ @click.option('--tag-value', help='Tag value to filter by (optional)')
1727
+ @click.option('--start-date', help='Start date (YYYY-MM-DD)')
1728
+ @click.option('--end-date', help='End date (YYYY-MM-DD)')
1729
+ @click.option('--days', type=int, default=30, help='Number of days to analyze (default: 30)')
1730
+ @click.option('--sso', help='AWS SSO profile name')
1731
+ @click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
1732
+ def tags(profile, tag_key, tag_value, start_date, end_date, days, sso, output_json):
1733
+ """
1734
+ Analyze costs by resource tags
1735
+
1736
+ Example:
1737
+ cc tags --profile khoros --tag-key "datadog:org" --days 30
1738
+ cc tags --profile khoros --tag-key "Environment" --tag-value "Production"
1739
+ """
1740
+ # Load profile
1741
+ config = load_profile(profile)
1742
+
1743
+ # Apply SSO if provided
1744
+ if sso:
1745
+ config['aws_profile'] = sso
1746
+
1747
+ # Calculate date range
1748
+ if end_date:
1749
+ end = datetime.strptime(end_date, '%Y-%m-%d')
1750
+ else:
1751
+ end = datetime.now()
1752
+
1753
+ if start_date:
1754
+ start = datetime.strptime(start_date, '%Y-%m-%d')
1755
+ else:
1756
+ start = end - timedelta(days=days)
1757
+
1758
+ if not output_json:
1759
+ click.echo(f"Tag-based cost analysis: {start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}")
1760
+ click.echo(f"Tag key: {tag_key}")
1761
+ if tag_value:
1762
+ click.echo(f"Tag value: {tag_value}")
1763
+ click.echo("")
1764
+
1765
+ # Get credentials
1766
+ try:
1767
+ if 'aws_profile' in config:
1768
+ session = boto3.Session(profile_name=config['aws_profile'])
1769
+ else:
1770
+ creds = config['credentials']
1771
+ session = boto3.Session(
1772
+ aws_access_key_id=creds['aws_access_key_id'],
1773
+ aws_secret_access_key=creds['aws_secret_access_key'],
1774
+ aws_session_token=creds.get('aws_session_token')
1775
+ )
1776
+
1777
+ ce_client = session.client('ce', region_name='us-east-1')
1778
+
1779
+ # Build filter
1780
+ filter_parts = [
1781
+ {
1782
+ "Dimensions": {
1783
+ "Key": "LINKED_ACCOUNT",
1784
+ "Values": config['accounts']
1785
+ }
1786
+ },
1787
+ {
1788
+ "Not": {
1789
+ "Dimensions": {
1790
+ "Key": "RECORD_TYPE",
1791
+ "Values": ["Tax", "Support"]
1792
+ }
1793
+ }
1794
+ }
1795
+ ]
1796
+
1797
+ # Add tag filter if value specified
1798
+ if tag_value:
1799
+ filter_parts.append({
1800
+ "Tags": {
1801
+ "Key": tag_key,
1802
+ "Values": [tag_value]
1803
+ }
1804
+ })
1805
+
1806
+ cost_filter = {"And": filter_parts}
1807
+
1808
+ # Get costs grouped by tag values
1809
+ response = ce_client.get_cost_and_usage(
1810
+ TimePeriod={
1811
+ 'Start': start.strftime('%Y-%m-%d'),
1812
+ 'End': (end + timedelta(days=1)).strftime('%Y-%m-%d')
1813
+ },
1814
+ Granularity='MONTHLY',
1815
+ Metrics=['UnblendedCost'],
1816
+ GroupBy=[{
1817
+ 'Type': 'TAG',
1818
+ 'Key': tag_key
1819
+ }],
1820
+ Filter=cost_filter
1821
+ )
1822
+
1823
+ # Collect results
1824
+ tag_costs = {}
1825
+ for period in response['ResultsByTime']:
1826
+ for group in period['Groups']:
1827
+ tag_val = group['Keys'][0].split('$')[1] if '$' in group['Keys'][0] else group['Keys'][0]
1828
+ cost = float(group['Metrics']['UnblendedCost']['Amount'])
1829
+ tag_costs[tag_val] = tag_costs.get(tag_val, 0) + cost
1830
+
1831
+ # Sort by cost
1832
+ sorted_tags = sorted(tag_costs.items(), key=lambda x: x[1], reverse=True)
1833
+
1834
+ total = sum(tag_costs.values())
1835
+ num_days = (end - start).days
1836
+ daily_avg = total / num_days if num_days > 0 else 0
1837
+
1838
+ # Output results
1839
+ if output_json:
1840
+ import json
1841
+ result = {
1842
+ 'period': {
1843
+ 'start': start.strftime('%Y-%m-%d'),
1844
+ 'end': end.strftime('%Y-%m-%d'),
1845
+ 'days': num_days
1846
+ },
1847
+ 'tag_key': tag_key,
1848
+ 'tag_value_filter': tag_value,
1849
+ 'tag_costs': [{'tag_value': k, 'cost': v, 'percentage': (v/total*100) if total > 0 else 0} for k, v in sorted_tags],
1850
+ 'summary': {
1851
+ 'total': total,
1852
+ 'daily_avg': daily_avg,
1853
+ 'annual_projection': daily_avg * 365
1854
+ }
1855
+ }
1856
+ click.echo(json.dumps(result, indent=2))
1857
+ else:
1858
+ click.echo(f"Tag Value{' '*(30-len('Tag Value'))} | Cost | %")
1859
+ click.echo("-" * 60)
1860
+ for tag_val, cost in sorted_tags:
1861
+ pct = (cost / total * 100) if total > 0 else 0
1862
+ tag_display = tag_val[:30].ljust(30)
1863
+ click.echo(f"{tag_display} | ${cost:>9,.2f} | {pct:>5.1f}%")
1864
+ click.echo("-" * 60)
1865
+ click.echo(f"{'Total'.ljust(30)} | ${total:>9,.2f} | 100.0%")
1866
+ click.echo("")
1867
+ click.echo(f"Daily Avg: ${daily_avg:,.2f}")
1868
+ click.echo(f"Annual Projection: ${daily_avg * 365:,.0f}")
1869
+
1870
+ except Exception as e:
1871
+ raise click.ClickException(f"Tag analysis failed: {e}")
1872
+
1873
+
1874
+ @cli.command()
1875
+ @click.option('--profile', required=True, help='Profile name')
1876
+ @click.option('--query', required=True, help='SQL query to execute')
1877
+ @click.option('--database', default='athenacurcfn_cloud_intelligence_dashboard', help='Athena database name')
1878
+ @click.option('--output-bucket', help='S3 bucket for query results (default: from profile)')
1879
+ @click.option('--sso', help='AWS SSO profile name')
1880
+ def query(profile, query, database, output_bucket, sso):
1881
+ """
1882
+ Execute custom Athena SQL query on CUR data
1883
+
1884
+ Example:
1885
+ cc query --profile khoros --query "SELECT line_item_usage_account_id, SUM(line_item_unblended_cost) as cost FROM cloud_intelligence_dashboard WHERE line_item_usage_start_date >= DATE '2025-11-01' GROUP BY 1 ORDER BY 2 DESC LIMIT 10"
1886
+ """
1887
+ # Load profile
1888
+ config = load_profile(profile)
1889
+
1890
+ # Apply SSO if provided
1891
+ if sso:
1892
+ config['aws_profile'] = sso
1893
+
1894
+ # Get credentials
1895
+ try:
1896
+ if 'aws_profile' in config:
1897
+ session = boto3.Session(profile_name=config['aws_profile'])
1898
+ else:
1899
+ creds = config['credentials']
1900
+ session = boto3.Session(
1901
+ aws_access_key_id=creds['aws_access_key_id'],
1902
+ aws_secret_access_key=creds['aws_secret_access_key'],
1903
+ aws_session_token=creds.get('aws_session_token')
1904
+ )
1905
+
1906
+ athena_client = session.client('athena', region_name='us-east-1')
1907
+
1908
+ # Default output location
1909
+ if not output_bucket:
1910
+ output_bucket = 's3://khoros-finops-athena/athena/'
1911
+
1912
+ click.echo(f"Executing query on database: {database}")
1913
+ click.echo(f"Output location: {output_bucket}")
1914
+ click.echo("")
1915
+
1916
+ # Execute query
1917
+ response = athena_client.start_query_execution(
1918
+ QueryString=query,
1919
+ QueryExecutionContext={'Database': database},
1920
+ ResultConfiguration={'OutputLocation': output_bucket}
1921
+ )
1922
+
1923
+ query_id = response['QueryExecutionId']
1924
+ click.echo(f"Query ID: {query_id}")
1925
+ click.echo("Waiting for query to complete...")
1926
+
1927
+ # Wait for completion
1928
+ import time
1929
+ max_wait = 60
1930
+ waited = 0
1931
+ while waited < max_wait:
1932
+ status_response = athena_client.get_query_execution(QueryExecutionId=query_id)
1933
+ status = status_response['QueryExecution']['Status']['State']
1934
+
1935
+ if status == 'SUCCEEDED':
1936
+ click.echo("✓ Query completed successfully")
1937
+ break
1938
+ elif status in ['FAILED', 'CANCELLED']:
1939
+ reason = status_response['QueryExecution']['Status'].get('StateChangeReason', 'Unknown')
1940
+ raise click.ClickException(f"Query {status}: {reason}")
1941
+
1942
+ time.sleep(2)
1943
+ waited += 2
1944
+
1945
+ if waited >= max_wait:
1946
+ raise click.ClickException(f"Query timeout after {max_wait}s. Check query ID: {query_id}")
1947
+
1948
+ # Get results
1949
+ results = athena_client.get_query_results(QueryExecutionId=query_id)
1950
+
1951
+ # Display results
1952
+ rows = results['ResultSet']['Rows']
1953
+ if not rows:
1954
+ click.echo("No results returned")
1955
+ return
1956
+
1957
+ # Header
1958
+ headers = [col['VarCharValue'] for col in rows[0]['Data']]
1959
+ click.echo(" | ".join(headers))
1960
+ click.echo("-" * (len(" | ".join(headers))))
1961
+
1962
+ # Data rows
1963
+ for row in rows[1:]:
1964
+ values = [col.get('VarCharValue', '') for col in row['Data']]
1965
+ click.echo(" | ".join(values))
1966
+
1967
+ click.echo("")
1968
+ click.echo(f"Returned {len(rows)-1} rows")
1969
+
1970
+ except Exception as e:
1971
+ raise click.ClickException(f"Query failed: {e}")
1972
+
1973
+
1343
1974
  if __name__ == '__main__':
1344
1975
  cli()
@@ -3,6 +3,7 @@ Executor that routes to either API or local execution.
3
3
  """
4
4
  import boto3
5
5
  import click
6
+ from pathlib import Path
6
7
  from cost_calculator.api_client import is_api_configured, call_lambda_api
7
8
 
8
9
 
@@ -22,6 +23,13 @@ def get_credentials_dict(config):
22
23
  try:
23
24
  session = boto3.Session(profile_name=config['aws_profile'])
24
25
  credentials = session.get_credentials()
26
+
27
+ if credentials is None:
28
+ raise Exception(
29
+ f"Could not get credentials for profile '{config['aws_profile']}'.\n"
30
+ f"Run: aws sso login --profile {config['aws_profile']}"
31
+ )
32
+
25
33
  frozen_creds = credentials.get_frozen_credentials()
26
34
 
27
35
  return {
@@ -29,9 +37,44 @@ def get_credentials_dict(config):
29
37
  'secret_key': frozen_creds.secret_key,
30
38
  'session_token': frozen_creds.token
31
39
  }
32
- except Exception:
33
- # If profile not found, return None (API will handle)
34
- return None
40
+ except Exception as e:
41
+ # Show the actual error instead of silently returning None
42
+ error_msg = str(e)
43
+
44
+ # If it's an SSO token error, provide better guidance
45
+ if 'SSO Token' in error_msg or 'sso' in error_msg.lower():
46
+ # Try to detect if using sso_session format
47
+ import subprocess
48
+ try:
49
+ result = subprocess.run(
50
+ ['grep', '-A', '3', f'profile {config["aws_profile"]}',
51
+ str(Path.home() / '.aws' / 'config')],
52
+ capture_output=True,
53
+ text=True,
54
+ timeout=2
55
+ )
56
+ if 'sso_session' in result.stdout:
57
+ # Extract session name
58
+ for line in result.stdout.split('\n'):
59
+ if 'sso_session' in line:
60
+ session_name = line.split('=')[1].strip()
61
+ raise Exception(
62
+ f"Failed to get AWS credentials for profile '{config['aws_profile']}'.\n"
63
+ f"Error: {error_msg}\n\n"
64
+ f"Your profile uses SSO session '{session_name}'.\n"
65
+ f"Try: aws sso login --sso-session {session_name}\n"
66
+ f"(Requires AWS CLI v2.9.0+)\n\n"
67
+ f"Or: aws sso login --profile {config['aws_profile']}\n"
68
+ f"(If using older AWS CLI)"
69
+ )
70
+ except:
71
+ pass
72
+
73
+ raise Exception(
74
+ f"Failed to get AWS credentials for profile '{config['aws_profile']}'.\n"
75
+ f"Error: {error_msg}\n"
76
+ f"Try: aws sso login --profile {config['aws_profile']}"
77
+ )
35
78
  else:
36
79
  # Use static credentials
37
80
  creds = config.get('credentials', {})
@@ -56,32 +99,18 @@ def execute_trends(config, weeks):
56
99
  """
57
100
  accounts = config['accounts']
58
101
 
59
- if is_api_configured():
60
- # Use API
61
- click.echo("Using Lambda API...")
62
- credentials = get_credentials_dict(config)
63
- return call_lambda_api('trends', credentials, accounts, weeks=weeks)
64
- else:
65
- # Use local execution
66
- click.echo("Using local execution...")
67
- from cost_calculator.trends import analyze_trends
68
-
69
- # Initialize boto3 client
70
- if 'aws_profile' in config:
71
- session = boto3.Session(profile_name=config['aws_profile'])
72
- else:
73
- creds = config['credentials']
74
- session_kwargs = {
75
- 'aws_access_key_id': creds['aws_access_key_id'],
76
- 'aws_secret_access_key': creds['aws_secret_access_key'],
77
- 'region_name': creds.get('region', 'us-east-1')
78
- }
79
- if 'aws_session_token' in creds:
80
- session_kwargs['aws_session_token'] = creds['aws_session_token']
81
- session = boto3.Session(**session_kwargs)
82
-
83
- ce_client = session.client('ce', region_name='us-east-1')
84
- return analyze_trends(ce_client, accounts, weeks)
102
+ if not is_api_configured():
103
+ raise Exception(
104
+ "API not configured. Set COST_API_SECRET environment variable.\n"
105
+ "Local execution is disabled. Use the Lambda API."
106
+ )
107
+
108
+ # Use API only
109
+ click.echo("Using Lambda API...")
110
+ credentials = get_credentials_dict(config)
111
+ if not credentials:
112
+ raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
113
+ return call_lambda_api('trends', credentials, accounts, weeks=weeks)
85
114
 
86
115
 
87
116
  def execute_monthly(config, months):
@@ -93,37 +122,23 @@ def execute_monthly(config, months):
93
122
  """
94
123
  accounts = config['accounts']
95
124
 
96
- if is_api_configured():
97
- # Use API
98
- click.echo("Using Lambda API...")
99
- credentials = get_credentials_dict(config)
100
- return call_lambda_api('monthly', credentials, accounts, months=months)
101
- else:
102
- # Use local execution
103
- click.echo("Using local execution...")
104
- from cost_calculator.monthly import analyze_monthly_trends
105
-
106
- # Initialize boto3 client
107
- if 'aws_profile' in config:
108
- session = boto3.Session(profile_name=config['aws_profile'])
109
- else:
110
- creds = config['credentials']
111
- session_kwargs = {
112
- 'aws_access_key_id': creds['aws_access_key_id'],
113
- 'aws_secret_access_key': creds['aws_secret_access_key'],
114
- 'region_name': creds.get('region', 'us-east-1')
115
- }
116
- if 'aws_session_token' in creds:
117
- session_kwargs['aws_session_token'] = creds['aws_session_token']
118
- session = boto3.Session(**session_kwargs)
119
-
120
- ce_client = session.client('ce', region_name='us-east-1')
121
- return analyze_monthly_trends(ce_client, accounts, months)
125
+ if not is_api_configured():
126
+ raise Exception(
127
+ "API not configured. Set COST_API_SECRET environment variable.\n"
128
+ "Local execution is disabled. Use the Lambda API."
129
+ )
130
+
131
+ # Use API only
132
+ click.echo("Using Lambda API...")
133
+ credentials = get_credentials_dict(config)
134
+ if not credentials:
135
+ raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
136
+ return call_lambda_api('monthly', credentials, accounts, months=months)
122
137
 
123
138
 
124
139
  def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None, resources=False):
125
140
  """
126
- Execute drill-down analysis via API or locally.
141
+ Execute drill-down analysis via API.
127
142
 
128
143
  Args:
129
144
  config: Profile configuration
@@ -138,86 +153,31 @@ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage
138
153
  """
139
154
  accounts = config['accounts']
140
155
 
156
+ if not is_api_configured():
157
+ raise Exception(
158
+ "API not configured. Set COST_API_SECRET environment variable.\n"
159
+ "Local execution is disabled. Use the Lambda API."
160
+ )
161
+
162
+ # Use API only
163
+ click.echo("Using Lambda API...")
164
+ credentials = get_credentials_dict(config)
165
+ if not credentials:
166
+ raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
167
+
168
+ kwargs = {'weeks': weeks}
169
+ if service_filter:
170
+ kwargs['service'] = service_filter
171
+ if account_filter:
172
+ kwargs['account'] = account_filter
173
+ if usage_type_filter:
174
+ kwargs['usage_type'] = usage_type_filter
141
175
  if resources:
142
- # Resource-level drill requires service filter
143
176
  if not service_filter:
144
177
  raise click.ClickException("--service is required when using --resources flag")
145
-
146
- if is_api_configured():
147
- # Use API
148
- click.echo("Using Lambda API for CUR resource query...")
149
- credentials = get_credentials_dict(config)
150
- kwargs = {
151
- 'weeks': weeks,
152
- 'service': service_filter,
153
- 'resources': True
154
- }
155
- if account_filter:
156
- kwargs['account'] = account_filter
157
- return call_lambda_api('drill', credentials, accounts, **kwargs)
158
- else:
159
- # Use local Athena client
160
- click.echo("Using local Athena client for CUR resource query...")
161
- from cost_calculator.cur import query_cur_resources
162
-
163
- # Initialize boto3 session
164
- if 'aws_profile' in config:
165
- session = boto3.Session(profile_name=config['aws_profile'])
166
- else:
167
- creds = config['credentials']
168
- session_kwargs = {
169
- 'aws_access_key_id': creds['aws_access_key_id'],
170
- 'aws_secret_access_key': creds['aws_secret_access_key'],
171
- 'region_name': creds.get('region', 'us-east-1')
172
- }
173
- if 'aws_session_token' in creds:
174
- session_kwargs['aws_session_token'] = creds['aws_session_token']
175
- session = boto3.Session(**session_kwargs)
176
-
177
- athena_client = session.client('athena', region_name='us-east-1')
178
- return query_cur_resources(
179
- athena_client, accounts, service_filter, account_filter, weeks
180
- )
181
- else:
182
- # Standard drill-down via Cost Explorer
183
- if is_api_configured():
184
- # Use API
185
- click.echo("Using Lambda API...")
186
- credentials = get_credentials_dict(config)
187
- kwargs = {'weeks': weeks}
188
- if service_filter:
189
- kwargs['service'] = service_filter
190
- if account_filter:
191
- kwargs['account'] = account_filter
192
- if usage_type_filter:
193
- kwargs['usage_type'] = usage_type_filter
194
- return call_lambda_api('drill', credentials, accounts, **kwargs)
195
- else:
196
- # Use local execution
197
- click.echo("Using local execution...")
198
- from cost_calculator.drill import analyze_drill_down
199
-
200
- # Initialize boto3 client
201
- if 'aws_profile' in config:
202
- session = boto3.Session(profile_name=config['aws_profile'])
203
- else:
204
- creds = config['credentials']
205
- session_kwargs = {
206
- 'aws_access_key_id': creds['aws_access_key_id'],
207
- 'aws_secret_access_key': creds['aws_secret_access_key'],
208
- 'region_name': creds.get('region', 'us-east-1')
209
- }
210
- if 'aws_session_token' in creds:
211
- session_kwargs['aws_session_token'] = creds['aws_session_token']
212
- session = boto3.Session(**session_kwargs)
213
-
214
- ce_client = session.client('ce', region_name='us-east-1')
215
- return analyze_drill_down(
216
- ce_client, accounts, weeks,
217
- service_filter=service_filter,
218
- account_filter=account_filter,
219
- usage_type_filter=usage_type_filter
220
- )
178
+ kwargs['resources'] = True
179
+
180
+ return call_lambda_api('drill', credentials, accounts, **kwargs)
221
181
 
222
182
 
223
183
  def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
@@ -236,7 +236,8 @@ def format_investigation_report(cost_data, inventories, cloudtrail_data=None):
236
236
  report.append("")
237
237
 
238
238
  for inv in inventories:
239
- report.append(f"### Account {inv['account_id']} ({inv['profile']})")
239
+ profile_name = inv.get('profile', inv['account_id'])
240
+ report.append(f"### Account {inv['account_id']} ({profile_name})")
240
241
  report.append(f"**Region:** {inv['region']}")
241
242
  report.append("")
242
243
 
@@ -288,7 +289,8 @@ def format_investigation_report(cost_data, inventories, cloudtrail_data=None):
288
289
  report.append("")
289
290
 
290
291
  for ct in cloudtrail_data:
291
- report.append(f"### Account {ct['account_id']} ({ct['profile']})")
292
+ profile_name = ct.get('profile', ct['account_id'])
293
+ report.append(f"### Account {ct['account_id']} ({profile_name})")
292
294
  report.append(f"**Period:** {ct['start_date'][:10]} to {ct['end_date'][:10]}")
293
295
  report.append("")
294
296
 
@@ -1,15 +0,0 @@
1
- aws_cost_calculator_cli-1.8.2.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=jkyPlHAdVgpk5LrP1T9YvBK4-K6hM5fd538VqIHw-7A,52255
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=aWLELeisDu6b-5U5LDPDPnK5uJKilYcoUPerxPUqtYg,10435
8
- cost_calculator/forensics.py,sha256=Ny5IbMhJF6kawQDiLDExQGb5C-zRDbbmziS5JoinmdA,12694
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.8.2.dist-info/METADATA,sha256=wCkJT5XL6m3ESmbK0mdfzSpkWiJn9PJQ6nuUBelILXU,11978
12
- aws_cost_calculator_cli-1.8.2.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
13
- aws_cost_calculator_cli-1.8.2.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
14
- aws_cost_calculator_cli-1.8.2.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
15
- aws_cost_calculator_cli-1.8.2.dist-info/RECORD,,