aws-cost-calculator-cli 1.11.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aws-cost-calculator-cli might be problematic. Click here for more details.
- {aws_cost_calculator_cli-1.11.0.dist-info → aws_cost_calculator_cli-2.0.1.dist-info}/METADATA +1 -1
- aws_cost_calculator_cli-2.0.1.dist-info/RECORD +15 -0
- cost_calculator/cli.py +202 -176
- aws_cost_calculator_cli-1.11.0.dist-info/RECORD +0 -15
- {aws_cost_calculator_cli-1.11.0.dist-info → aws_cost_calculator_cli-2.0.1.dist-info}/WHEEL +0 -0
- {aws_cost_calculator_cli-1.11.0.dist-info → aws_cost_calculator_cli-2.0.1.dist-info}/entry_points.txt +0 -0
- {aws_cost_calculator_cli-1.11.0.dist-info → aws_cost_calculator_cli-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {aws_cost_calculator_cli-1.11.0.dist-info → aws_cost_calculator_cli-2.0.1.dist-info}/top_level.txt +0 -0
{aws_cost_calculator_cli-1.11.0.dist-info → aws_cost_calculator_cli-2.0.1.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cost-calculator-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
|
|
5
5
|
Home-page: https://github.com/trilogy-group/aws-cost-calculator
|
|
6
6
|
Author: Cost Optimization Team
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
aws_cost_calculator_cli-2.0.1.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
|
|
2
|
+
cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
|
|
3
|
+
cost_calculator/api_client.py,sha256=4ZI2XcGIN3FBeQqb7xOxQ91kCoeM43-rExiOELXoKBQ,2485
|
|
4
|
+
cost_calculator/cli.py,sha256=7j_jK2PKOchF9oereH6GtDtTjDnlhiTaVuS5FFdeL6U,76371
|
|
5
|
+
cost_calculator/cur.py,sha256=QaZ_nyDSw5_cti-h5Ho6eYLbqzY5TWoub24DpyzIiSs,9502
|
|
6
|
+
cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
|
|
7
|
+
cost_calculator/executor.py,sha256=yZTCUgJc1OpB892O3mq9ZA0Yekc7N-HvaW8xLFyrXjo,8681
|
|
8
|
+
cost_calculator/forensics.py,sha256=uhRo3I_zOeMEaBENHfgq65URga31W0Z4vzS2UN6VmTY,12819
|
|
9
|
+
cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
|
|
10
|
+
cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
|
|
11
|
+
aws_cost_calculator_cli-2.0.1.dist-info/METADATA,sha256=vC_DLqKCR82e1D9uideemfOE6AihjB57fR4wS1F-WWc,11978
|
|
12
|
+
aws_cost_calculator_cli-2.0.1.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
13
|
+
aws_cost_calculator_cli-2.0.1.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
|
|
14
|
+
aws_cost_calculator_cli-2.0.1.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
|
|
15
|
+
aws_cost_calculator_cli-2.0.1.dist-info/RECORD,,
|
cost_calculator/cli.py
CHANGED
|
@@ -67,127 +67,107 @@ def apply_auth_options(config, sso=None, access_key_id=None, secret_access_key=N
|
|
|
67
67
|
return config
|
|
68
68
|
|
|
69
69
|
|
|
70
|
-
def
|
|
71
|
-
"""
|
|
70
|
+
def get_api_secret():
|
|
71
|
+
"""Get API secret from config file or environment variable"""
|
|
72
72
|
import os
|
|
73
|
-
import requests
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
config_file = config_dir / 'profiles.json'
|
|
77
|
-
creds_file = config_dir / 'credentials.json'
|
|
78
|
-
|
|
79
|
-
# Try DynamoDB API first if COST_API_SECRET is set
|
|
74
|
+
# Check environment variable first
|
|
80
75
|
api_secret = os.environ.get('COST_API_SECRET')
|
|
81
76
|
if api_secret:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
timeout=10
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
if response.status_code == 200:
|
|
91
|
-
response_data = response.json()
|
|
92
|
-
# API returns {"profile": {...}} wrapper
|
|
93
|
-
profile_data = response_data.get('profile', response_data)
|
|
94
|
-
profile = {'accounts': profile_data['accounts']}
|
|
95
|
-
|
|
96
|
-
# If profile has aws_profile field, use it
|
|
97
|
-
if 'aws_profile' in profile_data:
|
|
98
|
-
profile['aws_profile'] = profile_data['aws_profile']
|
|
99
|
-
# Check for AWS_PROFILE environment variable (SSO support)
|
|
100
|
-
elif os.environ.get('AWS_PROFILE'):
|
|
101
|
-
profile['aws_profile'] = os.environ['AWS_PROFILE']
|
|
102
|
-
# Use environment credentials
|
|
103
|
-
elif os.environ.get('AWS_ACCESS_KEY_ID'):
|
|
104
|
-
profile['credentials'] = {
|
|
105
|
-
'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
|
|
106
|
-
'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
107
|
-
'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
|
|
108
|
-
}
|
|
109
|
-
else:
|
|
110
|
-
# Try to find a matching AWS profile by name
|
|
111
|
-
# This allows "khoros" profile to work with "khoros_umbrella" AWS profile
|
|
112
|
-
import subprocess
|
|
113
|
-
try:
|
|
114
|
-
result = subprocess.run(
|
|
115
|
-
['aws', 'configure', 'list-profiles'],
|
|
116
|
-
capture_output=True,
|
|
117
|
-
text=True,
|
|
118
|
-
timeout=5
|
|
119
|
-
)
|
|
120
|
-
if result.returncode == 0:
|
|
121
|
-
available_profiles = result.stdout.strip().split('\n')
|
|
122
|
-
# Try exact match first
|
|
123
|
-
if profile_name in available_profiles:
|
|
124
|
-
profile['aws_profile'] = profile_name
|
|
125
|
-
# Try with common suffixes
|
|
126
|
-
elif f"{profile_name}_umbrella" in available_profiles:
|
|
127
|
-
profile['aws_profile'] = f"{profile_name}_umbrella"
|
|
128
|
-
elif f"{profile_name}-umbrella" in available_profiles:
|
|
129
|
-
profile['aws_profile'] = f"{profile_name}-umbrella"
|
|
130
|
-
elif f"{profile_name}_prod" in available_profiles:
|
|
131
|
-
profile['aws_profile'] = f"{profile_name}_prod"
|
|
132
|
-
# If no match found, leave it unset - user must provide --sso
|
|
133
|
-
except:
|
|
134
|
-
# If we can't list profiles, leave it unset - user must provide --sso
|
|
135
|
-
pass
|
|
136
|
-
|
|
137
|
-
return profile
|
|
138
|
-
else:
|
|
139
|
-
raise click.ClickException(
|
|
140
|
-
f"Profile '{profile_name}' not found in DynamoDB.\n"
|
|
141
|
-
f"Run: cc profile create --name {profile_name} --accounts \"...\""
|
|
142
|
-
)
|
|
143
|
-
except requests.exceptions.RequestException as e:
|
|
144
|
-
raise click.ClickException(
|
|
145
|
-
f"Failed to fetch profile from API: {e}\n"
|
|
146
|
-
)
|
|
77
|
+
return api_secret
|
|
78
|
+
|
|
79
|
+
# Check config file
|
|
80
|
+
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
81
|
+
config_file = config_dir / 'config.json'
|
|
147
82
|
|
|
148
|
-
# Fallback to local file if no API secret
|
|
149
83
|
if config_file.exists():
|
|
150
84
|
with open(config_file) as f:
|
|
151
|
-
|
|
85
|
+
config = json.load(f)
|
|
86
|
+
return config.get('api_secret')
|
|
87
|
+
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_profile(profile_name):
|
|
92
|
+
"""Load profile configuration from DynamoDB API (API-only, no local files)"""
|
|
93
|
+
import requests
|
|
94
|
+
|
|
95
|
+
# Get API secret
|
|
96
|
+
api_secret = get_api_secret()
|
|
97
|
+
|
|
98
|
+
if not api_secret:
|
|
99
|
+
raise click.ClickException(
|
|
100
|
+
"No API secret configured.\n"
|
|
101
|
+
"Run: cc configure --api-secret YOUR_SECRET\n"
|
|
102
|
+
"Or set environment variable: export COST_API_SECRET=YOUR_SECRET"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
response = requests.post(
|
|
107
|
+
'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
|
|
108
|
+
headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
|
|
109
|
+
json={'operation': 'get', 'profile_name': profile_name},
|
|
110
|
+
timeout=10
|
|
111
|
+
)
|
|
152
112
|
|
|
153
|
-
if
|
|
154
|
-
|
|
113
|
+
if response.status_code == 200:
|
|
114
|
+
response_data = response.json()
|
|
115
|
+
# API returns {"profile": {...}} wrapper
|
|
116
|
+
profile_data = response_data.get('profile', response_data)
|
|
117
|
+
profile = {'accounts': profile_data['accounts']}
|
|
155
118
|
|
|
156
|
-
#
|
|
157
|
-
if 'aws_profile'
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
f"Run: cc configure --profile {profile_name}"
|
|
119
|
+
# If profile has aws_profile field, use it
|
|
120
|
+
if 'aws_profile' in profile_data:
|
|
121
|
+
profile['aws_profile'] = profile_data['aws_profile']
|
|
122
|
+
# Check for AWS_PROFILE environment variable (SSO support)
|
|
123
|
+
elif os.environ.get('AWS_PROFILE'):
|
|
124
|
+
profile['aws_profile'] = os.environ['AWS_PROFILE']
|
|
125
|
+
# Use environment credentials
|
|
126
|
+
elif os.environ.get('AWS_ACCESS_KEY_ID'):
|
|
127
|
+
profile['credentials'] = {
|
|
128
|
+
'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
|
|
129
|
+
'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
|
|
130
|
+
'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
|
|
131
|
+
}
|
|
132
|
+
else:
|
|
133
|
+
# Try to find a matching AWS profile by name
|
|
134
|
+
# This allows "khoros" profile to work with "khoros_umbrella" AWS profile
|
|
135
|
+
import subprocess
|
|
136
|
+
try:
|
|
137
|
+
result = subprocess.run(
|
|
138
|
+
['aws', 'configure', 'list-profiles'],
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
timeout=5
|
|
180
142
|
)
|
|
181
|
-
|
|
182
|
-
|
|
143
|
+
if result.returncode == 0:
|
|
144
|
+
available_profiles = result.stdout.strip().split('\n')
|
|
145
|
+
# Try exact match first
|
|
146
|
+
if profile_name in available_profiles:
|
|
147
|
+
profile['aws_profile'] = profile_name
|
|
148
|
+
# Try with common suffixes
|
|
149
|
+
elif f"{profile_name}_umbrella" in available_profiles:
|
|
150
|
+
profile['aws_profile'] = f"{profile_name}_umbrella"
|
|
151
|
+
elif f"{profile_name}-umbrella" in available_profiles:
|
|
152
|
+
profile['aws_profile'] = f"{profile_name}-umbrella"
|
|
153
|
+
elif f"{profile_name}_prod" in available_profiles:
|
|
154
|
+
profile['aws_profile'] = f"{profile_name}_prod"
|
|
155
|
+
# If no match found, leave it unset - user must provide --sso
|
|
156
|
+
except:
|
|
157
|
+
# If we can't list profiles, leave it unset - user must provide --sso
|
|
158
|
+
pass
|
|
183
159
|
|
|
184
160
|
return profile
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
161
|
+
else:
|
|
162
|
+
raise click.ClickException(
|
|
163
|
+
f"Profile '{profile_name}' not found in DynamoDB.\n"
|
|
164
|
+
f"Run: cc profile create --name {profile_name} --accounts \"...\""
|
|
165
|
+
)
|
|
166
|
+
except requests.exceptions.RequestException as e:
|
|
167
|
+
raise click.ClickException(
|
|
168
|
+
f"Failed to fetch profile from API: {e}\n"
|
|
169
|
+
"Check your API secret and network connection."
|
|
170
|
+
)
|
|
191
171
|
|
|
192
172
|
|
|
193
173
|
def calculate_costs(profile_config, accounts, start_date, offset, window):
|
|
@@ -737,77 +717,81 @@ def setup():
|
|
|
737
717
|
|
|
738
718
|
|
|
739
719
|
@cli.command()
|
|
740
|
-
@click.option('--
|
|
741
|
-
@click.option('--
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
def configure(profile, access_key_id, secret_access_key, session_token, region):
|
|
746
|
-
"""Configure AWS credentials for a profile (alternative to SSO)"""
|
|
720
|
+
@click.option('--api-secret', help='API secret for DynamoDB profile access')
|
|
721
|
+
@click.option('--show', is_flag=True, help='Show current configuration')
|
|
722
|
+
def configure(api_secret, show):
|
|
723
|
+
"""
|
|
724
|
+
Configure Cost Calculator CLI settings.
|
|
747
725
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
creds_file = config_dir / 'credentials.json'
|
|
726
|
+
This tool requires an API secret to access profiles stored in DynamoDB.
|
|
727
|
+
The secret can be configured here or set via COST_API_SECRET environment variable.
|
|
751
728
|
|
|
752
|
-
|
|
729
|
+
Examples:
|
|
730
|
+
# Configure API secret
|
|
731
|
+
cc configure --api-secret YOUR_SECRET_KEY
|
|
732
|
+
|
|
733
|
+
# Show current configuration
|
|
734
|
+
cc configure --show
|
|
735
|
+
|
|
736
|
+
# Use environment variable instead (no configuration needed)
|
|
737
|
+
export COST_API_SECRET=YOUR_SECRET_KEY
|
|
738
|
+
"""
|
|
739
|
+
import os
|
|
740
|
+
|
|
741
|
+
config_dir = Path.home() / '.config' / 'cost-calculator'
|
|
753
742
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
743
|
+
config_file = config_dir / 'config.json'
|
|
754
744
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
745
|
+
if show:
|
|
746
|
+
# Show current configuration
|
|
747
|
+
if config_file.exists():
|
|
758
748
|
with open(config_file) as f:
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
749
|
+
config = json.load(f)
|
|
750
|
+
if 'api_secret' in config:
|
|
751
|
+
masked_secret = config['api_secret'][:8] + '...' + config['api_secret'][-4:]
|
|
752
|
+
click.echo(f"API Secret: {masked_secret} (configured)")
|
|
753
|
+
else:
|
|
754
|
+
click.echo("API Secret: Not configured")
|
|
755
|
+
else:
|
|
756
|
+
click.echo("No configuration file found")
|
|
757
|
+
|
|
758
|
+
# Check environment variable
|
|
759
|
+
import os
|
|
760
|
+
if os.environ.get('COST_API_SECRET'):
|
|
761
|
+
click.echo("Environment: COST_API_SECRET is set")
|
|
762
|
+
else:
|
|
763
|
+
click.echo("Environment: COST_API_SECRET is not set")
|
|
764
|
+
|
|
768
765
|
return
|
|
769
766
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
with open(config_file, 'w') as f:
|
|
776
|
-
json.dump(profiles, f, indent=2)
|
|
777
|
-
|
|
778
|
-
# Load or create credentials file
|
|
779
|
-
if creds_file.exists() and creds_file.stat().st_size > 0:
|
|
780
|
-
try:
|
|
781
|
-
with open(creds_file) as f:
|
|
782
|
-
creds = json.load(f)
|
|
783
|
-
except json.JSONDecodeError:
|
|
784
|
-
creds = {}
|
|
785
|
-
else:
|
|
786
|
-
creds = {}
|
|
767
|
+
if not api_secret:
|
|
768
|
+
raise click.ClickException(
|
|
769
|
+
"Please provide --api-secret or use --show to view current configuration\n"
|
|
770
|
+
"Example: cc configure --api-secret YOUR_SECRET_KEY"
|
|
771
|
+
)
|
|
787
772
|
|
|
788
|
-
#
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
}
|
|
773
|
+
# Load existing config
|
|
774
|
+
config = {}
|
|
775
|
+
if config_file.exists():
|
|
776
|
+
with open(config_file) as f:
|
|
777
|
+
config = json.load(f)
|
|
794
778
|
|
|
795
|
-
|
|
796
|
-
|
|
779
|
+
# Update API secret
|
|
780
|
+
config['api_secret'] = api_secret
|
|
797
781
|
|
|
798
|
-
# Save
|
|
799
|
-
with open(
|
|
800
|
-
json.dump(
|
|
782
|
+
# Save config
|
|
783
|
+
with open(config_file, 'w') as f:
|
|
784
|
+
json.dump(config, f, indent=2)
|
|
801
785
|
|
|
802
|
-
# Set
|
|
803
|
-
|
|
786
|
+
# Set restrictive permissions (Unix/Mac only - Windows uses different permission model)
|
|
787
|
+
import platform
|
|
788
|
+
if platform.system() != 'Windows':
|
|
789
|
+
os.chmod(config_file, 0o600)
|
|
804
790
|
|
|
805
|
-
|
|
806
|
-
click.echo(f"✓
|
|
807
|
-
click.echo(f"\
|
|
808
|
-
click.echo("\nNote:
|
|
809
|
-
click.echo(" you'll need to reconfigure when they expire.")
|
|
810
|
-
|
|
791
|
+
masked_secret = api_secret[:8] + '...' + api_secret[-4:]
|
|
792
|
+
click.echo(f"✓ API secret configured: {masked_secret}")
|
|
793
|
+
click.echo(f"\nYou can now run: cc calculate --profile PROFILE_NAME")
|
|
794
|
+
click.echo(f"\nNote: Profiles are stored in DynamoDB and accessed via the API.")
|
|
811
795
|
|
|
812
796
|
@cli.command()
|
|
813
797
|
@click.option('--profile', required=True, help='Profile name')
|
|
@@ -1391,10 +1375,22 @@ def find_account_profile(account_id):
|
|
|
1391
1375
|
@click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
|
|
1392
1376
|
def daily(profile, start_date, end_date, days, service, account, sso, output_json):
|
|
1393
1377
|
"""
|
|
1394
|
-
Get daily cost breakdown with granular detail
|
|
1378
|
+
Get daily cost breakdown with granular detail.
|
|
1395
1379
|
|
|
1396
|
-
|
|
1380
|
+
Shows day-by-day costs for specific services and accounts, useful for:
|
|
1381
|
+
- Identifying cost spikes on specific dates
|
|
1382
|
+
- Validating daily cost patterns
|
|
1383
|
+
- Calculating precise daily averages
|
|
1384
|
+
|
|
1385
|
+
Examples:
|
|
1386
|
+
# Last 10 days of CloudWatch costs for specific account
|
|
1397
1387
|
cc daily --profile khoros --days 10 --service AmazonCloudWatch --account 820054669588
|
|
1388
|
+
|
|
1389
|
+
# Custom date range with JSON output for automation
|
|
1390
|
+
cc daily --profile khoros --start-date 2025-10-28 --end-date 2025-11-06 --json
|
|
1391
|
+
|
|
1392
|
+
# Find high-cost days using jq
|
|
1393
|
+
cc daily --profile khoros --days 30 --json | jq '.daily_costs | map(select(.cost > 1000))'
|
|
1398
1394
|
"""
|
|
1399
1395
|
# Load profile
|
|
1400
1396
|
config = load_profile(profile)
|
|
@@ -1545,11 +1541,25 @@ def daily(profile, start_date, end_date, days, service, account, sso, output_jso
|
|
|
1545
1541
|
@click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
|
|
1546
1542
|
def compare(profile, account, service, before, after, expected_reduction, sso, output_json):
|
|
1547
1543
|
"""
|
|
1548
|
-
Compare costs between two periods
|
|
1544
|
+
Compare costs between two periods for validation and analysis.
|
|
1549
1545
|
|
|
1550
|
-
|
|
1546
|
+
Perfect for:
|
|
1547
|
+
- Validating cost optimization savings
|
|
1548
|
+
- Before/after migration analysis
|
|
1549
|
+
- Measuring impact of infrastructure changes
|
|
1550
|
+
- Automated savings validation in CI/CD
|
|
1551
|
+
|
|
1552
|
+
Examples:
|
|
1553
|
+
# Validate Datadog migration savings (expect 50% reduction)
|
|
1551
1554
|
cc compare --profile khoros --account 180770971501 --service AmazonCloudWatch \
|
|
1552
1555
|
--before "2025-10-28:2025-11-06" --after "2025-11-17:2025-11-26" --expected-reduction 50
|
|
1556
|
+
|
|
1557
|
+
# Compare total costs across all accounts
|
|
1558
|
+
cc compare --profile khoros --before "2025-10-01:2025-10-31" --after "2025-11-01:2025-11-30"
|
|
1559
|
+
|
|
1560
|
+
# JSON output for automated validation
|
|
1561
|
+
cc compare --profile khoros --service EC2 --before "2025-10-01:2025-10-07" \
|
|
1562
|
+
--after "2025-11-08:2025-11-14" --json | jq '.comparison.met_expectation'
|
|
1553
1563
|
"""
|
|
1554
1564
|
# Load profile
|
|
1555
1565
|
config = load_profile(profile)
|
|
@@ -1731,11 +1741,27 @@ def compare(profile, account, service, before, after, expected_reduction, sso, o
|
|
|
1731
1741
|
@click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
|
|
1732
1742
|
def tags(profile, tag_key, tag_value, start_date, end_date, days, sso, output_json):
|
|
1733
1743
|
"""
|
|
1734
|
-
Analyze costs by resource tags
|
|
1744
|
+
Analyze costs grouped by resource tags for cost attribution.
|
|
1735
1745
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1746
|
+
Useful for:
|
|
1747
|
+
- Cost allocation by team, project, or environment
|
|
1748
|
+
- Identifying untagged resources (cost attribution gaps)
|
|
1749
|
+
- Tracking costs by cost center or department
|
|
1750
|
+
- Validating tagging compliance
|
|
1751
|
+
|
|
1752
|
+
Examples:
|
|
1753
|
+
# See all costs by Environment tag
|
|
1754
|
+
cc tags --profile khoros --tag-key "Environment" --days 30
|
|
1755
|
+
|
|
1756
|
+
# Filter to specific tag value
|
|
1757
|
+
cc tags --profile khoros --tag-key "Team" --tag-value "Platform" --days 30
|
|
1758
|
+
|
|
1759
|
+
# Find top cost centers with JSON output
|
|
1760
|
+
cc tags --profile khoros --tag-key "CostCenter" --days 30 --json | \
|
|
1761
|
+
jq '.tag_costs | sort_by(-.cost) | .[:5]'
|
|
1762
|
+
|
|
1763
|
+
# Identify untagged resources (look for empty tag values)
|
|
1764
|
+
cc tags --profile khoros --tag-key "Owner" --days 7
|
|
1739
1765
|
"""
|
|
1740
1766
|
# Load profile
|
|
1741
1767
|
config = load_profile(profile)
|
|
@@ -1,15 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aws_cost_calculator_cli-1.11.0.dist-info → aws_cost_calculator_cli-2.0.1.dist-info}/top_level.txt
RENAMED
|
File without changes
|