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/core.py
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
"""Core AWS operations for AWS Query Tool."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
8
|
+
|
|
9
|
+
from .filters import extract_parameter_values, filter_resources
|
|
10
|
+
from .utils import debug_print, get_client, normalize_action_name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CallResult:
|
|
14
|
+
"""Track successful responses throughout call chain"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize CallResult with tracking lists."""
|
|
18
|
+
self.successful_responses = []
|
|
19
|
+
self.final_success = False
|
|
20
|
+
self.last_successful_response = None
|
|
21
|
+
self.error_messages = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def execute_with_tracking(service, action, parameters=None, session=None):
|
|
25
|
+
"""Execute AWS call with tracking for keys mode"""
|
|
26
|
+
result = CallResult()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
response = execute_aws_call(service, action, parameters, session)
|
|
30
|
+
|
|
31
|
+
# Check if response indicates a validation error (for multi-level calls)
|
|
32
|
+
if isinstance(response, dict) and "validation_error" in response:
|
|
33
|
+
result.final_success = False
|
|
34
|
+
result.error_messages.append(f"Validation error: {response['validation_error']}")
|
|
35
|
+
else:
|
|
36
|
+
# Successful response
|
|
37
|
+
result.successful_responses.append(response)
|
|
38
|
+
result.last_successful_response = response
|
|
39
|
+
result.final_success = True
|
|
40
|
+
debug_print(f"Tracking: Successful call to {service}.{action}")
|
|
41
|
+
except Exception as e:
|
|
42
|
+
result.final_success = False
|
|
43
|
+
result.error_messages.append(f"Call failed: {str(e)}")
|
|
44
|
+
debug_print(f"Tracking: Failed call to {service}.{action}: {e}")
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def execute_aws_call(service, action, parameters=None, session=None):
|
|
50
|
+
"""Execute AWS API call with pagination support and optional parameters"""
|
|
51
|
+
normalized_action = normalize_action_name(action)
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
client = get_client(service, session)
|
|
55
|
+
operation = getattr(client, normalized_action, None)
|
|
56
|
+
|
|
57
|
+
if not operation:
|
|
58
|
+
operation = getattr(client, action, None)
|
|
59
|
+
if not operation:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Action {action} (normalized: {normalized_action}) "
|
|
62
|
+
f"not available for service {service}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
call_params = parameters or {}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
paginator = client.get_paginator(normalized_action)
|
|
69
|
+
results = []
|
|
70
|
+
for page in paginator.paginate(**call_params):
|
|
71
|
+
results.append(page)
|
|
72
|
+
return results
|
|
73
|
+
except Exception as e:
|
|
74
|
+
exception_name = type(e).__name__
|
|
75
|
+
debug_print(f"Pagination exception type: {exception_name}, message: {e}")
|
|
76
|
+
|
|
77
|
+
if exception_name == "OperationNotPageableError":
|
|
78
|
+
debug_print(f"Operation not pageable, falling back to direct call")
|
|
79
|
+
return [operation(**call_params)]
|
|
80
|
+
elif exception_name == "ParamValidationError":
|
|
81
|
+
debug_print(f"ParamValidationError detected during pagination, re-raising")
|
|
82
|
+
raise e
|
|
83
|
+
elif isinstance(e, ClientError):
|
|
84
|
+
error_code = e.response.get("Error", {}).get("Code", "")
|
|
85
|
+
if error_code in ["ValidationException", "ValidationError"]:
|
|
86
|
+
debug_print(
|
|
87
|
+
f"ValidationError ({error_code}) detected during pagination, re-raising"
|
|
88
|
+
)
|
|
89
|
+
raise e
|
|
90
|
+
else:
|
|
91
|
+
debug_print(
|
|
92
|
+
f"ClientError ({error_code}) during pagination, falling back to direct call"
|
|
93
|
+
)
|
|
94
|
+
return [operation(**call_params)]
|
|
95
|
+
else:
|
|
96
|
+
debug_print(f"Unknown pagination error, falling back to direct call")
|
|
97
|
+
return [operation(**call_params)]
|
|
98
|
+
|
|
99
|
+
except NoCredentialsError:
|
|
100
|
+
print("ERROR: AWS credentials not found. Configure credentials first.", file=sys.stderr)
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
if type(e).__name__ == "ParamValidationError":
|
|
104
|
+
error_info = parse_validation_error(e)
|
|
105
|
+
if error_info:
|
|
106
|
+
return {"validation_error": error_info, "original_error": e}
|
|
107
|
+
else:
|
|
108
|
+
print(f"ERROR: Could not parse parameter validation error: {e}", file=sys.stderr)
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
if isinstance(e, ClientError):
|
|
112
|
+
error_info = parse_validation_error(e)
|
|
113
|
+
if error_info:
|
|
114
|
+
return {"validation_error": error_info, "original_error": e}
|
|
115
|
+
else:
|
|
116
|
+
print(f"ERROR: AWS API call failed: {e}", file=sys.stderr)
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
print(f"ERROR: Unexpected error: {e}", file=sys.stderr)
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def execute_multi_level_call_with_tracking(
|
|
124
|
+
service, action, resource_filters, value_filters, column_filters, session=None
|
|
125
|
+
):
|
|
126
|
+
"""Handle multi-level API calls with automatic parameter resolution and tracking"""
|
|
127
|
+
debug_print(f"Starting multi-level call with tracking for {service}.{action}")
|
|
128
|
+
debug_print(
|
|
129
|
+
f"Resource filters: {resource_filters}, "
|
|
130
|
+
f"Value filters: {value_filters}, Column filters: {column_filters}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
call_result = CallResult()
|
|
134
|
+
|
|
135
|
+
# First attempt - main call
|
|
136
|
+
try:
|
|
137
|
+
response = execute_aws_call(service, action, parameters=None, session=session)
|
|
138
|
+
|
|
139
|
+
if isinstance(response, dict) and "validation_error" in response:
|
|
140
|
+
call_result.error_messages.append(
|
|
141
|
+
f"Initial call validation error: {response['validation_error']}"
|
|
142
|
+
)
|
|
143
|
+
error_info = response["validation_error"]
|
|
144
|
+
parameter_name = error_info["parameter_name"]
|
|
145
|
+
debug_print(f"Validation error - missing parameter: {parameter_name}")
|
|
146
|
+
else:
|
|
147
|
+
# Initial call succeeded
|
|
148
|
+
call_result.successful_responses.append(response)
|
|
149
|
+
call_result.last_successful_response = response
|
|
150
|
+
call_result.final_success = True
|
|
151
|
+
debug_print(f"Tracking: Initial call to {service}.{action} succeeded")
|
|
152
|
+
|
|
153
|
+
from .formatters import flatten_response
|
|
154
|
+
|
|
155
|
+
resources = flatten_response(response)
|
|
156
|
+
debug_print(f"Final call returned {len(resources)} resources")
|
|
157
|
+
|
|
158
|
+
if value_filters:
|
|
159
|
+
filtered_resources = filter_resources(resources, value_filters)
|
|
160
|
+
debug_print(f"After value filtering: {len(filtered_resources)} resources")
|
|
161
|
+
else:
|
|
162
|
+
filtered_resources = resources
|
|
163
|
+
|
|
164
|
+
return call_result, filtered_resources
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
call_result.error_messages.append(f"Initial call failed: {str(e)}")
|
|
168
|
+
debug_print(f"Initial call failed: {e}")
|
|
169
|
+
# Continue to multi-level resolution
|
|
170
|
+
|
|
171
|
+
# Multi-level resolution needed
|
|
172
|
+
if isinstance(response, dict) and "validation_error" in response:
|
|
173
|
+
error_info = response["validation_error"]
|
|
174
|
+
parameter_name = error_info["parameter_name"]
|
|
175
|
+
debug_print(f"Validation error - missing parameter: {parameter_name}")
|
|
176
|
+
|
|
177
|
+
print(f"Resolving required parameter '{parameter_name}'", file=sys.stderr)
|
|
178
|
+
|
|
179
|
+
possible_operations = infer_list_operation(service, parameter_name, action)
|
|
180
|
+
|
|
181
|
+
list_response = None
|
|
182
|
+
successful_operation = None
|
|
183
|
+
|
|
184
|
+
for operation in possible_operations:
|
|
185
|
+
try:
|
|
186
|
+
debug_print(f"Trying list operation: {operation}")
|
|
187
|
+
print(f"Calling {operation} to find available resources...", file=sys.stderr)
|
|
188
|
+
|
|
189
|
+
list_response = execute_aws_call(service, operation, session=session)
|
|
190
|
+
|
|
191
|
+
if isinstance(list_response, list) and list_response:
|
|
192
|
+
successful_operation = operation
|
|
193
|
+
debug_print(f"Successfully executed: {operation}")
|
|
194
|
+
# Track the successful list operation
|
|
195
|
+
call_result.successful_responses.append(list_response)
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
debug_print(f"Operation {operation} failed: {e}")
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
if not list_response or not successful_operation:
|
|
203
|
+
call_result.error_messages.append(
|
|
204
|
+
f"Could not find working list operation for parameter '{parameter_name}'"
|
|
205
|
+
)
|
|
206
|
+
print(
|
|
207
|
+
f"ERROR: Could not find a working list operation for parameter '{parameter_name}'",
|
|
208
|
+
file=sys.stderr,
|
|
209
|
+
)
|
|
210
|
+
print(f"ERROR: Tried operations: {possible_operations}", file=sys.stderr)
|
|
211
|
+
return call_result, []
|
|
212
|
+
|
|
213
|
+
from .formatters import flatten_response
|
|
214
|
+
|
|
215
|
+
list_resources = flatten_response(list_response)
|
|
216
|
+
debug_print(f"Got {len(list_resources)} resources from {successful_operation}")
|
|
217
|
+
|
|
218
|
+
if resource_filters:
|
|
219
|
+
filtered_list_resources = filter_resources(list_resources, resource_filters)
|
|
220
|
+
debug_print(f"After resource filtering: {len(filtered_list_resources)} resources")
|
|
221
|
+
else:
|
|
222
|
+
filtered_list_resources = list_resources
|
|
223
|
+
|
|
224
|
+
print(f"Found {len(filtered_list_resources)} resources matching filters", file=sys.stderr)
|
|
225
|
+
|
|
226
|
+
if not filtered_list_resources:
|
|
227
|
+
call_result.error_messages.append(
|
|
228
|
+
f"No resources found matching resource filters: {resource_filters}"
|
|
229
|
+
)
|
|
230
|
+
print(
|
|
231
|
+
f"ERROR: No resources found matching resource filters: {resource_filters}",
|
|
232
|
+
file=sys.stderr,
|
|
233
|
+
)
|
|
234
|
+
return call_result, []
|
|
235
|
+
|
|
236
|
+
parameter_values = extract_parameter_values(filtered_list_resources, parameter_name)
|
|
237
|
+
|
|
238
|
+
if not parameter_values:
|
|
239
|
+
call_result.error_messages.append(
|
|
240
|
+
f"Could not extract parameter '{parameter_name}' from filtered results"
|
|
241
|
+
)
|
|
242
|
+
print(
|
|
243
|
+
f"ERROR: Could not extract parameter '{parameter_name}' from filtered results",
|
|
244
|
+
file=sys.stderr,
|
|
245
|
+
)
|
|
246
|
+
return call_result, []
|
|
247
|
+
|
|
248
|
+
expects_list = parameter_expects_list(parameter_name)
|
|
249
|
+
|
|
250
|
+
if expects_list:
|
|
251
|
+
param_value = parameter_values
|
|
252
|
+
else:
|
|
253
|
+
if len(parameter_values) > 1:
|
|
254
|
+
print(f"Multiple {parameter_name} values found matching filters:", file=sys.stderr)
|
|
255
|
+
display_values = parameter_values[:10]
|
|
256
|
+
for value in display_values:
|
|
257
|
+
print(f"- {value}", file=sys.stderr)
|
|
258
|
+
|
|
259
|
+
if len(parameter_values) > 10:
|
|
260
|
+
remaining = len(parameter_values) - 10
|
|
261
|
+
print(
|
|
262
|
+
f"... and {remaining} more "
|
|
263
|
+
f"(showing first 10 of {len(parameter_values)} total)",
|
|
264
|
+
file=sys.stderr,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
print(f"Using first match: {parameter_values[0]}", file=sys.stderr)
|
|
268
|
+
elif len(parameter_values) == 1:
|
|
269
|
+
print(f"Using: {parameter_values[0]}", file=sys.stderr)
|
|
270
|
+
|
|
271
|
+
param_value = parameter_values[0]
|
|
272
|
+
|
|
273
|
+
debug_print(f"Using parameter value(s): {param_value}")
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
client = get_client(service, session)
|
|
277
|
+
converted_parameter_name = get_correct_parameter_name(client, action, parameter_name)
|
|
278
|
+
debug_print(
|
|
279
|
+
f"Parameter name resolution: {parameter_name} -> {converted_parameter_name}"
|
|
280
|
+
)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
debug_print(f"Could not create client for parameter introspection: {e}")
|
|
283
|
+
converted_parameter_name = convert_parameter_name(parameter_name)
|
|
284
|
+
debug_print(
|
|
285
|
+
f"Fallback parameter name conversion: "
|
|
286
|
+
f"{parameter_name} -> {converted_parameter_name}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
parameters = {converted_parameter_name: param_value}
|
|
290
|
+
|
|
291
|
+
# Final call with resolved parameters
|
|
292
|
+
try:
|
|
293
|
+
final_response = execute_aws_call(service, action, parameters, session)
|
|
294
|
+
|
|
295
|
+
if isinstance(final_response, dict) and "validation_error" in final_response:
|
|
296
|
+
call_result.error_messages.append(
|
|
297
|
+
f"Still getting validation error after parameter resolution: "
|
|
298
|
+
f"{final_response['validation_error']}"
|
|
299
|
+
)
|
|
300
|
+
print(
|
|
301
|
+
f"ERROR: Still getting validation error after parameter resolution: "
|
|
302
|
+
f"{final_response['validation_error']}",
|
|
303
|
+
file=sys.stderr,
|
|
304
|
+
)
|
|
305
|
+
return call_result, []
|
|
306
|
+
else:
|
|
307
|
+
# Final call succeeded
|
|
308
|
+
call_result.successful_responses.append(final_response)
|
|
309
|
+
call_result.last_successful_response = final_response
|
|
310
|
+
call_result.final_success = True
|
|
311
|
+
debug_print(f"Tracking: Final call to {service}.{action} succeeded")
|
|
312
|
+
|
|
313
|
+
except Exception as e:
|
|
314
|
+
call_result.error_messages.append(f"Final call failed: {str(e)}")
|
|
315
|
+
debug_print(f"Final call failed: {e}")
|
|
316
|
+
return call_result, []
|
|
317
|
+
|
|
318
|
+
if call_result.last_successful_response:
|
|
319
|
+
from .formatters import flatten_response
|
|
320
|
+
|
|
321
|
+
resources = flatten_response(call_result.last_successful_response)
|
|
322
|
+
debug_print(f"Final call returned {len(resources)} resources")
|
|
323
|
+
|
|
324
|
+
if value_filters:
|
|
325
|
+
filtered_resources = filter_resources(resources, value_filters)
|
|
326
|
+
debug_print(f"After value filtering: {len(filtered_resources)} resources")
|
|
327
|
+
else:
|
|
328
|
+
filtered_resources = resources
|
|
329
|
+
|
|
330
|
+
return call_result, filtered_resources
|
|
331
|
+
else:
|
|
332
|
+
return call_result, []
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def execute_multi_level_call(
|
|
336
|
+
service, action, resource_filters, value_filters, column_filters, session=None
|
|
337
|
+
):
|
|
338
|
+
"""Handle multi-level API calls with automatic parameter resolution"""
|
|
339
|
+
debug_print(f"Starting multi-level call for {service}.{action}")
|
|
340
|
+
debug_print(
|
|
341
|
+
f"Resource filters: {resource_filters}, "
|
|
342
|
+
f"Value filters: {value_filters}, Column filters: {column_filters}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
response = execute_aws_call(service, action, session=session)
|
|
346
|
+
|
|
347
|
+
if isinstance(response, dict) and "validation_error" in response:
|
|
348
|
+
error_info = response["validation_error"]
|
|
349
|
+
parameter_name = error_info["parameter_name"]
|
|
350
|
+
|
|
351
|
+
debug_print(f"Validation error - missing parameter: {parameter_name}")
|
|
352
|
+
|
|
353
|
+
print(f"Resolving required parameter '{parameter_name}'", file=sys.stderr)
|
|
354
|
+
|
|
355
|
+
possible_operations = infer_list_operation(service, parameter_name, action)
|
|
356
|
+
|
|
357
|
+
list_response = None
|
|
358
|
+
successful_operation = None
|
|
359
|
+
|
|
360
|
+
for operation in possible_operations:
|
|
361
|
+
try:
|
|
362
|
+
debug_print(f"Trying list operation: {operation}")
|
|
363
|
+
|
|
364
|
+
print(f"Calling {operation} to find available resources...", file=sys.stderr)
|
|
365
|
+
|
|
366
|
+
list_response = execute_aws_call(service, operation, session=session)
|
|
367
|
+
|
|
368
|
+
if isinstance(list_response, list) and list_response:
|
|
369
|
+
successful_operation = operation
|
|
370
|
+
debug_print(f"Successfully executed: {operation}")
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
debug_print(f"Operation {operation} failed: {e}")
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
if not list_response or not successful_operation:
|
|
378
|
+
print(
|
|
379
|
+
f"ERROR: Could not find a working list operation for parameter '{parameter_name}'",
|
|
380
|
+
file=sys.stderr,
|
|
381
|
+
)
|
|
382
|
+
print(f"ERROR: Tried operations: {possible_operations}", file=sys.stderr)
|
|
383
|
+
sys.exit(1)
|
|
384
|
+
|
|
385
|
+
from .formatters import flatten_response
|
|
386
|
+
|
|
387
|
+
list_resources = flatten_response(list_response)
|
|
388
|
+
debug_print(f"Got {len(list_resources)} resources from {successful_operation}")
|
|
389
|
+
|
|
390
|
+
if resource_filters:
|
|
391
|
+
filtered_list_resources = filter_resources(list_resources, resource_filters)
|
|
392
|
+
debug_print(f"After resource filtering: {len(filtered_list_resources)} resources")
|
|
393
|
+
else:
|
|
394
|
+
filtered_list_resources = list_resources
|
|
395
|
+
|
|
396
|
+
print(f"Found {len(filtered_list_resources)} resources matching filters", file=sys.stderr)
|
|
397
|
+
|
|
398
|
+
if not filtered_list_resources:
|
|
399
|
+
print(
|
|
400
|
+
f"ERROR: No resources found matching resource filters: {resource_filters}",
|
|
401
|
+
file=sys.stderr,
|
|
402
|
+
)
|
|
403
|
+
sys.exit(1)
|
|
404
|
+
|
|
405
|
+
parameter_values = extract_parameter_values(filtered_list_resources, parameter_name)
|
|
406
|
+
|
|
407
|
+
if not parameter_values:
|
|
408
|
+
print(
|
|
409
|
+
f"ERROR: Could not extract parameter '{parameter_name}' from filtered results",
|
|
410
|
+
file=sys.stderr,
|
|
411
|
+
)
|
|
412
|
+
sys.exit(1)
|
|
413
|
+
|
|
414
|
+
expects_list = parameter_expects_list(parameter_name)
|
|
415
|
+
|
|
416
|
+
if expects_list:
|
|
417
|
+
param_value = parameter_values
|
|
418
|
+
else:
|
|
419
|
+
if len(parameter_values) > 1:
|
|
420
|
+
print(f"Multiple {parameter_name} values found matching filters:", file=sys.stderr)
|
|
421
|
+
display_values = parameter_values[:10]
|
|
422
|
+
for value in display_values:
|
|
423
|
+
print(f"- {value}", file=sys.stderr)
|
|
424
|
+
|
|
425
|
+
if len(parameter_values) > 10:
|
|
426
|
+
remaining = len(parameter_values) - 10
|
|
427
|
+
print(
|
|
428
|
+
f"... and {remaining} more "
|
|
429
|
+
f"(showing first 10 of {len(parameter_values)} total)",
|
|
430
|
+
file=sys.stderr,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
print(f"Using first match: {parameter_values[0]}", file=sys.stderr)
|
|
434
|
+
elif len(parameter_values) == 1:
|
|
435
|
+
print(f"Using: {parameter_values[0]}", file=sys.stderr)
|
|
436
|
+
|
|
437
|
+
param_value = parameter_values[0]
|
|
438
|
+
|
|
439
|
+
debug_print(f"Using parameter value(s): {param_value}")
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
client = get_client(service, session)
|
|
443
|
+
converted_parameter_name = get_correct_parameter_name(client, action, parameter_name)
|
|
444
|
+
debug_print(
|
|
445
|
+
f"Parameter name resolution: {parameter_name} -> {converted_parameter_name}"
|
|
446
|
+
)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
debug_print(f"Could not create client for parameter introspection: {e}")
|
|
449
|
+
converted_parameter_name = convert_parameter_name(parameter_name)
|
|
450
|
+
debug_print(
|
|
451
|
+
f"Fallback parameter name conversion: "
|
|
452
|
+
f"{parameter_name} -> {converted_parameter_name}"
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
parameters = {converted_parameter_name: param_value}
|
|
456
|
+
response = execute_aws_call(service, action, parameters, session)
|
|
457
|
+
|
|
458
|
+
if isinstance(response, dict) and "validation_error" in response:
|
|
459
|
+
print(
|
|
460
|
+
f"ERROR: Still getting validation error after parameter resolution: "
|
|
461
|
+
f"{response['validation_error']}",
|
|
462
|
+
file=sys.stderr,
|
|
463
|
+
)
|
|
464
|
+
sys.exit(1)
|
|
465
|
+
|
|
466
|
+
from .formatters import flatten_response
|
|
467
|
+
|
|
468
|
+
resources = flatten_response(response)
|
|
469
|
+
debug_print(f"Final call returned {len(resources)} resources")
|
|
470
|
+
|
|
471
|
+
if value_filters:
|
|
472
|
+
filtered_resources = filter_resources(resources, value_filters)
|
|
473
|
+
debug_print(f"After value filtering: {len(filtered_resources)} resources")
|
|
474
|
+
else:
|
|
475
|
+
filtered_resources = resources
|
|
476
|
+
|
|
477
|
+
return filtered_resources
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def parse_validation_error(error):
|
|
481
|
+
"""Extract missing parameter info from ValidationError"""
|
|
482
|
+
error_message = str(error)
|
|
483
|
+
|
|
484
|
+
pattern = r"Value null at '([^']+)'"
|
|
485
|
+
match = re.search(pattern, error_message)
|
|
486
|
+
|
|
487
|
+
if match:
|
|
488
|
+
parameter_name = match.group(1)
|
|
489
|
+
return {"parameter_name": parameter_name, "is_required": True, "error_type": "null_value"}
|
|
490
|
+
|
|
491
|
+
pattern = r"'([^']+)'[^:]*: Member must not be null"
|
|
492
|
+
match = re.search(pattern, error_message)
|
|
493
|
+
|
|
494
|
+
if match:
|
|
495
|
+
parameter_name = match.group(1)
|
|
496
|
+
return {
|
|
497
|
+
"parameter_name": parameter_name,
|
|
498
|
+
"is_required": True,
|
|
499
|
+
"error_type": "required_parameter",
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
pattern = r"Either (\w+) or \w+ must be specified"
|
|
503
|
+
match = re.search(pattern, error_message)
|
|
504
|
+
|
|
505
|
+
if match:
|
|
506
|
+
parameter_name = match.group(1)
|
|
507
|
+
return {
|
|
508
|
+
"parameter_name": parameter_name,
|
|
509
|
+
"is_required": True,
|
|
510
|
+
"error_type": "either_parameter",
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
pattern = r"Missing required parameter in input: ['\"]([^'\"]+)['\"]"
|
|
514
|
+
match = re.search(pattern, error_message)
|
|
515
|
+
|
|
516
|
+
if match:
|
|
517
|
+
parameter_name = match.group(1)
|
|
518
|
+
return {
|
|
519
|
+
"parameter_name": parameter_name,
|
|
520
|
+
"is_required": True,
|
|
521
|
+
"error_type": "missing_parameter",
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
debug_print(f"Could not parse validation error: {error_message}")
|
|
525
|
+
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def infer_list_operation(service, parameter_name, action):
|
|
530
|
+
"""Infer the list operation from parameter name first, then action name as fallback"""
|
|
531
|
+
|
|
532
|
+
possible_operations = []
|
|
533
|
+
|
|
534
|
+
if parameter_name.lower() not in ["name", "id", "arn"]:
|
|
535
|
+
resource_name = parameter_name
|
|
536
|
+
suffixes_to_remove = ["Name", "Id", "Arn", "ARN"]
|
|
537
|
+
for suffix in suffixes_to_remove:
|
|
538
|
+
if resource_name.endswith(suffix):
|
|
539
|
+
resource_name = resource_name[: -len(suffix)]
|
|
540
|
+
break
|
|
541
|
+
|
|
542
|
+
resource_name = resource_name.lower()
|
|
543
|
+
|
|
544
|
+
if resource_name.endswith("y"):
|
|
545
|
+
plural_name = resource_name[:-1] + "ies"
|
|
546
|
+
elif resource_name.endswith(("s", "sh", "ch", "x", "z")):
|
|
547
|
+
plural_name = resource_name + "es"
|
|
548
|
+
else:
|
|
549
|
+
plural_name = resource_name + "s"
|
|
550
|
+
|
|
551
|
+
possible_operations.extend(
|
|
552
|
+
[
|
|
553
|
+
f"list_{plural_name}",
|
|
554
|
+
f"describe_{plural_name}",
|
|
555
|
+
f"get_{plural_name}",
|
|
556
|
+
f"list_{resource_name}",
|
|
557
|
+
f"describe_{resource_name}",
|
|
558
|
+
f"get_{resource_name}",
|
|
559
|
+
]
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
debug_print(
|
|
563
|
+
f"Parameter-based inference: '{parameter_name}' -> '{resource_name}' -> "
|
|
564
|
+
f"{len(possible_operations)} operations"
|
|
565
|
+
)
|
|
566
|
+
else:
|
|
567
|
+
debug_print(
|
|
568
|
+
f"Parameter '{parameter_name}' is too generic, skipping parameter-based inference"
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
prefixes = ["describe", "get", "update", "delete", "create", "list"]
|
|
572
|
+
action_lower = action.lower().replace("-", "_")
|
|
573
|
+
|
|
574
|
+
action_resource = action_lower
|
|
575
|
+
for prefix in prefixes:
|
|
576
|
+
if action_lower.startswith(prefix + "_"):
|
|
577
|
+
action_resource = action_lower[len(prefix) + 1 :]
|
|
578
|
+
break
|
|
579
|
+
|
|
580
|
+
if action_resource.endswith("s") and len(action_resource) > 1:
|
|
581
|
+
action_plural = action_resource
|
|
582
|
+
elif action_resource.endswith("y"):
|
|
583
|
+
action_plural = action_resource[:-1] + "ies"
|
|
584
|
+
elif action_resource.endswith(("sh", "ch", "x", "z")):
|
|
585
|
+
action_plural = action_resource + "es"
|
|
586
|
+
else:
|
|
587
|
+
action_plural = action_resource + "s"
|
|
588
|
+
|
|
589
|
+
action_operations = [
|
|
590
|
+
f"list_{action_plural}",
|
|
591
|
+
f"describe_{action_plural}",
|
|
592
|
+
f"get_{action_plural}",
|
|
593
|
+
f"list_{action_resource}",
|
|
594
|
+
f"describe_{action_resource}",
|
|
595
|
+
f"get_{action_resource}",
|
|
596
|
+
]
|
|
597
|
+
|
|
598
|
+
for op in action_operations:
|
|
599
|
+
if op not in possible_operations:
|
|
600
|
+
possible_operations.append(op)
|
|
601
|
+
|
|
602
|
+
debug_print(
|
|
603
|
+
f"Action-based inference: '{action}' -> '{action_resource}' -> "
|
|
604
|
+
f"added {len(action_operations)} operations"
|
|
605
|
+
)
|
|
606
|
+
debug_print(f"Total possible operations: {possible_operations}")
|
|
607
|
+
|
|
608
|
+
return possible_operations
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def parameter_expects_list(parameter_name):
|
|
612
|
+
"""Determine if parameter expects list or single value"""
|
|
613
|
+
list_indicators = ["s", "Names", "Ids", "Arns", "ARNs"]
|
|
614
|
+
|
|
615
|
+
for indicator in list_indicators:
|
|
616
|
+
if parameter_name.endswith(indicator):
|
|
617
|
+
return True
|
|
618
|
+
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def convert_parameter_name(parameter_name):
|
|
623
|
+
"""Convert parameter name from camelCase to PascalCase for AWS API compatibility"""
|
|
624
|
+
if not parameter_name:
|
|
625
|
+
return parameter_name
|
|
626
|
+
|
|
627
|
+
return (
|
|
628
|
+
parameter_name[0].upper() + parameter_name[1:]
|
|
629
|
+
if len(parameter_name) > 0
|
|
630
|
+
else parameter_name
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def get_correct_parameter_name(client, action, parameter_name):
|
|
635
|
+
"""Get the correct case-sensitive parameter name for an operation.
|
|
636
|
+
|
|
637
|
+
By introspecting the service model.
|
|
638
|
+
"""
|
|
639
|
+
try:
|
|
640
|
+
action_words = action.replace("-", "_").replace("_", " ").split()
|
|
641
|
+
pascal_case_action = "".join(word.capitalize() for word in action_words)
|
|
642
|
+
|
|
643
|
+
operation_model = client.meta.service_model.operation_model(pascal_case_action)
|
|
644
|
+
|
|
645
|
+
debug_print(f"Introspecting parameter name for {action} (PascalCase: {pascal_case_action})")
|
|
646
|
+
|
|
647
|
+
if operation_model.input_shape:
|
|
648
|
+
members = operation_model.input_shape.members
|
|
649
|
+
debug_print(f"Available parameters: {list(members.keys())}")
|
|
650
|
+
|
|
651
|
+
if parameter_name in members:
|
|
652
|
+
debug_print(f"Found exact match: {parameter_name}")
|
|
653
|
+
return parameter_name
|
|
654
|
+
|
|
655
|
+
for member_name in members:
|
|
656
|
+
if member_name.lower() == parameter_name.lower():
|
|
657
|
+
debug_print(f"Found case-insensitive match: {parameter_name} -> {member_name}")
|
|
658
|
+
return member_name
|
|
659
|
+
|
|
660
|
+
pascal_case = parameter_name[0].upper() + parameter_name[1:]
|
|
661
|
+
if pascal_case in members:
|
|
662
|
+
debug_print(f"Found PascalCase match: {parameter_name} -> {pascal_case}")
|
|
663
|
+
return pascal_case
|
|
664
|
+
|
|
665
|
+
debug_print(
|
|
666
|
+
f"No parameter match found for '{parameter_name}' in {list(members.keys())}"
|
|
667
|
+
)
|
|
668
|
+
else:
|
|
669
|
+
debug_print(f"Operation {pascal_case_action} has no input shape")
|
|
670
|
+
|
|
671
|
+
debug_print(f"Using original parameter name: {parameter_name}")
|
|
672
|
+
return parameter_name
|
|
673
|
+
|
|
674
|
+
except Exception as e:
|
|
675
|
+
debug_print(f"Could not introspect parameter name: {e}")
|
|
676
|
+
fallback = convert_parameter_name(parameter_name)
|
|
677
|
+
debug_print(f"Falling back to PascalCase: {parameter_name} -> {fallback}")
|
|
678
|
+
return fallback
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def show_keys_from_result(call_result):
|
|
682
|
+
"""Show keys only if final call succeeded"""
|
|
683
|
+
if call_result.final_success and call_result.last_successful_response:
|
|
684
|
+
from .formatters import extract_and_sort_keys, flatten_response
|
|
685
|
+
|
|
686
|
+
resources = flatten_response(call_result.last_successful_response)
|
|
687
|
+
if not resources:
|
|
688
|
+
return "Error: No data to extract keys from in successful response"
|
|
689
|
+
|
|
690
|
+
# Use non-simplified keys to show full nested structure
|
|
691
|
+
sorted_keys = extract_and_sort_keys(resources, simplify=False)
|
|
692
|
+
return "\n".join(f" {key}" for key in sorted_keys)
|
|
693
|
+
else:
|
|
694
|
+
if call_result.error_messages:
|
|
695
|
+
error_msg = "; ".join(call_result.error_messages)
|
|
696
|
+
return f"Error: No successful response to show keys from ({error_msg})"
|
|
697
|
+
else:
|
|
698
|
+
return "Error: No successful response to show keys from"
|