awsquery 0.2.0__tar.gz → 0.3.1__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 (90) hide show
  1. {awsquery-0.2.0 → awsquery-0.3.1}/.devcontainer/devcontainer.json +16 -15
  2. {awsquery-0.2.0 → awsquery-0.3.1}/.github/workflows/release.yml +20 -0
  3. {awsquery-0.2.0 → awsquery-0.3.1}/.github/workflows/test.yml +38 -5
  4. {awsquery-0.2.0 → awsquery-0.3.1}/Makefile +13 -4
  5. {awsquery-0.2.0 → awsquery-0.3.1}/PKG-INFO +3 -12
  6. {awsquery-0.2.0 → awsquery-0.3.1}/README.md +2 -11
  7. {awsquery-0.2.0 → awsquery-0.3.1}/pyproject.toml +1 -1
  8. awsquery-0.3.1/pytest.ini +10 -0
  9. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/cli.py +49 -42
  10. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/default_filters.yaml +651 -228
  11. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/filters.py +6 -3
  12. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/formatters.py +7 -5
  13. awsquery-0.3.1/src/awsquery/security.py +120 -0
  14. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery.egg-info/PKG-INFO +3 -12
  15. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery.egg-info/SOURCES.txt +1 -1
  16. {awsquery-0.2.0 → awsquery-0.3.1}/tests/conftest.py +4 -4
  17. awsquery-0.3.1/tests/integration/test_autocomplete_resilience.py +363 -0
  18. {awsquery-0.2.0 → awsquery-0.3.1}/tests/integration/test_end_to_end.py +56 -64
  19. {awsquery-0.2.0 → awsquery-0.3.1}/tests/integration/test_feature_combinations.py +16 -27
  20. {awsquery-0.2.0 → awsquery-0.3.1}/tests/integration/test_keys_behavior.py +4 -14
  21. {awsquery-0.2.0 → awsquery-0.3.1}/tests/integration/test_multi_level.py +2 -2
  22. {awsquery-0.2.0 → awsquery-0.3.1}/tests/integration/test_session_management.py +3 -13
  23. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_cli_arg_processing.py +10 -17
  24. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_cli_flags.py +12 -32
  25. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_cli_parser.py +14 -36
  26. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_cli_session_args.py +7 -16
  27. awsquery-0.3.1/tests/unit/test_config_loading_simple.py +55 -0
  28. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_critical_mutations.py +2 -54
  29. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_default_column_filters.py +28 -61
  30. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_filter_matching.py +1 -5
  31. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_keys_display_fix.py +2 -6
  32. awsquery-0.3.1/tests/unit/test_security.py +192 -0
  33. awsquery-0.3.1/tests/unit/test_security_edge_cases.py +214 -0
  34. awsquery-0.2.0/pytest.ini +0 -13
  35. awsquery-0.2.0/src/awsquery/security.py +0 -108
  36. awsquery-0.2.0/tests/integration/test_autocomplete_resilience.py +0 -141
  37. awsquery-0.2.0/tests/unit/test_config_loading.py +0 -375
  38. awsquery-0.2.0/tests/unit/test_security.py +0 -525
  39. awsquery-0.2.0/tests/unit/test_security_edge_cases.py +0 -190
  40. {awsquery-0.2.0 → awsquery-0.3.1}/.claude/agents/code-reviewer.md +0 -0
  41. {awsquery-0.2.0 → awsquery-0.3.1}/.claude/agents/makefile-optimizer.md +0 -0
  42. {awsquery-0.2.0 → awsquery-0.3.1}/.claude/agents/python-infra-automator.md +0 -0
  43. {awsquery-0.2.0 → awsquery-0.3.1}/.claude/agents/test-writer.md +0 -0
  44. {awsquery-0.2.0 → awsquery-0.3.1}/.claude/settings.local.json +0 -0
  45. {awsquery-0.2.0 → awsquery-0.3.1}/.coveragerc +0 -0
  46. {awsquery-0.2.0 → awsquery-0.3.1}/.dockerignore +0 -0
  47. {awsquery-0.2.0 → awsquery-0.3.1}/.flake8 +0 -0
  48. {awsquery-0.2.0 → awsquery-0.3.1}/.github/workflows/quality.yml +0 -0
  49. {awsquery-0.2.0 → awsquery-0.3.1}/.gitignore +0 -0
  50. {awsquery-0.2.0 → awsquery-0.3.1}/CLAUDE.md +0 -0
  51. {awsquery-0.2.0 → awsquery-0.3.1}/Dockerfile +0 -0
  52. {awsquery-0.2.0 → awsquery-0.3.1}/Dockerfile.prod +0 -0
  53. {awsquery-0.2.0 → awsquery-0.3.1}/LICENSE +0 -0
  54. {awsquery-0.2.0 → awsquery-0.3.1}/Todo.md +0 -0
  55. {awsquery-0.2.0 → awsquery-0.3.1}/docker-compose.yml +0 -0
  56. {awsquery-0.2.0 → awsquery-0.3.1}/docs/DOCKER.md +0 -0
  57. {awsquery-0.2.0 → awsquery-0.3.1}/docs/mutation-testing.md +0 -0
  58. {awsquery-0.2.0 → awsquery-0.3.1}/policy.json +0 -0
  59. {awsquery-0.2.0 → awsquery-0.3.1}/requirements.txt +0 -0
  60. {awsquery-0.2.0 → awsquery-0.3.1}/setup.cfg +0 -0
  61. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/__init__.py +0 -0
  62. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/config.py +0 -0
  63. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/core.py +0 -0
  64. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/policy.json +0 -0
  65. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery/utils.py +0 -0
  66. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery.egg-info/dependency_links.txt +0 -0
  67. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery.egg-info/entry_points.txt +0 -0
  68. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery.egg-info/requires.txt +0 -0
  69. {awsquery-0.2.0 → awsquery-0.3.1}/src/awsquery.egg-info/top_level.txt +0 -0
  70. {awsquery-0.2.0 → awsquery-0.3.1}/tests/__init__.py +0 -0
  71. {awsquery-0.2.0 → awsquery-0.3.1}/tests/fixtures/__init__.py +0 -0
  72. {awsquery-0.2.0 → awsquery-0.3.1}/tests/fixtures/aws_responses.py +0 -0
  73. {awsquery-0.2.0 → awsquery-0.3.1}/tests/fixtures/policy_samples.py +0 -0
  74. {awsquery-0.2.0 → awsquery-0.3.1}/tests/integration/__init__.py +0 -0
  75. {awsquery-0.2.0 → awsquery-0.3.1}/tests/integration/test_multi_level_calls.py +0 -0
  76. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/__init__.py +0 -0
  77. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_column_ordering.py +0 -0
  78. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_core.py +0 -0
  79. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_core_inference.py +0 -0
  80. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_core_string_handling.py +0 -0
  81. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_filter_edge_cases.py +0 -0
  82. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_filter_implementation.py +0 -0
  83. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_filter_parameter_extraction.py +0 -0
  84. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_filters.py +0 -0
  85. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_filters_pattern_matching.py +0 -0
  86. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_formatter_edge_cases.py +0 -0
  87. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_formatters.py +0 -0
  88. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_formatters_data_processing.py +0 -0
  89. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_messaging_enhancements.py +0 -0
  90. {awsquery-0.2.0 → awsquery-0.3.1}/tests/unit/test_tags_transformation.py +0 -0
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "AWSQuery Development",
3
3
  "image": "mcr.microsoft.com/devcontainers/python:3.11-bullseye",
4
-
5
4
  "features": {
6
5
  "ghcr.io/anthropics/devcontainer-features/claude-code:1.0.5": {},
7
6
  "ghcr.io/devcontainers/features/aws-cli:1": {
@@ -10,10 +9,8 @@
10
9
  "ghcr.io/devcontainers/features/git:1": {
11
10
  "ppa": true,
12
11
  "version": "latest"
13
- },
14
- "ghcr.io/devcontainers-contrib/features/pre-commit:2": {}
12
+ }
15
13
  },
16
-
17
14
  "customizations": {
18
15
  "vscode": {
19
16
  "extensions": [
@@ -34,30 +31,37 @@
34
31
  "python.linting.enabled": true,
35
32
  "python.linting.pylintEnabled": true,
36
33
  "python.formatting.provider": "black",
37
- "python.formatting.blackArgs": ["--line-length=100"],
34
+ "python.formatting.blackArgs": [
35
+ "--line-length=100"
36
+ ],
38
37
  "python.linting.mypyEnabled": true,
39
38
  "python.testing.pytestEnabled": true,
40
39
  "python.testing.unittestEnabled": false,
41
- "python.testing.pytestArgs": ["tests"],
40
+ "python.testing.pytestArgs": [
41
+ "tests"
42
+ ],
42
43
  "editor.formatOnSave": true,
43
- "editor.rulers": [100],
44
+ "editor.rulers": [
45
+ 100
46
+ ],
44
47
  "[python]": {
45
48
  "editor.defaultFormatter": "ms-python.black-formatter",
46
49
  "editor.codeActionsOnSave": {
47
50
  "source.organizeImports": "explicit"
48
51
  }
49
52
  },
50
- "isort.args": ["--profile", "black", "--line-length", "100"]
53
+ "isort.args": [
54
+ "--profile",
55
+ "black",
56
+ "--line-length",
57
+ "100"
58
+ ]
51
59
  }
52
60
  }
53
61
  },
54
-
55
62
  "postCreateCommand": "pip install -e '.[dev]' && pre-commit install",
56
-
57
63
  "postStartCommand": "echo 'Welcome to AWSQuery development environment!'",
58
-
59
64
  "remoteUser": "vscode",
60
-
61
65
  "mounts": [
62
66
  {
63
67
  "source": "${localEnv:HOME}/.aws",
@@ -66,15 +70,12 @@
66
70
  "consistency": "cached"
67
71
  }
68
72
  ],
69
-
70
73
  "containerEnv": {
71
74
  "AWS_REGION": "${localEnv:AWS_REGION}",
72
75
  "AWS_PROFILE": "${localEnv:AWS_PROFILE}",
73
76
  "AWS_DEFAULT_REGION": "${localEnv:AWS_DEFAULT_REGION}"
74
77
  },
75
-
76
78
  "forwardPorts": [],
77
-
78
79
  "remoteEnv": {
79
80
  "PYTHONPATH": "/workspaces/awsquery/src:${PYTHONPATH}"
80
81
  }
@@ -39,6 +39,26 @@ jobs:
39
39
  python -m pip install --upgrade pip
40
40
  pip install build twine
41
41
 
42
+ - name: Update version in pyproject.toml
43
+ run: |
44
+ # Extract version from tag (remove 'v' prefix if present)
45
+ if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
46
+ VERSION=${GITHUB_REF#refs/tags/v}
47
+ elif [[ "${{ github.event.inputs.version }}" != "" ]]; then
48
+ VERSION="${{ github.event.inputs.version }}"
49
+ else
50
+ echo "No version found"
51
+ exit 1
52
+ fi
53
+ echo "Updating version to $VERSION for build"
54
+
55
+ # Update version using sed
56
+ sed -i "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml
57
+
58
+ # Verify the change
59
+ echo "Updated pyproject.toml:"
60
+ grep "^version =" pyproject.toml
61
+
42
62
  - name: Build distribution
43
63
  run: |
44
64
  python -m build
@@ -38,12 +38,46 @@ jobs:
38
38
  python -m pip install --upgrade pip
39
39
  pip install -e ".[dev]"
40
40
 
41
- - name: Run tests
41
+ - name: Run tests (fast, no coverage)
42
42
  run: |
43
- make test
43
+ # Run tests without coverage for speed on older Python versions
44
+ python -m pytest tests/ -n auto -q
45
+
46
+ - name: Archive test results
47
+ if: failure()
48
+ uses: actions/upload-artifact@v4
49
+ with:
50
+ name: test-results-${{ matrix.python-version }}
51
+ path: |
52
+ .pytest_cache/
53
+
54
+ coverage:
55
+ runs-on: ubuntu-latest
56
+ needs: test # Run after tests pass
57
+ steps:
58
+ - uses: actions/checkout@v5
59
+
60
+ - name: Set up Python 3.12 (latest)
61
+ uses: actions/setup-python@v5
62
+ with:
63
+ python-version: '3.12'
64
+
65
+ - name: Cache pip packages
66
+ uses: actions/cache@v4
67
+ with:
68
+ path: ~/.cache/pip
69
+ key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }}
70
+ restore-keys: |
71
+ ${{ runner.os }}-pip-
72
+
73
+ - name: Install dependencies
74
+ run: |
75
+ python -m pip install --upgrade pip
76
+ pip install -e ".[dev]"
44
77
 
