awsquery 0.1.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.
awsquery/formatters.py ADDED
@@ -0,0 +1,372 @@
1
+ """Output formatting for AWS Query Tool."""
2
+
3
+ import json
4
+
5
+ from tabulate import tabulate
6
+
7
+ from .utils import debug_print, simplify_key
8
+
9
+
10
+ def detect_aws_tags(obj):
11
+ """Detect if object contains AWS Tag structure"""
12
+ if isinstance(obj, dict) and "Tags" in obj:
13
+ tags = obj["Tags"]
14
+ if isinstance(tags, list) and len(tags) > 0:
15
+ # Check if first item has Key/Value structure
16
+ if isinstance(tags[0], dict) and "Key" in tags[0] and "Value" in tags[0]:
17
+ return True
18
+ return False
19
+
20
+
21
+ def _transform_aws_tags_list(tags_list):
22
+ """Transform AWS Tags list to map format."""
23
+ tag_map = {}
24
+ for tag in tags_list:
25
+ if isinstance(tag, dict) and "Key" in tag and "Value" in tag:
26
+ # Only add tags with non-empty keys
27
+ tag_key = tag["Key"]
28
+ if tag_key and tag_key.strip():
29
+ tag_map[tag_key] = tag["Value"]
30
+ return tag_map
31
+
32
+
33
+ def _is_aws_tags_structure(value):
34
+ """Check if value looks like AWS Tags structure."""
35
+ return (
36
+ isinstance(value, list)
37
+ and value
38
+ and isinstance(value[0], dict)
39
+ and "Key" in value[0]
40
+ and "Value" in value[0]
41
+ )
42
+
43
+
44
+ def transform_tags_structure(data, visited=None):
45
+ """Transform AWS Tag lists to searchable maps recursively with circular reference protection
46
+
47
+ Converts Tags from [{"Key": "Name", "Value": "web-server"}] format
48
+ to {"Name": "web-server"} format for easier searching and filtering.
49
+ Preserves original data alongside transformed data for debugging.
50
+ """
51
+ if visited is None:
52
+ visited = set()
53
+
54
+ # Circular reference protection
55
+ if isinstance(data, (dict, list)) and id(data) in visited:
56
+ return data # Return original to avoid infinite recursion
57
+
58
+ if isinstance(data, dict):
59
+ visited.add(id(data))
60
+ result = {}
61
+ for key, value in data.items():
62
+ if key == "Tags" and _is_aws_tags_structure(value):
63
+ # Transform Tag list to map
64
+ tag_map = _transform_aws_tags_list(value)
65
+ result[key] = tag_map
66
+ # Preserve original for debugging
67
+ result[f"{key}_Original"] = value
68
+ debug_print(f"Transformed {len(tag_map)} AWS Tags to map format")
69
+ else:
70
+ # Recursively transform nested structures
71
+ result[key] = transform_tags_structure(value, visited)
72
+ visited.remove(id(data))
73
+ return result
74
+ elif isinstance(data, list):
75
+ visited.add(id(data))
76
+ list_result = [transform_tags_structure(item, visited) for item in data]
77
+ visited.remove(id(data))
78
+ return list_result
79
+ else:
80
+ return data
81
+
82
+
83
+ def flatten_response(data):
84
+ """Flatten AWS response to extract resource lists"""
85
+ # First, transform tags in the entire response
86
+ transformed_data = transform_tags_structure(data)
87
+
88
+ if isinstance(transformed_data, list):
89
+ debug_print(f"Paginated response with {len(transformed_data)} pages")
90
+ all_items = []
91
+ for i, page in enumerate(transformed_data):
92
+ debug_print(f"Processing page {i+1}")
93
+ items = flatten_single_response(page)
94
+ all_items.extend(items)
95
+ debug_print(f"Total resources extracted from all pages: {len(all_items)}")
96
+ return all_items
97
+ else:
98
+ debug_print("Single response (not paginated)")
99
+ result = flatten_single_response(transformed_data)
100
+ debug_print(f"Total resources extracted: {len(result)}")
101
+ return result
102
+
103
+
104
+ def flatten_single_response(response):
105
+ """Simple extraction of data from AWS API responses"""
106
+ if not response:
107
+ debug_print("Empty response, returning empty list")
108
+ return []
109
+
110
+ if isinstance(response, list):
111
+ debug_print(f"Direct list response with {len(response)} items")
112
+ return response
113
+
114
+ if not isinstance(response, dict):
115
+ debug_print(f"Non-dict response ({type(response)}), wrapping in list")
116
+ return [response]
117
+
118
+ original_keys = list(response.keys())
119
+ debug_print(f"Original response keys: {original_keys}")
120
+
121
+ filtered_response = {k: v for k, v in response.items() if k != "ResponseMetadata"}
122
+ filtered_keys = list(filtered_response.keys())
123
+
124
+ if "ResponseMetadata" in response:
125
+ debug_print(f"Removed ResponseMetadata. Filtered keys: {filtered_keys}")
126
+ else:
127
+ debug_print(f"No ResponseMetadata found. Keys remain: {filtered_keys}")
128
+
129
+ if len(filtered_response) == 0:
130
+ debug_print("Only ResponseMetadata present -> RETURNING EMPTY LIST")
131
+ return []
132
+
133
+ list_keys = []
134
+ non_list_keys = []
135
+ for key, value in filtered_response.items():
136
+ if isinstance(value, list):
137
+ list_keys.append((key, len(value)))
138
+ else:
139
+ non_list_keys.append(key)
140
+
141
+ debug_print(f"Found {len(list_keys)} list keys and {len(non_list_keys)} non-list keys")
142
+ if list_keys:
143
+ debug_print(f"List keys: {[(k, l) for k, l in list_keys]}")
144
+ if non_list_keys:
145
+ debug_print(f"Non-list keys: {non_list_keys}")
146
+
147
+ if len(list_keys) == 1:
148
+ list_key, list_length = list_keys[0]
149
+ list_value = filtered_response[list_key]
150
+ if non_list_keys:
151
+ debug_print(
152
+ f"Single list key '{list_key}' with {list_length} items, "
153
+ f"ignoring metadata {non_list_keys} -> EXTRACTING LIST ONLY"
154
+ )
155
+ else:
156
+ debug_print(f"Single list key '{list_key}' with {list_length} items -> EXTRACTING LIST")
157
+ return list_value
158
+ elif len(list_keys) > 1:
159
+ list_keys.sort(key=lambda x: x[1], reverse=True)
160
+ largest_key, largest_length = list_keys[0]
161
+ largest_list = filtered_response[largest_key]
162
+ debug_print(
163
+ f"Multiple list keys found, using '{largest_key}' with {largest_length} "
164
+ f"items (largest) -> EXTRACTING LARGEST LIST"
165
+ )
166
+ return largest_list
167
+ else:
168
+ debug_print(f"No list keys found among {non_list_keys} -> USING WHOLE RESPONSE")
169
+ return [filtered_response]
170
+
171
+
172
+ def flatten_dict_keys(d, parent_key="", sep="."):
173
+ """Flatten nested dictionary keys with dot notation"""
174
+ if not isinstance(d, dict):
175
+ key = parent_key if parent_key else "value"
176
+ return {key: d}
177
+
178
+ items = []
179
+ for k, v in d.items():
180
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
181
+ if isinstance(v, dict):
182
+ items.extend(flatten_dict_keys(v, new_key, sep=sep).items())
183
+ elif isinstance(v, list):
184
+ for i, item in enumerate(v):
185
+ if isinstance(item, dict):
186
+ items.extend(flatten_dict_keys(item, f"{new_key}.{i}", sep=sep).items())
187
+ else:
188
+ items.append((f"{new_key}.{i}", item))
189
+ else:
190
+ items.append((new_key, v))
191
+ return dict(items)
192
+
193
+
194
+ def format_table_output(resources, column_filters=None):
195
+ """Format resources as table using tabulate"""
196
+ if not resources:
197
+ return "No results found."
198
+
199
+ # Apply tag transformation before processing
200
+ transformed_resources = []
201
+ for resource in resources:
202
+ transformed = transform_tags_structure(resource)
203
+ transformed_resources.append(transformed)
204
+
205
+ flattened_resources = []
206
+ all_keys = set()
207
+
208
+ for resource in transformed_resources:
209
+ flat = flatten_dict_keys(resource)
210
+ flattened_resources.append(flat)
211
+ all_keys.update(flat.keys())
212
+
213
+ if column_filters:
214
+ for filter_word in column_filters:
215
+ debug_print(f"Applying column filter: {filter_word}")
216
+
217
+ selected_keys = []
218
+ for filter_word in column_filters:
219
+ matching_keys = []
220
+ for key in all_keys:
221
+ simplified = simplify_key(key)
222
+ if filter_word.lower() in key.lower() or filter_word.lower() in simplified.lower():
223
+ matching_keys.append(key)
224
+ selected_keys.extend(matching_keys)
225
+ if matching_keys:
226
+ debug_print(
227
+ f"Column filter '{filter_word}' matched: "
228
+ f"{', '.join(matching_keys[:5])}{'...' if len(matching_keys) > 5 else ''}"
229
+ )
230
+ else:
231
+ debug_print(f"Column filter '{filter_word}' matched no columns")
232
+ selected_keys = list(dict.fromkeys(selected_keys))
233
+ else:
234
+ selected_keys = sorted(list(all_keys))
235
+
236
+ if not selected_keys:
237
+ return "No matching columns found."
238
+
239
+ simplified_to_full_keys: dict[str, list[str]] = {}
240
+ unique_headers_ordered = []
241
+
242
+ for key in selected_keys:
243
+ simplified = simplify_key(key)
244
+ if simplified not in simplified_to_full_keys:
245
+ simplified_to_full_keys[simplified] = []
246
+ unique_headers_ordered.append(simplified)
247
+ simplified_to_full_keys[simplified].append(key)
248
+
249
+ unique_headers = unique_headers_ordered
250
+
251
+ table_data = []
252
+ for resource in flattened_resources:
253
+ row = []
254
+ for simplified_key in unique_headers:
255
+ values = set()
256
+ for full_key in simplified_to_full_keys[simplified_key]:
257
+ value = resource.get(full_key, "")
258
+ if value:
259
+ if isinstance(value, str) and len(value) > 80:
260
+ value = value[:77] + "..."
261
+ values.add(str(value))
262
+
263
+ if values:
264
+ cell_value = ", ".join(sorted(values)) if len(values) > 1 else list(values)[0]
265
+ else:
266
+ cell_value = ""
267
+ row.append(cell_value)
268
+
269
+ if any(cell.strip() for cell in row):
270
+ table_data.append(row)
271
+
272
+ return tabulate(table_data, headers=unique_headers, tablefmt="grid")
273
+
274
+
275
+ def format_json_output(resources, column_filters=None):
276
+ """Format resources as JSON output"""
277
+ if not resources:
278
+ return json.dumps({"results": []}, indent=2)
279
+
280
+ # Apply tag transformation before processing
281
+ transformed_resources = []
282
+ for resource in resources:
283
+ transformed = transform_tags_structure(resource)
284
+ transformed_resources.append(transformed)
285
+
286
+ if column_filters:
287
+ for filter_word in column_filters:
288
+ debug_print(f"Applying column filter to JSON: {filter_word}")
289
+
290
+ filtered_resources = []
291
+ for resource in transformed_resources:
292
+ flat = flatten_dict_keys(resource)
293
+
294
+ simplified_groups: dict[str, set[str]] = {}
295
+ for key, value in flat.items():
296
+ simplified = simplify_key(key)
297
+ if any(
298
+ cf.lower() in key.lower() or cf.lower() in simplified.lower()
299
+ for cf in column_filters
300
+ ):
301
+ if simplified not in simplified_groups:
302
+ simplified_groups[simplified] = set()
303
+ if value:
304
+ simplified_groups[simplified].add(str(value))
305
+
306
+ filtered = {}
307
+ for simplified_key, values in simplified_groups.items():
308
+ if values:
309
+ filtered[simplified_key] = (
310
+ ", ".join(sorted(values)) if len(values) > 1 else list(values)[0]
311
+ )
312
+
313
+ if filtered:
314
+ filtered_resources.append(filtered)
315
+ return json.dumps({"results": filtered_resources}, indent=2, default=str)
316
+ else:
317
+ return json.dumps({"results": transformed_resources}, indent=2, default=str)
318
+
319
+
320
+ def extract_and_sort_keys(resources, simplify=True):
321
+ """Extract all keys from resources and sort them case-insensitively"""
322
+ if not resources:
323
+ return []
324
+
325
+ # Apply tag transformation before processing
326
+ transformed_resources = []
327
+ for resource in resources:
328
+ transformed = transform_tags_structure(resource)
329
+ transformed_resources.append(transformed)
330
+
331
+ all_keys = set()
332
+ for resource in transformed_resources:
333
+ flat = flatten_dict_keys(resource)
334
+ all_keys.update(flat.keys())
335
+
336
+ if simplify:
337
+ # Simplify keys for column filtering and basic display
338
+ simplified_keys = set()
339
+ for key in all_keys:
340
+ simplified = simplify_key(key)
341
+ simplified_keys.add(simplified)
342
+ sorted_keys = sorted(list(simplified_keys), key=str.lower)
343
+ else:
344
+ # Return full nested keys for detailed structure display
345
+ sorted_keys = sorted(list(all_keys), key=str.lower)
346
+
347
+ return sorted_keys
348
+
349
+
350
+ def show_keys(service, action):
351
+ """Show all available keys from API response"""
352
+ from .core import execute_aws_call
353
+
354
+ response = execute_aws_call(service, action, session=None)
355
+ resources = flatten_response(response)
356
+ if not resources:
357
+ return "No data to extract keys from."
358
+
359
+ sorted_keys = extract_and_sort_keys(resources)
360
+ return "\n".join(f" {key}" for key in sorted_keys)
361
+
362
+
363
+ def convert_parameter_name(parameter_name):
364
+ """Convert parameter name from camelCase to PascalCase for AWS API compatibility"""
365
+ if not parameter_name:
366
+ return parameter_name
367
+
368
+ return (
369
+ parameter_name[0].upper() + parameter_name[1:]
370
+ if len(parameter_name) > 0
371
+ else parameter_name
372
+ )