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/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"