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/__init__.py +7 -0
- awsquery/cli.py +312 -0
- awsquery/config.py +61 -0
- awsquery/core.py +698 -0
- awsquery/default_filters.yaml +76 -0
- awsquery/filters.py +308 -0
- awsquery/formatters.py +372 -0
- awsquery/policy.json +2521 -0
- awsquery/security.py +95 -0
- awsquery/utils.py +115 -0
- awsquery-0.1.0.dist-info/METADATA +447 -0
- awsquery-0.1.0.dist-info/RECORD +16 -0
- awsquery-0.1.0.dist-info/WHEEL +5 -0
- awsquery-0.1.0.dist-info/entry_points.txt +2 -0
- awsquery-0.1.0.dist-info/licenses/LICENSE +21 -0
- awsquery-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
)
|