aws-cost-calculator-cli 2.1.0__tar.gz → 2.3.2__tar.gz

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.
Files changed (23) hide show
  1. {aws_cost_calculator_cli-2.1.0/aws_cost_calculator_cli.egg-info → aws_cost_calculator_cli-2.3.2}/PKG-INFO +1 -1
  2. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2/aws_cost_calculator_cli.egg-info}/PKG-INFO +1 -1
  3. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/SOURCES.txt +1 -0
  4. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/api_client.py +18 -21
  5. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/cli.py +92 -133
  6. aws_cost_calculator_cli-2.3.2/cost_calculator/dimensions.py +141 -0
  7. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/executor.py +20 -10
  8. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/setup.py +1 -1
  9. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/CHANGES.md +0 -0
  10. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/LICENSE +0 -0
  11. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/MANIFEST.in +0 -0
  12. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/README.md +0 -0
  13. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/dependency_links.txt +0 -0
  14. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/entry_points.txt +0 -0
  15. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/requires.txt +0 -0
  16. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/aws_cost_calculator_cli.egg-info/top_level.txt +0 -0
  17. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/__init__.py +0 -0
  18. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/cur.py +0 -0
  19. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/drill.py +0 -0
  20. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/forensics.py +0 -0
  21. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/monthly.py +0 -0
  22. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/cost_calculator/trends.py +0 -0
  23. {aws_cost_calculator_cli-2.1.0 → aws_cost_calculator_cli-2.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 2.1.0
3
+ Version: 2.3.2
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 2.1.0
3
+ Version: 2.3.2
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
@@ -13,6 +13,7 @@ cost_calculator/__init__.py
13
13
  cost_calculator/api_client.py
14
14
  cost_calculator/cli.py
15
15
  cost_calculator/cur.py
16
+ cost_calculator/dimensions.py
16
17
  cost_calculator/drill.py
17
18
  cost_calculator/executor.py
18
19
  cost_calculator/forensics.py
@@ -23,14 +23,15 @@ def get_api_config():
23
23
  return None
24
24
 
25
25
 
26
- def call_lambda_api(endpoint, credentials, accounts, **kwargs):
26
+ def call_lambda_api(endpoint, credentials, accounts=None, profile=None, **kwargs):
27
27
  """
28
- Call Lambda API endpoint.
28
+ Call unified API Gateway endpoint.
29
29
 
30
30
  Args:
31
- endpoint: API endpoint name ('trends', 'monthly', 'drill')
31
+ endpoint: API endpoint name ('calculate', 'trends', 'monthly', 'drill', 'query', etc.)
32
32
  credentials: dict with AWS credentials
33
- accounts: list of account IDs
33
+ accounts: list of account IDs (deprecated - use profile instead)
34
+ profile: profile name (preferred over accounts)
34
35
  **kwargs: additional parameters for the specific endpoint
35
36
 
36
37
  Returns:
@@ -44,26 +45,22 @@ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
44
45
  if not api_config:
45
46
  raise Exception("API not configured. Set COST_API_SECRET environment variable.")
46
47
 
47
- # Map endpoint names to Lambda URLs
48
- endpoint_urls = {
49
- 'trends': 'https://pq3mqntc6vuwi4zw5flulsoleq0yiqtl.lambda-url.us-east-1.on.aws/',
50
- 'monthly': 'https://6aueebodw6q4zdeu3aaexb6tle0fqhhr.lambda-url.us-east-1.on.aws/',
51
- 'drill': 'https://3ncm2gzxrsyptrhud3ua3x5lju0akvsr.lambda-url.us-east-1.on.aws/',
52
- 'analyze': 'https://y6npmidtxwzg62nrqzkbacfs5q0edwgs.lambda-url.us-east-1.on.aws/',
53
- 'profiles': 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
54
- 'forensics': 'https://gaekfzz7sc2hwn4mjyk64sieke0vadfo.lambda-url.us-east-1.on.aws/' # Will be populated after deployment
55
- }
56
-
57
- url = endpoint_urls.get(endpoint)
58
-
59
- if not url:
60
- raise Exception(f"Unknown endpoint: {endpoint}")
48
+ # Get base URL from environment or use default
49
+ base_url = os.environ.get('COST_CALCULATOR_API_URL', 'https://api.costcop.cloudfix.dev')
50
+ url = f"{base_url}/{endpoint}"
61
51
 
62
52
  # Build request payload
63
53
  payload = {
64
- 'credentials': credentials,
65
- 'accounts': accounts
54
+ 'credentials': credentials
66
55
  }
56
+
57
+ # Add profile or accounts
58
+ if profile:
59
+ payload['profile'] = profile
60
+ elif accounts:
61
+ payload['accounts'] = accounts
62
+
63
+ # Add additional parameters
67
64
  payload.update(kwargs)
68
65
 
69
66
  # Make API call
@@ -72,7 +69,7 @@ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
72
69
  'Content-Type': 'application/json'
73
70
  }
74
71
 
75
- response = requests.post(url, headers=headers, json=payload, timeout=300)
72
+ response = requests.post(url, headers=headers, json=payload, timeout=900) # 15 min timeout
76
73
 
77
74
  if response.status_code != 200:
78
75
  raise Exception(f"API call failed: {response.status_code} - {response.text}")
@@ -20,9 +20,14 @@ from cost_calculator.monthly import format_monthly_markdown
20
20
  from cost_calculator.drill import format_drill_down_markdown
21
21
 
22
22
  # API Configuration - can be overridden via environment variable
23
+ API_BASE_URL = os.environ.get(
24
+ 'COST_CALCULATOR_API_URL',
25
+ 'https://api.costcop.cloudfix.dev'
26
+ )
27
+ # Legacy profiles URL for backward compatibility
23
28
  PROFILES_API_URL = os.environ.get(
24
29
  'COST_CALCULATOR_PROFILES_URL',
25
- 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
30
+ f'{API_BASE_URL}/profiles'
26
31
  )
27
32
  from cost_calculator.executor import execute_trends, execute_monthly, execute_drill
28
33
 
@@ -210,7 +215,10 @@ def load_profile(profile_name):
210
215
  response_data = response.json()
211
216
  # API returns {"profile": {...}} wrapper
212
217
  profile_data = response_data.get('profile', response_data)
213
- profile = {'accounts': profile_data['accounts']}
218
+ profile = {
219
+ 'accounts': profile_data['accounts'],
220
+ 'profile_name': profile_name # Store profile name for API calls
221
+ }
214
222
 
215
223
  # If profile has aws_profile field, use it
216
224
  if 'aws_profile' in profile_data:
@@ -669,145 +677,61 @@ def calculate(profile, start_date, offset, window, json_output, sso, access_key_
669
677
  click.echo("=" * 60)
670
678
 
671
679
 
672
- @cli.command()
673
- @click.option('--profile', required=True, help='Profile name to create')
674
- @click.option('--aws-profile', required=True, help='AWS CLI profile name')
675
- @click.option('--accounts', required=True, help='Comma-separated list of account IDs')
676
- def init(profile, aws_profile, accounts):
677
- """Initialize a new profile configuration"""
678
-
679
- config_dir = Path.home() / '.config' / 'cost-calculator'
680
- config_file = config_dir / 'profiles.json'
681
-
682
- # Create config directory if it doesn't exist
683
- config_dir.mkdir(parents=True, exist_ok=True)
684
-
685
- # Load existing profiles or create new
686
- if config_file.exists() and config_file.stat().st_size > 0:
687
- try:
688
- with open(config_file) as f:
689
- profiles = json.load(f)
690
- except json.JSONDecodeError:
691
- profiles = {}
692
- else:
693
- profiles = {}
694
-
695
- # Parse accounts
696
- account_list = [acc.strip() for acc in accounts.split(',')]
697
-
698
- # Add new profile
699
- profiles[profile] = {
700
- 'aws_profile': aws_profile,
701
- 'accounts': account_list
702
- }
703
-
704
- # Save
705
- with open(config_file, 'w') as f:
706
- json.dump(profiles, f, indent=2)
707
-
708
- click.echo(f"✓ Profile '{profile}' created with {len(account_list)} accounts")
709
- click.echo(f"✓ Configuration saved to {config_file}")
710
- click.echo(f"\nUsage: cc calculate --profile {profile}")
680
+ # init command removed - use backend API via 'cc profile create' instead
711
681
 
712
682
 
713
683
  @cli.command()
714
684
  def list_profiles():
715
- """List all configured profiles"""
716
-
717
- config_file = Path.home() / '.config' / 'cost-calculator' / 'profiles.json'
718
-
719
- if not config_file.exists():
720
- click.echo("No profiles configured. Run: cc init --profile <name>")
721
- return
722
-
723
- with open(config_file) as f:
724
- profiles = json.load(f)
685
+ """List all profiles from backend API (no local caching)"""
686
+ import requests
725
687
 
726
- if not profiles:
727
- click.echo("No profiles configured.")
688
+ api_secret = get_api_secret()
689
+ if not api_secret:
690
+ click.echo("No API secret configured.")
691
+ click.echo("Run: cc configure --api-secret YOUR_SECRET")
728
692
  return
729
693
 
730
- click.echo("Configured profiles:")
731
- click.echo("")
732
- for name, config in profiles.items():
733
- click.echo(f" {name}")
734
- if 'aws_profile' in config:
735
- click.echo(f" AWS Profile: {config['aws_profile']} (SSO)")
736
- else:
737
- click.echo(f" AWS Credentials: Configured (Static)")
738
- click.echo(f" Accounts: {len(config['accounts'])}")
694
+ try:
695
+ response = requests.post(
696
+ f"{API_BASE_URL}/profiles",
697
+ headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
698
+ json={'operation': 'list'},
699
+ timeout=10
700
+ )
701
+
702
+ if response.status_code != 200:
703
+ click.echo(f"Error: {response.status_code} - {response.text}")
704
+ return
705
+
706
+ result = response.json()
707
+ profiles = result.get('profiles', [])
708
+
709
+ if not profiles:
710
+ click.echo("No profiles found in backend.")
711
+ click.echo("Contact admin to create profiles in DynamoDB.")
712
+ return
713
+
714
+ click.echo("Profiles (from backend API):")
739
715
  click.echo("")
716
+ for profile in profiles:
717
+ # Each profile is a dict, extract the profile_name
718
+ if isinstance(profile, dict):
719
+ name = profile.get('profile_name', 'unknown')
720
+ # Skip exclusions entries
721
+ if not name.startswith('exclusions:'):
722
+ accounts = profile.get('accounts', [])
723
+ click.echo(f" {name}")
724
+ click.echo(f" Accounts: {len(accounts)}")
725
+ click.echo("")
726
+ else:
727
+ click.echo(f" {profile}")
728
+ click.echo(f"Total: {len([p for p in profiles if isinstance(p, dict) and not p.get('profile_name', '').startswith('exclusions:')])} profile(s)")
729
+
730
+ except Exception as e:
731
+ click.echo(f"Error loading profiles: {e}")
740
732
 
741
733
 
742
- @cli.command()
743
- def setup():
744
- """Show setup instructions for manual profile configuration"""
745
- import platform
746
-
747
- system = platform.system()
748
-
749
- if system == "Windows":
750
- config_path = "%USERPROFILE%\\.config\\cost-calculator\\profiles.json"
751
- config_path_example = "C:\\Users\\YourName\\.config\\cost-calculator\\profiles.json"
752
- mkdir_cmd = "mkdir %USERPROFILE%\\.config\\cost-calculator"
753
- edit_cmd = "notepad %USERPROFILE%\\.config\\cost-calculator\\profiles.json"
754
- else: # macOS/Linux
755
- config_path = "~/.config/cost-calculator/profiles.json"
756
- config_path_example = "/Users/yourname/.config/cost-calculator/profiles.json"
757
- mkdir_cmd = "mkdir -p ~/.config/cost-calculator"
758
- edit_cmd = "nano ~/.config/cost-calculator/profiles.json"
759
-
760
- click.echo("=" * 70)
761
- click.echo("AWS Cost Calculator - Manual Profile Setup")
762
- click.echo("=" * 70)
763
- click.echo("")
764
- click.echo(f"Platform: {system}")
765
- click.echo(f"Config location: {config_path}")
766
- click.echo("")
767
- click.echo("Step 1: Create the config directory")
768
- click.echo(f" {mkdir_cmd}")
769
- click.echo("")
770
- click.echo("Step 2: Create the profiles.json file")
771
- click.echo(f" {edit_cmd}")
772
- click.echo("")
773
- click.echo("Step 3: Add your profile configuration (JSON format):")
774
- click.echo("")
775
- click.echo(' {')
776
- click.echo(' "myprofile": {')
777
- click.echo(' "aws_profile": "my_aws_profile",')
778
- click.echo(' "accounts": [')
779
- click.echo(' "123456789012",')
780
- click.echo(' "234567890123",')
781
- click.echo(' "345678901234"')
782
- click.echo(' ]')
783
- click.echo(' }')
784
- click.echo(' }')
785
- click.echo("")
786
- click.echo("Step 4: Save the file")
787
- click.echo("")
788
- click.echo("Step 5: Verify it works")
789
- click.echo(" cc list-profiles")
790
- click.echo("")
791
- click.echo("Step 6: Configure AWS credentials")
792
- click.echo(" Option A (SSO):")
793
- click.echo(" aws sso login --profile my_aws_profile")
794
- click.echo(" cc calculate --profile myprofile")
795
- click.echo("")
796
- click.echo(" Option B (Static credentials):")
797
- click.echo(" cc configure --profile myprofile")
798
- click.echo(" cc calculate --profile myprofile")
799
- click.echo("")
800
- click.echo("=" * 70)
801
- click.echo("")
802
- click.echo("For multiple profiles, add more entries to the JSON:")
803
- click.echo("")
804
- click.echo(' {')
805
- click.echo(' "profile1": { ... },')
806
- click.echo(' "profile2": { ... }')
807
- click.echo(' }')
808
- click.echo("")
809
- click.echo(f"Full path example: {config_path_example}")
810
- click.echo("=" * 70)
734
+ # setup command removed - profiles are managed in DynamoDB backend only
811
735
 
812
736
 
813
737
  @cli.command()
@@ -1033,6 +957,10 @@ def monthly(profile, months, output, json_output, sso, access_key_id, secret_acc
1033
957
  @click.option('--service', help='Filter by service name (e.g., "EC2 - Other")')
1034
958
  @click.option('--account', help='Filter by account ID')
1035
959
  @click.option('--usage-type', help='Filter by usage type')
960
+ @click.option('--dimension', type=click.Choice(['service', 'account', 'region', 'usage_type', 'resource', 'instance_type', 'operation', 'availability_zone']),
961
+ help='Dimension to analyze by (overrides --service/--account filters)')
962
+ @click.option('--backend', type=click.Choice(['auto', 'ce', 'athena']), default='auto',
963
+ help='Data source: auto (smart selection), ce (Cost Explorer), athena (CUR). Default: auto')
1036
964
  @click.option('--resources', is_flag=True, help='Show individual resource IDs (requires CUR, uses Athena)')
1037
965
  @click.option('--output', default='drill_down.md', help='Output markdown file (default: drill_down.md)')
1038
966
  @click.option('--json-output', is_flag=True, help='Output as JSON')
@@ -1040,19 +968,49 @@ def monthly(profile, months, output, json_output, sso, access_key_id, secret_acc
1040
968
  @click.option('--access-key-id', help='AWS Access Key ID')
1041
969
  @click.option('--secret-access-key', help='AWS Secret Access Key')
1042
970
  @click.option('--session-token', help='AWS Session Token')
1043
- def drill(profile, weeks, service, account, usage_type, resources, output, json_output, sso, access_key_id, secret_access_key, session_token):
971
+ def drill(profile, weeks, service, account, usage_type, dimension, backend, resources, output, json_output, sso, access_key_id, secret_access_key, session_token):
1044
972
  """
1045
973
  Drill down into cost changes by service, account, or usage type
1046
974
 
1047
975
  Add --resources flag to see individual resource IDs and costs (requires CUR data via Athena)
976
+
977
+ Examples:
978
+ # Analyze by service (auto-selects Cost Explorer for speed)
979
+ cc drill --profile khoros --dimension service
980
+
981
+ # Analyze by region with specific service filter
982
+ cc drill --profile khoros --dimension region --service AWSELB
983
+
984
+ # Analyze by resource (auto-selects Athena - only source with resource IDs)
985
+ cc drill --profile khoros --dimension resource --service AWSELB --account 820054669588
986
+
987
+ # Force Athena backend for detailed analysis
988
+ cc drill --profile khoros --dimension service --backend athena
1048
989
  """
1049
990
 
1050
991
  # Load profile
1051
992
  config = load_profile(profile)
1052
993
  config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
1053
994
 
995
+ # Smart backend selection
996
+ def select_backend(dimension, resources_flag, backend_choice):
997
+ """Auto-select backend based on query requirements"""
998
+ if backend_choice != 'auto':
999
+ return backend_choice
1000
+
1001
+ # Must use Athena if:
1002
+ if dimension == 'resource' or resources_flag:
1003
+ return 'athena'
1004
+
1005
+ # Prefer CE for speed (unless explicitly requesting Athena)
1006
+ return 'ce'
1007
+
1008
+ selected_backend = select_backend(dimension, resources, backend)
1009
+
1054
1010
  # Show filters
1055
1011
  click.echo(f"Analyzing last {weeks} weeks...")
1012
+ if dimension:
1013
+ click.echo(f" Dimension: {dimension}")
1056
1014
  if service:
1057
1015
  click.echo(f" Service filter: {service}")
1058
1016
  if account:
@@ -1061,10 +1019,11 @@ def drill(profile, weeks, service, account, usage_type, resources, output, json_
1061
1019
  click.echo(f" Usage type filter: {usage_type}")
1062
1020
  if resources:
1063
1021
  click.echo(f" Mode: Resource-level (CUR via Athena)")
1022
+ click.echo(f" Backend: {selected_backend.upper()}")
1064
1023
  click.echo("")
1065
1024
 
1066
1025
  # Execute via API or locally
1067
- drill_data = execute_drill(config, weeks, service, account, usage_type, resources)
1026
+ drill_data = execute_drill(config, weeks, service, account, usage_type, resources, dimension, selected_backend)
1068
1027
 
1069
1028
  # Handle resource-level output differently
1070
1029
  if resources:
@@ -0,0 +1,141 @@
1
+ """
2
+ Dimension mapping between CLI, Cost Explorer, and Athena CUR.
3
+
4
+ This module provides utilities for translating dimension names across different backends.
5
+ """
6
+
7
+ # Dimension mapping: CLI dimension -> (CE dimension, Athena column)
8
+ DIMENSION_MAP = {
9
+ 'service': ('SERVICE', 'line_item_product_code'),
10
+ 'account': ('LINKED_ACCOUNT', 'line_item_usage_account_id'),
11
+ 'region': ('REGION', 'product_region'),
12
+ 'usage_type': ('USAGE_TYPE', 'line_item_usage_type'),
13
+ 'resource': (None, 'line_item_resource_id'), # Athena only
14
+ 'instance_type': ('INSTANCE_TYPE', 'product_instance_type'),
15
+ 'operation': ('OPERATION', 'line_item_operation'),
16
+ 'availability_zone': ('AVAILABILITY_ZONE', 'product_availability_zone'),
17
+ }
18
+
19
+
20
+ def get_ce_dimension(cli_dimension):
21
+ """
22
+ Get Cost Explorer dimension key for a CLI dimension.
23
+
24
+ Args:
25
+ cli_dimension: CLI dimension name (e.g., 'service', 'account')
26
+
27
+ Returns:
28
+ str: Cost Explorer dimension key (e.g., 'SERVICE', 'LINKED_ACCOUNT')
29
+ None: If dimension is not available in Cost Explorer
30
+
31
+ Raises:
32
+ ValueError: If dimension is unknown
33
+ """
34
+ if cli_dimension not in DIMENSION_MAP:
35
+ raise ValueError(f"Unknown dimension: {cli_dimension}")
36
+
37
+ ce_dim, _ = DIMENSION_MAP[cli_dimension]
38
+ return ce_dim
39
+
40
+
41
+ def get_athena_column(cli_dimension):
42
+ """
43
+ Get Athena CUR column name for a CLI dimension.
44
+
45
+ Args:
46
+ cli_dimension: CLI dimension name (e.g., 'service', 'account')
47
+
48
+ Returns:
49
+ str: Athena column name (e.g., 'line_item_product_code', 'line_item_usage_account_id')
50
+
51
+ Raises:
52
+ ValueError: If dimension is unknown
53
+ """
54
+ if cli_dimension not in DIMENSION_MAP:
55
+ raise ValueError(f"Unknown dimension: {cli_dimension}")
56
+
57
+ _, athena_col = DIMENSION_MAP[cli_dimension]
58
+ return athena_col
59
+
60
+
61
+ def is_athena_only(cli_dimension):
62
+ """
63
+ Check if a dimension is only available in Athena (not in Cost Explorer).
64
+
65
+ Args:
66
+ cli_dimension: CLI dimension name
67
+
68
+ Returns:
69
+ bool: True if dimension requires Athena, False otherwise
70
+ """
71
+ if cli_dimension not in DIMENSION_MAP:
72
+ raise ValueError(f"Unknown dimension: {cli_dimension}")
73
+
74
+ ce_dim, _ = DIMENSION_MAP[cli_dimension]
75
+ return ce_dim is None
76
+
77
+
78
+ def get_available_dimensions(backend='auto'):
79
+ """
80
+ Get list of available dimensions for a given backend.
81
+
82
+ Args:
83
+ backend: 'auto', 'ce', or 'athena'
84
+
85
+ Returns:
86
+ list: List of available dimension names
87
+ """
88
+ if backend == 'athena':
89
+ # All dimensions available in Athena
90
+ return list(DIMENSION_MAP.keys())
91
+ elif backend == 'ce':
92
+ # Only dimensions with CE mapping
93
+ return [dim for dim, (ce_dim, _) in DIMENSION_MAP.items() if ce_dim is not None]
94
+ else: # auto
95
+ # All dimensions (backend will be auto-selected)
96
+ return list(DIMENSION_MAP.keys())
97
+
98
+
99
+ def validate_dimension_backend(dimension, backend):
100
+ """
101
+ Validate that a dimension is available for the specified backend.
102
+
103
+ Args:
104
+ dimension: CLI dimension name
105
+ backend: 'auto', 'ce', or 'athena'
106
+
107
+ Returns:
108
+ tuple: (is_valid, error_message)
109
+
110
+ Examples:
111
+ >>> validate_dimension_backend('resource', 'ce')
112
+ (False, "Dimension 'resource' requires Athena backend (not available in Cost Explorer)")
113
+
114
+ >>> validate_dimension_backend('service', 'ce')
115
+ (True, None)
116
+ """
117
+ if dimension not in DIMENSION_MAP:
118
+ return False, f"Unknown dimension: {dimension}"
119
+
120
+ if backend == 'ce' and is_athena_only(dimension):
121
+ return False, f"Dimension '{dimension}' requires Athena backend (not available in Cost Explorer)"
122
+
123
+ return True, None
124
+
125
+
126
+ # Human-readable dimension descriptions
127
+ DIMENSION_DESCRIPTIONS = {
128
+ 'service': 'AWS Service (e.g., EC2, S3, RDS)',
129
+ 'account': 'AWS Account ID',
130
+ 'region': 'AWS Region (e.g., us-east-1, eu-west-1)',
131
+ 'usage_type': 'Usage Type (e.g., BoxUsage:t3.micro)',
132
+ 'resource': 'Resource ID/ARN (Athena only)',
133
+ 'instance_type': 'Instance Type (e.g., t3.micro, m5.large)',
134
+ 'operation': 'Operation (e.g., RunInstances, CreateBucket)',
135
+ 'availability_zone': 'Availability Zone (e.g., us-east-1a)',
136
+ }
137
+
138
+
139
+ def get_dimension_description(dimension):
140
+ """Get human-readable description for a dimension."""
141
+ return DIMENSION_DESCRIPTIONS.get(dimension, dimension)
@@ -92,12 +92,16 @@ def get_credentials_dict(config):
92
92
 
93
93
  def execute_trends(config, weeks):
94
94
  """
95
- Execute trends analysis via API or locally.
95
+ Execute trends analysis via API.
96
+
97
+ Args:
98
+ config: Profile configuration
99
+ weeks: Number of weeks to analyze
96
100
 
97
101
  Returns:
98
102
  dict: trends data
99
103
  """
100
- accounts = config['accounts']
104
+ profile_name = config.get('profile_name', config.get('name'))
101
105
 
102
106
  if not is_api_configured():
103
107
  raise Exception(
@@ -110,7 +114,7 @@ def execute_trends(config, weeks):
110
114
  credentials = get_credentials_dict(config)
111
115
  if not credentials:
112
116
  raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
113
- return call_lambda_api('trends', credentials, accounts, weeks=weeks)
117
+ return call_lambda_api('trends', credentials, profile=profile_name, weeks=weeks)
114
118
 
115
119
 
116
120
  def execute_monthly(config, months):
@@ -120,7 +124,7 @@ def execute_monthly(config, months):
120
124
  Returns:
121
125
  dict: monthly data
122
126
  """
123
- accounts = config['accounts']
127
+ profile_name = config.get('profile_name', config.get('name'))
124
128
 
125
129
  if not is_api_configured():
126
130
  raise Exception(
@@ -133,10 +137,10 @@ def execute_monthly(config, months):
133
137
  credentials = get_credentials_dict(config)
134
138
  if not credentials:
135
139
  raise Exception("Failed to get AWS credentials. Check your AWS SSO session.")
136
- return call_lambda_api('monthly', credentials, accounts, months=months)
140
+ return call_lambda_api('monthly', credentials, profile=profile_name, months=months)
137
141
 
138
142
 
139
- def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None, resources=False):
143
+ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage_type_filter=None, resources=False, dimension=None, backend='auto'):
140
144
  """
141
145
  Execute drill-down analysis via API.
142
146
 
@@ -147,11 +151,13 @@ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage
147
151
  account_filter: Optional account ID filter
148
152
  usage_type_filter: Optional usage type filter
149
153
  resources: If True, query CUR for resource-level details
154
+ dimension: Dimension to analyze by (service, account, region, usage_type, resource, etc.)
155
+ backend: Backend to use ('auto', 'ce', 'athena')
150
156
 
151
157
  Returns:
152
158
  dict: drill data or resource data
153
159
  """
154
- accounts = config['accounts']
160
+ profile_name = config.get('profile_name', config.get('name'))
155
161
 
156
162
  if not is_api_configured():
157
163
  raise Exception(
@@ -172,12 +178,16 @@ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage
172
178
  kwargs['account'] = account_filter
173
179
  if usage_type_filter:
174
180
  kwargs['usage_type'] = usage_type_filter
181
+ if dimension:
182
+ kwargs['dimension'] = dimension
183
+ if backend and backend != 'auto':
184
+ kwargs['backend'] = backend
175
185
  if resources:
176
- if not service_filter:
177
- raise click.ClickException("--service is required when using --resources flag")
186
+ if not service_filter and not dimension:
187
+ raise click.ClickException("--service or --dimension is required when using --resources flag")
178
188
  kwargs['resources'] = True
179
189
 
180
- return call_lambda_api('drill', credentials, accounts, **kwargs)
190
+ return call_lambda_api('drill', credentials, profile=profile_name, **kwargs)
181
191
 
182
192
 
183
193
  def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name='aws-cost-calculator-cli',
8
- version='2.1.0',
8
+ version='2.3.2',
9
9
  packages=['cost_calculator'],
10
10
  install_requires=[
11
11
  'click>=8.0.0',