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 ADDED
@@ -0,0 +1,7 @@
1
+ """AWS Query Tool - A modular tool for querying AWS APIs with flexible filtering."""
2
+
3
+ from .cli import main
4
+ from .utils import debug_enabled, debug_print
5
+
6
+ __version__ = "1.0.0"
7
+ __all__ = ["main", "debug_print", "debug_enabled"]
awsquery/cli.py ADDED
@@ -0,0 +1,312 @@
1
+ """Command-line interface for AWS Query Tool."""
2
+
3
+ import argparse
4
+ import re
5
+ import sys
6
+
7
+ import argcomplete
8
+ import boto3
9
+
10
+ from .config import apply_default_filters
11
+ from .core import (
12
+ execute_aws_call,
13
+ execute_multi_level_call,
14
+ execute_multi_level_call_with_tracking,
15
+ execute_with_tracking,
16
+ show_keys_from_result,
17
+ )
18
+ from .filters import filter_resources, parse_multi_level_filters_for_mode
19
+ from .formatters import (
20
+ extract_and_sort_keys,
21
+ flatten_response,
22
+ format_json_output,
23
+ format_table_output,
24
+ show_keys,
25
+ )
26
+ from .security import action_to_policy_format, load_security_policy, validate_security
27
+ from .utils import create_session, debug_print, get_aws_services, sanitize_input
28
+
29
+
30
+ def service_completer(prefix, parsed_args, **kwargs):
31
+ """Autocomplete AWS service names"""
32
+ session = boto3.Session()
33
+ services = session.get_available_services()
34
+ return [s for s in services if s.startswith(prefix)]
35
+
36
+
37
+ def determine_column_filters(column_filters, service, action):
38
+ """Determine which column filters to apply - user specified or defaults"""
39
+ if column_filters:
40
+ debug_print(f"Using user-specified column filters: {column_filters}")
41
+ return column_filters
42
+
43
+ # Check for defaults - normalize action name for lookup
44
+ from .utils import normalize_action_name
45
+
46
+ normalized_action = normalize_action_name(action)
47
+ default_columns = apply_default_filters(service, normalized_action)
48
+ if default_columns:
49
+ debug_print(
50
+ f"Applying default column filters for {service}.{normalized_action}: {default_columns}"
51
+ )
52
+ return default_columns
53
+
54
+ debug_print(f"No column filters (user or default) for {service}.{normalized_action}")
55
+ return None
56
+
57
+
58
+ def action_completer(prefix, parsed_args, **kwargs):
59
+ """Autocomplete action names based on selected service"""
60
+ if not parsed_args.service:
61
+ return []
62
+
63
+ try:
64
+ client = boto3.client(parsed_args.service)
65
+ operations = client.meta.service_model.operation_names
66
+
67
+ try:
68
+ allowed_actions = load_security_policy()
69
+ except:
70
+ allowed_actions = set()
71
+ for op in operations:
72
+ if any(op.startswith(prefix) for prefix in ["Describe", "List", "Get"]):
73
+ allowed_actions.add(f"{parsed_args.service}:{op}")
74
+
75
+ cli_operations = []
76
+ for op in operations:
77
+ if not validate_security(parsed_args.service, op, allowed_actions):
78
+ continue
79
+
80
+ kebab_case = re.sub("([a-z0-9])([A-Z])", r"\1-\2", op).lower()
81
+ cli_operations.append(kebab_case)
82
+
83
+ matched_ops = [op for op in cli_operations if op.startswith(prefix)]
84
+ return sorted(list(set(matched_ops)))
85
+ except:
86
+ return []
87
+
88
+
89
+ def main():
90
+ parser = argparse.ArgumentParser(
91
+ description="Query AWS APIs with flexible filtering and automatic parameter resolution",
92
+ formatter_class=argparse.RawDescriptionHelpFormatter,
93
+ epilog="""
94
+ Examples:
95
+ awsquery ec2 describe_instances prod web -- Tags.Name State InstanceId
96
+ awsquery s3 list_buckets backup
97
+ awsquery ec2 describe_instances (shows available keys)
98
+ awsquery cloudformation describe-stack-events prod -- Created -- StackName (multi-level)
99
+ awsquery ec2 describe_instances --keys (show all keys)
100
+ awsquery cloudformation describe-stack-resources workers --keys -- EKS (multi-level keys)
101
+ awsquery ec2 describe_instances --debug (enable debug output)
102
+ awsquery cloudformation describe-stack-resources workers --debug -- EKS (debug multi-level)
103
+ """,
104
+ )
105
+
106
+ parser.add_argument(
107
+ "-j", "--json", action="store_true", help="Output results in JSON format instead of table"
108
+ )
109
+ parser.add_argument(
110
+ "-k", "--keys", action="store_true", help="Show all available keys for the command"
111
+ )
112
+ parser.add_argument("-d", "--debug", action="store_true", help="Enable debug output")
113
+ parser.add_argument("--region", help="AWS region to use for requests")
114
+ parser.add_argument("--profile", help="AWS profile to use for requests")
115
+
116
+ service_arg = parser.add_argument("service", nargs="?", help="AWS service name")
117
+ service_arg.completer = service_completer # type: ignore[attr-defined]
118
+
119
+ action_arg = parser.add_argument("action", nargs="?", help="Service action name")
120
+ action_arg.completer = action_completer # type: ignore[attr-defined]
121
+
122
+ argcomplete.autocomplete(parser)
123
+
124
+ # First pass: parse known args to get service and action
125
+ args, remaining = parser.parse_known_args()
126
+
127
+ # If there are remaining args, check if any are flags that should be parsed
128
+ # This handles cases where flags appear after service/action or after --
129
+ if remaining:
130
+ # Re-parse with the full argument list to catch all flags
131
+ # We need to build a new argv that puts flags before positional args
132
+ reordered_argv = [sys.argv[0]] # Program name
133
+ flags = []
134
+ non_flags = []
135
+
136
+ # Separate flags from non-flags in remaining args
137
+ i = 0
138
+ while i < len(remaining):
139
+ arg = remaining[i]
140
+ if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
141
+ flags.append(arg)
142
+ elif arg in ["--region", "--profile"]:
143
+ # These flags take a value
144
+ flags.append(arg)
145
+ if i + 1 < len(remaining):
146
+ flags.append(remaining[i + 1])
147
+ i += 1
148
+ else:
149
+ non_flags.append(arg)
150
+ i += 1
151
+
152
+ # Add original flags from sys.argv that were already parsed
153
+ for arg in sys.argv[1:]:
154
+ if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
155
+ if arg not in flags:
156
+ reordered_argv.append(arg)
157
+ elif arg == "--region" and args.region:
158
+ reordered_argv.extend(["--region", args.region])
159
+ elif arg == "--profile" and args.profile:
160
+ reordered_argv.extend(["--profile", args.profile])
161
+
162
+ # Add newly found flags
163
+ reordered_argv.extend(flags)
164
+
165
+ # Add service and action
166
+ if args.service:
167
+ reordered_argv.append(args.service)
168
+ if args.action:
169
+ reordered_argv.append(args.action)
170
+
171
+ # Re-parse with reordered arguments
172
+ args, remaining = parser.parse_known_args(reordered_argv[1:])
173
+
174
+ # Remaining should now only be non-flag arguments
175
+ remaining = non_flags
176
+
177
+ # Set debug mode globally
178
+ from . import utils
179
+
180
+ utils.debug_enabled = args.debug
181
+
182
+ # Build the argv for filter parsing (service, action, and remaining arguments)
183
+ filter_argv = []
184
+ if args.service:
185
+ filter_argv.append(args.service)
186
+ if args.action:
187
+ filter_argv.append(args.action)
188
+ # Add the remaining arguments (filters, --, column names, etc.)
189
+ filter_argv.extend(remaining)
190
+
191
+ base_command, resource_filters, value_filters, column_filters = (
192
+ parse_multi_level_filters_for_mode(filter_argv, mode="single")
193
+ )
194
+
195
+ if not args.service or not args.action:
196
+ services = get_aws_services()
197
+ print("Available services:", ", ".join(services))
198
+ sys.exit(0)
199
+
200
+ service = sanitize_input(args.service)
201
+ action = sanitize_input(args.action)
202
+ resource_filters = [sanitize_input(f) for f in resource_filters] if resource_filters else []
203
+ value_filters = [sanitize_input(f) for f in value_filters] if value_filters else []
204
+ column_filters = [sanitize_input(f) for f in column_filters] if column_filters else []
205
+
206
+ allowed_actions = load_security_policy()
207
+
208
+ policy_action = action_to_policy_format(action)
209
+
210
+ debug_print(
211
+ f"DEBUG: Checking security for service='{service}', "
212
+ f"action='{action}', policy_action='{policy_action}'"
213
+ )
214
+ debug_print(f"DEBUG: Policy has {len(allowed_actions)} allowed actions")
215
+
216
+ if not validate_security(service, policy_action, allowed_actions):
217
+ print(f"ERROR: Action {service}:{action} not permitted by security policy", file=sys.stderr)
218
+ sys.exit(1)
219
+ else:
220
+ debug_print(f"DEBUG: Action {service}:{policy_action} IS ALLOWED by security policy")
221
+
222
+ # Create session with region/profile if specified
223
+ session = create_session(region=args.region, profile=args.profile)
224
+ debug_print(f"DEBUG: Created session with region={args.region}, profile={args.profile}")
225
+
226
+ # Determine final column filters (user-specified or defaults)
227
+ final_column_filters = determine_column_filters(column_filters, service, action)
228
+
229
+ if args.keys:
230
+ print(f"Showing all available keys for {service}.{action}:", file=sys.stderr)
231
+
232
+ try:
233
+ # Use tracking to get keys from the last successful request
234
+ call_result = execute_with_tracking(service, action, session=session)
235
+
236
+ # If the initial call failed, try multi-level resolution
237
+ if not call_result.final_success:
238
+ debug_print("Keys mode: Initial call failed, trying multi-level resolution")
239
+ _, multi_resource_filters, multi_value_filters, multi_column_filters = (
240
+ parse_multi_level_filters_for_mode(filter_argv, mode="multi")
241
+ )
242
+ call_result, _ = execute_multi_level_call_with_tracking(
243
+ service,
244
+ action,
245
+ multi_resource_filters,
246
+ multi_value_filters,
247
+ multi_column_filters,
248
+ )
249
+
250
+ result = show_keys_from_result(call_result)
251
+ print(result)
252
+ return
253
+ except Exception as e:
254
+ print(f"Could not retrieve keys: {e}", file=sys.stderr)
255
+ sys.exit(1)
256
+
257
+ try:
258
+ debug_print(f"Using single-level execution first")
259
+ response = execute_aws_call(service, action, session=session)
260
+
261
+ if isinstance(response, dict) and "validation_error" in response:
262
+ debug_print(f"ValidationError detected in single-level call, switching to multi-level")
263
+ _, multi_resource_filters, multi_value_filters, multi_column_filters = (
264
+ parse_multi_level_filters_for_mode(filter_argv, mode="multi")
265
+ )
266
+ debug_print(
267
+ f"Re-parsed filters for multi-level - "
268
+ f"Resource: {multi_resource_filters}, Value: {multi_value_filters}, "
269
+ f"Column: {multi_column_filters}"
270
+ )
271
+ # Apply defaults for multi-level if no user columns specified
272
+ final_multi_column_filters = determine_column_filters(
273
+ multi_column_filters, service, action
274
+ )
275
+ filtered_resources = execute_multi_level_call(
276
+ service,
277
+ action,
278
+ multi_resource_filters,
279
+ multi_value_filters,
280
+ final_multi_column_filters,
281
+ session,
282
+ )
283
+ debug_print(f"Multi-level call completed with {len(filtered_resources)} resources")
284
+ else:
285
+ resources = flatten_response(response)
286
+ debug_print(f"Total resources extracted: {len(resources)}")
287
+
288
+ filtered_resources = filter_resources(resources, value_filters)
289
+
290
+ if final_column_filters:
291
+ for filter_word in final_column_filters:
292
+ debug_print(f"Applying column filter: {filter_word}")
293
+
294
+ if args.keys:
295
+ sorted_keys = extract_and_sort_keys(filtered_resources)
296
+ output = "\n".join(f" {key}" for key in sorted_keys)
297
+ print(f"All available keys:", file=sys.stderr)
298
+ print(output)
299
+ else:
300
+ if args.json:
301
+ output = format_json_output(filtered_resources, final_column_filters)
302
+ else:
303
+ output = format_table_output(filtered_resources, final_column_filters)
304
+ print(output)
305
+
306
+ except KeyboardInterrupt:
307
+ print("\nOperation cancelled by user.", file=sys.stderr)
308
+ sys.exit(1)
309
+
310
+
311
+ if __name__ == "__main__":
312
+ main()
awsquery/config.py ADDED
@@ -0,0 +1,61 @@
1
+ """Configuration management for AWS Query Tool."""
2
+
3
+ import os
4
+ from functools import lru_cache
5
+
6
+ import yaml
7
+
8
+ from .utils import debug_print
9
+
10
+
11
+ @lru_cache(maxsize=1)
12
+ def load_default_filters():
13
+ """Load default filters with caching and error handling"""
14
+ # Load default_filters.yaml from the package directory only
15
+ config_path = os.path.join(os.path.dirname(__file__), "default_filters.yaml")
16
+
17
+ try:
18
+ with open(config_path, "r") as f:
19
+ config = yaml.safe_load(f)
20
+ debug_print(f"Loaded default filters configuration from {config_path}")
21
+ return config
22
+ except FileNotFoundError:
23
+ debug_print(f"Warning: {config_path} not found, no defaults will be applied")
24
+ return {}
25
+ except yaml.YAMLError as e:
26
+ debug_print(f"Warning: Could not parse {config_path}: {e}")
27
+ return {}
28
+ except Exception as e:
29
+ debug_print(f"Warning: Could not load default filters from {config_path}: {e}")
30
+ return {}
31
+
32
+
33
+ def get_default_columns(service, action):
34
+ """Get default columns for service.action combination"""
35
+ config = load_default_filters()
36
+
37
+ service_config = config.get(service.lower(), {})
38
+ action_config = service_config.get(action.lower(), {})
39
+
40
+ columns = action_config.get("columns", [])
41
+ if columns:
42
+ debug_print(f"Found default columns for {service}.{action}: {columns}")
43
+ else:
44
+ debug_print(f"No default columns configured for {service}.{action}")
45
+
46
+ return columns
47
+
48
+
49
+ def apply_default_filters(service, action, user_columns=None):
50
+ """Apply default filters if no user columns specified"""
51
+ if user_columns:
52
+ debug_print("User specified columns, skipping defaults")
53
+ return user_columns
54
+
55
+ defaults = get_default_columns(service, action)
56
+ if defaults:
57
+ debug_print(f"Using default columns for {service}.{action}: {defaults}")
58
+ return defaults
59
+
60
+ debug_print(f"No default columns found for {service}.{action}")
61
+ return None # No filtering applied