awsquery 0.3.1__tar.gz → 0.3.2__tar.gz

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.
Files changed (84) hide show
  1. {awsquery-0.3.1 → awsquery-0.3.2}/PKG-INFO +1 -1
  2. {awsquery-0.3.1 → awsquery-0.3.2}/pyproject.toml +1 -1
  3. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/cli.py +41 -3
  4. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery.egg-info/PKG-INFO +1 -1
  5. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_cli_arg_processing.py +170 -0
  6. {awsquery-0.3.1 → awsquery-0.3.2}/.claude/agents/code-reviewer.md +0 -0
  7. {awsquery-0.3.1 → awsquery-0.3.2}/.claude/agents/makefile-optimizer.md +0 -0
  8. {awsquery-0.3.1 → awsquery-0.3.2}/.claude/agents/python-infra-automator.md +0 -0
  9. {awsquery-0.3.1 → awsquery-0.3.2}/.claude/agents/test-writer.md +0 -0
  10. {awsquery-0.3.1 → awsquery-0.3.2}/.claude/settings.local.json +0 -0
  11. {awsquery-0.3.1 → awsquery-0.3.2}/.coveragerc +0 -0
  12. {awsquery-0.3.1 → awsquery-0.3.2}/.devcontainer/devcontainer.json +0 -0
  13. {awsquery-0.3.1 → awsquery-0.3.2}/.dockerignore +0 -0
  14. {awsquery-0.3.1 → awsquery-0.3.2}/.flake8 +0 -0
  15. {awsquery-0.3.1 → awsquery-0.3.2}/.github/workflows/quality.yml +0 -0
  16. {awsquery-0.3.1 → awsquery-0.3.2}/.github/workflows/release.yml +0 -0
  17. {awsquery-0.3.1 → awsquery-0.3.2}/.github/workflows/test.yml +0 -0
  18. {awsquery-0.3.1 → awsquery-0.3.2}/.gitignore +0 -0
  19. {awsquery-0.3.1 → awsquery-0.3.2}/CLAUDE.md +0 -0
  20. {awsquery-0.3.1 → awsquery-0.3.2}/Dockerfile +0 -0
  21. {awsquery-0.3.1 → awsquery-0.3.2}/Dockerfile.prod +0 -0
  22. {awsquery-0.3.1 → awsquery-0.3.2}/LICENSE +0 -0
  23. {awsquery-0.3.1 → awsquery-0.3.2}/Makefile +0 -0
  24. {awsquery-0.3.1 → awsquery-0.3.2}/README.md +0 -0
  25. {awsquery-0.3.1 → awsquery-0.3.2}/Todo.md +0 -0
  26. {awsquery-0.3.1 → awsquery-0.3.2}/docker-compose.yml +0 -0
  27. {awsquery-0.3.1 → awsquery-0.3.2}/docs/DOCKER.md +0 -0
  28. {awsquery-0.3.1 → awsquery-0.3.2}/docs/mutation-testing.md +0 -0
  29. {awsquery-0.3.1 → awsquery-0.3.2}/policy.json +0 -0
  30. {awsquery-0.3.1 → awsquery-0.3.2}/pytest.ini +0 -0
  31. {awsquery-0.3.1 → awsquery-0.3.2}/requirements.txt +0 -0
  32. {awsquery-0.3.1 → awsquery-0.3.2}/setup.cfg +0 -0
  33. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/__init__.py +0 -0
  34. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/config.py +0 -0
  35. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/core.py +0 -0
  36. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/default_filters.yaml +0 -0
  37. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/filters.py +0 -0
  38. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/formatters.py +0 -0
  39. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/policy.json +0 -0
  40. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/security.py +0 -0
  41. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery/utils.py +0 -0
  42. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery.egg-info/SOURCES.txt +0 -0
  43. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery.egg-info/dependency_links.txt +0 -0
  44. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery.egg-info/entry_points.txt +0 -0
  45. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery.egg-info/requires.txt +0 -0
  46. {awsquery-0.3.1 → awsquery-0.3.2}/src/awsquery.egg-info/top_level.txt +0 -0
  47. {awsquery-0.3.1 → awsquery-0.3.2}/tests/__init__.py +0 -0
  48. {awsquery-0.3.1 → awsquery-0.3.2}/tests/conftest.py +0 -0
  49. {awsquery-0.3.1 → awsquery-0.3.2}/tests/fixtures/__init__.py +0 -0
  50. {awsquery-0.3.1 → awsquery-0.3.2}/tests/fixtures/aws_responses.py +0 -0
  51. {awsquery-0.3.1 → awsquery-0.3.2}/tests/fixtures/policy_samples.py +0 -0
  52. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/__init__.py +0 -0
  53. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/test_autocomplete_resilience.py +0 -0
  54. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/test_end_to_end.py +0 -0
  55. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/test_feature_combinations.py +0 -0
  56. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/test_keys_behavior.py +0 -0
  57. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/test_multi_level.py +0 -0
  58. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/test_multi_level_calls.py +0 -0
  59. {awsquery-0.3.1 → awsquery-0.3.2}/tests/integration/test_session_management.py +0 -0
  60. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/__init__.py +0 -0
  61. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_cli_flags.py +0 -0
  62. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_cli_parser.py +0 -0
  63. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_cli_session_args.py +0 -0
  64. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_column_ordering.py +0 -0
  65. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_config_loading_simple.py +0 -0
  66. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_core.py +0 -0
  67. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_core_inference.py +0 -0
  68. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_core_string_handling.py +0 -0
  69. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_critical_mutations.py +0 -0
  70. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_default_column_filters.py +0 -0
  71. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_filter_edge_cases.py +0 -0
  72. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_filter_implementation.py +0 -0
  73. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_filter_matching.py +0 -0
  74. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_filter_parameter_extraction.py +0 -0
  75. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_filters.py +0 -0
  76. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_filters_pattern_matching.py +0 -0
  77. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_formatter_edge_cases.py +0 -0
  78. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_formatters.py +0 -0
  79. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_formatters_data_processing.py +0 -0
  80. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_keys_display_fix.py +0 -0
  81. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_messaging_enhancements.py +0 -0
  82. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_security.py +0 -0
  83. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_security_edge_cases.py +0 -0
  84. {awsquery-0.3.1 → awsquery-0.3.2}/tests/unit/test_tags_transformation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awsquery
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Advanced CLI tool for querying AWS APIs with flexible filtering and automatic parameter resolution
5
5
  Author-email: Florian Motlik <flo@flomotlik.me>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "awsquery"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Advanced CLI tool for querying AWS APIs with flexible filtering and automatic parameter resolution"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Florian Motlik", email = "flo@flomotlik.me" }]
