diff --git a/MANIFEST.in b/MANIFEST.in index 34fb540e8276..98ea78d55fa6 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,3 +6,4 @@ include .pre-commit-config.yaml recursive-include awscli/examples *.rst *.txt recursive-include awscli/data *.json recursive-include awscli/topics *.rst *.json +prune awsclilinter \ No newline at end of file diff --git a/awsclilinter/.gitignore b/awsclilinter/.gitignore new file mode 100644 index 000000000000..c491fb8c56a9 --- /dev/null +++ b/awsclilinter/.gitignore @@ -0,0 +1,27 @@ +*.py[co] +*.DS_Store + +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +dist/ +build/ +.pytest_cache/ + +# Unit test / coverage reports +.coverage +htmlcov/ + +# Virtualenvs +.venv/ +venv/ +env/ + +# Keep lockfiles +!*.lock + +# Pyenv +.python-version diff --git a/awsclilinter/LICENSE.txt b/awsclilinter/LICENSE.txt new file mode 100644 index 000000000000..3d176f2a1677 --- /dev/null +++ b/awsclilinter/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You +may not use this file except in compliance with the License. A copy of +the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is +distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. diff --git a/awsclilinter/MANIFEST.in b/awsclilinter/MANIFEST.in new file mode 100644 index 000000000000..5de61f385a65 --- /dev/null +++ b/awsclilinter/MANIFEST.in @@ -0,0 +1,4 @@ +include README.md +recursive-include awsclilinter *.py +recursive-exclude tests * +recursive-exclude examples * diff --git a/awsclilinter/README.md b/awsclilinter/README.md new file mode 100644 index 000000000000..a9636356d408 --- /dev/null +++ b/awsclilinter/README.md @@ -0,0 +1,122 @@ +# AWS CLI v1-to-v2 Upgrade Linter + +A CLI tool that lints bash scripts for AWS CLI v1 usage and updates them to avoid breaking +changes introduced in AWS CLI v2. Not all breaking changes can be detected statically, +thus not all of them are supported by this tool. + +For a full list of the breaking changes introduced with AWS CLI v2, see +[Breaking changes between AWS CLI version 1 and AWS CLI version 2](https://docs.aws.amazon.com/cli/latest/userguide/cliv2-migration-changes.html#cliv2-migration-changes-breaking). + +## Installation + +Most users should install AWS CLI Linter via `pip` in a `virtualenv`: + +```shell +$ python3 -m pip install awsclilinter +``` + +or, if you are not installing in a `virtualenv`, to install globally: + +```shell +$ sudo python3 -m pip install awsclilinter +``` + +or for your user: + +```shell +$ python3 -m pip install --user awsclilinter +``` + +If you have the `awsclilinter` package installed and want to upgrade to the latest version, you can run: + +```shell +$ python3 -m pip install --upgrade awsclilinter +``` + +This will install the `awsclilinter` package as well as all dependencies. + +If you want to run `awsclilinter` from source, see the [Installing development versions](#installing-development-versions) section. + +## Usage + +### Dry-run mode (default) +Display issues without modifying the script: +```bash +upgrade-aws-cli --script upload_s3_files.sh +``` + +### Fix mode +Automatically update the input script: +```bash +upgrade-aws-cli --script upload_s3_files.sh --fix +``` + +### Output mode +Create a new fixed script without modifying the original: +```bash +upgrade-aws-cli --script upload_s3_files.sh --output upload_s3_files_v2.sh +``` + +### Interactive mode +Review and accept/reject each change individually: +```bash +upgrade-aws-cli --script upload_s3_files.sh --interactive --output upload_s3_files_v2.sh +``` + +In interactive mode, you can: +- Press `y` to accept the current change +- Press `n` to skip the current change +- Press `u` to accept all remaining changes +- Press `s` to save and exit (applies all accepted changes so far) + +## Development + +### Installing development versions + +If you are interested in using the latest released version of the AWS CLI Linter, please see the [Installation](#installation) section. +This section is for anyone who wants to install the development version of the AWS CLI Linter. You might need to do this if: + +* You are developing a feature for the AWS CLI Linter and plan on submitting a Pull Request. +* You want to test the latest changes of the AWS CLI Linter before they make it into an official release. + +Install [uv](https://docs.astral.sh/uv/) if you haven't already, then set up the development environment: + +```bash +uv sync --extra dev +``` + +This will create a virtual environment, install all dependencies, and install the package in development mode. + +Activate the virtual environment: +```bash +source .venv/bin/activate +``` + +### Running the CLI +```bash +upgrade-aws-cli --script +``` + +### Running tests +```bash +uv run pytest tests/ -v +``` + +### Code formatting +```bash +uv run ruff format awsclilinter tests +uv run ruff check --select I --fix awsclilinter tests +``` + +### Code linting +```bash +uv run ruff format --check awsclilinter tests +uv run ruff check awsclilinter tests +``` + +### Clean local workspace +```bash +rm -rf .venv build dist *.egg-info +find . -type d -name __pycache__ -exec rm -rf {} + +find . -type f -name "*.pyc" -delete +``` diff --git a/awsclilinter/awsclilinter/__init__.py b/awsclilinter/awsclilinter/__init__.py new file mode 100644 index 000000000000..5becc17c04a9 --- /dev/null +++ b/awsclilinter/awsclilinter/__init__.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/awsclilinter/awsclilinter/cli.py b/awsclilinter/awsclilinter/cli.py new file mode 100644 index 000000000000..5bc7d26ac981 --- /dev/null +++ b/awsclilinter/awsclilinter/cli.py @@ -0,0 +1,159 @@ +import argparse +import difflib +import sys +from pathlib import Path +from typing import List + +from awsclilinter.linter import ScriptLinter +from awsclilinter.rules import LintFinding +from awsclilinter.rules.base64_rule import Base64BinaryFormatRule + +# ANSI color codes +RED = "\033[31m" +GREEN = "\033[32m" +CYAN = "\033[36m" +RESET = "\033[0m" + +# The number of lines to show before an after a fix suggestion, for context within the script +CONTEXT_SIZE = 3 + + +def get_user_choice(prompt: str) -> str: + """Get user input for interactive mode.""" + while True: + choice = input(prompt).lower().strip() + if choice in ["y", "n", "u", "s"]: + return choice + print("Invalid choice. Please enter y, n, u, or s.") + + +def display_finding(finding: LintFinding, index: int, total: int, script_content: str): + """Display a finding to the user with context.""" + src_lines = script_content.splitlines(keepends=True) + + # Apply the edit to get the fixed content + fixed_content = ( + script_content[: finding.edit.start_pos] + + finding.edit.inserted_text + + script_content[finding.edit.end_pos :] + ) + dest_lines = fixed_content.splitlines(keepends=True) + + start_line = finding.line_start + end_line = finding.line_end + context_start = max(0, start_line - CONTEXT_SIZE) + context_end = min(len(src_lines), end_line + CONTEXT_SIZE + 1) + + src_context = src_lines[context_start:context_end] + dest_context = dest_lines[context_start:context_end] + + if len(src_context) != len(dest_context): + raise RuntimeError( + f"Original and new context lengths must be equal. " + f"{len(src_context)} != {len(dest_context)}." + ) + + print(f"\n[{index}/{total}] {finding.rule_name}") + print(f"{finding.description}") + + diff = difflib.unified_diff(src_context, dest_context, lineterm="") + for line_num, line in enumerate(diff): + if line_num < 2: + # First 2 lines are the --- and +++ lines, we don't print those. + continue + elif line_num == 2: + # The 3rd line is the context control line. + print(f"\n{CYAN}{line}{RESET}") + elif line.startswith("-"): + # Removed line + print(f"{RED}{line}{RESET}", end="") + elif line.startswith("+"): + # Added line + print(f"{GREEN}{line}{RESET}", end="") + else: + # Context (unchanged) lines always start with whitespace. + print(line, end="") + + +def interactive_mode(findings: List[LintFinding], script_content: str) -> List[LintFinding]: + """Run interactive mode and return accepted findings.""" + accepted = [] + for i, finding in enumerate(findings, 1): + display_finding(finding, i, len(findings), script_content) + choice = get_user_choice("\nApply this fix? [y]es, [n]o, [u]pdate all, [s]ave and exit: ") + + if choice == "y": + accepted.append(finding) + elif choice == "u": + accepted.extend(findings[i - 1 :]) + break + elif choice == "s": + break + + return accepted + + +def main(): + """Main entry point for the CLI tool.""" + parser = argparse.ArgumentParser( + description="Lint and upgrade bash scripts from AWS CLI v1 to v2" + ) + parser.add_argument("--script", required=True, help="Path to the bash script to lint") + parser.add_argument( + "--fix", action="store_true", help="Apply fixes to the script (modifies in place)" + ) + parser.add_argument("--output", help="Output path for the fixed script") + parser.add_argument( + "-i", + "--interactive", + action="store_true", + help="Interactive mode to review each change", + ) + + args = parser.parse_args() + + if args.fix and args.output: + print("Error: Cannot use both --fix and --output") + sys.exit(1) + + if args.fix and args.interactive: + print("Error: Cannot use both --fix and --interactive") + sys.exit(1) + + script_path = Path(args.script) + if not script_path.exists(): + print(f"Error: Script not found: {args.script}") + sys.exit(1) + + script_content = script_path.read_text() + + rules = [Base64BinaryFormatRule()] + linter = ScriptLinter(rules) + findings = linter.lint(script_content) + + if not findings: + print("No issues found.") + return + + if args.interactive: + findings = interactive_mode(findings, script_content) + if not findings: + print("No changes accepted.") + return + + if args.fix or args.output or args.interactive: + # Interactive mode is functionally equivalent to --fix, except the user + # can select a subset of the changes to apply. + fixed_content = linter.apply_fixes(script_content, findings) + output_path = Path(args.output) if args.output else script_path + output_path.write_text(fixed_content) + print(f"Fixed script written to: {output_path}") + else: + print(f"\nFound {len(findings)} issue(s):\n") + for i, finding in enumerate(findings, 1): + display_finding(finding, i, len(findings), script_content) + print("\n\nRun with --fix to apply changes or --interactive to review each change.") + + +if __name__ == "__main__": + main() diff --git a/awsclilinter/awsclilinter/linter.py b/awsclilinter/awsclilinter/linter.py new file mode 100644 index 000000000000..25b7c311e26b --- /dev/null +++ b/awsclilinter/awsclilinter/linter.py @@ -0,0 +1,26 @@ +from typing import List + +from ast_grep_py import SgRoot + +from awsclilinter.rules import LintFinding, LintRule + + +class ScriptLinter: + """Linter for bash scripts to detect AWS CLI v1 to v2 migration issues.""" + + def __init__(self, rules: List[LintRule]): + self.rules = rules + + def lint(self, script_content: str) -> List[LintFinding]: + """Lint the script and return all findings.""" + root = SgRoot(script_content, "bash") + findings = [] + for rule in self.rules: + findings.extend(rule.check(root)) + return sorted(findings, key=lambda f: (f.line_start, f.line_end)) + + def apply_fixes(self, script_content: str, findings: List[LintFinding]) -> str: + """Apply fixes to the script content.""" + root = SgRoot(script_content, "bash") + node = root.root() + return node.commit_edits([f.edit for f in findings]) diff --git a/awsclilinter/awsclilinter/rules/__init__.py b/awsclilinter/awsclilinter/rules/__init__.py new file mode 100644 index 000000000000..fc4132589762 --- /dev/null +++ b/awsclilinter/awsclilinter/rules/__init__.py @@ -0,0 +1,3 @@ +from awsclilinter.rules.base import LintFinding, LintRule + +__all__ = ["LintRule", "LintFinding"] diff --git a/awsclilinter/awsclilinter/rules/base.py b/awsclilinter/awsclilinter/rules/base.py new file mode 100644 index 000000000000..5900a2c09ff8 --- /dev/null +++ b/awsclilinter/awsclilinter/rules/base.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import List + +from ast_grep_py.ast_grep_py import Edit, SgRoot + + +@dataclass +class LintFinding: + """Represents a linting issue found in the script.""" + + line_start: int + line_end: int + edit: Edit + original_text: str + rule_name: str + description: str + + +class LintRule(ABC): + """Base class for all linting rules.""" + + @property + @abstractmethod + def name(self) -> str: + """Return the name of the rule.""" + pass + + @property + @abstractmethod + def description(self) -> str: + """Return a description of what the rule checks.""" + pass + + @abstractmethod + def check(self, root: SgRoot) -> List[LintFinding]: + """Check the AST root for violations and return findings.""" + pass diff --git a/awsclilinter/awsclilinter/rules/base64_rule.py b/awsclilinter/awsclilinter/rules/base64_rule.py new file mode 100644 index 000000000000..6da9ad61fcf8 --- /dev/null +++ b/awsclilinter/awsclilinter/rules/base64_rule.py @@ -0,0 +1,56 @@ +from typing import List + +from ast_grep_py.ast_grep_py import SgRoot + +from awsclilinter.rules import LintFinding, LintRule + + +class Base64BinaryFormatRule(LintRule): + """Detects AWS CLI commands with file:// that need --cli-binary-format. This is a best-effort + attempt at statically detecting the breaking change with how AWS CLI v2 treats binary + parameters.""" + + @property + def name(self) -> str: + return "binary-params-base64" + + @property + def description(self) -> str: + return ( + "In AWS CLI v2, an input parameter typed as binary large object (BLOB) expects " + "the input to be base64-encoded. To retain v1 behavior after upgrading to AWS CLI v2, " + "add `--cli-binary-format raw-in-base64-out`." + ) + + def check(self, root: SgRoot) -> List[LintFinding]: + """Check for AWS CLI commands with file:// missing --cli-binary-format.""" + node = root.root() + base64_broken_nodes = node.find_all( + all=[ + {"kind": "command"}, + {"pattern": "aws $SERVICE $OPERATION $$$ARGS"}, + {"has": {"kind": "word", "regex": r"\Afile://"}}, + {"not": {"has": {"kind": "word", "pattern": "--cli-binary-format"}}}, + ] + ) + + findings = [] + for stmt in base64_broken_nodes: + original = stmt.text() + # To retain v1 behavior after migrating to v2, append + # --cli-binary-format raw-in-base64-out + suggested = original + " --cli-binary-format raw-in-base64-out" + edit = stmt.replace(suggested) + + findings.append( + LintFinding( + line_start=stmt.range().start.line, + line_end=stmt.range().end.line, + edit=edit, + original_text=original, + rule_name=self.name, + description=self.description, + ) + ) + + return findings diff --git a/awsclilinter/examples/upload_s3_files.sh b/awsclilinter/examples/upload_s3_files.sh new file mode 100644 index 000000000000..81a24d17278d --- /dev/null +++ b/awsclilinter/examples/upload_s3_files.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Example script with AWS CLI v1 patterns + +aws secretsmanager put-secret-value --secret-id secret1213 \ + --secret-binary file://data.json + +if + aws kinesis put-record --stream-name samplestream --data file://data --partition-key samplepartitionkey ; then + echo "command succeeded." +fi + +aws s3 ls s3://mybucket diff --git a/awsclilinter/pyproject.toml b/awsclilinter/pyproject.toml new file mode 100644 index 000000000000..3b070b8cfe53 --- /dev/null +++ b/awsclilinter/pyproject.toml @@ -0,0 +1,61 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "awsclilinter" +version = "1.0.0" +description = "CLI tool to lint and upgrade bash scripts from AWS CLI v1 to v2" +readme = "README.md" +requires-python = ">=3.9" +license = "Apache-2.0" +keywords = ["aws", "cli", "linter", "bash", "script", "migration", "v1", "v2"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Utilities", +] +dependencies = [ + "ast-grep-py==0.39.6", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0,<8.2.0", + "ruff>=0.8.0,<0.14.0", +] + +[project.urls] +Homepage = "https://github.com/aws/aws-cli" +"Bug Tracker" = "https://github.com/aws/aws-cli/issues" +Documentation = "https://github.com/aws/aws-cli" +"Source Code" = "https://github.com/aws/aws-cli" + +[project.scripts] +upgrade-aws-cli = "awsclilinter.cli:main" + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] diff --git a/awsclilinter/tests/__init__.py b/awsclilinter/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/awsclilinter/tests/test_cli.py b/awsclilinter/tests/test_cli.py new file mode 100644 index 000000000000..8e507a382008 --- /dev/null +++ b/awsclilinter/tests/test_cli.py @@ -0,0 +1,176 @@ +from unittest.mock import patch + +import pytest + +from awsclilinter.cli import main + + +class TestCLI: + """Test cases for CLI interface.""" + + def test_script_not_found(self, capsys): + """Test error when script file doesn't exist.""" + with patch("sys.argv", ["upgrade-aws-cli", "--script", "nonexistent.sh"]): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 + + def test_fix_and_output_conflict(self, capsys): + """Test error when both --fix and --output are provided.""" + with patch( + "sys.argv", ["upgrade-aws-cli", "--script", "test.sh", "--fix", "--output", "out.sh"] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 + + def test_fix_and_interactive_conflict(self, capsys): + """Test error when both --fix and --interactive are provided.""" + with patch( + "sys.argv", ["upgrade-aws-cli", "--script", "test.sh", "--fix", "--interactive"] + ): + with pytest.raises(SystemExit) as exc_info: + main() + assert exc_info.value.code == 1 + + def test_no_issues_found(self, tmp_path, capsys): + """Test output when no issues are found.""" + script_file = tmp_path / "test.sh" + script_file.write_text("echo 'hello world'") + + with patch("sys.argv", ["upgrade-aws-cli", "--script", str(script_file)]): + main() + captured = capsys.readouterr() + assert "No issues found" in captured.out + + def test_dry_run_mode(self, tmp_path, capsys): + """Test dry run mode displays findings.""" + script_file = tmp_path / "test.sh" + script_file.write_text( + "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json" + ) + + with patch("sys.argv", ["upgrade-aws-cli", "--script", str(script_file)]): + main() + captured = capsys.readouterr() + assert "Found" in captured.out + assert "issue" in captured.out + + def test_fix_mode(self, tmp_path): + """Test fix mode modifies the script.""" + script_file = tmp_path / "test.sh" + script_file.write_text( + "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json" + ) + + with patch("sys.argv", ["upgrade-aws-cli", "--script", str(script_file), "--fix"]): + main() + fixed_content = script_file.read_text() + assert "--cli-binary-format" in fixed_content + + def test_output_mode(self, tmp_path): + """Test output mode creates new file.""" + script_file = tmp_path / "test.sh" + output_file = tmp_path / "output.sh" + script_file.write_text( + "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json" + ) + + with patch( + "sys.argv", + ["upgrade-aws-cli", "--script", str(script_file), "--output", str(output_file)], + ): + main() + assert output_file.exists() + assert "--cli-binary-format" in output_file.read_text() + + def test_interactive_mode_accept_all(self, tmp_path): + """Test interactive mode with 'y' to accept all changes.""" + script_file = tmp_path / "test.sh" + output_file = tmp_path / "output.sh" + script_file.write_text( + "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json\n" + "aws kinesis put-record --stream-name samplestream --data file://data " + "--partition-key samplepartitionkey" + ) + + with patch( + "sys.argv", + [ + "upgrade-aws-cli", + "--script", + str(script_file), + "--interactive", + "--output", + str(output_file), + ], + ): + with patch("builtins.input", side_effect=["y", "y"]): + main() + fixed_content = output_file.read_text() + assert fixed_content.count("--cli-binary-format") == 2 + + def test_interactive_mode_reject_all(self, tmp_path, capsys): + """Test interactive mode with 'n' to reject all changes.""" + script_file = tmp_path / "test.sh" + original = "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json" + script_file.write_text(original) + + with patch("sys.argv", ["upgrade-aws-cli", "--script", str(script_file), "--interactive"]): + with patch("builtins.input", return_value="n"): + main() + captured = capsys.readouterr() + assert "No changes accepted" in captured.out + + def test_interactive_mode_update_all(self, tmp_path): + """Test interactive mode with 'u' to accept remaining changes.""" + script_file = tmp_path / "test.sh" + output_file = tmp_path / "output.sh" + script_file.write_text( + "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json\n" + "aws kinesis put-record --stream-name samplestream --data file://data " + "--partition-key samplepartitionkey" + ) + + with patch( + "sys.argv", + [ + "upgrade-aws-cli", + "--script", + str(script_file), + "--interactive", + "--output", + str(output_file), + ], + ): + with patch("builtins.input", return_value="u"): + main() + fixed_content = output_file.read_text() + assert fixed_content.count("--cli-binary-format") == 2 + + def test_interactive_mode_save_and_exit(self, tmp_path): + """Test interactive mode with 's' to save and exit.""" + script_file = tmp_path / "test.sh" + output_file = tmp_path / "output.sh" + script_file.write_text( + "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json\n" + "aws kinesis put-record --stream-name samplestream --data file://data " + "--partition-key samplepartitionkey" + ) + + with patch( + "sys.argv", + [ + "upgrade-aws-cli", + "--script", + str(script_file), + "--interactive", + "--output", + str(output_file), + ], + ): + with patch("builtins.input", side_effect=["y", "s"]): + main() + fixed_content = output_file.read_text() + # Only first change should be applied since we pressed 's' on the second + assert fixed_content.count("--cli-binary-format") == 1 diff --git a/awsclilinter/tests/test_linter.py b/awsclilinter/tests/test_linter.py new file mode 100644 index 000000000000..1f3afb274259 --- /dev/null +++ b/awsclilinter/tests/test_linter.py @@ -0,0 +1,39 @@ +from awsclilinter.linter import ScriptLinter +from awsclilinter.rules.base64_rule import Base64BinaryFormatRule + + +class TestScriptLinter: + """Test cases for ScriptLinter.""" + + def test_lint_finds_issues(self): + """Test that linter finds issues in script.""" + script = "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json" + linter = ScriptLinter([Base64BinaryFormatRule()]) + findings = linter.lint(script) + + assert len(findings) == 1 + assert findings[0].rule_name == "binary-params-base64" + assert "file://" in findings[0].original_text + + def test_apply_fixes(self): + """Test that fixes are applied correctly.""" + script = "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json" + linter = ScriptLinter([Base64BinaryFormatRule()]) + findings = linter.lint(script) + fixed = linter.apply_fixes(script, findings) + + assert "--cli-binary-format raw-in-base64-out" in fixed + assert "file://data.json" in fixed + + def test_multiple_issues(self): + """Test linter with multiple issues.""" + script = ( + "aws secretsmanager put-secret-value --secret-id secret1213 " + "--secret-binary file://data.json\n" + " aws kinesis put-record --stream-name samplestream " + "--data file://data --partition-key samplepartitionkey" + ) + linter = ScriptLinter([Base64BinaryFormatRule()]) + findings = linter.lint(script) + + assert len(findings) == 2 diff --git a/awsclilinter/tests/test_rules.py b/awsclilinter/tests/test_rules.py new file mode 100644 index 000000000000..dc886b4b6fb0 --- /dev/null +++ b/awsclilinter/tests/test_rules.py @@ -0,0 +1,47 @@ +from ast_grep_py import SgRoot + +from awsclilinter.rules.base64_rule import Base64BinaryFormatRule + + +class TestBase64BinaryFormatRule: + """Test cases for Base64BinaryFormatRule.""" + + def test_rule_properties(self): + """Test rule description.""" + rule = Base64BinaryFormatRule() + assert "cli-binary-format" in rule.description + + def test_detects_missing_flag(self): + """Test detection of missing --cli-binary-format flag.""" + script = "aws secretsmanager put-secret-value --secret-id secret1213 --secret-binary file://data.json" + root = SgRoot(script, "bash") + rule = Base64BinaryFormatRule() + findings = rule.check(root) + + assert len(findings) == 1 + assert "--cli-binary-format" in findings[0].edit.inserted_text + + def test_no_detection_with_flag(self): + """Test no detection when flag is present.""" + script = ( + "aws secretsmanager put-secret-value --secret-id secret1213 " + "--secret-binary file://data.json --cli-binary-format raw-in-base64-out" + ) + root = SgRoot(script, "bash") + rule = Base64BinaryFormatRule() + findings = rule.check(root) + + assert len(findings) == 0 + + def test_no_detection_without_file_protocol(self): + """Test no detection when file:// is not used. Even though the breaking change may + still occur without the use of file://, only the case where file:// is used can be detected + statically.""" + script = ( + "aws secretsmanager put-secret-value --secret-id secret1213 --secret-string secret123" + ) + root = SgRoot(script, "bash") + rule = Base64BinaryFormatRule() + findings = rule.check(root) + + assert len(findings) == 0 diff --git a/awsclilinter/uv.lock b/awsclilinter/uv.lock new file mode 100644 index 000000000000..a87bc73a6c88 --- /dev/null +++ b/awsclilinter/uv.lock @@ -0,0 +1,228 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "ast-grep-py" +version = "0.39.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/d1/542b01bd11990e136ce1a67f4fd7e0e201e792754771594e481d08843307/ast_grep_py-0.39.6.tar.gz", hash = "sha256:931229ec21c3b116f1bffb65fca8f67bddf7b31f3b89d02230ae331e30997768", size = 136502, upload-time = "2025-10-05T04:15:48.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/2e/6e410f976f9731b25d85dbeb68833d0f31a4ddba847305c371692e26afc5/ast_grep_py-0.39.6-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:828dd474c2504fc7544733b8f200245be0d2ae67060f6e3d0fe7c5852d0bf9cf", size = 4814178, upload-time = "2025-10-05T04:14:56.043Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7f/39e577bf63f026320634289baa70607c2ad6021891a6936ec41edbea0b86/ast_grep_py-0.39.6-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:63cce05fa68dd9d626711ed46a775f1f091fc6e075e685471c02d492b2d77a8a", size = 4916531, upload-time = "2025-10-05T04:14:59.223Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/b2c7278499f81e33fd0b37eb864dea661d69b8e435da5e33e8bd4d776cd8/ast_grep_py-0.39.6-cp310-cp310-win32.whl", hash = "sha256:3358b5d26d90cb928951923b4a7ac3973f7d7303d1e372321279023447b1dd33", size = 4532245, upload-time = "2025-10-05T04:15:01.368Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d3/dbe75875f0b2946dc4bf84062128ef1ec602112284bce0414c661322d1ec/ast_grep_py-0.39.6-cp310-cp310-win_amd64.whl", hash = "sha256:c5194c8ec3bf05fc25dda5a1f9d77a47c7ad08f997c1d579bdfbb0bc12afa683", size = 4655916, upload-time = "2025-10-05T04:15:03.421Z" }, + { url = "https://files.pythonhosted.org/packages/a6/36/f615e3fc9dd97ee375ed583934570e0c1b5aff151fd0952b18c935e6c275/ast_grep_py-0.39.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:292a2eb0cd76b0ed39ef512f4489bb9936978ef54b5d601918bc697f263d1357", size = 4848770, upload-time = "2025-10-05T04:15:05.621Z" }, + { url = "https://files.pythonhosted.org/packages/7e/73/a650fab3a19b9aae99a6f60a74a5841ab32a4163c9ff2c66fcf4ec435d1d/ast_grep_py-0.39.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01d3e4a7dfea92ee43ff712843f795a9f9a821c4bd0139fd2ce773851dee263d", size = 4999485, upload-time = "2025-10-05T04:15:07.618Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b0/e605ddd32e7750280caa8529cd1ceb1f38ee2d6e553d836acc94f554aba2/ast_grep_py-0.39.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:fbe02b082474831fc2716cf8e8b312c5da97a992967e50ac5e37f83de385fe18", size = 4814292, upload-time = "2025-10-05T04:15:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/79/9e/54a69b08555e48f7cab13596a3c5ebbb08ce009f3559965421579b6825f3/ast_grep_py-0.39.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4fd970464f63af98e66cceabee836ac0304a612ff060e9461cd026c2193c809f", size = 4917639, upload-time = "2025-10-05T04:15:11.954Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/d13ba0e86d2fa58ef86ebe542f6f77c3b541decf8fde43a1d4281439059a/ast_grep_py-0.39.6-cp311-cp311-win32.whl", hash = "sha256:4380389a83fd7f06fe4c30e6c68ac786b99a8ce513ac0742148d27a1c987c0c0", size = 4532060, upload-time = "2025-10-05T04:15:14.52Z" }, + { url = "https://files.pythonhosted.org/packages/b1/26/ec04132de1d2dc2c3224d57f3d48ed9aec961fd5be284bf3b032ffc1a83b/ast_grep_py-0.39.6-cp311-cp311-win_amd64.whl", hash = "sha256:b70440bfdbc1ed71b26430dd05ee4fc19bb97b43b1d1f0fea8ee073f5a4e1bec", size = 4656022, upload-time = "2025-10-05T04:15:16.484Z" }, + { url = "https://files.pythonhosted.org/packages/9f/fe/09127b2bc6c1981ab346e736f2dd5df17e81d6fed09e43b777e862ea84dc/ast_grep_py-0.39.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3ef4cc66c228654b2fb41e72d90bd5276e1cf947f8300ffc78abd67274136b1b", size = 4848540, upload-time = "2025-10-05T04:15:18.425Z" }, + { url = "https://files.pythonhosted.org/packages/96/e6/d43412548357f6119877690b923ddcf29c836d4d9eb8bca00c87a26b701e/ast_grep_py-0.39.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:65172cf9514f633d5922ba4074cd2e34470ee08abb29e6d8eb4059ac370ec45f", size = 4994646, upload-time = "2025-10-05T04:15:20.327Z" }, + { url = "https://files.pythonhosted.org/packages/b1/37/b9d9e1cba73ec2b512be6414465a29a1751f754e004b78ed3a23d8c8b275/ast_grep_py-0.39.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:678a68502ea4887e3724842429b5adc0da8a719cb4720e2bdd95a1d4f9f208ed", size = 4814179, upload-time = "2025-10-05T04:15:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fe/bd0535b659a1c3328c51e8b5300390a03238248162d75b2d1b49e40c4ca3/ast_grep_py-0.39.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4fc47c76d12f03407616ae60a4e210e4e337fcfd278ad24d6cf17f81cdb2b338", size = 4917932, upload-time = "2025-10-05T04:15:24.51Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0b/af64a8bcd98ccf24aef052c0ec82831546550ea864c94cd43736acb7cde1/ast_grep_py-0.39.6-cp312-cp312-win32.whl", hash = "sha256:2a7fffe7dcc55ea7678628072b511ea0252433748f6f3b19b6f2483f11874e3c", size = 4536390, upload-time = "2025-10-05T04:15:26.782Z" }, + { url = "https://files.pythonhosted.org/packages/76/11/c73fe8f4dfe8701c53124ccf915253e695c22d7a78711bb0105f18a25381/ast_grep_py-0.39.6-cp312-cp312-win_amd64.whl", hash = "sha256:d540136365e95b767cbc2772fd08b8968bd151e1aaaafaa51d91303659367120", size = 4658301, upload-time = "2025-10-05T04:15:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/f0ec566f2bfa356bdca8e2e12c6ee21b86fb8cf340ff386317fc45c6a822/ast_grep_py-0.39.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:cb804e6e048c35c873a396737f616124fb54343d392e87390e3cd515d44d281c", size = 4849750, upload-time = "2025-10-05T04:15:30.488Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e6/f59d92e69569ff6d9fd299c814a58385f3c691178c6c9157ee5591029397/ast_grep_py-0.39.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:46de5659378e2c1f1915629eb4f7e9f2add7b1a69ad774835c263c6e4b61613c", size = 4996290, upload-time = "2025-10-05T04:15:32.675Z" }, + { url = "https://files.pythonhosted.org/packages/0f/89/8f27e86d3e30dd01d349033d02184125e176b8549a30a87eecca219ba2d2/ast_grep_py-0.39.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3f61646794e1c74023e8881ad93fd8b13778bfe5e50b61a4e4f8d3e8802bc914", size = 4814171, upload-time = "2025-10-05T04:15:34.516Z" }, + { url = "https://files.pythonhosted.org/packages/5c/10/5ab318861edb3b6863c8b0d77a0274b26fa6da623f51642f7a074abda18a/ast_grep_py-0.39.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ca8e8cd36aa81f89448cdadf00875e5ac0e14cff0cc11526dd1a1821a35cdae9", size = 4917194, upload-time = "2025-10-05T04:15:36.513Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/62ef817a5536467732fc9f9f492741b3224c72994dff96deef4a9baaf68c/ast_grep_py-0.39.6-cp313-cp313-win32.whl", hash = "sha256:f4227d320719de840ed0bb32a281de3b9d2fa33897dcb3f93d2ae3391affc70e", size = 4539296, upload-time = "2025-10-05T04:15:38.551Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/b02ca991581d831ec0a8cd7d944ed591d5f43dfd743d7d32effda00f6b3d/ast_grep_py-0.39.6-cp313-cp313-win_amd64.whl", hash = "sha256:74a2e7fab3da96e403c7c257652218cbe7e5fc15055842215a91f14d158c9589", size = 4658417, upload-time = "2025-10-05T04:15:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/89/83/56f6bb739e9fb37b34f677744c1ad34872ff6fbbf679715a11e3910c6d38/ast_grep_py-0.39.6-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:e473ef786fb3e12192ef53682f68af634b30716859dbc44764164981be777fcd", size = 4815592, upload-time = "2025-10-05T04:15:41.972Z" }, + { url = "https://files.pythonhosted.org/packages/c7/53/dbe3a1d51abf3fab4e1e56b9fcacf35afcd7cc8469a730a656aa5c7c924b/ast_grep_py-0.39.6-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc18747c0f2614984c855636e2f7430998bd83c952cb964aa115eca096bfcb8b", size = 4919011, upload-time = "2025-10-05T04:15:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/8d/85/ddb9928a8ae7c828cf22e7b7e645fbfd6fa0636270b1cc73bf0fa2e5a76b/ast_grep_py-0.39.6-cp39-cp39-win32.whl", hash = "sha256:a77156ea53c6e6efaf7cfb8cb45f8731b198dc0ef2ea1e5b31b1c92fe281c203", size = 4533582, upload-time = "2025-10-05T04:15:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/89ae9941735d362fb0302d5ade69f26b401e476d1c365fb69fda9d6c6a57/ast_grep_py-0.39.6-cp39-cp39-win_amd64.whl", hash = "sha256:c11245346a78deedb6b7bc65cca6a6c22783f2a33e1e999f3ba7d7bf00d89db8", size = 4656717, upload-time = "2025-10-05T04:15:47.361Z" }, +] + +[[package]] +name = "awsclilinter" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "ast-grep-py" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "ast-grep-py", specifier = "==0.39.6" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0,<8.2.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0,<0.14.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pytest" +version = "8.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/1f/dd3960a2369182720e8cbd580523a4f75292c0c75197dd0254c95f4a0add/pytest-8.1.2.tar.gz", hash = "sha256:f3c45d1d5eed96b01a2aea70dee6a4a366d51d38f9957768083e4fecfc77f3ef", size = 1410060, upload-time = "2024-04-26T18:05:17.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/d7/a812a0bd0f5160823e647524488cc9734f93782f2c273b8f77e8afc60a37/pytest-8.1.2-py3-none-any.whl", hash = "sha256:6c06dc309ff46a05721e6fd48e492a775ed8165d2ecdf57f156a80c7e95bb142", size = 337456, upload-time = "2024-04-26T18:05:15.452Z" }, +] + +[[package]] +name = "ruff" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" }, + { url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" }, + { url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" }, + { url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" }, + { url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" }, + { url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" }, + { url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" }, + { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/setup.py b/setup.py index 300d0084cb93..723d269901e7 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def find_version(*file_paths): scripts=['bin/aws', 'bin/aws.cmd', 'bin/aws_completer', 'bin/aws_zsh_completer.sh', 'bin/aws_bash_completer'], - packages=find_packages(exclude=['tests*']), + packages=find_packages(exclude=['tests*', 'awsclilinter']), include_package_data=True, install_requires=install_requires, extras_require={},