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.
- {aws_inventory_manager-0.2.0.dist-info → aws_inventory_manager-0.3.0.dist-info}/METADATA +31 -5
- {aws_inventory_manager-0.2.0.dist-info → aws_inventory_manager-0.3.0.dist-info}/RECORD +12 -8
- src/cli/main.py +273 -0
- src/models/report.py +276 -0
- src/snapshot/report_formatter.py +250 -0
- src/snapshot/reporter.py +189 -0
- src/utils/export.py +219 -1
- src/utils/pagination.py +41 -0
- {aws_inventory_manager-0.2.0.dist-info → aws_inventory_manager-0.3.0.dist-info}/WHEEL +0 -0
- {aws_inventory_manager-0.2.0.dist-info → aws_inventory_manager-0.3.0.dist-info}/entry_points.txt +0 -0
- {aws_inventory_manager-0.2.0.dist-info → aws_inventory_manager-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {aws_inventory_manager-0.2.0.dist-info → aws_inventory_manager-0.3.0.dist-info}/top_level.txt +0 -0
src/models/report.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for snapshot resource reporting.
|
|
3
|
+
|
|
4
|
+
This module defines the data structures used for generating and displaying
|
|
5
|
+
snapshot resource reports, including metadata, summaries, filtered views,
|
|
6
|
+
and detailed resource information.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Dict, List, Literal, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class SnapshotMetadata:
|
|
18
|
+
"""
|
|
19
|
+
Snapshot identification information for report header.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
name: Snapshot name (e.g., "baseline-2025-01-29")
|
|
23
|
+
created_at: Snapshot creation timestamp
|
|
24
|
+
account_id: AWS account ID
|
|
25
|
+
regions: List of AWS regions included in snapshot
|
|
26
|
+
inventory_name: Parent inventory name
|
|
27
|
+
total_resource_count: Total number of resources in snapshot
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
created_at: datetime
|
|
32
|
+
account_id: str
|
|
33
|
+
regions: List[str]
|
|
34
|
+
inventory_name: str
|
|
35
|
+
total_resource_count: int
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def region_summary(self) -> str:
|
|
39
|
+
"""Human-readable region list (e.g., 'us-east-1, us-west-2 (2 regions)')."""
|
|
40
|
+
if len(self.regions) <= 3:
|
|
41
|
+
return ", ".join(self.regions)
|
|
42
|
+
else:
|
|
43
|
+
return f"{', '.join(self.regions[:3])} ... ({len(self.regions)} regions)"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ResourceSummary:
|
|
48
|
+
"""
|
|
49
|
+
Aggregated resource counts for summary view.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
total_count: Total number of resources
|
|
53
|
+
by_service: Count per AWS service (e.g., {"EC2": 100, "S3": 50})
|
|
54
|
+
by_region: Count per AWS region (e.g., {"us-east-1": 80, "us-west-2": 70})
|
|
55
|
+
by_type: Count per resource type (e.g., {"AWS::EC2::Instance": 50})
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
total_count: int = 0
|
|
59
|
+
by_service: Dict[str, int] = field(default_factory=dict)
|
|
60
|
+
by_region: Dict[str, int] = field(default_factory=dict)
|
|
61
|
+
by_type: Dict[str, int] = field(default_factory=dict)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def service_count(self) -> int:
|
|
65
|
+
"""Number of distinct AWS services."""
|
|
66
|
+
return len(self.by_service)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def region_count(self) -> int:
|
|
70
|
+
"""Number of distinct AWS regions."""
|
|
71
|
+
return len(self.by_region)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def type_count(self) -> int:
|
|
75
|
+
"""Number of distinct resource types."""
|
|
76
|
+
return len(self.by_type)
|
|
77
|
+
|
|
78
|
+
def top_services(self, limit: int = 5) -> List[Tuple[str, int]]:
|
|
79
|
+
"""Return top N services by resource count."""
|
|
80
|
+
return sorted(self.by_service.items(), key=lambda x: x[1], reverse=True)[:limit]
|
|
81
|
+
|
|
82
|
+
def top_regions(self, limit: int = 5) -> List[Tuple[str, int]]:
|
|
83
|
+
"""Return top N regions by resource count."""
|
|
84
|
+
return sorted(self.by_region.items(), key=lambda x: x[1], reverse=True)[:limit]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class FilteredResource:
|
|
89
|
+
"""
|
|
90
|
+
Minimal resource info for filtered view (non-detailed).
|
|
91
|
+
|
|
92
|
+
Attributes:
|
|
93
|
+
arn: AWS Resource Name (unique identifier)
|
|
94
|
+
resource_type: CloudFormation resource type (e.g., "AWS::EC2::Instance")
|
|
95
|
+
name: Resource name or identifier
|
|
96
|
+
region: AWS region (e.g., "us-east-1")
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
arn: str
|
|
100
|
+
resource_type: str
|
|
101
|
+
name: str
|
|
102
|
+
region: str
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def service(self) -> str:
|
|
106
|
+
"""Extract service name from resource type (e.g., "EC2" from "AWS::EC2::Instance")."""
|
|
107
|
+
parts = self.resource_type.split("::")
|
|
108
|
+
return parts[1] if len(parts) >= 2 else "Unknown"
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def short_type(self) -> str:
|
|
112
|
+
"""Short resource type (e.g., "Instance" from "AWS::EC2::Instance")."""
|
|
113
|
+
parts = self.resource_type.split("::")
|
|
114
|
+
return parts[-1] if parts else self.resource_type
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class DetailedResource:
|
|
119
|
+
"""
|
|
120
|
+
Full resource details for detailed view.
|
|
121
|
+
|
|
122
|
+
Attributes:
|
|
123
|
+
arn: AWS Resource Name
|
|
124
|
+
resource_type: CloudFormation resource type
|
|
125
|
+
name: Resource name or identifier
|
|
126
|
+
region: AWS region
|
|
127
|
+
tags: Key-value tag pairs
|
|
128
|
+
created_at: Resource creation timestamp (if available)
|
|
129
|
+
config_hash: Configuration hash for change detection
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
arn: str
|
|
133
|
+
resource_type: str
|
|
134
|
+
name: str
|
|
135
|
+
region: str
|
|
136
|
+
tags: Dict[str, str]
|
|
137
|
+
created_at: Optional[datetime]
|
|
138
|
+
config_hash: str
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def service(self) -> str:
|
|
142
|
+
"""Extract service name from resource type."""
|
|
143
|
+
parts = self.resource_type.split("::")
|
|
144
|
+
return parts[1] if len(parts) >= 2 else "Unknown"
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def age_days(self) -> Optional[int]:
|
|
148
|
+
"""Calculate resource age in days (if creation date available)."""
|
|
149
|
+
if self.created_at:
|
|
150
|
+
from datetime import timezone
|
|
151
|
+
|
|
152
|
+
# Ensure we're comparing timezone-aware datetimes
|
|
153
|
+
now_utc = datetime.now(timezone.utc)
|
|
154
|
+
created = self.created_at if self.created_at.tzinfo else self.created_at.replace(tzinfo=timezone.utc)
|
|
155
|
+
return (now_utc - created).days
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def tag_count(self) -> int:
|
|
160
|
+
"""Number of tags applied to resource."""
|
|
161
|
+
return len(self.tags)
|
|
162
|
+
|
|
163
|
+
def has_tag(self, key: str, value: Optional[str] = None) -> bool:
|
|
164
|
+
"""Check if resource has specific tag (optionally with value)."""
|
|
165
|
+
if key not in self.tags:
|
|
166
|
+
return False
|
|
167
|
+
if value is not None:
|
|
168
|
+
return self.tags[key] == value
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class FilterCriteria:
|
|
174
|
+
"""
|
|
175
|
+
Filter specification for narrowing report results.
|
|
176
|
+
|
|
177
|
+
Attributes:
|
|
178
|
+
resource_types: List of resource types to include (flexible matching)
|
|
179
|
+
regions: List of AWS regions to include (exact matching)
|
|
180
|
+
match_mode: Matching strategy ("flexible" or "exact")
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
resource_types: Optional[List[str]] = None
|
|
184
|
+
regions: Optional[List[str]] = None
|
|
185
|
+
match_mode: Literal["flexible", "exact"] = "flexible"
|
|
186
|
+
|
|
187
|
+
def __post_init__(self) -> None:
|
|
188
|
+
"""Normalize filter values (lowercase for case-insensitive matching)."""
|
|
189
|
+
if self.resource_types:
|
|
190
|
+
self.resource_types = [rt.lower() for rt in self.resource_types]
|
|
191
|
+
if self.regions:
|
|
192
|
+
self.regions = [r.lower() for r in self.regions]
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def has_filters(self) -> bool:
|
|
196
|
+
"""Check if any filters are applied."""
|
|
197
|
+
return bool(self.resource_types or self.regions)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def filter_count(self) -> int:
|
|
201
|
+
"""Total number of filter criteria."""
|
|
202
|
+
count = 0
|
|
203
|
+
if self.resource_types:
|
|
204
|
+
count += len(self.resource_types)
|
|
205
|
+
if self.regions:
|
|
206
|
+
count += len(self.regions)
|
|
207
|
+
return count
|
|
208
|
+
|
|
209
|
+
def matches_resource(self, resource: FilteredResource) -> bool:
|
|
210
|
+
"""
|
|
211
|
+
Check if resource matches filter criteria.
|
|
212
|
+
|
|
213
|
+
Uses flexible matching for resource types (see research.md Task 2).
|
|
214
|
+
Uses exact matching for regions (case-insensitive).
|
|
215
|
+
"""
|
|
216
|
+
# Region filter (exact match, case-insensitive)
|
|
217
|
+
if self.regions and resource.region.lower() not in self.regions:
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# Resource type filter (flexible matching)
|
|
221
|
+
if self.resource_types:
|
|
222
|
+
type_match = any(
|
|
223
|
+
self._match_resource_type(resource.resource_type, filter_type) for filter_type in self.resource_types
|
|
224
|
+
)
|
|
225
|
+
if not type_match:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
def _match_resource_type(self, resource_type: str, filter_value: str) -> bool:
|
|
231
|
+
"""Three-tier matching: exact → prefix → contains (see research.md)."""
|
|
232
|
+
resource_lower = resource_type.lower()
|
|
233
|
+
filter_lower = filter_value.lower()
|
|
234
|
+
|
|
235
|
+
# Tier 1: Exact match
|
|
236
|
+
if resource_lower == filter_lower:
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
# Tier 2: Service prefix match
|
|
240
|
+
service_prefix = f"aws::{filter_lower}::"
|
|
241
|
+
if resource_lower.startswith(service_prefix):
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
# Tier 3: Contains match
|
|
245
|
+
if filter_lower in resource_lower:
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass
|
|
252
|
+
class ResourceReport:
|
|
253
|
+
"""
|
|
254
|
+
Complete report data structure.
|
|
255
|
+
|
|
256
|
+
Attributes:
|
|
257
|
+
snapshot_metadata: Information about the snapshot being reported
|
|
258
|
+
summary: Aggregated resource counts by service/region/type
|
|
259
|
+
filtered_resources: Minimal resource list (if filtering applied)
|
|
260
|
+
detailed_resources: Full resource details (if detailed view requested)
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
snapshot_metadata: SnapshotMetadata
|
|
264
|
+
summary: ResourceSummary
|
|
265
|
+
filtered_resources: Optional[List[FilteredResource]] = None
|
|
266
|
+
detailed_resources: Optional[List[DetailedResource]] = None
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def has_filters(self) -> bool:
|
|
270
|
+
"""Check if report includes filtered results."""
|
|
271
|
+
return self.filtered_resources is not None
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def has_details(self) -> bool:
|
|
275
|
+
"""Check if report includes detailed resource info."""
|
|
276
|
+
return self.detailed_resources is not None
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Report output formatting using Rich for terminal UI.
|
|
3
|
+
|
|
4
|
+
This module contains the ReportFormatter class responsible for rendering
|
|
5
|
+
reports to the terminal with Rich tables, progress bars, and formatted output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
|
|
15
|
+
from src.models.report import DetailedResource, ResourceSummary, SnapshotMetadata
|
|
16
|
+
from src.utils.pagination import paginate_resources
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ReportFormatter:
|
|
20
|
+
"""
|
|
21
|
+
Formats and renders reports using Rich terminal UI.
|
|
22
|
+
|
|
23
|
+
Handles rendering of headers, summaries, tables, and progress bars
|
|
24
|
+
for snapshot resource reports.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, console: Optional[Console] = None) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initialize formatter with Rich Console.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
console: Rich Console instance (creates default if not provided)
|
|
33
|
+
"""
|
|
34
|
+
self.console = console or Console()
|
|
35
|
+
|
|
36
|
+
def format_summary(
|
|
37
|
+
self,
|
|
38
|
+
metadata: SnapshotMetadata,
|
|
39
|
+
summary: ResourceSummary,
|
|
40
|
+
has_filters: bool = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Format and display complete summary report.
|
|
44
|
+
|
|
45
|
+
Orchestrates all render methods to display header, service breakdown,
|
|
46
|
+
region breakdown, and type breakdown.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
metadata: Snapshot metadata for header
|
|
50
|
+
summary: Resource summary with aggregated counts
|
|
51
|
+
has_filters: Whether filters were applied (shows "Filtered" indicator)
|
|
52
|
+
"""
|
|
53
|
+
# Render header
|
|
54
|
+
self._render_header(metadata, has_filters)
|
|
55
|
+
|
|
56
|
+
# Add spacing
|
|
57
|
+
self.console.print()
|
|
58
|
+
|
|
59
|
+
# Render summary sections
|
|
60
|
+
self.console.print("📊 [bold]Resource Summary[/bold]\n")
|
|
61
|
+
self.console.print(f"Total Resources: [bold cyan]{summary.total_count:,}[/bold cyan]\n")
|
|
62
|
+
|
|
63
|
+
if summary.total_count > 0:
|
|
64
|
+
# Render service breakdown
|
|
65
|
+
self._render_service_breakdown(summary)
|
|
66
|
+
|
|
67
|
+
# Render region breakdown
|
|
68
|
+
self._render_region_breakdown(summary)
|
|
69
|
+
|
|
70
|
+
# Render type breakdown (top 10)
|
|
71
|
+
self._render_type_breakdown(summary)
|
|
72
|
+
|
|
73
|
+
def _render_header(self, metadata: SnapshotMetadata, has_filters: bool = False) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Render report header with snapshot metadata.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
metadata: Snapshot metadata
|
|
79
|
+
has_filters: Whether to show "Filtered" indicator
|
|
80
|
+
"""
|
|
81
|
+
title = f"Snapshot Report: {metadata.name}"
|
|
82
|
+
if has_filters:
|
|
83
|
+
title += " (Filtered)"
|
|
84
|
+
|
|
85
|
+
# Create header panel
|
|
86
|
+
header_content = f"""[bold]Inventory:[/bold] {metadata.inventory_name}
|
|
87
|
+
[bold]Account ID:[/bold] {metadata.account_id}
|
|
88
|
+
[bold]Created:[/bold] {metadata.created_at.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
|
89
|
+
[bold]Regions:[/bold] {metadata.region_summary}"""
|
|
90
|
+
|
|
91
|
+
panel = Panel(
|
|
92
|
+
header_content,
|
|
93
|
+
title=title,
|
|
94
|
+
border_style="blue",
|
|
95
|
+
expand=False,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.console.print(panel)
|
|
99
|
+
|
|
100
|
+
def _render_service_breakdown(self, summary: ResourceSummary) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Render service breakdown with progress bars.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
summary: Resource summary with service counts
|
|
106
|
+
"""
|
|
107
|
+
if not summary.by_service:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
self.console.print("[bold]By Service:[/bold]")
|
|
111
|
+
|
|
112
|
+
# Get top services (sorted by count)
|
|
113
|
+
top_services = summary.top_services(limit=10)
|
|
114
|
+
|
|
115
|
+
for service, count in top_services:
|
|
116
|
+
percentage = (count / summary.total_count) * 100
|
|
117
|
+
bar_length = int((count / summary.total_count) * 30)
|
|
118
|
+
bar = "█" * bar_length + "░" * (30 - bar_length)
|
|
119
|
+
|
|
120
|
+
self.console.print(f" {service:15} {count:5} {bar} ({percentage:.1f}%)")
|
|
121
|
+
|
|
122
|
+
self.console.print()
|
|
123
|
+
|
|
124
|
+
def _render_region_breakdown(self, summary: ResourceSummary) -> None:
|
|
125
|
+
"""
|
|
126
|
+
Render region breakdown with progress bars.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
summary: Resource summary with region counts
|
|
130
|
+
"""
|
|
131
|
+
if not summary.by_region:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
self.console.print("[bold]By Region:[/bold]")
|
|
135
|
+
|
|
136
|
+
# Get top regions (sorted by count)
|
|
137
|
+
top_regions = summary.top_regions(limit=10)
|
|
138
|
+
|
|
139
|
+
for region, count in top_regions:
|
|
140
|
+
percentage = (count / summary.total_count) * 100
|
|
141
|
+
bar_length = int((count / summary.total_count) * 30)
|
|
142
|
+
bar = "█" * bar_length + "░" * (30 - bar_length)
|
|
143
|
+
|
|
144
|
+
self.console.print(f" {region:15} {count:5} {bar} ({percentage:.1f}%)")
|
|
145
|
+
|
|
146
|
+
self.console.print()
|
|
147
|
+
|
|
148
|
+
def _render_type_breakdown(self, summary: ResourceSummary) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Render resource type breakdown showing top 10 types.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
summary: Resource summary with type counts
|
|
154
|
+
"""
|
|
155
|
+
if not summary.by_type:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
self.console.print("[bold]Top 10 Resource Types:[/bold]")
|
|
159
|
+
|
|
160
|
+
# Get top 10 resource types
|
|
161
|
+
sorted_types = sorted(
|
|
162
|
+
summary.by_type.items(),
|
|
163
|
+
key=lambda x: x[1],
|
|
164
|
+
reverse=True,
|
|
165
|
+
)[:10]
|
|
166
|
+
|
|
167
|
+
for resource_type, count in sorted_types:
|
|
168
|
+
percentage = (count / summary.total_count) * 100
|
|
169
|
+
self.console.print(f" {resource_type:45} {count:5} ({percentage:.1f}%)")
|
|
170
|
+
|
|
171
|
+
self.console.print()
|
|
172
|
+
|
|
173
|
+
def format_detailed(
|
|
174
|
+
self,
|
|
175
|
+
metadata: SnapshotMetadata,
|
|
176
|
+
resources: List[DetailedResource],
|
|
177
|
+
page_size: int = 100,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""
|
|
180
|
+
Format and display detailed resource view with pagination.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
metadata: Snapshot metadata for header
|
|
184
|
+
resources: List of detailed resources to display
|
|
185
|
+
page_size: Number of resources per page (default: 100)
|
|
186
|
+
"""
|
|
187
|
+
# Render header
|
|
188
|
+
self._render_header(metadata, has_filters=False)
|
|
189
|
+
self.console.print()
|
|
190
|
+
|
|
191
|
+
total_resources = len(resources)
|
|
192
|
+
self.console.print(f"Total Resources: [bold cyan]{total_resources:,}[/bold cyan]\n")
|
|
193
|
+
|
|
194
|
+
if total_resources == 0:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Paginate resources
|
|
198
|
+
pages = list(paginate_resources(resources, page_size=page_size))
|
|
199
|
+
total_pages = len(pages)
|
|
200
|
+
|
|
201
|
+
# Display pages
|
|
202
|
+
for page_num, page in enumerate(pages, start=1):
|
|
203
|
+
for idx, resource in enumerate(page, start=1):
|
|
204
|
+
global_idx = (page_num - 1) * page_size + idx
|
|
205
|
+
self._render_detailed_resource(resource, global_idx, total_resources)
|
|
206
|
+
|
|
207
|
+
# Show pagination info (but no prompts - non-interactive for now)
|
|
208
|
+
if page_num < total_pages:
|
|
209
|
+
self.console.print(f"\n[dim]─── Page {page_num} of {total_pages} ───[/dim]\n")
|
|
210
|
+
|
|
211
|
+
def _render_detailed_resource(
|
|
212
|
+
self,
|
|
213
|
+
resource: DetailedResource,
|
|
214
|
+
index: int,
|
|
215
|
+
total: int,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Render a single detailed resource.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
resource: DetailedResource to display
|
|
222
|
+
index: Resource index (1-based)
|
|
223
|
+
total: Total number of resources
|
|
224
|
+
"""
|
|
225
|
+
self.console.print("─" * 65)
|
|
226
|
+
self.console.print(f"[bold]Resource {index}/{total}[/bold]\n")
|
|
227
|
+
|
|
228
|
+
self.console.print(f"[bold]ARN:[/bold] {resource.arn}")
|
|
229
|
+
self.console.print(f"[bold]Type:[/bold] {resource.resource_type}")
|
|
230
|
+
self.console.print(f"[bold]Name:[/bold] {resource.name}")
|
|
231
|
+
self.console.print(f"[bold]Region:[/bold] {resource.region}")
|
|
232
|
+
|
|
233
|
+
# Show creation date if available
|
|
234
|
+
if resource.created_at:
|
|
235
|
+
age_str = ""
|
|
236
|
+
if resource.age_days is not None:
|
|
237
|
+
age_str = f" ({resource.age_days} days ago)"
|
|
238
|
+
created_str = resource.created_at.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
239
|
+
self.console.print(f"[bold]Created:[/bold] {created_str}{age_str}")
|
|
240
|
+
|
|
241
|
+
# Show tags
|
|
242
|
+
self.console.print()
|
|
243
|
+
if resource.tags:
|
|
244
|
+
self.console.print("[bold]Tags:[/bold]")
|
|
245
|
+
for key, value in resource.tags.items():
|
|
246
|
+
self.console.print(f" {key:20} {value}")
|
|
247
|
+
else:
|
|
248
|
+
self.console.print("[dim]No tags[/dim]")
|
|
249
|
+
|
|
250
|
+
self.console.print()
|