@@ -7,6 +7,7 @@ import sys
7
7
 
8
8
  import argcomplete
9
9
  import boto3
10
+ from argcomplete import warn
10
11
 
11
12
  from .config import apply_default_filters
12
13
  from .core import (
@@ -201,6 +202,30 @@ def determine_column_filters(column_filters, service, action):
201
202
  return None
202
203
 
203
204
 
205
+ def _enhanced_completion_validator(completion_candidate, current_input):
206
+ """Custom argcomplete validator for enhanced action matching."""
207
+ if not current_input:
208
+ return True
209
+
210
+ current_input_lower = current_input.lower()
211
+ candidate_lower = completion_candidate.lower()
212
+
213
+ # 1. Exact prefix match (highest priority)
214
+ if candidate_lower.startswith(current_input_lower):
215
+ return True
216
+
217
+ # 2. Partial substring match
218
+ if current_input_lower in candidate_lower:
219
+ return True
220
+
221
+ # 3. Split match - all parts must be found as substrings
222
+ parts = [part for part in current_input_lower.split("-") if part] # Filter empty parts
223
+ if len(parts) > 1 and all(part in candidate_lower for part in parts):
224
+ return True
225
+
226
+ return False
227
+
228
+
204
229
  def action_completer(prefix, parsed_args, **kwargs):
205
230
  """Autocomplete action names based on selected service"""
206
231
  if not parsed_args.service:
@@ -240,8 +265,21 @@ def action_completer(prefix, parsed_args, **kwargs):
240
265
  kebab_case = re.sub("([a-z0-9])([A-Z])", r"\1-\2", op).lower()
241
266
  cli_operations.append(kebab_case)
242
267
 
243
- matched_ops = [op for op in cli_operations if op.startswith(prefix)]
244
- return sorted(matched_ops)
268
+ # Return all valid operations - let argcomplete validator handle filtering
269
+ all_operations = sorted(cli_operations)
270
+
271
+ if prefix:
272
+ # Check what will actually match with our validator
273
+ matched = [op for op in all_operations if _enhanced_completion_validator(op, prefix)]
274
+
275
+ # If multiple matches share a common prefix shorter than input, warn user
276
+ if len(matched) > 1:
277
+ common = os.path.commonprefix(matched)
278
+ if common and len(common) <= len(prefix):
279
+ # Show user what matches were found
280
+ warn(f"Matches for '{prefix}': {', '.join(matched)}")
281
+
282
+ return all_operations
245
283
  except Exception:
246
284
  # If we can't get operations, return empty
247
285
  return []
@@ -299,7 +337,7 @@ Examples:
299
337
  ) # pragma: no mutate