45
78
  - name: Generate coverage report
46
79
  run: |
80
+ # Run with coverage on latest Python where it's fast
47
81
  make coverage
48
82
 
49
83
  - name: Upload coverage to Codecov
@@ -54,11 +88,10 @@ jobs:
54
88
  name: codecov-umbrella
55
89
  fail_ci_if_error: false
56
90
 
57
- - name: Archive test results
58
- if: always()
91
+ - name: Archive coverage results
59
92
  uses: actions/upload-artifact@v4
60
93
  with:
61
- name: test-results-${{ matrix.python-version }}
94
+ name: coverage-report
62
95
  path: |
63
96
  htmlcov/
64
97
  .coverage
@@ -11,10 +11,10 @@ NC := \033[0m
11
11
  # Default target
12
12
  .DEFAULT_GOAL := help
13
13
 
14
- .PHONY: help clean install-dev test test-unit test-integration test-critical test-slow coverage \
15
- coverage-report lint format format-check type-check security-check ci build publish-test \
16
- publish watch-tests version release update-policy validate-policy all ec2-instances s3-buckets iam-users \
17
- iam-roles lambda-functions cloudformation-stacks dynamodb-tables ec2-volumes \
14
+ .PHONY: help clean install-dev test test-unit test-integration test-critical test-slow test-fast test-unit-fast \
15
+ test-integration-fast coverage coverage-report lint format format-check type-check security-check ci build \
16
+ publish-test publish watch-tests version release update-policy validate-policy all ec2-instances s3-buckets \
17
+ iam-users iam-roles lambda-functions cloudformation-stacks dynamodb-tables ec2-volumes \
18
18
  ec2-security-groups s3-bucket-versioning cloudwatch-alarms route53-zones shell \
19
19
  docker-build docker-clean test-in-docker test-awsquery
20
20
 
@@ -48,6 +48,15 @@ test-integration: ## Run integration tests only
48
48
  test-slow: ## Run slow tests
49
49
  python3 -m pytest tests/ -v -m "slow"
50
50
 
51
+ test-fast: ## Run all tests with parallel execution (optimized for Python 3.8-3.10)
52
+ python3 -m pytest tests/ -n auto -q
53
+
54
+ test-unit-fast: ## Run unit tests with parallel execution (2-5 seconds)
55
+ python3 -m pytest tests/unit/ -n auto -q
56
+
57
+ test-integration-fast: ## Run integration tests with parallel execution
58
+ python3 -m pytest tests/integration/ -n auto -q
59
+
51
60
  test-critical: ## Run critical path tests
52
61
  python3 -m pytest tests/ -v -m "critical"
53
62
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: awsquery
3
- Version: 0.2.0
3
+ Version: 0.3.1
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
@@ -112,16 +112,7 @@ pip install -e ".[dev]"
112
112
 
113
113
  awsquery supports tab completion for AWS services and actions through argcomplete.
114
114
 
115
- #### Quick Setup (Automatic)
116
- ```bash
117
- # Run the setup script
118
- ./setup-autocomplete.sh
119
-
120
- # Then reload your shell configuration
121
- source ~/.bashrc # or ~/.zshrc, etc.
122
- ```
123
-
124
- #### Manual Setup
115
+ #### Setup
125
116
 
126
117
  ##### Bash
127
118
  ```bash
@@ -510,4 +501,4 @@ MIT License - see LICENSE file for details.
510
501
 
511
502
  ## Contributing
512
503
 
513
- Please see CONTRIBUTING.md for development guidelines and testing procedures.
504
+ Contributions are welcome! Please ensure tests pass and follow the existing code style.
@@ -49,16 +49,7 @@ pip install -e ".[dev]"
49
49
 
50
50
  awsquery supports tab completion for AWS services and actions through argcomplete.
51
51
 
52
- #### Quick Setup (Automatic)
53
- ```bash
54
- # Run the setup script
55
- ./setup-autocomplete.sh
56
-
57
- # Then reload your shell configuration
58
- source ~/.bashrc # or ~/.zshrc, etc.
59
- ```
60
-
61
- #### Manual Setup
52
+ #### Setup
62
53
 
63
54
  ##### Bash
64
55
  ```bash
@@ -447,4 +438,4 @@ MIT License - see LICENSE file for details.
447
438
 
448
439
  ## Contributing
449
440
 
450
- Please see CONTRIBUTING.md for development guidelines and testing procedures.
441
+ Contributions are welcome! Please ensure tests pass and follow the existing code style.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "awsquery"
7
- version = "0.2.0"
7
+ version = "0.3.1"
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" }]
@@ -0,0 +1,10 @@
1
+ [pytest]
2
+ minversion = 7.0
3
+ testpaths = tests
4
+ python_files = test_*.py
5
+ python_classes = Test*
6
+ python_functions = test_*
7
+ # Coverage disabled by default for Python 3.8-3.10 performance
8
+ # Use: pytest --cov=src/awsquery --cov-report=term-missing
9
+ # Or: make coverage
10
+ addopts = -ra --durations=0 --tb=short
@@ -24,9 +24,18 @@ from .formatters import (
24
24
  format_table_output,
25
25
  show_keys,
26
26
  )
27
- from .security import action_to_policy_format, load_security_policy, validate_security
27
+ from .security import (
28
+ action_to_policy_format,
29
+ get_service_valid_operations,
30
+ is_readonly_operation,
31
+ validate_readonly,
32
+ )
28
33
  from .utils import create_session, debug_print, get_aws_services, sanitize_input
29
34
 
35
+ # CLI flag constants
36
+ SIMPLE_FLAGS = ["-d", "--debug", "-j", "--json", "-k", "--keys", "--allow-unsafe"]
37
+ VALUE_FLAGS = ["--region", "--profile"]
38
+
30
39
 
31
40
  def _extract_flags_from_args(remaining_args):
32
41
  """Extract CLI flags from remaining arguments."""
@@ -35,9 +44,9 @@ def _extract_flags_from_args(remaining_args):
35
44
  i = 0
36
45
  while i < len(remaining_args):
37
46
  arg = remaining_args[i]
38
- if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
47
+ if arg in SIMPLE_FLAGS:
39
48
  flags.append(arg)
40
- elif arg in ["--region", "--profile"]:
49
+ elif arg in VALUE_FLAGS:
41
50
  flags.append(arg)
42
51
  # Check for value after the flag
43
52
  if i + 1 < len(remaining_args):
@@ -60,6 +69,8 @@ def _preserve_parsed_flags(args):
60
69
  flags.append("-j")
61
70
  if getattr(args, "keys", False):
62
71
  flags.append("-k")
72
+ if getattr(args, "allow_unsafe", False):
73
+ flags.append("--allow-unsafe")
63
74
  if getattr(args, "region", None):
64
75
  flags.extend(["--region", args.region])
65
76
  if getattr(args, "profile", None):
@@ -96,7 +107,7 @@ def _extract_flag_and_value(args, i):
96
107
  """Extract a flag and optionally its value from args list."""
97
108
  flags = []
98
109
  flags.append(args[i])
99
- if args[i] in ["--region", "--profile"]:
110
+ if args[i] in VALUE_FLAGS:
100
111
  if i + 1 < len(args) and not args[i + 1].startswith("-") and args[i + 1] != "--":
101
112
  flags.append(args[i + 1])
102
113
  return flags, 2 # consumed 2 args
@@ -110,10 +121,10 @@ def _process_remaining_args_after_separator(remaining):
110
121
  i = 0
111
122
  while i < len(remaining):
112
123
  arg = remaining[i]
113
- if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
124
+ if arg in SIMPLE_FLAGS:
114
125
  flags.append(arg)
115
126
  i += 1
116
- elif arg in ["--region", "--profile"]:
127
+ elif arg in VALUE_FLAGS:
117
128
  extracted, consumed = _extract_flag_and_value(remaining, i)
118
129
  flags.extend(extracted)
119
130
  i += consumed
@@ -130,10 +141,10 @@ def _process_remaining_args(remaining):
130
141
  i = 0
131
142
  while i < len(remaining):
132
143
  arg = remaining[i]
133
- if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
144
+ if arg in SIMPLE_FLAGS:
134
145
  flags.append(arg)
135
146
  i += 1
136
- elif arg in ["--region", "--profile"]:
147
+ elif arg in VALUE_FLAGS:
137
148
  extracted, consumed = _extract_flag_and_value(remaining, i)
138
149
  flags.extend(extracted)
139
150
  i += consumed
@@ -154,10 +165,10 @@ def _build_filter_argv(args, remaining):
154
165
  i = 0
155
166
  while i < len(remaining):
156
167
  arg = remaining[i]
157
- if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
168
+ if arg in SIMPLE_FLAGS:
158
169
  i += 1
159
170
  continue
160
- if arg in ["--region", "--profile"]:
171
+ if arg in VALUE_FLAGS:
161
172
  i += 1
162
173
  if i < len(remaining) and not remaining[i].startswith("-"):
163
174
  i += 1
@@ -195,6 +206,8 @@ def action_completer(prefix, parsed_args, **kwargs):
195
206
  if not parsed_args.service:
196
207
  return []
197
208
 
209
+ service = parsed_args.service
210
+
198
211
  try:
199
212
  # Create a client with minimal configuration to get operation names
200
213
  # This doesn't require valid AWS credentials, just the service model
@@ -206,36 +219,29 @@ def action_completer(prefix, parsed_args, **kwargs):
206
219
  session = botocore.session.Session()
207
220
 
208
221
  # Check if service exists in botocore's data
209
- if parsed_args.service not in session.get_available_services():
222
+ if service not in session.get_available_services():
210
223
  return []
211
224
 
212
225
  # Load the service model to get operations
213
- service_model = session.get_service_model(parsed_args.service)
226
+ service_model = session.get_service_model(service)
214
227
  operations = list(service_model.operation_names)
215
228
  finally:
216
229
  # Restore AWS_PROFILE if it existed
217
230
  if old_profile is not None:
218
231
  os.environ["AWS_PROFILE"] = old_profile
219
232
 
220
- try:
221
- allowed_actions = load_security_policy()
222
- except:
223
- # If policy loading fails, allow common read operations
224
- allowed_actions = set()
225
- for op in operations:
226
- if any(op.startswith(prefix) for prefix in ["Describe", "List", "Get"]):
227
- allowed_actions.add(f"{parsed_args.service}:{op}")
233
+ # Filter operations to only show read-only ones in autocomplete
234
+ valid_operations = get_service_valid_operations(service, operations)
228
235
 
236
+ # Convert to CLI format
229
237
  cli_operations = []
230
238
  for op in operations:
231
- if not validate_security(parsed_args.service, op, allowed_actions):
232
- continue
233
-
234
- kebab_case = re.sub("([a-z0-9])([A-Z])", r"\1-\2", op).lower()
235
- cli_operations.append(kebab_case)
239
+ if op in valid_operations:
240
+ kebab_case = re.sub("([a-z0-9])([A-Z])", r"\1-\2", op).lower()
241
+ cli_operations.append(kebab_case)
236
242
 
237
243
  matched_ops = [op for op in cli_operations if op.startswith(prefix)]
238
- return sorted(list(set(matched_ops)))
244
+ return sorted(matched_ops)
239
245
  except Exception:
240
246
  # If we can't get operations, return empty
241
247
  return []
@@ -277,6 +283,11 @@ Examples:
277
283
  ) # pragma: no mutate
