diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 10ea44b..b5b686a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: # --write-changes (Don't use this to stop typos making auto-corrections) ] - repo: https://github.com/owenlamont/uv-secure - rev: 0.1.2 + rev: 0.2.0 hooks: - id: uv-secure - repo: https://github.com/adrienverge/yamllint.git diff --git a/README.md b/README.md index d403b8f..e9ea9bf 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Below are some ideas (in no particular order) I have for improving uv-secure: - Support reading configuration from pyproject.toml - Support reading configuration for multiple pyproject.toml files for mono repos +- Package for conda on conda-forge - Add rate limiting on how hard the PyPi json API is hit to query package vulnerabilities (this hasn't been a problem yet but I suspect may be for uv.lock files with many dependencies). @@ -105,15 +106,19 @@ Below are some ideas (in no particular order) I have for improving uv-secure: ## Related Work and Motivation I created this package as I wanted a dependency vulnerability scanner but I wasn't -completely happy with the options that seemed available. I use +completely happy with the options that were available. I use [uv](https://docs.astral.sh/uv/) and wanted something that works with uv.lock files but -neither of the main package options I found fitted my requirements: - -- [pip-audit](https://pypi.org/project/pip-audit/) only works with requirements.txt - files but even if you convert a uv.lock file to a requirements.txt file, pip-audit - wants to create a whole virtual environment to check all transitive dependencies (but - that should be completely unnecessary when the lock file already contains the full - dependencies). +neither of the main package options I found were as frictionless as I had hoped: + +- [pip-audit](https://pypi.org/project/pip-audit/) uv-secure is very much based on doing + the same vulnerability check that pip-audit does using PyPi's json API. pip-audit + however only works with requirements.txt so to make it work with uv projects you need + additional steps to convert your uv.lock file to a requirements.txt then you need to + run pip-audit with the --no-deps and/or --no-pip options to stop pip-audit trying to + create a virtual environment from the requirements.txt file. In short, you can use + pip-audit instead of uv-secure albeit with a bit more friction for uv projects. I hope + to add extra features beyond what pip-audit does or optimise things better (given the + more specialised case of only needing to support uv.lock files) in the future. - [safety](https://pypi.org/project/safety/) also doesn't work with uv.lock file out of the box, it does apparently work statically without needing to build a virtual environment but it does require you to create an account on the diff --git a/pyproject.toml b/pyproject.toml index d7ee58e..3fe3c4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,12 +30,15 @@ classifiers = [ ] dependencies = [ + "anyio>=4.7.0", "httpx>=0.28.1", "inflect>=7.4.0", "pydantic>=2.10.3", "rich>=13.9.4", 'tomli; python_version < "3.11"', "typer>=0.15.1", + "uvloop>=0.21.0 ; sys_platform != 'win32'", + "winloop>=0.1.7 ; sys_platform == 'win32'", ] [project.scripts] diff --git a/src/uv_secure/__version__.py b/src/uv_secure/__version__.py index d3ec452..3ced358 100644 --- a/src/uv_secure/__version__.py +++ b/src/uv_secure/__version__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/src/uv_secure/dependency_checker/dependency_checker.py b/src/uv_secure/dependency_checker/dependency_checker.py index 4f258f1..3c0da81 100644 --- a/src/uv_secure/dependency_checker/dependency_checker.py +++ b/src/uv_secure/dependency_checker/dependency_checker.py @@ -1,8 +1,11 @@ import asyncio +from collections.abc import Iterable from pathlib import Path +import sys +from anyio import Path as APath import inflect -from rich.console import Console +from rich.console import Console, ConsoleRenderable from rich.panel import Panel from rich.table import Table from rich.text import Text @@ -11,20 +14,31 @@ from uv_secure.package_info import download_vulnerabilities, parse_uv_lock_file -def check_dependencies(uv_lock_path: Path, ignore_ids: list[str]) -> int: +if sys.platform in ("win32", "cygwin", "cli"): + from winloop import run +else: + # if we're on apple or linux do this instead + from uvloop import run + + +async def check_dependencies( + uv_lock_path: APath, ignore_ids: set[str] +) -> tuple[int, Iterable[ConsoleRenderable]]: """Checks dependencies for vulnerabilities and summarizes the results.""" - console = Console() + console_outputs = [] - if not uv_lock_path.exists(): - console.print(f"[bold red]Error:[/] File {uv_lock_path} does not exist.") + if not await uv_lock_path.exists(): + console_outputs.append( + f"[bold red]Error:[/] File {uv_lock_path} does not exist." + ) raise typer.Exit(1) - dependencies = parse_uv_lock_file(uv_lock_path) - console.print( + dependencies = await parse_uv_lock_file(uv_lock_path) + console_outputs.append( f"[bold cyan]Checking {uv_lock_path} dependencies for vulnerabilities...[/]" ) - results = asyncio.run(download_vulnerabilities(dependencies)) + results = await download_vulnerabilities(dependencies) total_dependencies = len(results) vulnerable_count = 0 @@ -44,7 +58,7 @@ def check_dependencies(uv_lock_path: Path, ignore_ids: list[str]) -> int: vulnerable_plural = inf.plural("dependency", vulnerable_count) if vulnerable_count > 0: - console.print( + console_outputs.append( Panel.fit( f"[bold red]Vulnerabilities detected![/]\n" f"Checked: [bold]{total_dependencies}[/] {total_plural}\n" @@ -71,14 +85,46 @@ def check_dependencies(uv_lock_path: Path, ignore_ids: list[str]) -> int: ) table.add_row(dep.name, dep.version, vuln_id_hyperlink, vuln.details) - console.print(table) - return 1 # Exit with failure status + console_outputs.append(table) + return 1, console_outputs # Exit with failure status - console.print( + console_outputs.append( Panel.fit( f"[bold green]No vulnerabilities detected![/]\n" f"Checked: [bold]{total_dependencies}[/] {total_plural}\n" f"All dependencies appear safe!" ) ) - return 0 # Exit successfully + return 0, console_outputs # Exit successfully + + +async def process_lock_files( + uv_lock_paths: Iterable[Path], ignore_ids: set[str] +) -> Iterable[tuple[int, Iterable[ConsoleRenderable]]]: + status_output_tasks = [ + check_dependencies(APath(uv_lock_path), ignore_ids) + for uv_lock_path in uv_lock_paths + ] + return await asyncio.gather(*status_output_tasks) + + +def check_lock_files(uv_lock_paths: Iterable[Path], ignore_ids: set[str]) -> bool: + """ + Checks + + Args: + uv_lock_paths: paths to uv_lock files + ignore_ids: Vulnerabilities IDs to ignore + + Returns + ------- + True if vulnerabilities were found, False otherwise. + """ + status_outputs = run(process_lock_files(uv_lock_paths, ignore_ids)) + console = Console() + vulnerabilities_found = False + for status, console_output in status_outputs: + console.print(*console_output) + if status != 0: + vulnerabilities_found = True + return vulnerabilities_found diff --git a/src/uv_secure/package_info/lock_file_parser.py b/src/uv_secure/package_info/lock_file_parser.py index 2af5c11..d43084b 100644 --- a/src/uv_secure/package_info/lock_file_parser.py +++ b/src/uv_secure/package_info/lock_file_parser.py @@ -1,6 +1,6 @@ -from pathlib import Path import sys +from anyio import Path from pydantic import BaseModel @@ -16,10 +16,9 @@ class Dependency(BaseModel): version: str -def parse_uv_lock_file(file_path: Path) -> list[Dependency]: +async def parse_uv_lock_file(file_path: Path) -> list[Dependency]: """Parses a uv.lock TOML file and extracts package PyPi dependencies""" - with file_path.open("rb") as f: - data = toml.load(f) + data = toml.loads(await file_path.read_text()) package_data = data.get("package", []) return [ diff --git a/src/uv_secure/run.py b/src/uv_secure/run.py index 36c9db0..6850d77 100644 --- a/src/uv_secure/run.py +++ b/src/uv_secure/run.py @@ -4,7 +4,7 @@ import typer from uv_secure.__version__ import __version__ -from uv_secure.dependency_checker import check_dependencies +from uv_secure.dependency_checker.dependency_checker import check_lock_files app = typer.Typer() @@ -44,12 +44,11 @@ def main( """Parse uv.lock files, check vulnerabilities, and display summary.""" if not uv_lock_paths: uv_lock_paths = [Path("./uv.lock")] - ignore_ids = [vuln_id.strip() for vuln_id in ignore.split(",") if vuln_id.strip()] + ignore_ids = {vuln_id.strip() for vuln_id in ignore.split(",") if vuln_id.strip()} - for uv_lock_path in uv_lock_paths: - status = check_dependencies(uv_lock_path, ignore_ids) - if status != 0: - raise typer.Exit(code=status) + vulnerabilities_found = check_lock_files(uv_lock_paths, ignore_ids) + if vulnerabilities_found: + raise typer.Exit(code=1) if __name__ == "__main__": diff --git a/uv.lock b/uv.lock index baa8859..b387002 100644 --- a/uv.lock +++ b/uv.lock @@ -554,12 +554,15 @@ name = "uv-secure" version = "0.2.0" source = { editable = "." } dependencies = [ + { name = "anyio" }, { name = "httpx" }, { name = "inflect" }, { name = "pydantic" }, { name = "rich" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typer" }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, + { name = "winloop", marker = "sys_platform == 'win32'" }, ] [package.dev-dependencies] @@ -573,12 +576,15 @@ dev = [ [package.metadata] requires-dist = [ + { name = "anyio", specifier = ">=4.7.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "inflect", specifier = ">=7.4.0" }, { name = "pydantic", specifier = ">=2.10.3" }, { name = "rich", specifier = ">=13.9.4" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typer", specifier = ">=0.15.1" }, + { name = "uvloop", marker = "sys_platform != 'win32'", specifier = ">=0.21.0" }, + { name = "winloop", marker = "sys_platform == 'win32'", specifier = ">=0.1.7" }, ] [package.metadata.requires-dev] @@ -590,6 +596,57 @@ dev = [ { name = "pytest-mock", specifier = ">=3.14.0" }, ] +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019 }, + { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898 }, + { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735 }, + { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789 }, + { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523 }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, + { url = "https://files.pythonhosted.org/packages/3c/a4/646a9d0edff7cde25fc1734695d3dfcee0501140dd0e723e4df3f0a50acb/uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", size = 1439646 }, + { url = "https://files.pythonhosted.org/packages/01/2e/e128c66106af9728f86ebfeeb52af27ecd3cb09336f3e2f3e06053707a15/uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", size = 800931 }, + { url = "https://files.pythonhosted.org/packages/2d/1a/9fbc2b1543d0df11f7aed1632f64bdf5ecc4053cf98cdc9edb91a65494f9/uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", size = 3829660 }, + { url = "https://files.pythonhosted.org/packages/b8/c0/392e235e4100ae3b95b5c6dac77f82b529d2760942b1e7e0981e5d8e895d/uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", size = 3827185 }, + { url = "https://files.pythonhosted.org/packages/e1/24/a5da6aba58f99aed5255eca87d58d1760853e8302d390820cc29058408e3/uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", size = 3705833 }, + { url = "https://files.pythonhosted.org/packages/1a/5c/6ba221bb60f1e6474474102e17e38612ec7a06dc320e22b687ab563d877f/uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", size = 3804696 }, +] + +[[package]] +name = "winloop" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/a5/4d148127cc9cf16ee07edb7a2c3438b3265a0a719c2c6e92d4ba5c57a90b/winloop-0.1.7.tar.gz", hash = "sha256:53d6f4dfd621e8e9e36af1856c5844d9b4807fb535f971ca19de27d6f27bfdbd", size = 1819581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/78/9eabed2f29e171ddd192d05de528a8ed29ea748515532b51e1ea8edceb3b/winloop-0.1.7-cp310-cp310-win_amd64.whl", hash = "sha256:51d01c5593b6428aa3305adb3bdf088e959dbd7e6182845130bedf4986ddcf10", size = 696561 }, + { url = "https://files.pythonhosted.org/packages/d7/17/138751f45bf37d9b5980b42142373bd4a9e7cc8add0982a4b633c88c7a0f/winloop-0.1.7-cp311-cp311-win_amd64.whl", hash = "sha256:b1830e2e5ff1dcf967e3358b602f720e924279b35a8fd591b1289c15b0adc19c", size = 705407 }, + { url = "https://files.pythonhosted.org/packages/78/f9/a654c241d9e1b508768bd13a429dd293e0dbf35305b47c839c245922a356/winloop-0.1.7-cp312-cp312-win_amd64.whl", hash = "sha256:8a2e3f29770ebe2d949224b92cab2309315c75c47676a94c76398cb1b39a1a4a", size = 711310 }, + { url = "https://files.pythonhosted.org/packages/8c/cb/297af09c32bec1031fca873e06b0ddcc21b8caa9c3725cbd1363501ca4c9/winloop-0.1.7-cp313-cp313-win_amd64.whl", hash = "sha256:c1d3f68cccc689f13bc4617a7b44ad558e5fdc8845968f9d2661ee1d6d6ed4e4", size = 711350 }, + { url = "https://files.pythonhosted.org/packages/90/78/bea51ca90a94a65e8cf2963146d25d1897886d1f789756a3b6aedbf200e7/winloop-0.1.7-cp39-cp39-win_amd64.whl", hash = "sha256:f0915c80a78a4f1cc6b2e5401cb9f9b139ec5eedbe5a4fc8b54b9c3e7d67ef06", size = 696765 }, +] + [[package]] name = "zipp" version = "3.21.0"