aws-cost-calculator-cli 1.4.0__py3-none-any.whl → 1.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cost_calculator/cli.py CHANGED
@@ -19,49 +19,143 @@ from cost_calculator.drill import format_drill_down_markdown
19
19
  from cost_calculator.executor import execute_trends, execute_monthly, execute_drill
20
20
 
21
21
 
22
+ def apply_auth_options(config, sso=None, access_key_id=None, secret_access_key=None, session_token=None):
23
+ """Apply authentication options to profile config
24
+
25
+ Args:
26
+ config: Profile configuration dict
27
+ sso: AWS SSO profile name
28
+ access_key_id: AWS Access Key ID
29
+ secret_access_key: AWS Secret Access Key
30
+ session_token: AWS Session Token
31
+
32
+ Returns:
33
+ Updated config dict
34
+ """
35
+ import subprocess
36
+
37
+ if sso:
38
+ # SSO authentication - trigger login if needed
39
+ try:
40
+ # Test if SSO session is valid
41
+ result = subprocess.run(
42
+ ['aws', 'sts', 'get-caller-identity', '--profile', sso],
43
+ capture_output=True,
44
+ text=True,
45
+ timeout=5
46
+ )
47
+ if result.returncode != 0:
48
+ if 'expired' in result.stderr.lower() or 'token' in result.stderr.lower():
49
+ click.echo(f"SSO session expired or not initialized. Logging in...")
50
+ subprocess.run(['aws', 'sso', 'login', '--profile', sso], check=True)
51
+ except Exception as e:
52
+ click.echo(f"Warning: Could not verify SSO session: {e}")
53
+
54
+ config['aws_profile'] = sso
55
+ elif access_key_id and secret_access_key:
56
+ # Static credentials provided via CLI
57
+ config['credentials'] = {
58
+ 'aws_access_key_id': access_key_id,
59
+ 'aws_secret_access_key': secret_access_key,
60
+ 'region': 'us-east-1'
61
+ }
62
+ if session_token:
63
+ config['credentials']['aws_session_token'] = session_token
64
+
65
+ return config
66
+
67
+
22
68
  def load_profile(profile_name):
23
- """Load profile configuration from ~/.config/cost-calculator/profiles.json"""
69
+ """Load profile configuration from local file or DynamoDB API"""
70
+ import os
71
+ import requests
72
+
24
73
  config_dir = Path.home() / '.config' / 'cost-calculator'
25
74
  config_file = config_dir / 'profiles.json'
26
75
  creds_file = config_dir / 'credentials.json'
27
76
 
28
- if not config_file.exists():
77
+ # Try local file first
78
+ if config_file.exists():
79
+ with open(config_file) as f:
80
+ profiles = json.load(f)
81
+
82
+ if profile_name in profiles:
83
+ profile = profiles[profile_name]
84
+
85
+ # Load credentials if using static credentials (not SSO)
86
+ if 'aws_profile' not in profile:
87
+ if not creds_file.exists():
88
+ # Try environment variables
89
+ if os.environ.get('AWS_ACCESS_KEY_ID'):
90
+ profile['credentials'] = {
91
+ 'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
92
+ 'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
93
+ 'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
94
+ }
95
+ return profile
96
+
97
+ raise click.ClickException(
98
+ f"No credentials found for profile '{profile_name}'.\n"
99
+ f"Run: cc configure --profile {profile_name}"
100
+ )
101
+
102
+ with open(creds_file) as f:
103
+ creds = json.load(f)
104
+
105
+ if profile_name not in creds:
106
+ raise click.ClickException(
107
+ f"No credentials found for profile '{profile_name}'.\n"
108
+ f"Run: cc configure --profile {profile_name}"
109
+ )
110
+
111
+ profile['credentials'] = creds[profile_name]
112
+
113
+ return profile
114
+
115
+ # Profile not found locally - try DynamoDB API
116
+ api_secret = os.environ.get('COST_API_SECRET')
117
+ if not api_secret:
29
118
  raise click.ClickException(
30
- f"Profile configuration not found at {config_file}\n"
119
+ f"Profile '{profile_name}' not found locally and COST_API_SECRET not set.\n"
31
120
  f"Run: cc init --profile {profile_name}"
32
121
  )