300
338
  action_arg.completer = action_completer # type: ignore[attr-defined]
301
339
 
302
- argcomplete.autocomplete(parser)
340
+ argcomplete.autocomplete(parser, validator=_enhanced_completion_validator)
303
341
 
304
342
  # First pass: parse known args to get service and action
305
343
  args, remaining = parser.parse_known_args()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awsquery
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Advanced CLI tool for querying AWS APIs with flexible filtering and automatic parameter resolution
5
5
  Author-email: Florian Motlik <flo@flomotlik.me>
6
6
  License: MIT
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock, patch
7
7
  import pytest
8
8
 
9
9
  from awsquery.cli import (
10
+ _enhanced_completion_validator,
10
11
  _extract_flag_and_value,
11
12
  _extract_flags_from_args,
12
13
  _preserve_parsed_flags,
@@ -409,3 +410,172 @@ class TestActionCompleter:
409
410
 
410
411
  # Should include describe action but not create (fallback behavior)
411
412
  assert "describe-instances" in result
413
+
414
+
415
+ class TestActionCompleterEnhanced:
416
+ """Test enhanced completion validator with partial and split matching."""
417
+
418
+ def test_partial_string_match_middle(self):
419
+ """Test partial string matching in the middle of action name."""
420
+ result = _enhanced_completion_validator("get-caller-identity", "caller")
421
+ assert result is True
422
+
423
+ result = _enhanced_completion_validator("get-session-token", "caller")
424
+ assert result is False
425
+
426
+ def test_partial_string_match_end(self):
427
+ """Test partial string matching at the end of action name."""
428
+ result = _enhanced_completion_validator("get-caller-identity", "identity")
429
+ assert result is True
430
+
431
+ result = _enhanced_completion_validator("get-session-token", "identity")
432
+ assert result is False
433
+
434
+ def test_split_match_basic(self):
435
+ """Test split matching with basic example call-ide -> get-caller-identity."""
436
+ result = _enhanced_completion_validator("get-caller-identity", "call-ide")
437
+ assert result is True
438
+
439
+ result = _enhanced_completion_validator("get-session-token", "call-ide")
440
+ assert result is False
441
+
442
+ def test_split_match_multiple_parts(self):
443
+ """Test split matching with ses-tok -> get-session-token."""
444
+ result = _enhanced_completion_validator("get-session-token", "ses-tok")
445
+ assert result is True
446
+
447
+ result = _enhanced_completion_validator("get-caller-identity", "ses-tok")
448
+ assert result is False
449
+
450
+ def test_split_match_partial_segments(self):
451
+ """Test split matching with partial segments cal-iden -> get-caller-identity."""
452
+ result = _enhanced_completion_validator("get-caller-identity", "cal-iden")
453
+ assert result is True
454
+
455
+ result = _enhanced_completion_validator("get-session-token", "cal-iden")
456
+ assert result is False
457
+
458
+ def test_prefix_matching_priority(self):
459
+ """Test that exact prefix match works."""
460
+ result = _enhanced_completion_validator("get-caller-identity", "get")
461
+ assert result is True
462
+
463
+ result = _enhanced_completion_validator("get-session-token", "get")
464
+ assert result is True
465
+
466
+ result = _enhanced_completion_validator("assume-role", "get")
467
+ assert result is False
468
+
469
+ def test_case_insensitive_matching(self):
470
+ """Test that matching is case insensitive."""
471
+ result = _enhanced_completion_validator("get-caller-identity", "caller")
472
+ assert result is True
473
+
474
+ result = _enhanced_completion_validator("get-caller-identity", "CALLER")
475
+ assert result is True
476
+
477
+ result = _enhanced_completion_validator("get-caller-identity", "CaLLer")
478
+ assert result is True
479
+
480
+ result = _enhanced_completion_validator("GET-CALLER-IDENTITY", "caller")
481
+ assert result is True
482
+
483
+ def test_empty_input_returns_true(self):
484
+ """Test that empty input returns True (shows all options)."""
485
+ result = _enhanced_completion_validator("get-caller-identity", "")
486
+ assert result is True
487
+
488
+ result = _enhanced_completion_validator("any-action", "")
489
+ assert result is True
490
+
491
+ def test_no_matches_returns_false(self):
492
+ """Test that non-matching input returns False."""
493
+ result = _enhanced_completion_validator("get-caller-identity", "xyz123")
494
+ assert result is False
495
+
496
+ result = _enhanced_completion_validator("get-session-token", "nonexistent")
497
+ assert result is False
498
+
499
+ def test_split_match_with_empty_parts(self):
500
+ """Test split matching handles empty parts correctly (e.g., 'call--ide')."""
501
+ result = _enhanced_completion_validator("get-caller-identity", "call--ide")
502
+ assert result is True
503
+
504
+ result = _enhanced_completion_validator("get-session-token", "call--ide")
505
+ assert result is False
506
+
507
+ def test_split_match_requires_multiple_parts(self):
508
+ """Test that split matching only applies when input has multiple parts."""
509
+ # Single part should use substring matching only
510
+ result = _enhanced_completion_validator("get-caller-identity", "caller")
511
+ assert result is True
512
+
513
+ # Multi-part should use split matching
514
+ result = _enhanced_completion_validator("get-caller-identity", "get-caller")
515
+ assert result is True
516
+
517
+ result = _enhanced_completion_validator("get-caller-identity", "caller-identity")
518
+ assert result is True
519
+
520
+ def test_action_completer_returns_all_operations(self):
521
+ """Test that action_completer now returns all operations without filtering."""
522
+ with patch("awsquery.cli.validate_readonly") as mock_validate, patch(
523
+ "botocore.session.Session"
524
+ ) as mock_session_class, patch.dict("os.environ", {}, clear=True):
525
+ mock_session = MagicMock()
526
+ mock_session.get_available_services.return_value = ["sts"]
527
+ mock_service_model = MagicMock()
528
+ mock_service_model.operation_names = ["GetCallerIdentity", "GetSessionToken"]
529
+ mock_session.get_service_model.return_value = mock_service_model
530
+ mock_session_class.return_value = mock_session
531
+ mock_validate.return_value = True
532
+
533
+ parsed_args = argparse.Namespace(service="sts")
534
+
535
+ # Should return all operations regardless of prefix
536
+ result_all = action_completer("", parsed_args)
537
+ result_specific = action_completer("caller", parsed_args)
538
+ result_nonmatch = action_completer("xyz123", parsed_args)
539
+
540
+ # All should return the same complete list
541
+ expected = ["get-caller-identity", "get-session-token"]
542
+ assert result_all == expected
543
+ assert result_specific == expected
544
+ assert result_nonmatch == expected
545
+
546
+ @patch("awsquery.cli.warn")
547
+ @patch("awsquery.cli.validate_readonly")
548
+ @patch("botocore.session.Session")
549
+ @patch.dict("os.environ", {}, clear=True)
550
+ def test_action_completer_warns_on_common_prefix_with_split_matching(
551
+ self, mock_session_class, mock_validate, mock_warn
552
+ ):
553
+ """Test that action_completer warns with split matching and common prefix."""
554
+ mock_session = MagicMock()
555
+ mock_session.get_available_services.return_value = ["elasticache"]
556
+ mock_service_model = MagicMock()
557
+ mock_service_model.operation_names = [
558
+ "DescribeReplicationGroups",
559
+ "DescribeGlobalReplicationGroups",
560
+ ]
561
+ mock_session.get_service_model.return_value = mock_service_model
562
+ mock_session_class.return_value = mock_session
563
+ mock_validate.return_value = True
564
+
565
+ parsed_args = argparse.Namespace(service="elasticache")
566
+
567
+ # This should trigger a warning because:
568
+ # - "repli-group" matches both commands via split matching
569
+ # - Common prefix "describe-" (9 chars) <= input "repli-group" (11 chars)
570
+ result = action_completer("repli-group", parsed_args)
571
+
572
+ # Should return all valid operations
573
+ expected = ["describe-global-replication-groups", "describe-replication-groups"]
574
+ assert result == expected
575
+
576
+ # Should have called warn with the matching operations
577
+ mock_warn.assert_called_once()
578
+ call_args = mock_warn.call_args[0][0]
579
+ assert "repli-group" in call_args
580
+ assert "describe-replication-groups" in call_args
581
+ assert "describe-global-replication-groups" in call_args
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes