diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/ci.yaml similarity index 56% rename from .github/workflows/run-tests.yaml rename to .github/workflows/ci.yaml index f0fd597..557eae4 100644 --- a/.github/workflows/run-tests.yaml +++ b/.github/workflows/ci.yaml @@ -1,4 +1,4 @@ -name: Run tests on Pull Request +name: ci on: pull_request: @@ -15,5 +15,14 @@ jobs: - name: Checkout code uses: actions/checkout@v3 + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Setup project + run: uv sync --locked --all-extras + + - name: Run check + run: uv run ruff check && uv run ty check + - name: Run tests run: make test \ No newline at end of file diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/docs.yaml similarity index 94% rename from .github/workflows/deploy-docs.yaml rename to .github/workflows/docs.yaml index 048fed7..965320c 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/docs.yaml @@ -1,4 +1,4 @@ -name: Publish docs to GitHub Pages +name: docs on: push: branches: @@ -6,7 +6,7 @@ on: permissions: contents: write jobs: - deploy: + docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/Makefile b/Makefile index d9dc807..f47b78f 100644 --- a/Makefile +++ b/Makefile @@ -14,16 +14,18 @@ check: ## Lint, format, and type-check the code test: ## Run tests in a Docker container @docker compose build 1>/dev/null - @docker compose run --rm test + @docker compose run --rm no-sast + @docker compose run --rm with-sast test-force: ## Run tests in a Docker container while ignoring any stored state @docker volume rm codesectools_pytest-cache 2>&1 1>/dev/null || true @docker compose build 1>/dev/null - @docker compose run --rm test + @docker compose run --rm no-sast + @docker compose run --rm with-sast test-debug: ## Spawn an interactive shell in the test container to debug @docker compose build @docker compose run --rm test /bin/bash -doc-serve: ## Serve the documentation locally +docs-serve: ## Serve the documentation locally @mkdocs serve \ No newline at end of file diff --git a/codesectools/cli.py b/codesectools/cli.py index f949c04..d03e013 100755 --- a/codesectools/cli.py +++ b/codesectools/cli.py @@ -135,7 +135,7 @@ def get_downloadable() -> dict[str, DownloadableRequirement | Dataset]: sast = sast_data["sast"] for req in sast.requirements.all: if isinstance(req, DownloadableRequirement): - if not req.is_fulfilled(): + if not req.is_fulfilled() and req.dependencies_fulfilled(): downloadable[req.name] = req for dataset_name, dataset in DATASETS_ALL.items(): diff --git a/codesectools/sasts/core/cli.py b/codesectools/sasts/core/cli.py index 178af99..c5e9184 100644 --- a/codesectools/sasts/core/cli.py +++ b/codesectools/sasts/core/cli.py @@ -114,7 +114,9 @@ def install() -> None: install_help += ( f"{'❌' if req in missing_reqs else '✅'} [b]{req}[/b]\n" ) - if req.instruction: + if req.depends_on: + install_help += f"- Depends on: [red]{', '.join([str(r) for r in req.depends_on])}[/red]\n" + elif req.instruction: install_help += f"- Instruction: [red]{req.instruction}[/red]\n" if req.url: install_help += f"- URL: [u]{req.url}[/u]\n" diff --git a/codesectools/sasts/core/sast/requirements.py b/codesectools/sasts/core/sast/requirements.py index 9cab7f4..99e1007 100644 --- a/codesectools/sasts/core/sast/requirements.py +++ b/codesectools/sasts/core/sast/requirements.py @@ -3,7 +3,7 @@ import shutil from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Literal +from typing import Any, Literal, Self import requests import typer @@ -21,6 +21,7 @@ class SASTRequirement(ABC): def __init__( self, name: str, + depends_on: list[Self] | None = None, instruction: str | None = None, url: str | None = None, doc: bool = False, @@ -29,12 +30,14 @@ def __init__( Args: name: The name of the requirement. + depends_on: A list of other requirements that must be fulfilled first. instruction: A short instruction on how to download the requirement. url: A URL for more detailed instructions. - doc: A flag indicating if the instruction is available in the documentaton. + doc: A flag indicating if the instruction is available in the documentation. """ self.name = name + self.depends_on = depends_on self.instruction = instruction self.url = url self.doc = doc @@ -44,6 +47,12 @@ def is_fulfilled(self, **kwargs: Any) -> bool: """Check if the requirement is met.""" pass + def dependencies_fulfilled(self) -> bool: + """Check if all dependencies for this requirement are fulfilled.""" + if not self.depends_on: + return True + return all(dependency.is_fulfilled() for dependency in self.depends_on) + def __repr__(self) -> str: """Return a developer-friendly string representation of the requirement.""" return f"{self.__class__.__name__}({self.name})" @@ -55,6 +64,7 @@ class DownloadableRequirement(SASTRequirement): def __init__( self, name: str, + depends_on: list[SASTRequirement] | None = None, instruction: str | None = None, url: str | None = None, doc: bool = False, @@ -65,13 +75,16 @@ def __init__( Args: name: The name of the requirement. + depends_on: A list of other requirements that must be fulfilled first. instruction: A short instruction on how to download the requirement. url: A URL for more detailed instructions. - doc: A flag indicating if the instruction is available in the documentaton. + doc: A flag indicating if the instruction is available in the documentation. """ instruction = f"cstools download {name}" - super().__init__(name, instruction, url, doc) + super().__init__( + name=name, depends_on=depends_on, instruction=instruction, url=url, doc=doc + ) @abstractmethod def download(self, **kwargs: Any) -> None: @@ -85,6 +98,7 @@ class Config(SASTRequirement): def __init__( self, name: str, + depends_on: list[SASTRequirement] | None = None, instruction: str | None = None, url: str | None = None, doc: bool = False, @@ -93,12 +107,15 @@ def __init__( Args: name: The name of the requirement. + depends_on: A list of other requirements that must be fulfilled first. instruction: A short instruction on how to download the requirement. url: A URL for more detailed instructions. - doc: A flag indicating if the instruction is available in the documentaton. + doc: A flag indicating if the instruction is available in the documentation. """ - super().__init__(name, instruction, url, doc) + super().__init__( + name=name, depends_on=depends_on, instruction=instruction, url=url, doc=doc + ) def is_fulfilled(self, sast_name: str, **kwargs: Any) -> bool: """Check if the configuration file exists for the given SAST tool.""" @@ -111,6 +128,7 @@ class Binary(SASTRequirement): def __init__( self, name: str, + depends_on: list[SASTRequirement] | None = None, instruction: str | None = None, url: str | None = None, doc: bool = False, @@ -119,12 +137,15 @@ def __init__( Args: name: The name of the requirement. + depends_on: A list of other requirements that must be fulfilled first. instruction: A short instruction on how to download the requirement. url: A URL for more detailed instructions. - doc: A flag indicating if the instruction is available in the documentaton. + doc: A flag indicating if the instruction is available in the documentation. """ - super().__init__(name, instruction, url, doc) + super().__init__( + name=name, depends_on=depends_on, instruction=instruction, url=url, doc=doc + ) def is_fulfilled(self, **kwargs: Any) -> bool: """Check if the binary is available in the system's PATH.""" @@ -140,6 +161,7 @@ def __init__( repo_url: str, license: str, license_url: str, + depends_on: list[SASTRequirement] | None = None, instruction: str | None = None, url: str | None = None, doc: bool = False, @@ -151,12 +173,15 @@ def __init__( repo_url: The URL of the Git repository to clone. license: The license of the repository. license_url: A URL for the repository's license. + depends_on: A list of other requirements that must be fulfilled first. instruction: A short instruction on how to download the requirement. url: A URL for more detailed instructions. - doc: A flag indicating if the instruction is available in the documentaton. + doc: A flag indicating if the instruction is available in the documentation. """ - super().__init__(name, instruction, url, doc) + super().__init__( + name=name, depends_on=depends_on, instruction=instruction, url=url, doc=doc + ) self.repo_url = repo_url self.license = license self.license_url = license_url @@ -206,6 +231,7 @@ def __init__( file_url: str, license: str, license_url: str, + depends_on: list[SASTRequirement] | None = None, instruction: str | None = None, url: str | None = None, doc: bool = False, @@ -218,12 +244,15 @@ def __init__( file_url: The URL to download the file from. license: The license of the file. license_url: A URL for the file's license. + depends_on: A list of other requirements that must be fulfilled first. instruction: A short instruction on how to download the requirement. url: A URL for more detailed instructions. - doc: A flag indicating if the instruction is available in the documentaton. + doc: A flag indicating if the instruction is available in the documentation. """ - super().__init__(name, instruction, url, doc) + super().__init__( + name=name, depends_on=depends_on, instruction=instruction, url=url, doc=doc + ) self.parent_dir = parent_dir self.file_url = file_url self.license = license diff --git a/codesectools/sasts/tools/SpotBugs/sast.py b/codesectools/sasts/tools/SpotBugs/sast.py index 63ae8dd..d4a28d9 100644 --- a/codesectools/sasts/tools/SpotBugs/sast.py +++ b/codesectools/sasts/tools/SpotBugs/sast.py @@ -40,10 +40,13 @@ class SpotBugsSAST(PrebuiltSAST): properties = SASTProperties(free=True, offline=True) requirements = SASTRequirements( full_reqs=[ - Binary("spotbugs", url="https://github.com/spotbugs/spotbugs"), + binary := Binary("spotbugs", url="https://github.com/spotbugs/spotbugs"), File( name="findsecbugs-plugin-1.14.0.jar", - parent_dir=Path(shutil.which("spotbugs")).parent.parent / "plugin", + depends_on=[binary], + parent_dir=Path(shutil.which("spotbugs")).parent.parent / "plugin" + if shutil.which("spotbugs") + else Path("/tmp"), file_url="https://search.maven.org/remotecontent?filepath=com/h3xstream/findsecbugs/findsecbugs-plugin/1.14.0/findsecbugs-plugin-1.14.0.jar", license="LGPL-3.0", license_url="https://find-sec-bugs.github.io/license.htm", diff --git a/docker-compose.yml b/docker-compose.yml index 31f4b01..a92f2d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,26 @@ services: - test: - image: cstools_test + no-sast: + image: cstools_no-sast build: context: . dockerfile: tests/Dockerfile + target: no-sast + + tty: true + stdin_open: true + + volumes: + - pytest-cache:/app/.pytest_cache + + environment: + _TYPER_STANDARD_TRACEBACK: 1 + + with-sast: + image: cstools_with-sast + build: + context: . + dockerfile: tests/Dockerfile + target: with-sast tty: true stdin_open: true diff --git a/pyproject.toml b/pyproject.toml index 78209e9..972dbe0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "CodeSecTools" -version = "0.10.0" +version = "0.10.1" description = "A framework for code security that provides abstractions for static analysis tools and datasets to support their integration, testing, and evaluation." readme = "README.md" license = "AGPL-3.0-only" diff --git a/requirements.txt b/requirements.txt index 9407aab..e79c7db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -497,9 +497,9 @@ tqdm==4.67.1 \ --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \ --hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2 # via codesectools -typer==0.19.2 \ - --hash=sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9 \ - --hash=sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca +typer==0.20.0 \ + --hash=sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37 \ + --hash=sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a # via codesectools typing-extensions==4.15.0 \ --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ diff --git a/tests/Dockerfile b/tests/Dockerfile index aaa1cca..cced92f 100644 --- a/tests/Dockerfile +++ b/tests/Dockerfile @@ -12,8 +12,8 @@ COPY codesectools /app/codesectools RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-dev --extra test -# =========================== Test stage =========================== -FROM python:3.12-slim-bookworm AS test +# =========================== Base =========================== +FROM python:3.12-slim-bookworm AS test-base RUN apt update -qq && \ DEBIAN_FRONTEND=noninteractive \ @@ -24,6 +24,23 @@ RUN apt update -qq && \ -y -qq --no-install-recommends && \ rm -rf /var/lib/apt/lists/* +# =========================== No SAST Tools =========================== +FROM test-base AS no-sast +ENV TEST_TYPE=no-sast + +# === Run tests === +COPY --from=builder --chown=app:app /app /app +ENV PATH="/app/.venv/bin:$PATH" + +WORKDIR /app +COPY tests /app/tests + +CMD ["pytest"] + +# =========================== With SAST Tools =========================== +FROM test-base AS with-sast +ENV TEST_TYPE=with-sast + # === Free SAST tools only === # Semgrep Community Edition RUN pip install --no-cache semgrep diff --git a/tests/conftest.py b/tests/conftest.py index e5a496e..2475ab3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,9 @@ import pytest +test_type = os.environ.get("TEST_TYPE") +state_file = Path(f".pytest_cache/state_{test_type}.json") + def gen_state() -> dict[str, str]: """Generate a state dictionary of source file paths and their SHA256 hashes. @@ -29,7 +32,6 @@ def source_code_changed() -> bool: Compares the current state with a saved state in '.pytest_cache/state.json'. """ - state_file = Path(".pytest_cache/state.json") if not state_file.is_file(): return True @@ -67,7 +69,6 @@ def pytest_sessionfinish(session: pytest.Session) -> None: """ if session.testscollected > 0 and session.testsfailed == 0: new_state = gen_state() - state_file = Path(".pytest_cache/state.json") state_file.parent.mkdir(exist_ok=True, parents=True) with state_file.open("w") as f: json.dump(new_state, f, indent=2) diff --git a/tests/test_all_sasts.py b/tests/test_all_sasts.py index b2127cc..4d69fbe 100644 --- a/tests/test_all_sasts.py +++ b/tests/test_all_sasts.py @@ -1,6 +1,7 @@ """Test the 'allsast' command integration.""" import logging +import os from pathlib import Path from types import GeneratorType @@ -13,6 +14,11 @@ from codesectools.sasts.all.sast import AllSAST from codesectools.utils import run_command +if os.environ.get("TEST_TYPE") == "no-sast": + pytest.skip( + "Skipping SAST tools testing in no-sast environment", allow_module_level=True + ) + all_sast = AllSAST() diff --git a/tests/test_sasts.py b/tests/test_sasts.py index 83169b9..7608e64 100644 --- a/tests/test_sasts.py +++ b/tests/test_sasts.py @@ -1,6 +1,7 @@ """Test SAST integration functionalities.""" import logging +import os import tempfile from pathlib import Path from types import GeneratorType @@ -18,6 +19,11 @@ from codesectools.sasts import SASTS_ALL from codesectools.utils import run_command +if os.environ.get("TEST_TYPE") == "no-sast": + pytest.skip( + "Skipping SAST tools testing in no-sast environment", allow_module_level=True + ) + @pytest.fixture(autouse=True, scope="module") def update_sast_module_state() -> GeneratorType: diff --git a/uv.lock b/uv.lock index 2d01fa7..b7fdf7a 100644 --- a/uv.lock +++ b/uv.lock @@ -221,7 +221,7 @@ wheels = [ [[package]] name = "codesectools" -version = "0.10.0" +version = "0.10.1" source = { editable = "." } dependencies = [ { name = "gitpython" }, @@ -544,11 +544,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -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" } +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/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { 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]] @@ -957,7 +957,7 @@ wheels = [ [[package]] name = "mkdocs-mermaid2-plugin" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -967,9 +967,9 @@ dependencies = [ { name = "requests" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/d4/efbabe9d04252b3007bc79b0d6db2206b40b74e20619cbed23c1e1d03b2a/mkdocs_mermaid2_plugin-1.2.2.tar.gz", hash = "sha256:20a44440d32cf5fd1811b3e261662adb3e1b98be272e6f6fb9a476f3e28fd507", size = 16209, upload-time = "2025-08-27T23:51:51.078Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/6d/308f443a558b6a97ce55782658174c0d07c414405cfc0a44d36ad37e36f9/mkdocs_mermaid2_plugin-1.2.3.tar.gz", hash = "sha256:fb6f901d53e5191e93db78f93f219cad926ccc4d51e176271ca5161b6cc5368c", size = 16220, upload-time = "2025-10-17T19:38:53.047Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/d5/15f6eeeb755e57a501fad6dcfb3fe406dea5f6a6347a77c3be114294f7bb/mkdocs_mermaid2_plugin-1.2.2-py3-none-any.whl", hash = "sha256:a003dddd6346ecc0ad530f48f577fe6f8b21ea23fbee09eabf0172bbc1f23df8", size = 17300, upload-time = "2025-08-27T23:51:49.988Z" }, + { url = "https://files.pythonhosted.org/packages/1a/4b/6fd6dd632019b7f522f1b1f794ab6115cd79890330986614be56fd18f0eb/mkdocs_mermaid2_plugin-1.2.3-py3-none-any.whl", hash = "sha256:33f60c582be623ed53829a96e19284fc7f1b74a1dbae78d4d2e47fe00c3e190d", size = 17299, upload-time = "2025-10-17T19:38:51.874Z" }, ] [[package]] @@ -1387,28 +1387,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071, upload-time = "2025-10-07T18:21:55.763Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532, upload-time = "2025-10-07T18:21:00.373Z" }, - { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768, upload-time = "2025-10-07T18:21:04.73Z" }, - { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376, upload-time = "2025-10-07T18:21:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055, upload-time = "2025-10-07T18:21:10.72Z" }, - { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544, upload-time = "2025-10-07T18:21:13.741Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280, upload-time = "2025-10-07T18:21:16.411Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286, upload-time = "2025-10-07T18:21:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506, upload-time = "2025-10-07T18:21:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384, upload-time = "2025-10-07T18:21:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976, upload-time = "2025-10-07T18:21:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850, upload-time = "2025-10-07T18:21:31.842Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825, upload-time = "2025-10-07T18:21:35.074Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599, upload-time = "2025-10-07T18:21:38.08Z" }, - { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828, upload-time = "2025-10-07T18:21:41.216Z" }, - { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617, upload-time = "2025-10-07T18:21:44.04Z" }, - { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872, upload-time = "2025-10-07T18:21:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628, upload-time = "2025-10-07T18:21:50.318Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" }, +version = "0.14.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/34/8218a19b2055b80601e8fd201ec723c74c7fe1ca06d525a43ed07b6d8e85/ruff-0.14.2.tar.gz", hash = "sha256:98da787668f239313d9c902ca7c523fe11b8ec3f39345553a51b25abc4629c96", size = 5539663, upload-time = "2025-10-23T19:37:00.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/dd/23eb2db5ad9acae7c845700493b72d3ae214dce0b226f27df89216110f2b/ruff-0.14.2-py3-none-linux_armv6l.whl", hash = "sha256:7cbe4e593505bdec5884c2d0a4d791a90301bc23e49a6b1eb642dd85ef9c64f1", size = 12533390, upload-time = "2025-10-23T19:36:18.044Z" }, + { url = "https://files.pythonhosted.org/packages/5a/8c/5f9acff43ddcf3f85130d0146d0477e28ccecc495f9f684f8f7119b74c0d/ruff-0.14.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8d54b561729cee92f8d89c316ad7a3f9705533f5903b042399b6ae0ddfc62e11", size = 12887187, upload-time = "2025-10-23T19:36:22.664Z" }, + { url = "https://files.pythonhosted.org/packages/99/fa/047646491479074029665022e9f3dc6f0515797f40a4b6014ea8474c539d/ruff-0.14.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5c8753dfa44ebb2cde10ce5b4d2ef55a41fb9d9b16732a2c5df64620dbda44a3", size = 11925177, upload-time = "2025-10-23T19:36:24.778Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/c44cf7fe6e59ab24a9d939493a11030b503bdc2a16622cede8b7b1df0114/ruff-0.14.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d0bbeffb8d9f4fccf7b5198d566d0bad99a9cb622f1fc3467af96cb8773c9e3", size = 12358285, upload-time = "2025-10-23T19:36:26.979Z" }, + { url = "https://files.pythonhosted.org/packages/45/01/47701b26254267ef40369aea3acb62a7b23e921c27372d127e0f3af48092/ruff-0.14.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7047f0c5a713a401e43a88d36843d9c83a19c584e63d664474675620aaa634a8", size = 12303832, upload-time = "2025-10-23T19:36:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5c/ae7244ca4fbdf2bee9d6405dcd5bc6ae51ee1df66eb7a9884b77b8af856d/ruff-0.14.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bf8d2f9aa1602599217d82e8e0af7fd33e5878c4d98f37906b7c93f46f9a839", size = 13036995, upload-time = "2025-10-23T19:36:31.861Z" }, + { url = "https://files.pythonhosted.org/packages/27/4c/0860a79ce6fd4c709ac01173f76f929d53f59748d0dcdd662519835dae43/ruff-0.14.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1c505b389e19c57a317cf4b42db824e2fca96ffb3d86766c1c9f8b96d32048a7", size = 14512649, upload-time = "2025-10-23T19:36:33.915Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7f/d365de998069720a3abfc250ddd876fc4b81a403a766c74ff9bde15b5378/ruff-0.14.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a307fc45ebd887b3f26b36d9326bb70bf69b01561950cdcc6c0bdf7bb8e0f7cc", size = 14088182, upload-time = "2025-10-23T19:36:36.983Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ea/d8e3e6b209162000a7be1faa41b0a0c16a133010311edc3329753cc6596a/ruff-0.14.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61ae91a32c853172f832c2f40bd05fd69f491db7289fb85a9b941ebdd549781a", size = 13599516, upload-time = "2025-10-23T19:36:39.208Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ea/c7810322086db68989fb20a8d5221dd3b79e49e396b01badca07b433ab45/ruff-0.14.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1967e40286f63ee23c615e8e7e98098dedc7301568bd88991f6e544d8ae096", size = 13272690, upload-time = "2025-10-23T19:36:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/39/10b05acf8c45786ef501d454e00937e1b97964f846bf28883d1f9619928a/ruff-0.14.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2877f02119cdebf52a632d743a2e302dea422bfae152ebe2f193d3285a3a65df", size = 13496497, upload-time = "2025-10-23T19:36:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/59/a1/1f25f8301e13751c30895092485fada29076e5e14264bdacc37202e85d24/ruff-0.14.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e681c5bc777de5af898decdcb6ba3321d0d466f4cb43c3e7cc2c3b4e7b843a05", size = 12266116, upload-time = "2025-10-23T19:36:45.625Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/0029bfc9ce16ae78164e6923ef392e5f173b793b26cc39aa1d8b366cf9dc/ruff-0.14.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e21be42d72e224736f0c992cdb9959a2fa53c7e943b97ef5d081e13170e3ffc5", size = 12281345, upload-time = "2025-10-23T19:36:47.618Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ab/ece7baa3c0f29b7683be868c024f0838770c16607bea6852e46b202f1ff6/ruff-0.14.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b8264016f6f209fac16262882dbebf3f8be1629777cf0f37e7aff071b3e9b92e", size = 12629296, upload-time = "2025-10-23T19:36:49.789Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7f/638f54b43f3d4e48c6a68062794e5b367ddac778051806b9e235dfb7aa81/ruff-0.14.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ca36b4cb4db3067a3b24444463ceea5565ea78b95fe9a07ca7cb7fd16948770", size = 13371610, upload-time = "2025-10-23T19:36:51.882Z" }, + { url = "https://files.pythonhosted.org/packages/8d/35/3654a973ebe5b32e1fd4a08ed2d46755af7267da7ac710d97420d7b8657d/ruff-0.14.2-py3-none-win32.whl", hash = "sha256:41775927d287685e08f48d8eb3f765625ab0b7042cc9377e20e64f4eb0056ee9", size = 12415318, upload-time = "2025-10-23T19:36:53.961Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/3758bcf9e0b6a4193a6f51abf84254aba00887dfa8c20aba18aa366c5f57/ruff-0.14.2-py3-none-win_amd64.whl", hash = "sha256:0df3424aa5c3c08b34ed8ce099df1021e3adaca6e90229273496b839e5a7e1af", size = 13565279, upload-time = "2025-10-23T19:36:56.578Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5d/aa883766f8ef9ffbe6aa24f7192fb71632f31a30e77eb39aa2b0dc4290ac/ruff-0.14.2-py3-none-win_arm64.whl", hash = "sha256:ea9d635e83ba21569fbacda7e78afbfeb94911c9434aff06192d9bc23fd5495a", size = 12554956, upload-time = "2025-10-23T19:36:58.714Z" }, ] [[package]] @@ -1519,7 +1519,7 @@ wheels = [ [[package]] name = "typer" -version = "0.19.2" +version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1527,9 +1527,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, ] [[package]] @@ -1552,16 +1552,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.3" +version = "20.35.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" }, + { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, ] [[package]]