33
122
 
34
- with open(config_file) as f:
35
- profiles = json.load(f)
36
-
37
- if profile_name not in profiles:
38
- raise click.ClickException(
39
- f"Profile '{profile_name}' not found in {config_file}\n"
40
- f"Available profiles: {', '.join(profiles.keys())}"
123
+ try:
124
+ response = requests.post(
125
+ 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/',
126
+ headers={'X-API-Secret': api_secret, 'Content-Type': 'application/json'},
127
+ json={'operation': 'get', 'profile_name': profile_name},
128
+ timeout=10
41
129
  )
42
-
43
- profile = profiles[profile_name]
44
-
45
- # Load credentials if using static credentials (not SSO)
46
- if 'aws_profile' not in profile:
47
- if not creds_file.exists():
48
- raise click.ClickException(
49
- f"No credentials found for profile '{profile_name}'.\n"
50
- f"Run: cc configure --profile {profile_name}"
51
- )
52
-
53
- with open(creds_file) as f:
54
- creds = json.load(f)
55
130
 
56
- if profile_name not in creds:
131
+ if response.status_code == 200:
132
+ response_data = response.json()
133
+ # API returns {"profile": {...}} wrapper
134
+ profile_data = response_data.get('profile', response_data)
135
+ profile = {'accounts': profile_data['accounts']}
136
+
137
+ # Check for AWS_PROFILE environment variable (SSO support)
138
+ if os.environ.get('AWS_PROFILE'):
139
+ profile['aws_profile'] = os.environ['AWS_PROFILE']
140
+ # Use environment credentials
141
+ elif os.environ.get('AWS_ACCESS_KEY_ID'):
142
+ profile['credentials'] = {
143
+ 'aws_access_key_id': os.environ['AWS_ACCESS_KEY_ID'],
144
+ 'aws_secret_access_key': os.environ['AWS_SECRET_ACCESS_KEY'],
145
+ 'aws_session_token': os.environ.get('AWS_SESSION_TOKEN')
146
+ }
147
+
148
+ return profile
149
+ else:
57
150
  raise click.ClickException(
58
- f"No credentials found for profile '{profile_name}'.\n"
59
- f"Run: cc configure --profile {profile_name}"
151
+ f"Profile '{profile_name}' not found in DynamoDB.\n"
152
+ f"Run: cc profile create --name {profile_name} --accounts \"...\""
60
153
  )
61
-
62
- profile['credentials'] = creds[profile_name]
63
-
64
- return profile
154
+ except requests.exceptions.RequestException as e:
155
+ raise click.ClickException(
156
+ f"Failed to fetch profile from API: {e}\n"
157
+ f"Run: cc init --profile {profile_name}"
158
+ )
65
159
 
66
160
 
67
161
  def calculate_costs(profile_config, accounts, start_date, offset, window):
@@ -231,7 +325,7 @@ def calculate_costs(profile_config, accounts, start_date, offset, window):
231
325
  support_month = support_month_date - timedelta(days=1) # Go back to previous month
232
326
  days_in_support_month = support_month.day # This gives us the last day of the month
233
327
 
234
- # Support allocation: divide by 2 (half to Khoros), then by days in month
328
+ # Support allocation: divide by 2 (50% allocation), then by days in month
235
329
  support_per_day = (support_cost / 2) / days_in_support_month
236
330
 
237
331
  # Calculate daily rate
@@ -296,12 +390,31 @@ def cli():
296
390
  @click.option('--offset', default=2, help='Days to go back from start date (default: 2)')
297
391
  @click.option('--window', default=30, help='Number of days to analyze (default: 30)')
298
392
  @click.option('--json-output', is_flag=True, help='Output as JSON')
299
- def calculate(profile, start_date, offset, window, json_output):
300
- """Calculate AWS costs for the specified period"""
393
+ @click.option('--sso', help='AWS SSO profile name (e.g., my_sso_profile)')
394
+ @click.option('--access-key-id', help='AWS Access Key ID (for static credentials)')
395
+ @click.option('--secret-access-key', help='AWS Secret Access Key (for static credentials)')
396
+ @click.option('--session-token', help='AWS Session Token (for static credentials)')
397
+ def calculate(profile, start_date, offset, window, json_output, sso, access_key_id, secret_access_key, session_token):
398
+ """
399
+ Calculate AWS costs for the specified period
400
+
401
+ \b
402
+ Authentication Options:
403
+ 1. SSO: --sso <profile_name>
404
+ Example: cc calculate --profile myprofile --sso my_sso_profile
405
+
406
+ 2. Static Credentials: --access-key-id, --secret-access-key, --session-token
407
+ Example: cc calculate --profile myprofile --access-key-id ASIA... --secret-access-key ... --session-token ...
408
+
409
+ 3. Environment Variables: AWS_PROFILE or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
410
+ """
301
411
 
302
412
  # Load profile configuration
303
413
  config = load_profile(profile)
304
414
 
415
+ # Apply authentication options
416
+ config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
417
+
305
418
  # Calculate costs
306
419
  result = calculate_costs(
307
420
  profile_config=config,
@@ -547,12 +660,17 @@ def configure(profile, access_key_id, secret_access_key, session_token, region):
547
660
  @click.option('--profile', required=True, help='Profile name')
548
661
  @click.option('--weeks', default=3, help='Number of weeks to analyze (default: 3)')
549
662
  @click.option('--output', default='cost_trends.md', help='Output markdown file (default: cost_trends.md)')
550
- @click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
551
- def trends(profile, weeks, output, json_output):
663
+ @click.option('--json-output', is_flag=True, help='Output as JSON')
664
+ @click.option('--sso', help='AWS SSO profile name')
665
+ @click.option('--access-key-id', help='AWS Access Key ID')
666
+ @click.option('--secret-access-key', help='AWS Secret Access Key')
667
+ @click.option('--session-token', help='AWS Session Token')
668
+ def trends(profile, weeks, output, json_output, sso, access_key_id, secret_access_key, session_token):
552
669
  """Analyze cost trends with Week-over-Week and Trailing 30-Day comparisons"""
553
670
 
554
671
  # Load profile configuration
555
672
  config = load_profile(profile)
673
+ config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
556
674
 
557
675
  click.echo(f"Analyzing last {weeks} weeks...")
558
676
  click.echo("")
@@ -613,12 +731,17 @@ def trends(profile, weeks, output, json_output):
613
731
  @click.option('--profile', required=True, help='Profile name')
614
732
  @click.option('--months', default=6, help='Number of months to analyze (default: 6)')
615
733
  @click.option('--output', default='monthly_trends.md', help='Output markdown file (default: monthly_trends.md)')
616
- @click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
617
- def monthly(profile, months, output, json_output):
734
+ @click.option('--json-output', is_flag=True, help='Output as JSON')
735
+ @click.option('--sso', help='AWS SSO profile name')
736
+ @click.option('--access-key-id', help='AWS Access Key ID')
737
+ @click.option('--secret-access-key', help='AWS Secret Access Key')
738
+ @click.option('--session-token', help='AWS Session Token')
739
+ def monthly(profile, months, output, json_output, sso, access_key_id, secret_access_key, session_token):
618
740
  """Analyze month-over-month cost trends at service level"""
619
741
 
620
- # Load profile configuration
742
+ # Load profile
621
743
  config = load_profile(profile)
744
+ config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
622
745
 
623
746
  click.echo(f"Analyzing last {months} months...")
624
747
  click.echo("")
@@ -680,12 +803,17 @@ def monthly(profile, months, output, json_output):
680
803
  @click.option('--account', help='Filter by account ID')
681
804
  @click.option('--usage-type', help='Filter by usage type')
682
805
  @click.option('--output', default='drill_down.md', help='Output markdown file (default: drill_down.md)')
683
- @click.option('--json-output', is_flag=True, help='Output as JSON instead of markdown')
684
- def drill(profile, weeks, service, account, usage_type, output, json_output):
806
+ @click.option('--json-output', is_flag=True, help='Output as JSON')
807
+ @click.option('--sso', help='AWS SSO profile name')
808
+ @click.option('--access-key-id', help='AWS Access Key ID')
809
+ @click.option('--secret-access-key', help='AWS Secret Access Key')
810
+ @click.option('--session-token', help='AWS Session Token')
811
+ def drill(profile, weeks, service, account, usage_type, output, json_output, sso, access_key_id, secret_access_key, session_token):
685
812
  """Drill down into cost changes by service, account, or usage type"""
686
813
 
687
- # Load profile configuration
814
+ # Load profile
688
815
  config = load_profile(profile)
816
+ config = apply_auth_options(config, sso, access_key_id, secret_access_key, session_token)
689
817
 
690
818
  # Show filters
691
819
  click.echo(f"Analyzing last {weeks} weeks...")
@@ -757,5 +885,119 @@ def drill(profile, weeks, service, account, usage_type, output, json_output):
757
885
  click.echo("")
758
886
 
759
887
 
888
+ @cli.command()
889
+ @click.option('--profile', required=True, help='Profile name')
890
+ @click.option('--type', 'analysis_type', default='summary',
891
+ type=click.Choice(['summary', 'volatility', 'trends', 'search']),
892
+ help='Analysis type')
893
+ @click.option('--weeks', default=12, help='Number of weeks (default: 12)')
894
+ @click.option('--pattern', help='Service search pattern (for search type)')
895
+ @click.option('--min-cost', type=float, help='Minimum cost filter (for search type)')
896
+ @click.option('--json-output', is_flag=True, help='Output as JSON')
897
+ def analyze(profile, analysis_type, weeks, pattern, min_cost, json_output):
898
+ """Perform pandas-based analysis (aggregations, volatility, trends, search)"""
899
+
900
+ config = load_profile(profile)
901
+
902
+ if not json_output:
903
+ click.echo(f"Running {analysis_type} analysis for {weeks} weeks...")
904
+
905
+ from cost_calculator.executor import execute_analyze
906
+ result = execute_analyze(config, weeks, analysis_type, pattern, min_cost)
907
+
908
+ if json_output:
909
+ import json
910
+ click.echo(json.dumps(result, indent=2, default=str))
911
+ else:
912
+ # Format output based on type
913
+ if analysis_type == 'summary':
914
+ click.echo(f"\n📊 Summary ({result.get('total_services', 0)} services)")
915
+ click.echo(f"Weeks analyzed: {result.get('weeks_analyzed', 0)}")
916
+ click.echo(f"\nTop 10 Services (by total change):")
917
+ for svc in result.get('services', [])[:10]:
918
+ click.echo(f" {svc['service']}")
919
+ click.echo(f" Total: ${svc['change_sum']:,.2f}")
920
+ click.echo(f" Average: ${svc['change_mean']:,.2f}")
921
+ click.echo(f" Volatility: {svc['volatility']:.3f}")
922
+
923
+ elif analysis_type == 'volatility':
924
+ click.echo(f"\n📈 High Volatility Services:")
925
+ for svc in result.get('high_volatility_services', [])[:10]:
926
+ click.echo(f" {svc['service']}: CV={svc['coefficient_of_variation']:.3f}")
927
+
928
+ outliers = result.get('outliers', [])
929
+ if outliers:
930
+ click.echo(f"\n⚠️ Outliers ({len(outliers)}):")
931
+ for o in outliers[:5]:
932
+ click.echo(f" {o['service']} ({o['week']}): ${o['change']:,.2f} (z={o['z_score']:.2f})")
933
+
934
+ elif analysis_type == 'trends':
935
+ inc = result.get('increasing_trends', [])
936
+ dec = result.get('decreasing_trends', [])
937
+
938
+ click.echo(f"\n📈 Increasing Trends ({len(inc)}):")
939
+ for t in inc[:5]:
940
+ click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
941
+
942
+ click.echo(f"\n📉 Decreasing Trends ({len(dec)}):")
943
+ for t in dec[:5]:
944
+ click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
945
+
946
+ elif analysis_type == 'search':
947
+ matches = result.get('matches', [])
948
+ click.echo(f"\n🔍 Search Results ({len(matches)} matches)")
949
+ if pattern:
950
+ click.echo(f"Pattern: {pattern}")
951
+ if min_cost:
952
+ click.echo(f"Min cost: ${min_cost:,.2f}")
953
+
954
+ for m in matches[:20]:
955
+ click.echo(f" {m['service']}: ${m['curr_cost']:,.2f}")
956
+
957
+
958
+ @cli.command()
959
+ @click.argument('operation', type=click.Choice(['list', 'get', 'create', 'update', 'delete']))
960
+ @click.option('--name', help='Profile name')
961
+ @click.option('--accounts', help='Comma-separated account IDs')
962
+ @click.option('--description', help='Profile description')
963
+ def profile(operation, name, accounts, description):
964
+ """Manage profiles (CRUD operations)"""
965
+
966
+ from cost_calculator.executor import execute_profile_operation
967
+
968
+ # Parse accounts if provided
969
+ account_list = None
970
+ if accounts:
971
+ account_list = [a.strip() for a in accounts.split(',')]
972
+
973
+ result = execute_profile_operation(
974
+ operation=operation,
975
+ profile_name=name,
976
+ accounts=account_list,
977
+ description=description
978
+ )
979
+
980
+ if operation == 'list':
981
+ profiles = result.get('profiles', [])
982
+ click.echo(f"\n📋 Profiles ({len(profiles)}):")
983
+ for p in profiles:
984
+ click.echo(f" {p['profile_name']}: {len(p.get('accounts', []))} accounts")
985
+ if p.get('description'):
986
+ click.echo(f" {p['description']}")
987
+
988
+ elif operation == 'get':
989
+ profile_data = result.get('profile', {})
990
+ click.echo(f"\n📋 Profile: {profile_data.get('profile_name')}")
991
+ click.echo(f"Accounts: {len(profile_data.get('accounts', []))}")
992
+ if profile_data.get('description'):
993
+ click.echo(f"Description: {profile_data['description']}")
994
+ click.echo(f"\nAccounts:")
995
+ for acc in profile_data.get('accounts', []):
996
+ click.echo(f" {acc}")
997
+
998
+ else:
999
+ click.echo(result.get('message', 'Operation completed'))
1000
+
1001
+
760
1002
  if __name__ == '__main__':
761
1003
  cli()
@@ -11,22 +11,33 @@ def get_credentials_dict(config):
11
11
  Extract credentials from config in format needed for API.
12
12
 
13
13
  Returns:
14
- dict with access_key, secret_key, session_token
14
+ dict with access_key, secret_key, session_token, or None if profile is 'dummy'
15
15
  """
16
16
  if 'aws_profile' in config:
17
- # Get temporary credentials from SSO session
18
- session = boto3.Session(profile_name=config['aws_profile'])
19
- credentials = session.get_credentials()
20
- frozen_creds = credentials.get_frozen_credentials()
17
+ # Skip credential loading for dummy profile (API-only mode)
18
+ if config['aws_profile'] == 'dummy':
19
+ return None
21
20
 
22
- return {
23
- 'access_key': frozen_creds.access_key,
24
- 'secret_key': frozen_creds.secret_key,
25
- 'session_token': frozen_creds.token
26
- }
21
+ # Get temporary credentials from SSO session
22
+ try:
23
+ session = boto3.Session(profile_name=config['aws_profile'])
24
+ credentials = session.get_credentials()
25
+ frozen_creds = credentials.get_frozen_credentials()
26
+
27
+ return {
28
+ 'access_key': frozen_creds.access_key,
29
+ 'secret_key': frozen_creds.secret_key,
30
+ 'session_token': frozen_creds.token
31
+ }
32
+ except Exception:
33
+ # If profile not found, return None (API will handle)
34
+ return None
27
35
  else:
28
36
  # Use static credentials
29
- creds = config['credentials']
37
+ creds = config.get('credentials', {})
38
+ if not creds:
39
+ return None
40
+
30
41
  result = {
31
42
  'access_key': creds['aws_access_key_id'],
32
43
  'secret_key': creds['aws_secret_access_key']
@@ -157,3 +168,74 @@ def execute_drill(config, weeks, service_filter=None, account_filter=None, usage
157
168
  account_filter=account_filter,
158
169
  usage_type_filter=usage_type_filter
159
170
  )
171
+
172
+
173
+ def execute_analyze(config, weeks, analysis_type, pattern=None, min_cost=None):
174
+ """
175
+ Execute pandas-based analysis via API.
176
+ Note: This only works via API (requires pandas layer).
177
+
178
+ Returns:
179
+ dict: analysis results
180
+ """
181
+ accounts = config['accounts']
182
+
183
+ if not is_api_configured():
184
+ raise click.ClickException(
185
+ "Analyze command requires API configuration.\n"
186
+ "Set COST_API_SECRET environment variable."
187
+ )
188
+
189
+ credentials = get_credentials_dict(config)
190
+ kwargs = {'weeks': weeks, 'type': analysis_type}
191
+
192
+ if pattern:
193
+ kwargs['pattern'] = pattern
194
+ if min_cost:
195
+ kwargs['min_cost'] = min_cost
196
+
197
+ return call_lambda_api('analyze', credentials, accounts, **kwargs)
198
+
199
+
200
+ def execute_profile_operation(operation, profile_name=None, accounts=None, description=None):
201
+ """
202
+ Execute profile CRUD operations via API.
203
+
204
+ Returns:
205
+ dict: operation result
206
+ """
207
+ if not is_api_configured():
208
+ raise click.ClickException(
209
+ "Profile commands require API configuration.\n"
210
+ "Set COST_API_SECRET environment variable."
211
+ )
212
+
213
+ # Profile operations don't need AWS credentials, just API secret
214
+ import os
215
+ import requests
216
+ import json
217
+
218
+ api_secret = os.environ.get('COST_API_SECRET', '')
219
+
220
+ # Use profiles endpoint (hardcoded URL)
221
+ url = 'https://64g7jq7sjygec2zmll5lsghrpi0txrzo.lambda-url.us-east-1.on.aws/'
222
+
223
+ payload = {'operation': operation}
224
+ if profile_name:
225
+ payload['profile_name'] = profile_name
226
+ if accounts:
227
+ payload['accounts'] = accounts
228
+ if description:
229
+ payload['description'] = description
230
+
231
+ headers = {
232
+ 'X-API-Secret': api_secret,
233
+ 'Content-Type': 'application/json'
234
+ }
235
+
236
+ response = requests.post(url, headers=headers, json=payload, timeout=60)
237
+
238
+ if response.status_code != 200:
239
+ raise Exception(f"API call failed: {response.status_code} - {response.text}")
240
+
241
+ return response.json()
@@ -1,13 +0,0 @@
1
- aws_cost_calculator_cli-1.4.0.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
2
- cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
3
- cost_calculator/api_client.py,sha256=pSH2U0tOghDd3fisPcKqEJG3TghQYZi18HZmianHd6Y,2932
4
- cost_calculator/cli.py,sha256=qK6WQcAM5W15NciGOUtPpqcEvgyua5n5GRRysB4NPWw,27631
5
- cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
6
- cost_calculator/executor.py,sha256=RZ45GuA8tzKqj_pJaZ-BVSc8xbxxWTi4yCGcKqvNsUg,5601
7
- cost_calculator/monthly.py,sha256=6k9F8S7djhX1wGV3-T1MZP7CvWbbfhSTEaddwCfVu5M,7932
8
- cost_calculator/trends.py,sha256=k_s4ylBX50sqoiM_fwepi58HW01zz767FMJhQUPDznk,12246
9
- aws_cost_calculator_cli-1.4.0.dist-info/METADATA,sha256=LJkphxt8op0vOE_UJv-rGYRI4obstONupdOS8tYHGKg,7793
10
- aws_cost_calculator_cli-1.4.0.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
11
- aws_cost_calculator_cli-1.4.0.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
12
- aws_cost_calculator_cli-1.4.0.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
13
- aws_cost_calculator_cli-1.4.0.dist-info/RECORD,,