aws-cost-calculator-cli 1.0.3__py3-none-any.whl → 1.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cost-calculator-cli
3
- Version: 1.0.3
3
+ Version: 1.5.2
4
4
  Summary: AWS Cost Calculator CLI - Calculate daily and annual AWS costs across multiple accounts
5
5
  Home-page: https://github.com/yourusername/cost-calculator
6
6
  Author: Cost Optimization Team
@@ -23,6 +23,7 @@ Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
24
  Requires-Dist: click>=8.0.0
25
25
  Requires-Dist: boto3>=1.26.0
26
+ Requires-Dist: requests>=2.28.0
26
27
  Dynamic: author
27
28
  Dynamic: classifier
28
29
  Dynamic: description
@@ -80,7 +81,23 @@ cc calculate --profile myprofile --offset 2 --window 30
80
81
  cc calculate --profile myprofile --json-output
81
82
  ```
82
83
 
83
- ### 4. List profiles
84
+ ### 4. Analyze cost trends
85
+
86
+ ```bash
87
+ # Analyze last 3 weeks (default)
88
+ cc trends --profile myprofile
89
+
90
+ # Analyze more weeks
91
+ cc trends --profile myprofile --weeks 5
92
+
93
+ # Custom output file
94
+ cc trends --profile myprofile --output weekly_trends.md
95
+
96
+ # JSON output
97
+ cc trends --profile myprofile --json-output
98
+ ```
99
+
100
+ ### 5. List profiles
84
101
 
85
102
  ```bash
86
103
  cc list-profiles
@@ -138,8 +155,118 @@ cc calculate --profile myprofile --json-output > costs.json
138
155
 
139
156
  # Different window size
140
157
  cc calculate --profile myprofile --window 60
158
+
159
+ # Weekly cost trends analysis
160
+ cc trends --profile myprofile
161
+
162
+ # Analyze last 8 weeks
163
+ cc trends --profile myprofile --weeks 8
164
+
165
+ # Monthly cost trends analysis
166
+ cc monthly --profile myprofile
167
+
168
+ # Analyze last 12 months
169
+ cc monthly --profile myprofile --months 12
170
+
171
+ # Drill down into specific service
172
+ cc drill --profile myprofile --service "EC2 - Other"
173
+
174
+ # Drill down into specific account
175
+ cc drill --profile myprofile --account 123456789012
176
+
177
+ # Drill down into service within account
178
+ cc drill --profile myprofile --service "EC2 - Other" --account 123456789012
141
179
  ```
142
180
 
181
+ ## Trends Report
182
+
183
+ The `trends` command generates a markdown report with **two types of analysis**:
184
+
185
+ ### 1. Week-over-Week (WoW)
186
+ Compares each week to the previous week - good for catching immediate spikes and changes.
187
+
188
+ ### 2. Trailing 30-Day (T-30)
189
+ Compares each week to the same week 4 weeks ago - filters out noise and shows sustained trends.
190
+
191
+ **Features:**
192
+ - **Service-level aggregation**: Shows total cost per service (not individual usage types)
193
+ - **Top 10 Increases/Decreases**: For each comparison period
194
+ - **Total rows**: Sum of top 10 changes for quick assessment
195
+ - **Filters**: Only shows changes >$10 and >5%
196
+
197
+ Example output:
198
+ ```
199
+ Week of Oct 19 → Week of Oct 26 (WoW)
200
+ Increases: 4, Decreases: 10
201
+ Top: EC2 - Other (+$949.12)
202
+
203
+ Week of Oct 26 vs Week of Sep 28 (T-30)
204
+ Increases: 10, Decreases: 10
205
+ Top: EC2 - Other (+$886.39)
206
+ ```
207
+
208
+ The report is saved to `cost_trends.md` by default and includes:
209
+ - Service name
210
+ - Previous/baseline cost
211
+ - Current cost
212
+ - Change amount and percentage
213
+ - Total of top 10 changes
214
+
215
+ ## Monthly Report
216
+
217
+ The `monthly` command generates month-over-month cost comparisons:
218
+
219
+ **Features:**
220
+ - **Service-level aggregation**: Shows total cost per service
221
+ - **Calendar month comparisons**: October vs September, September vs August, etc.
222
+ - **Top 10 Increases/Decreases**: For each month comparison
223
+ - **Total rows**: Sum of top 10 changes
224
+ - **Filters**: Only shows changes >$50 and >5%
225
+
226
+ Example output:
227
+ ```
228
+ October 2025 → November 2025
229
+ Increases: 1, Decreases: 10
230
+ Top: Savings Plans for AWS Compute usage (+$231,161.46)
231
+ ```
232
+
233
+ The report is saved to `monthly_trends.md` by default.
234
+
235
+ ## Drill-Down Analysis
236
+
237
+ The `drill` command allows you to investigate cost changes at different levels of detail:
238
+
239
+ **Funnel Approach:**
240
+ 1. **Start broad:** `cc trends` → See EC2 costs up $1000
241
+ 2. **Drill by service:** `cc drill --service "EC2 - Other"` → See which accounts
242
+ 3. **Drill deeper:** `cc drill --service "EC2 - Other" --account 123` → See usage types
243
+
244
+ **Features:**
245
+ - **Week-over-week cost analysis**: Compare costs between consecutive weeks
246
+ - **Month-over-month cost analysis**: Compare costs between consecutive months
247
+ - **Drill-down analysis**: Analyze costs by service, account, or usage type
248
+ - **Pandas aggregations**: Time series analysis with sum, avg, std across all weeks
249
+ - **Volatility detection**: Identify services with high cost variability and outliers
250
+ - **Trend detection**: Auto-detect increasing/decreasing cost patterns
251
+ - **Search & filter**: Find services by pattern or cost threshold
252
+ - **Profile management**: CRUD operations for account profiles in DynamoDB
253
+ - **Markdown reports**: Generate formatted reports
254
+ - **JSON output**: Machine-readable output for automation
255
+ - **Lambda API backend**: Serverless backend with pandas/numpy support
256
+
257
+ Example output:
258
+ ```
259
+ # Drill by service
260
+ cc drill --profile myprofile --service "EC2 - Other"
261
+
262
+ Showing top accounts:
263
+ Week of Oct 19 → Week of Oct 26
264
+ Increases: 3, Decreases: 2
265
+ Top: 887649220066 (+$450.23)
266
+ ```
267
+
268
+ The report is saved to `drill_down.md` by default.
269
+
143
270
  ## Output
144
271
 
145
272
  ```
@@ -0,0 +1,13 @@
1
+ aws_cost_calculator_cli-1.5.2.dist-info/licenses/LICENSE,sha256=cYtmQZHNGGTXOtg3T7LHDRneleaH0dHXHfxFV3WR50Y,1079
2
+ cost_calculator/__init__.py,sha256=PJeIqvWh5AYJVrJxPPkI4pJnAt37rIjasrNS0I87kaM,52
3
+ cost_calculator/api_client.py,sha256=LUzQmveDF0X9MqAyThp9mbSzJzkOO73Pk4F7IEJjASU,2353
4
+ cost_calculator/cli.py,sha256=ufK28divdvrceEryWd8cCWjvG5pT2owaqprskX2epeQ,32589
5
+ cost_calculator/drill.py,sha256=hGi-prLgZDvNMMICQc4fl3LenM7YaZ3To_Ei4LKwrdc,10543
6
+ cost_calculator/executor.py,sha256=tVyyBtXIj9OPyG-xQj8CUmyFjDhb9IVK639360dUZDc,8076
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.5.2.dist-info/METADATA,sha256=0wxy-jgVC-paubGHN87mDObETGr_u9qU4ZIP3xV49hM,8176
10
+ aws_cost_calculator_cli-1.5.2.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
11
+ aws_cost_calculator_cli-1.5.2.dist-info/entry_points.txt,sha256=_5Qy4EcHbYVYrdgOu1E48faMHb9fLUl5VJ3djDHuJBo,47
12
+ aws_cost_calculator_cli-1.5.2.dist-info/top_level.txt,sha256=PRwGPPlNqASfyhGHDjSfyl4SXeE7GF3OVTu1tY1Uqyc,16
13
+ aws_cost_calculator_cli-1.5.2.dist-info/RECORD,,
@@ -0,0 +1,84 @@
1
+ """
2
+ API client for calling Lambda backend.
3
+ Falls back to local execution if API is not configured.
4
+ """
5
+ import os
6
+ import json
7
+ import requests
8
+ from pathlib import Path
9
+
10
+
11
+ def get_api_config():
12
+ """
13
+ Get API configuration.
14
+
15
+ Returns:
16
+ dict: API configuration with api_secret, or None if not configured
17
+ """
18
+ api_secret = os.environ.get('COST_API_SECRET', '')
19
+
20
+ if api_secret:
21
+ return {'api_secret': api_secret}
22
+
23
+ return None
24
+
25
+
26
+ def call_lambda_api(endpoint, credentials, accounts, **kwargs):
27
+ """
28
+ Call Lambda API endpoint.
29
+
30
+ Args:
31
+ endpoint: API endpoint name ('trends', 'monthly', 'drill')
32
+ credentials: dict with AWS credentials
33
+ accounts: list of account IDs
34
+ **kwargs: additional parameters for the specific endpoint
35
+
36
+ Returns:
37
+ dict: API response data
38
+
39
+ Raises:
40
+ Exception: if API call fails
41
+ """
42
+ api_config = get_api_config()
43
+
44
+ if not api_config:
45
+ raise Exception("API not configured. Set COST_API_SECRET environment variable.")
46
+
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
+ }
55
+
56
+ url = endpoint_urls.get(endpoint)
57
+
58
+ if not url:
59
+ raise Exception(f"Unknown endpoint: {endpoint}")
60
+
61
+ # Build request payload
62
+ payload = {
63
+ 'credentials': credentials,
64
+ 'accounts': accounts
65
+ }
66
+ payload.update(kwargs)
67
+
68
+ # Make API call
69
+ headers = {
70
+ 'X-API-Secret': api_config['api_secret'],
71
+ 'Content-Type': 'application/json'
72
+ }
73
+
74
+ response = requests.post(url, headers=headers, json=payload, timeout=300)
75
+
76
+ if response.status_code != 200:
77
+ raise Exception(f"API call failed: {response.status_code} - {response.text}")
78
+
79
+ return response.json()
80
+
81
+
82
+ def is_api_configured():
83
+ """Check if API is configured."""
84
+ return get_api_config() is not None
cost_calculator/cli.py CHANGED
@@ -13,6 +13,10 @@ import boto3
13
13
  import json