278
284
  parser.add_argument("--region", help="AWS region to use for requests") # pragma: no mutate
279
285
  parser.add_argument("--profile", help="AWS profile to use for requests") # pragma: no mutate
286
+ parser.add_argument(
287
+ "--allow-unsafe",
288
+ action="store_true",
289
+ help="Allow potentially unsafe (non-readonly) operations without prompting",
290
+ ) # pragma: no mutate
280
291
 
281
292
  service_arg = parser.add_argument(
282
293
  "service", nargs="?", help="AWS service name"
@@ -315,6 +326,8 @@ Examples:
315
326
  flags.append("-j")
316
327
  if getattr(args, "keys", False):
317
328
  flags.append("-k")
329
+ if getattr(args, "allow_unsafe", False):
330
+ flags.append("--allow-unsafe")
318
331
  if getattr(args, "region", None):
319
332
  flags.extend(["--region", args.region])
320
333
  if getattr(args, "profile", None):
@@ -373,23 +386,17 @@ Examples:
373
386
  value_filters = [sanitize_input(f) for f in value_filters] if value_filters else []
374
387
  column_filters = [sanitize_input(f) for f in column_filters] if column_filters else []
375
388
 
376
- allowed_actions = load_security_policy()
377
-
378
- policy_action = action_to_policy_format(action)
379
-
380
- debug_print(
381
- f"DEBUG: Checking security for service='{service}', "
382
- f"action='{action}', policy_action='{policy_action}'"
383
- ) # pragma: no mutate
384
- debug_print(f"DEBUG: Policy has {len(allowed_actions)} allowed actions") # pragma: no mutate
385
-
386
- if not validate_security(service, policy_action, allowed_actions):
387
- print(f"ERROR: Action {service}:{action} not permitted by security policy", file=sys.stderr)
389
+ # Validate operation safety (only if we have a non-empty action)
390
+ if (
391
+ action is not None
392
+ and action
393
+ and str(action).strip() != "None"
394
+ and not validate_readonly(service, action, allow_unsafe=args.allow_unsafe)
395
+ ):
396
+ print(f"ERROR: Operation {service}:{action} was not allowed", file=sys.stderr)
388
397
  sys.exit(1)
389
- else:
390
- debug_print(
391
- f"DEBUG: Action {service}:{policy_action} IS ALLOWED by security policy"
392
- ) # pragma: no mutate
398
+
399
+ debug_print(f"DEBUG: Operation {service}:{action} validated successfully") # pragma: no mutate
393
400
 
394
401
  # Create session with region/profile if specified
395
402
  session = create_session(region=args.region, profile=args.profile)