aws-inventory-manager 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aws-inventory-manager might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-inventory-manager
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: AWS Resource Inventory Management & Delta Tracking CLI tool
5
5
  Author-email: Troy Larson <troy@calvinware.com>
6
6
  License: MIT
@@ -63,6 +63,7 @@ A Python CLI tool that captures point-in-time snapshots of AWS resources organiz
63
63
 
64
64
  - **📦 Inventory Management**: Organize snapshots into named inventories with optional tag-based filters
65
65
  - **📸 Resource Snapshots**: Capture complete inventory of AWS resources across multiple regions
66
+ - **📋 Snapshot Reporting**: Generate comprehensive reports with filtering, detailed views, and export to JSON/CSV/TXT
66
67
  - **🔄 Delta Tracking**: Identify resources added, modified, or removed since a snapshot
67
68
  - **💰 Cost Analysis**: Analyze costs for resources within a specific inventory
68
69
  - **🔧 Resource Restoration**: Remove resources added since a snapshot to return to that state
@@ -109,13 +110,28 @@ This captures all resources in `us-east-1` and stores them in the `prod-baseline
109
110
  - Deploy new resources, update configurations, etc.
110
111
  - Then take another snapshot to track what changed
111
112
 
112
- **4. Compare snapshots** (see what changed)
113
+ **4. View snapshot report** (see what's in your snapshot)
114
+ ```bash
115
+ # Summary view with resource counts by service, region, and type
116
+ awsinv snapshot report --inventory prod-baseline
117
+
118
+ # Detailed view showing all resources with tags and metadata
119
+ awsinv snapshot report --inventory prod-baseline --detailed
120
+
121
+ # Filter by resource type
122
+ awsinv snapshot report --inventory prod-baseline --resource-type ec2
123
+
124
+ # Export to JSON, CSV, or TXT
125
+ awsinv snapshot report --inventory prod-baseline --export report.json
126
+ ```
127
+
128
+ **5. Compare snapshots** (see what changed)
113
129
  ```bash
114
130
  awsinv delta --snapshot initial --inventory prod-baseline
115
131
  ```
116
132
  This shows all resources added, removed, or modified since the `initial` snapshot.
117
133
 
118
- **5. Analyze costs**
134
+ **6. Analyze costs**
119
135
  ```bash
120
136
  # Costs since snapshot was created
121
137
  awsinv cost --snapshot initial --inventory prod-baseline
@@ -125,7 +141,7 @@ awsinv cost --snapshot initial --inventory prod-baseline \
125
141
  --start-date 2025-01-01 --end-date 2025-01-31
126
142
  ```
127
143
 
128
- **6. List your resources**
144
+ **7. List your resources**
129
145
  ```bash
130
146
  # List all inventories
131
147
  awsinv inventory list
@@ -456,6 +472,16 @@ awsinv snapshot create [name] \
456
472
  [--compress] \
457
473
  [--profile <aws-profile>]
458
474
 
475
+ # Generate snapshot report
476
+ awsinv snapshot report [snapshot-name] \
477
+ [--inventory <inventory-name>] \
478
+ [--resource-type <type>] \
479
+ [--region <region>] \
480
+ [--detailed] \
481
+ [--page-size <number>] \
482
+ [--export <file.json|file.csv|file.txt>] \
483
+ [--profile <aws-profile>]
484
+
459
485
  # List all snapshots
460
486
  awsinv snapshot list [--profile <aws-profile>]
461
487
 
@@ -503,6 +529,6 @@ MIT License - see LICENSE file for details
503
529
 
504
530
  ---
505
531
 
506
- **Version**: 0.2.0
532
+ **Version**: 0.3.0
507
533
  **Status**: Alpha
508
534
  **Python**: 3.8 - 3.13
@@ -1,4 +1,4 @@
1
- aws_inventory_manager-0.2.0.dist-info/licenses/LICENSE,sha256=-lY65BqqcGV9QVjIoTpYEB0Jaddm9j-cS5ICDRBRQo0,1071
1
+ aws_inventory_manager-0.3.0.dist-info/licenses/LICENSE,sha256=-lY65BqqcGV9QVjIoTpYEB0Jaddm9j-cS5ICDRBRQo0,1071
2
2
  src/__init__.py,sha256=XRWrw_3Q5Lb1Kxh8DnBcuiOeNWsIamXW6pUN6tKiXyA,74
3
3
  src/aws/__init__.py,sha256=FcFY2cn8ZQCnActrZYH1hD0x4uFkM9bMBDOmuHA3_mw,253
4
4
  src/aws/client.py,sha256=6HKHoOwAjlxfT2gJoSAZnzLCv-hIlLxXtGHqW8qu4xo,4082
@@ -6,7 +6,7 @@ src/aws/credentials.py,sha256=mxj8lQf7TRG5dznQJ37rGQqfB-viHh_5VCt1C4xgqG0,6374
6
6
  src/aws/rate_limiter.py,sha256=b3dV4A9ftUsGgVMOQmv3IVO0Fkym2o1QDHKpFOdSYbg,5464
7
7
  src/cli/__init__.py,sha256=aqMdryo16KNn_92UfbL8Gn6C1kUJK2McWIt9PY7BrHQ,113
8
8
  src/cli/config.py,sha256=FMLDg5OrmFzBJR_jO5Qg5ery-E9oXt-VPbs_5M0KtVA,4280
9
- src/cli/main.py,sha256=DR4VjmpXIHSfhUyUPBz9CoZfpNBllMiW17XVFTn_wjs,57572
9
+ src/cli/main.py,sha256=VJU79I-tN4iJ-jrfGykAijlv96cHWAYAI-0qT0Owcu0,71349
10
10
  src/cost/__init__.py,sha256=WpTwsWAm8wyCkB65gsCnzu89-r5G_hgrbL9zTqvQ8j8,122
11
11
  src/cost/analyzer.py,sha256=02WFkZErD-zfu1Jp8UDym3wTjuB0c3l5jadZmbGbBJk,10201
12
12
  src/cost/explorer.py,sha256=c4Yj7w5gmvoeD9iY7hseinj3Ij-g9Rh8YXWYZVkO6u4,6847
@@ -18,12 +18,15 @@ src/models/__init__.py,sha256=GPMBPxpV0-GAEZdCYbPn7p7t_ICSFfzDuu-aYSrQXs8,395
18
18
  src/models/cost_report.py,sha256=BzyWrMewHau7EpQyLm4WoesVY3WDPBOtGVdk_H8USuM,3079
19
19
  src/models/delta_report.py,sha256=IUY1ulWvwPzLyVcj3NKFrkHoiTQTilHsiMU8Oe4OyOk,4500
20
20
  src/models/inventory.py,sha256=x5rm9pjHryjsMvjNWY-I9MDNC7hSzs79zYriO9-K7dM,4840
21
+ src/models/report.py,sha256=Ubd2OdBavCRA4LN8aJiLrjhDzPCcx4-US4cTkZII5SM,8853
21
22
  src/models/resource.py,sha256=6B7MQL_0mW2PKx0YvnAGjp0549vP8R61U9QZ-P0Zbj8,3192
22
23
  src/models/snapshot.py,sha256=JLZzS4toRDyEvFWefHZFhBbZBY4_eyOrlkSkqWcKPLY,4204
23
24
  src/snapshot/__init__.py,sha256=trHfRe_8jxjQVb48cDLVH3gs2Y7YQtsX2gFL4OB9qRc,237
24
25
  src/snapshot/capturer.py,sha256=YVeUHLSufuoZD49vTERPQGR95IZAZb0yqFZ6IGI1Urs,13059
25
26
  src/snapshot/filter.py,sha256=WjR96C5mpyjWvxyDcD9gniIYRgTCDRQTJZR0PedHQtU,8503
26
27
  src/snapshot/inventory_storage.py,sha256=Ocq9GfMb8e-E80rewmK1LPwYNfNxbO-tPg4p1LFnerE,9079
28
+ src/snapshot/report_formatter.py,sha256=WR7Adqq5FjJqu3iIWAnOZ9yp6AuJH1kA6uKKpT7jrT4,8266
29
+ src/snapshot/reporter.py,sha256=CZp5fj6RcHoD5a3-1unwq5KKIxrQA38rLWCjspnyvvI,6628
27
30
  src/snapshot/storage.py,sha256=MQZN2W4aBFG69NU-gwGw8LNvae0z6UMJPeL02qq-dYM,8466
28
31
  src/snapshot/resource_collectors/__init__.py,sha256=QSJiStvbmqd_uCI6c4T03-3PMb0UmxAROZwVHPk25gc,94
29
32
  src/snapshot/resource_collectors/apigateway.py,sha256=X916NdfRzUMNmMuyJdB_BrgTu1p7P7NwslPxWcPW4CU,4748
@@ -53,13 +56,14 @@ src/snapshot/resource_collectors/stepfunctions.py,sha256=I8TYniEzSX2jKZbSX6WI02e
53
56
  src/snapshot/resource_collectors/vpcendpoints.py,sha256=tclcLPRxFmE5vwQQPwFQv2nbOc-4qVFnUaSJ4oDMU7w,2994
54
57
  src/snapshot/resource_collectors/waf.py,sha256=HUNshQ_WHbIK3nAyNwhb21uHkMC7ywSOke28b-5ECRg,6461
55
58
  src/utils/__init__.py,sha256=1qjjL3wxB7ye6s3AT4h5hQ9Xgi7y0WllUMmTpiZWC54,284
56
- src/utils/export.py,sha256=FeD7Dg7gzTGHJQIA8zGNI64hrwbzQ2KwDMqI74fVgYM,2390
59
+ src/utils/export.py,sha256=lBUxLGkvtdzTFq2VKRmHgf18ZROiorHGGHlbeG7ixxc,8929
57
60
  src/utils/hash.py,sha256=w6jJqNR64IEcgMIrIY-M1QXuo_CkH0sbT8I6JcFQJqo,1747
58
61
  src/utils/logging.py,sha256=D39u9xrDSr_HlqedKa2yz-vmbCKulNAkJNVfG571UXY,2558
62
+ src/utils/pagination.py,sha256=BoMk2lP65-tGP1U6aUAuJPU7M-JroSvyfpScLKcGYxg,1153
59
63
  src/utils/paths.py,sha256=EgO41-XE1KOmF0drY2YlJkzNQIb3WPsrGtPvsc0hVqY,1607
60
64
  src/utils/progress.py,sha256=H8_6AtVsw-_-P40E57ECofkFw0v1Lq6uEVSofuxCXxY,936
61
- aws_inventory_manager-0.2.0.dist-info/METADATA,sha256=WmZP6ZJIn5zmUSpoO0WABgGCyHMaNs0M9CP-dRSY4pw,14427
62
- aws_inventory_manager-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
63
- aws_inventory_manager-0.2.0.dist-info/entry_points.txt,sha256=Ktdhto-PER5BtwWKYBtU_-FS3ngiGtAGGeoLzRW2qUw,44
64
- aws_inventory_manager-0.2.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
65
- aws_inventory_manager-0.2.0.dist-info/RECORD,,
65
+ aws_inventory_manager-0.3.0.dist-info/METADATA,sha256=3vuy5TJ4vheil4LsQtsmJQEQK7GIhNM2iNGIiB7Mxl8,15326
66
+ aws_inventory_manager-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
67
+ aws_inventory_manager-0.3.0.dist-info/entry_points.txt,sha256=Ktdhto-PER5BtwWKYBtU_-FS3ngiGtAGGeoLzRW2qUw,44
68
+ aws_inventory_manager-0.3.0.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
69
+ aws_inventory_manager-0.3.0.dist-info/RECORD,,
src/cli/main.py CHANGED
@@ -1127,6 +1127,279 @@ def snapshot_delete(
1127
1127
  raise typer.Exit(code=1)
1128
1128
 
1129
1129
 
1130
+ @snapshot_app.command("report")
1131
+ def snapshot_report(
1132
+ snapshot_name: Optional[str] = typer.Argument(None, help="Snapshot name (default: active snapshot)"),
1133
+ inventory: Optional[str] = typer.Option(None, "--inventory", help="Inventory name (required if multiple exist)"),
1134
+ profile: Optional[str] = typer.Option(None, "--profile", help="AWS profile name"),
1135
+ storage_path: Optional[str] = typer.Option(None, "--storage-path", help="Override storage location"),
1136
+ resource_type: Optional[list[str]] = typer.Option(
1137
+ None, "--resource-type", help="Filter by resource type (can specify multiple)"
1138
+ ),
1139
+ region: Optional[list[str]] = typer.Option(None, "--region", help="Filter by region (can specify multiple)"),
1140
+ detailed: bool = typer.Option(
1141
+ False, "--detailed", help="Show detailed resource information (ARN, tags, creation date)"
1142
+ ),
1143
+ page_size: int = typer.Option(100, "--page-size", help="Resources per page in detailed view (default: 100)"),
1144
+ export: Optional[str] = typer.Option(
1145
+ None, "--export", help="Export report to file (format detected from extension: .json, .csv, .txt)"
1146
+ ),
1147
+ ):
1148
+ """Display resource summary report for a snapshot.
1149
+
1150
+ Shows aggregated resource counts by service, region, and type with
1151
+ visual progress bars and formatted output. Can export to JSON, CSV, or TXT formats.
1152
+
1153
+ Snapshot Selection (in order of precedence):
1154
+ 1. Explicit snapshot name argument
1155
+ 2. Most recent snapshot from specified --inventory
1156
+ 3. Active snapshot (set via 'awsinv snapshot set-active')
1157
+
1158
+ Examples:
1159
+ awsinv snapshot report # Report on active snapshot
1160
+ awsinv snapshot report baseline-2025-01 # Report on specific snapshot
1161
+ awsinv snapshot report --inventory prod # Most recent snapshot from 'prod' inventory
1162
+ awsinv snapshot report --resource-type ec2 # Filter by resource type
1163
+ awsinv snapshot report --region us-east-1 # Filter by region
1164
+ awsinv snapshot report --resource-type ec2 --resource-type lambda # Multiple filters
1165
+ awsinv snapshot report --export report.json # Export full report to JSON
1166
+ awsinv snapshot report --export resources.csv # Export resources to CSV
1167
+ awsinv snapshot report --export summary.txt # Export summary to TXT
1168
+ awsinv snapshot report --detailed --export details.json # Export detailed view
1169
+ """
1170
+ from ..models.report import FilterCriteria
1171
+ from ..snapshot.report_formatter import ReportFormatter
1172
+ from ..snapshot.reporter import SnapshotReporter
1173
+ from ..utils.export import detect_format, export_report_csv, export_report_json, export_report_txt
1174
+
1175
+ try:
1176
+ # Use provided storage path or default from config
1177
+ storage = SnapshotStorage(storage_path or config.storage_path)
1178
+
1179
+ # Determine which snapshot to load
1180
+ target_snapshot_name: str
1181
+ if snapshot_name:
1182
+ # Explicit snapshot name provided
1183
+ target_snapshot_name = snapshot_name
1184
+ elif inventory:
1185
+ # Inventory specified - find most recent snapshot from that inventory
1186
+ from datetime import datetime as dt
1187
+ from typing import TypedDict
1188
+
1189
+ class InventorySnapshot(TypedDict):
1190
+ name: str
1191
+ created_at: dt
1192
+
1193
+ all_snapshots = storage.list_snapshots()
1194
+ inventory_snapshots: list[InventorySnapshot] = []
1195
+
1196
+ for snap_meta in all_snapshots:
1197
+ try:
1198
+ snap = storage.load_snapshot(snap_meta["name"])
1199
+ if snap.inventory_name == inventory:
1200
+ inventory_snapshots.append(
1201
+ InventorySnapshot(
1202
+ name=snap.name,
1203
+ created_at=snap.created_at,
1204
+ )
1205
+ )
1206
+ except Exception:
1207
+ continue
1208
+
1209
+ if not inventory_snapshots:
1210
+ console.print(f"✗ No snapshots found for inventory '{inventory}'", style="bold red")
1211
+ console.print("\nCreate a snapshot first:")
1212
+ console.print(f" awsinv snapshot create --inventory {inventory}")
1213
+ raise typer.Exit(code=1)
1214
+
1215
+ # Sort by created_at and pick most recent
1216
+ inventory_snapshots.sort(key=lambda x: x["created_at"], reverse=True)
1217
+ target_snapshot_name = inventory_snapshots[0]["name"]
1218
+ console.print(
1219
+ f"ℹ Using most recent snapshot from inventory '{inventory}': {target_snapshot_name}", style="dim"
1220
+ )
1221
+ else:
1222
+ # Try to get active snapshot
1223
+ active_name = storage.get_active_snapshot_name()
1224
+ if not active_name:
1225
+ console.print("✗ No active snapshot found", style="bold red")
1226
+ console.print("\nSet an active snapshot with:")
1227
+ console.print(" awsinv snapshot set-active <name>")
1228
+ console.print("\nOr specify a snapshot explicitly:")
1229
+ console.print(" awsinv snapshot report <snapshot-name>")
1230
+ console.print("\nOr specify an inventory to use the most recent snapshot:")
1231
+ console.print(" awsinv snapshot report --inventory <inventory-name>")
1232
+ raise typer.Exit(code=1)
1233
+ target_snapshot_name = active_name
1234
+
1235
+ # Load the snapshot
1236
+ try:
1237
+ snapshot = storage.load_snapshot(target_snapshot_name)
1238
+ except FileNotFoundError:
1239
+ console.print(f"✗ Snapshot '{target_snapshot_name}' not found", style="bold red")
1240
+
1241
+ # Show available snapshots
1242
+ try:
1243
+ all_snapshots = storage.list_snapshots()
1244
+ if all_snapshots:
1245
+ console.print("\nAvailable snapshots:")
1246
+ for snap_name in all_snapshots[:5]:
1247
+ console.print(f" • {snap_name}")
1248
+ if len(all_snapshots) > 5:
1249
+ console.print(f" ... and {len(all_snapshots) - 5} more")
1250
+ console.print("\nRun 'awsinv snapshot list' to see all snapshots.")
1251
+ except Exception:
1252
+ pass
1253
+
1254
+ raise typer.Exit(code=1)
1255
+
1256
+ # Handle empty snapshot
1257
+ if snapshot.resource_count == 0:
1258
+ console.print(f"⚠️ Warning: Snapshot '{snapshot.name}' contains 0 resources", style="yellow")
1259
+ console.print("\nNo report to generate.")
1260
+ raise typer.Exit(code=0)
1261
+
1262
+ # Create filter criteria if filters provided
1263
+ has_filters = bool(resource_type or region)
1264
+ criteria = None
1265
+ if has_filters:
1266
+ criteria = FilterCriteria(
1267
+ resource_types=resource_type if resource_type else None,
1268
+ regions=region if region else None,
1269
+ )
1270
+
1271
+ # Generate report
1272
+ reporter = SnapshotReporter(snapshot)
1273
+ metadata = reporter._extract_metadata()
1274
+
1275
+ # Detailed view vs Summary view
1276
+ if detailed:
1277
+ # Get detailed resources (with optional filtering)
1278
+ detailed_resources = list(reporter.get_detailed_resources(criteria))
1279
+
1280
+ # Export mode
1281
+ if export:
1282
+ try:
1283
+ # Detect format from file extension
1284
+ export_format = detect_format(export)
1285
+
1286
+ # Export based on format
1287
+ if export_format == "json":
1288
+ # For JSON, export full report structure with detailed resources
1289
+ summary = (
1290
+ reporter.generate_filtered_summary(criteria) if criteria else reporter.generate_summary()
1291
+ )
1292
+ export_path = export_report_json(export, metadata, summary, detailed_resources)
1293
+ console.print(
1294
+ f"✓ Exported {len(detailed_resources):,} resources to JSON: {export_path}",
1295
+ style="bold green",
1296
+ )
1297
+ elif export_format == "csv":
1298
+ # For CSV, export detailed resources
1299
+ export_path = export_report_csv(export, detailed_resources)
1300
+ console.print(
1301
+ f"✓ Exported {len(detailed_resources):,} resources to CSV: {export_path}",
1302
+ style="bold green",
1303
+ )
1304
+ elif export_format == "txt":
1305
+ # For TXT, export summary (detailed view doesn't make sense for plain text)
1306
+ summary = (
1307
+ reporter.generate_filtered_summary(criteria) if criteria else reporter.generate_summary()
1308
+ )
1309
+ export_path = export_report_txt(export, metadata, summary)
1310
+ console.print(f"✓ Exported summary to TXT: {export_path}", style="bold green")
1311
+ except FileExistsError as e:
1312
+ console.print(f"✗ {e}", style="bold red")
1313
+ console.print("\nUse a different filename or delete the existing file.", style="yellow")
1314
+ raise typer.Exit(code=1)
1315
+ except FileNotFoundError as e:
1316
+ console.print(f"✗ {e}", style="bold red")
1317
+ raise typer.Exit(code=1)
1318
+ except ValueError as e:
1319
+ console.print(f"✗ {e}", style="bold red")
1320
+ raise typer.Exit(code=1)
1321
+ else:
1322
+ # Display mode - show filter information if applied
1323
+ if criteria:
1324
+ console.print("\n[bold cyan]Filters Applied:[/bold cyan]")
1325
+ if resource_type:
1326
+ console.print(f" • Resource Types: {', '.join(resource_type)}")
1327
+ if region:
1328
+ console.print(f" • Regions: {', '.join(region)}")
1329
+ console.print(
1330
+ f" • Matching Resources: {len(detailed_resources):,} (of {snapshot.resource_count:,} total)\n"
1331
+ )
1332
+
1333
+ # Format and display detailed view
1334
+ formatter = ReportFormatter(console)
1335
+ formatter.format_detailed(metadata, detailed_resources, page_size=page_size)
1336
+ else:
1337
+ # Generate summary (filtered or full)
1338
+ if criteria:
1339
+ summary = reporter.generate_filtered_summary(criteria)
1340
+ else:
1341
+ summary = reporter.generate_summary()
1342
+
1343
+ # Export mode
1344
+ if export:
1345
+ try:
1346
+ # Detect format from file extension
1347
+ export_format = detect_format(export)
1348
+
1349
+ # Export based on format
1350
+ if export_format == "json":
1351
+ # For JSON, export full report structure
1352
+ # Get all resources for complete export
1353
+ all_resources = list(reporter.get_detailed_resources(criteria))
1354
+ export_path = export_report_json(export, metadata, summary, all_resources)
1355
+ console.print(
1356
+ f"✓ Exported {summary.total_count:,} resources to JSON: {export_path}", style="bold green"
1357
+ )
1358
+ elif export_format == "csv":
1359
+ # For CSV, export resources
1360
+ all_resources = list(reporter.get_detailed_resources(criteria))
1361
+ export_path = export_report_csv(export, all_resources)
1362
+ console.print(
1363
+ f"✓ Exported {len(all_resources):,} resources to CSV: {export_path}", style="bold green"
1364
+ )
1365
+ elif export_format == "txt":
1366
+ # For TXT, export summary only
1367
+ export_path = export_report_txt(export, metadata, summary)
1368
+ console.print(f"✓ Exported summary to TXT: {export_path}", style="bold green")
1369
+ except FileExistsError as e:
1370
+ console.print(f"✗ {e}", style="bold red")
1371
+ console.print("\nUse a different filename or delete the existing file.", style="yellow")
1372
+ raise typer.Exit(code=1)
1373
+ except FileNotFoundError as e:
1374
+ console.print(f"✗ {e}", style="bold red")
1375
+ raise typer.Exit(code=1)
1376
+ except ValueError as e:
1377
+ console.print(f"✗ {e}", style="bold red")
1378
+ raise typer.Exit(code=1)
1379
+ else:
1380
+ # Display mode - show filter information
1381
+ if criteria:
1382
+ console.print("\n[bold cyan]Filters Applied:[/bold cyan]")
1383
+ if resource_type:
1384
+ console.print(f" • Resource Types: {', '.join(resource_type)}")
1385
+ if region:
1386
+ console.print(f" • Regions: {', '.join(region)}")
1387
+ console.print(
1388
+ f" • Matching Resources: {summary.total_count:,} (of {snapshot.resource_count:,} total)\n"
1389
+ )
1390
+
1391
+ # Format and display summary report
1392
+ formatter = ReportFormatter(console)
1393
+ formatter.format_summary(metadata, summary, has_filters=has_filters)
1394
+
1395
+ except typer.Exit:
1396
+ raise
1397
+ except Exception as e:
1398
+ console.print(f"✗ Error generating report: {e}", style="bold red")
1399
+ logger.exception("Error in snapshot report command")
1400
+ raise typer.Exit(code=2)
1401
+
1402
+
1130
1403
  @app.command()
1131
1404
  def delta(
1132
1405
  snapshot: Optional[str] = typer.Option(