14
14
  from datetime import datetime, timedelta
15
15
  from pathlib import Path
16
+ from cost_calculator.trends import format_trends_markdown
17
+ from cost_calculator.monthly import format_monthly_markdown
18
+ from cost_calculator.drill import format_drill_down_markdown
19
+ from cost_calculator.executor import execute_trends, execute_monthly, execute_drill
16
20
 
17
21
 
18
22
  def load_profile(profile_name):
@@ -539,5 +543,333 @@ def configure(profile, access_key_id, secret_access_key, session_token, region):
539
543
  click.echo(" you'll need to reconfigure when they expire.")
540
544
 
541
545
 
546
+ @cli.command()
547
+ @click.option('--profile', required=True, help='Profile name')
548
+ @click.option('--weeks', default=3, help='Number of weeks to analyze (default: 3)')
549
+ @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):
552
+ """Analyze cost trends with Week-over-Week and Trailing 30-Day comparisons"""
553
+
554
+ # Load profile configuration
555
+ config = load_profile(profile)
556
+
557
+ click.echo(f"Analyzing last {weeks} weeks...")
558
+ click.echo("")
559
+
560
+ # Execute via API or locally
561
+ trends_data = execute_trends(config, weeks)
562
+
563
+ if json_output:
564
+ # Output as JSON
565
+ import json
566
+ click.echo(json.dumps(trends_data, indent=2, default=str))
567
+ else:
568
+ # Generate markdown report
569
+ markdown = format_trends_markdown(trends_data)
570
+
571
+ # Save to file
572
+ with open(output, 'w') as f:
573
+ f.write(markdown)
574
+
575
+ click.echo(f"✓ Trends report saved to {output}")
576
+ click.echo("")
577
+
578
+ # Show summary
579
+ click.echo("WEEK-OVER-WEEK:")
580
+ for comparison in trends_data['wow_comparisons']:
581
+ prev_week = comparison['prev_week']['label']
582
+ curr_week = comparison['curr_week']['label']
583
+ num_increases = len(comparison['increases'])
584
+ num_decreases = len(comparison['decreases'])
585
+
586
+ click.echo(f" {prev_week} → {curr_week}")
587
+ click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
588
+
589
+ if comparison['increases']:
590
+ top = comparison['increases'][0]
591
+ click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
592
+
593
+ click.echo("")
594
+
595
+ click.echo("TRAILING 30-DAY (T-30):")
596
+ for comparison in trends_data['t30_comparisons']:
597
+ baseline_week = comparison['baseline_week']['label']
598
+ curr_week = comparison['curr_week']['label']
599
+ num_increases = len(comparison['increases'])
600
+ num_decreases = len(comparison['decreases'])
601
+
602
+ click.echo(f" {curr_week} vs {baseline_week}")
603
+ click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
604
+
605
+ if comparison['increases']:
606
+ top = comparison['increases'][0]
607
+ click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
608
+
609
+ click.echo("")
610
+
611
+
612
+ @cli.command()
613
+ @click.option('--profile', required=True, help='Profile name')
614
+ @click.option('--months', default=6, help='Number of months to analyze (default: 6)')
615
+ @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):
618
+ """Analyze month-over-month cost trends at service level"""
619
+
620
+ # Load profile configuration
621
+ config = load_profile(profile)
622
+
623
+ click.echo(f"Analyzing last {months} months...")
624
+ click.echo("")
625
+
626
+ # Execute via API or locally
627
+ monthly_data = execute_monthly(config, months)
628
+
629
+ if json_output:
630
+ # Output as JSON
631
+ output_data = {
632
+ 'generated': datetime.now().isoformat(),
633
+ 'months': months,
634
+ 'comparisons': []
635
+ }
636
+
637
+ for comparison in monthly_data['comparisons']:
638
+ output_data['comparisons'].append({
639
+ 'prev_month': comparison['prev_month']['label'],
640
+ 'curr_month': comparison['curr_month']['label'],
641
+ 'increases': comparison['increases'],
642
+ 'decreases': comparison['decreases'],
643
+ 'total_increase': comparison['total_increase'],
644
+ 'total_decrease': comparison['total_decrease']
645
+ })
646
+
647
+ click.echo(json.dumps(output_data, indent=2))
648
+ else:
649
+ # Generate markdown report
650
+ markdown = format_monthly_markdown(monthly_data)
651
+
652
+ # Save to file
653
+ with open(output, 'w') as f:
654
+ f.write(markdown)
655
+
656
+ click.echo(f"✓ Monthly trends report saved to {output}")
657
+ click.echo("")
658
+
659
+ # Show summary
660
+ for comparison in monthly_data['comparisons']:
661
+ prev_month = comparison['prev_month']['label']
662
+ curr_month = comparison['curr_month']['label']
663
+ num_increases = len(comparison['increases'])
664
+ num_decreases = len(comparison['decreases'])
665
+
666
+ click.echo(f"{prev_month} → {curr_month}")
667
+ click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
668
+
669
+ if comparison['increases']:
670
+ top = comparison['increases'][0]
671
+ click.echo(f" Top: {top['service']} (+${top['change']:,.2f})")
672
+
673
+ click.echo("")
674
+
675
+
676
+ @cli.command()
677
+ @click.option('--profile', required=True, help='Profile name')
678
+ @click.option('--weeks', default=4, help='Number of weeks to analyze (default: 4)')
679
+ @click.option('--service', help='Filter by service name (e.g., "EC2 - Other")')
680
+ @click.option('--account', help='Filter by account ID')
681
+ @click.option('--usage-type', help='Filter by usage type')
682
+ @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):
685
+ """Drill down into cost changes by service, account, or usage type"""
686
+
687
+ # Load profile configuration
688
+ config = load_profile(profile)
689
+
690
+ # Show filters
691
+ click.echo(f"Analyzing last {weeks} weeks...")
692
+ if service:
693
+ click.echo(f" Service filter: {service}")
694
+ if account:
695
+ click.echo(f" Account filter: {account}")
696
+ if usage_type:
697
+ click.echo(f" Usage type filter: {usage_type}")
698
+ click.echo("")
699
+
700
+ # Execute via API or locally
701
+ drill_data = execute_drill(config, weeks, service, account, usage_type)
702
+
703
+ if json_output:
704
+ # Output as JSON
705
+ output_data = {
706
+ 'generated': datetime.now().isoformat(),
707
+ 'weeks': weeks,
708
+ 'filters': drill_data['filters'],
709
+ 'group_by': drill_data['group_by'],
710
+ 'comparisons': []
711
+ }
712
+
713
+ for comparison in drill_data['comparisons']:
714
+ output_data['comparisons'].append({
715
+ 'prev_week': comparison['prev_week']['label'],
716
+ 'curr_week': comparison['curr_week']['label'],
717
+ 'increases': comparison['increases'],
718
+ 'decreases': comparison['decreases'],
719
+ 'total_increase': comparison['total_increase'],
720
+ 'total_decrease': comparison['total_decrease']
721
+ })
722
+
723
+ click.echo(json.dumps(output_data, indent=2))
724
+ else:
725
+ # Generate markdown report
726
+ markdown = format_drill_down_markdown(drill_data)
727
+
728
+ # Save to file
729
+ with open(output, 'w') as f:
730
+ f.write(markdown)
731
+
732
+ click.echo(f"✓ Drill-down report saved to {output}")
733
+ click.echo("")
734
+
735
+ # Show summary
736
+ group_by_label = {
737
+ 'SERVICE': 'services',
738
+ 'LINKED_ACCOUNT': 'accounts',
739
+ 'USAGE_TYPE': 'usage types',
740
+ 'REGION': 'regions'
741
+ }.get(drill_data['group_by'], 'items')
742
+
743
+ click.echo(f"Showing top {group_by_label}:")
744
+ for comparison in drill_data['comparisons']:
745
+ prev_week = comparison['prev_week']['label']
746
+ curr_week = comparison['curr_week']['label']
747
+ num_increases = len(comparison['increases'])
748
+ num_decreases = len(comparison['decreases'])
749
+
750
+ click.echo(f"{prev_week} → {curr_week}")
751
+ click.echo(f" Increases: {num_increases}, Decreases: {num_decreases}")
752
+
753
+ if comparison['increases']:
754
+ top = comparison['increases'][0]
755
+ click.echo(f" Top: {top['dimension'][:50]} (+${top['change']:,.2f})")
756
+
757
+ click.echo("")
758
+
759
+
760
+ @cli.command()
761
+ @click.option('--profile', required=True, help='Profile name')
762
+ @click.option('--type', 'analysis_type', default='summary',
763
+ type=click.Choice(['summary', 'volatility', 'trends', 'search']),
764
+ help='Analysis type')
765
+ @click.option('--weeks', default=12, help='Number of weeks (default: 12)')
766
+ @click.option('--pattern', help='Service search pattern (for search type)')
767
+ @click.option('--min-cost', type=float, help='Minimum cost filter (for search type)')
768
+ @click.option('--json-output', is_flag=True, help='Output as JSON')
769
+ def analyze(profile, analysis_type, weeks, pattern, min_cost, json_output):
770
+ """Perform pandas-based analysis (aggregations, volatility, trends, search)"""
771
+
772
+ config = load_profile(profile)
773
+
774
+ if not json_output:
775
+ click.echo(f"Running {analysis_type} analysis for {weeks} weeks...")
776
+
777
+ from cost_calculator.executor import execute_analyze
778
+ result = execute_analyze(config, weeks, analysis_type, pattern, min_cost)
779
+
780
+ if json_output:
781
+ import json
782
+ click.echo(json.dumps(result, indent=2, default=str))
783
+ else:
784
+ # Format output based on type
785
+ if analysis_type == 'summary':
786
+ click.echo(f"\n📊 Summary ({result.get('total_services', 0)} services)")
787
+ click.echo(f"Weeks analyzed: {result.get('weeks_analyzed', 0)}")
788
+ click.echo(f"\nTop 10 Services (by total change):")
789
+ for svc in result.get('services', [])[:10]:
790
+ click.echo(f" {svc['service']}")
791
+ click.echo(f" Total: ${svc['change_sum']:,.2f}")
792
+ click.echo(f" Average: ${svc['change_mean']:,.2f}")
793
+ click.echo(f" Volatility: {svc['volatility']:.3f}")
794
+
795
+ elif analysis_type == 'volatility':
796
+ click.echo(f"\n📈 High Volatility Services:")
797
+ for svc in result.get('high_volatility_services', [])[:10]:
798
+ click.echo(f" {svc['service']}: CV={svc['coefficient_of_variation']:.3f}")
799
+
800
+ outliers = result.get('outliers', [])
801
+ if outliers:
802
+ click.echo(f"\n⚠️ Outliers ({len(outliers)}):")
803
+ for o in outliers[:5]:
804
+ click.echo(f" {o['service']} ({o['week']}): ${o['change']:,.2f} (z={o['z_score']:.2f})")
805
+
806
+ elif analysis_type == 'trends':
807
+ inc = result.get('increasing_trends', [])
808
+ dec = result.get('decreasing_trends', [])
809
+
810
+ click.echo(f"\n📈 Increasing Trends ({len(inc)}):")
811
+ for t in inc[:5]:
812
+ click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
813
+
814
+ click.echo(f"\n📉 Decreasing Trends ({len(dec)}):")
815
+ for t in dec[:5]:
816
+ click.echo(f" {t['service']}: ${t['avg_change']:,.2f}/week")
817
+
818
+ elif analysis_type == 'search':
819
+ matches = result.get('matches', [])
820
+ click.echo(f"\n🔍 Search Results ({len(matches)} matches)")
821
+ if pattern:
822
+ click.echo(f"Pattern: {pattern}")
823
+ if min_cost:
824
+ click.echo(f"Min cost: ${min_cost:,.2f}")
825
+
826
+ for m in matches[:20]:
827
+ click.echo(f" {m['service']}: ${m['curr_cost']:,.2f}")
828
+
829
+
830
+ @cli.command()
831
+ @click.argument('operation', type=click.Choice(['list', 'get', 'create', 'update', 'delete']))
832
+ @click.option('--name', help='Profile name')
833
+ @click.option('--accounts', help='Comma-separated account IDs')
834
+ @click.option('--description', help='Profile description')
835
+ def profile(operation, name, accounts, description):
836
+ """Manage profiles (CRUD operations)"""
837
+
838
+ from cost_calculator.executor import execute_profile_operation
839
+
840
+ # Parse accounts if provided
841
+ account_list = None
842
+ if accounts:
843
+ account_list = [a.strip() for a in accounts.split(',')]
844
+
845
+ result = execute_profile_operation(
846
+ operation=operation,
847
+ profile_name=name,
848
+ accounts=account_list,
849
+ description=description
850
+ )
851
+
852
+ if operation == 'list':
853
+ profiles = result.get('profiles', [])
854
+ click.echo(f"\n📋 Profiles ({len(profiles)}):")
855
+ for p in profiles:
856
+ click.echo(f" {p['profile_name']}: {len(p.get('accounts', []))} accounts")
857
+ if p.get('description'):
858
+ click.echo(f" {p['description']}")
859
+
860
+ elif operation == 'get':
861
+ profile_data = result.get('profile', {})
862
+ click.echo(f"\n📋 Profile: {profile_data.get('profile_name')}")
863
+ click.echo(f"Accounts: {len(profile_data.get('accounts', []))}")
864
+ if profile_data.get('description'):
865
+ click.echo(f"Description: {profile_data['description']}")
866
+ click.echo(f"\nAccounts:")
867
+ for acc in profile_data.get('accounts', []):
868
+ click.echo(f" {acc}")
869
+
870
+ else:
871
+ click.echo(result.get('message', 'Operation completed'))
872
+
873
+
542
874
  if __name__ == '__main__':
543
875
  cli()