diff --git a/pyproject.toml b/pyproject.toml index 4507e5048..7bc8d3820 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "rich~=14.3", "click~=8.3", "huggingface-hub~=1.4", + "uv~=0.12.10", "numpy>=2.4.6; python_version >= '3.14'", ] requires-python = ">=3.10" diff --git a/src/cloudai/_core/installables/git_repo.py b/src/cloudai/_core/installables/git_repo.py index 38d1334cf..a219f3836 100644 --- a/src/cloudai/_core/installables/git_repo.py +++ b/src/cloudai/_core/installables/git_repo.py @@ -38,6 +38,7 @@ class GitRepo(Installable, BaseModel): init_submodules: bool = False installed_path: Optional[Path] = None mount_as: Optional[str] = None + python_version: Optional[str] = None def __repr__(self) -> str: return f"GitRepo(url={self.url}, commit={self.commit})" diff --git a/src/cloudai/_core/installables/python_environment.py b/src/cloudai/_core/installables/python_environment.py index 9f2fbac09..4c7a01c6b 100644 --- a/src/cloudai/_core/installables/python_environment.py +++ b/src/cloudai/_core/installables/python_environment.py @@ -24,6 +24,8 @@ import sys from typing import TYPE_CHECKING +import uv + from .base import Installable, InstallStatusResult if TYPE_CHECKING: @@ -70,22 +72,23 @@ def install(self, installer: "BaseInstaller") -> InstallStatusResult: if installed.success: return installed - uv = shutil.which("uv") - if uv is None: - return InstallStatusResult(False, "Cannot install Python environment: 'uv' is not available.") + try: + uv_bin = uv.find_uv_bin() + except OSError as e: + return InstallStatusResult(False, f"Cannot install Python environment: {e}") - res = self._ensure_python_version(uv) + res = self._ensure_python_version(uv_bin) if not res.success: return res venv_path = installer.system.install_path / self.venv_name - res = self._create_venv(uv, venv_path) + res = self._create_venv(uv_bin, venv_path) if not res.success: self._cleanup_venv(venv_path) self.venv_path = None return res - res = self._install_requirements(uv, installer) + res = self._install_requirements(uv_bin, installer) if not res.success: self._cleanup_venv(venv_path) self.venv_path = None diff --git a/src/cloudai/_core/installables/python_executable.py b/src/cloudai/_core/installables/python_executable.py index 55da19841..1c6f8342b 100644 --- a/src/cloudai/_core/installables/python_executable.py +++ b/src/cloudai/_core/installables/python_executable.py @@ -17,10 +17,14 @@ import logging import shutil import subprocess +import sys +import typing from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Optional +import uv + from .base import Installable, InstallStatusResult from .git_repo import GitRepo @@ -28,6 +32,9 @@ from ..base_installer import BaseInstaller +_PYTHON_VERSION_MARKER = ".cloudai-python-version" + + @dataclass class PythonExecutable(Installable): """Python executable object.""" @@ -93,6 +100,11 @@ def is_installed(self, installer: "BaseInstaller") -> InstallStatusResult: venv_path = self.venv_path if self.venv_path else installer.system.install_path / self.venv_name if not venv_path.exists(): return InstallStatusResult(False, f"Virtual environment not created for {self.git_repo.url}") + + python_version = self._resolve_python_version(repo_path) + if not self._python_version_matches(venv_path, python_version): + return InstallStatusResult(False, f"Virtual environment uses a different Python than {python_version}") + self.venv_path = venv_path return InstallStatusResult(True, "Python executable installed") @@ -104,21 +116,35 @@ def mark_as_installed(self, installer: "BaseInstaller") -> InstallStatusResult: def _create_venv(self, installer: "BaseInstaller") -> InstallStatusResult: venv_path = installer.system.install_path / self.venv_name + project_dir = typing.cast(Path, self.git_repo.installed_path) + python_version = self._resolve_python_version(project_dir) logging.debug(f"Creating virtual environment in {venv_path}") - if venv_path.exists(): + if venv_path.exists() and self._python_version_matches(venv_path, python_version): msg = f"Virtual environment already exists at {venv_path}." logging.debug(msg) return InstallStatusResult(True, msg) - cmd = ["python", "-m", "venv", str(venv_path)] + project_dir = project_dir / self.project_subpath if self.project_subpath else project_dir + if not project_dir.is_dir(): + return InstallStatusResult(False, f"Python project directory does not exist: {project_dir}") + + try: + uv_bin = uv.find_uv_bin() + except OSError as e: + return InstallStatusResult(False, f"Cannot create virtual environment: {e}") + + if venv_path.exists(): + shutil.rmtree(venv_path) + + cmd = [uv_bin, "venv", "--python", python_version, "--seed", str(venv_path)] logging.debug(f"Creating venv using cmd: {' '.join(cmd)}") - result = subprocess.run(cmd, capture_output=True, text=True) + result = subprocess.run(cmd, cwd=str(project_dir), capture_output=True, text=True) logging.debug(f"venv creation STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}") if result.returncode != 0: if venv_path.exists(): shutil.rmtree(venv_path) return InstallStatusResult( - False, f"Failed to create venv:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + False, f"Failed to create venv using uv:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" ) res = self._install_dependencies(installer) @@ -127,10 +153,42 @@ def _create_venv(self, installer: "BaseInstaller") -> InstallStatusResult: shutil.rmtree(venv_path) return res + if python_version != sys.executable: + (venv_path / _PYTHON_VERSION_MARKER).write_text(f"{python_version}\n", encoding="utf-8") + self.venv_path = installer.system.install_path / self.venv_name return InstallStatusResult(True) + def _resolve_python_version(self, repo_path: Path) -> str: + if self.git_repo.python_version: + return self.git_repo.python_version.strip() + + repo_root = repo_path.resolve() + current = (repo_path / self.project_subpath if self.project_subpath else repo_path).resolve() + if current != repo_root and repo_root not in current.parents: + return sys.executable + + while True: + version_file = current / ".python-version" + if version_file.is_file(): + python_version = version_file.read_text(encoding="utf-8").strip() + if python_version: + return python_version + + if current == repo_root: + break + current = current.parent + + return sys.executable + + @staticmethod + def _python_version_matches(venv_path: Path, python_version: str) -> bool: + if python_version == sys.executable: + return True + marker = venv_path / _PYTHON_VERSION_MARKER + return marker.is_file() and marker.read_text(encoding="utf-8").strip() == python_version + def _install_dependencies(self, installer: "BaseInstaller") -> InstallStatusResult: venv_path = installer.system.install_path / self.venv_name diff --git a/tests/core/installables/test_git_repo.py b/tests/core/installables/test_git_repo.py index 10e602458..12aabef1b 100644 --- a/tests/core/installables/test_git_repo.py +++ b/tests/core/installables/test_git_repo.py @@ -21,7 +21,8 @@ import pytest -from cloudai.core import BaseInstaller, GitRepo, InstallStatusResult +from cloudai.core import BaseInstaller, GitRepo, InstallStatusResult, TestDefinition +from cloudai.models.scenario import TestRunModel @pytest.fixture @@ -72,6 +73,49 @@ def test_git_repo_name(url: str, expected: str): assert GitRepo(url=url, commit="commit").repo_name == expected +def test_python_version_does_not_change_git_clone_identity() -> None: + py311 = GitRepo(url="./repo", commit="main", python_version="3.11.9") + py314 = GitRepo(url="./repo", commit="main", python_version="3.14.0") + + assert py311 == py314 + assert hash(py311) == hash(py314) + assert py311.repo_name == py314.repo_name + assert py311.container_mount == py314.container_mount + + +def test_test_definition_git_repo_accepts_python_version() -> None: + tdef = TestDefinition.model_validate( + { + "name": "test", + "description": "description", + "test_template_name": "Example", + "cmd_args": {}, + "git_repos": [{"url": "./repo", "commit": "main", "python_version": "3.11.9"}], + } + ) + + assert tdef.git_repos[0].python_version == "3.11.9" + + +def test_scenario_git_repo_accepts_and_preserves_python_version() -> None: + model = TestRunModel.model_validate( + { + "id": "case", + "test_name": "base-test", + "git_repos": [{"url": "./repo", "commit": "main", "python_version": "3.11.9"}], + } + ) + + assert model.git_repos is not None + assert model.git_repos[0].python_version == "3.11.9" + + +def test_git_repo_toml_without_python_version_remains_valid() -> None: + repo = GitRepo.model_validate({"url": "./repo", "commit": "main"}) + + assert repo.python_version is None + + @pytest.mark.parametrize("init_submodules", [True, False]) def test_check_submodules_state_no_submodules(git_unmocked: GitRepo, init_submodules: bool): git_unmocked.init_submodules = init_submodules diff --git a/tests/core/installables/test_python_environment.py b/tests/core/installables/test_python_environment.py index 243ecd2c1..ccc43b888 100644 --- a/tests/core/installables/test_python_environment.py +++ b/tests/core/installables/test_python_environment.py @@ -43,23 +43,30 @@ def test_python_environment_identity_uses_stable_configuration() -> None: def test_python_environment_install_uses_uv(installer: BaseInstaller) -> None: env = PythonEnvironment(name="aiconfigurator", python_version="3.10", requirements=["aiconfigurator~=0.5.0"]) - with patch("shutil.which", return_value="/usr/bin/uv"), patch("subprocess.run") as run: + with ( + patch( + "cloudai._core.installables.python_environment.uv.find_uv_bin", + return_value="/cloudai/bin/uv", + ) as resolve_uv, + patch("subprocess.run") as run, + ): run.return_value = CompletedProcess(args=[], returncode=0, stdout="", stderr="") res = env.install(installer) assert res.success + resolve_uv.assert_called_once_with() assert env.venv_path == installer.system.install_path / env.venv_name - assert run.call_args_list[0].args[0] == ["/usr/bin/uv", "python", "install", "3.10"] + assert run.call_args_list[0].args[0] == ["/cloudai/bin/uv", "python", "install", "3.10"] assert run.call_args_list[1].args[0] == [ - "/usr/bin/uv", + "/cloudai/bin/uv", "venv", "--python", "3.10", str(installer.system.install_path / env.venv_name), ] assert run.call_args_list[2].args[0] == [ - "/usr/bin/uv", + "/cloudai/bin/uv", "pip", "install", "--python", @@ -68,14 +75,17 @@ def test_python_environment_install_uses_uv(installer: BaseInstaller) -> None: ] -def test_python_environment_install_requires_uv(installer: BaseInstaller) -> None: +def test_python_environment_reports_uv_resolution_failure(installer: BaseInstaller) -> None: env = PythonEnvironment(name="aiconfigurator", python_version="3.10") - with patch("shutil.which", return_value=None): + with patch( + "cloudai._core.installables.python_environment.uv.find_uv_bin", + side_effect=OSError("uv is unavailable"), + ): res = env.install(installer) assert not res.success - assert res.message == "Cannot install Python environment: 'uv' is not available." + assert res.message == "Cannot install Python environment: uv is unavailable" def test_python_environment_is_installed_checks_python_executable(installer: BaseInstaller) -> None: diff --git a/tests/core/installables/test_python_executable.py b/tests/core/installables/test_python_executable.py index 69944d2f0..9fad2279c 100644 --- a/tests/core/installables/test_python_executable.py +++ b/tests/core/installables/test_python_executable.py @@ -53,17 +53,92 @@ def setup_repo(installer: BaseInstaller, git: GitRepo): return repo_dir, subdir, pyproject_file, requirements_file +def test_explicit_python_version_overrides_repo_version(tmp_path: Path): + repo_path = tmp_path / "repo" + repo_path.mkdir() + (repo_path / ".python-version").write_text("3.10.16\n") + py = PythonExecutable(GitRepo(url="./git_url", commit="commit_hash", python_version="3.11.9")) + + assert py._resolve_python_version(repo_path) == "3.11.9" + + py.git_repo.python_version = "" + assert py._resolve_python_version(repo_path) == "3.10.16" + + +def test_nearest_repo_python_version_is_used(tmp_path: Path): + repo_path = tmp_path / "repo" + project_path = repo_path / "packages" / "project" + project_path.mkdir(parents=True) + (repo_path / ".python-version").write_text("3.10.16\n") + (repo_path / "packages" / ".python-version").write_text("3.11.9\n") + py = PythonExecutable( + GitRepo(url="./git_url", commit="commit_hash"), + project_subpath=Path("packages/project"), + ) + + assert py._resolve_python_version(repo_path) == "3.11.9" + + +def test_python_version_falls_back_to_cloudai_interpreter(tmp_path: Path): + repo_path = tmp_path / "repo" + repo_path.mkdir() + (tmp_path / ".python-version").write_text("3.11.9\n") + py = PythonExecutable(GitRepo(url="./git_url", commit="commit_hash")) + + with patch("cloudai._core.installables.python_executable.sys.executable", "/cloudai/bin/python"): + assert py._resolve_python_version(repo_path) == "/cloudai/bin/python" + + def test_venv_created(installer: BaseInstaller, git: GitRepo): + git.python_version = "3.11.9" + git.installed_path = installer.system.install_path / git.repo_name + git.installed_path.mkdir() py = PythonExecutable(git) venv_path = installer.system.install_path / py.venv_name with ( + patch("cloudai._core.installables.python_executable.uv.find_uv_bin", return_value="/cloudai/bin/uv"), patch.object(PythonExecutable, "_install_dependencies", return_value=InstallStatusResult(True)), patch("subprocess.run") as mock_run, ): - mock_run.return_value = CompletedProcess(args=[], returncode=0) + mock_run.side_effect = lambda *args, **kwargs: venv_path.mkdir() or CompletedProcess(args=[], returncode=0) res = py._create_venv(installer) assert res.success - mock_run.assert_called_once_with(["python", "-m", "venv", str(venv_path)], capture_output=True, text=True) + mock_run.assert_called_once_with( + ["/cloudai/bin/uv", "venv", "--python", "3.11.9", "--seed", str(venv_path)], + cwd=str(git.installed_path), + capture_output=True, + text=True, + ) + assert (venv_path / ".cloudai-python-version").read_text().strip() == "3.11.9" + + +def test_existing_pinned_venv_without_version_marker_is_recreated(installer: BaseInstaller, git: GitRepo): + repo_path = installer.system.install_path / git.repo_name + repo_path.mkdir() + (repo_path / ".python-version").write_text("3.11.9\n") + git.installed_path = repo_path + py = PythonExecutable(git) + venv_path = installer.system.install_path / py.venv_name + venv_path.mkdir() + stale_file = venv_path / "stale" + stale_file.touch() + + assert not py.is_installed(installer).success + + def create_venv(*args, **kwargs): + assert not stale_file.exists() + venv_path.mkdir() + return CompletedProcess(args=[], returncode=0) + + with ( + patch("cloudai._core.installables.python_executable.uv.find_uv_bin", return_value="/cloudai/bin/uv"), + patch.object(PythonExecutable, "_install_dependencies", return_value=InstallStatusResult(True)), + patch("subprocess.run", side_effect=create_venv), + ): + res = py._create_venv(installer) + + assert res.success + assert (venv_path / ".cloudai-python-version").read_text().strip() == "3.11.9" @pytest.mark.parametrize("failure_on_venv_creation,reqs_install_failure", [(True, False), (False, True)]) @@ -73,6 +148,8 @@ def test_error_creating_venv( failure_on_venv_creation: bool, reqs_install_failure: bool, ): + git.installed_path = installer.system.install_path / git.repo_name + git.installed_path.mkdir() py = PythonExecutable(git) venv_path = installer.system.install_path / py.venv_name @@ -84,19 +161,21 @@ def mock_run(*args, **kwargs): dependencies_result = InstallStatusResult(False, "err") if reqs_install_failure else InstallStatusResult(True) with ( + patch("cloudai._core.installables.python_executable.uv.find_uv_bin", return_value="/cloudai/bin/uv"), patch.object(PythonExecutable, "_install_dependencies", return_value=dependencies_result), patch("subprocess.run", side_effect=mock_run), ): res = py._create_venv(installer) assert not res.success if failure_on_venv_creation: - assert res.message == "Failed to create venv:\nSTDOUT:\nNone\nSTDERR:\nerr" + assert res.message == "Failed to create venv using uv:\nSTDOUT:\nNone\nSTDERR:\nerr" else: assert res.message == "err" assert not venv_path.exists(), "venv folder wasn't removed after unsuccessful installation" def test_venv_already_exists(installer: BaseInstaller, git: GitRepo): + git.installed_path = installer.system.install_path / git.repo_name py = PythonExecutable(git) venv_path = installer.system.install_path / py.venv_name venv_path.mkdir() @@ -158,7 +237,10 @@ def test_all_good_flow(installer: BaseInstaller, git: GitRepo): pyproject_file = repo_dir / "pyproject.toml" pyproject_file.write_text("[tool.poetry]\nname = 'dummy_project'") - with patch("subprocess.run") as mock_run: + with ( + patch("cloudai._core.installables.python_executable.uv.find_uv_bin", return_value="/cloudai/bin/uv"), + patch("subprocess.run") as mock_run, + ): mock_run.return_value = CompletedProcess(args=[], returncode=0, stdout=f"{git.commit}\n", stderr="") res = py.install(installer) diff --git a/uv.lock b/uv.lock index 4405ede12..e60af1b2a 100644 --- a/uv.lock +++ b/uv.lock @@ -281,6 +281,7 @@ dependencies = [ { name = "rich" }, { name = "tbparse" }, { name = "toml" }, + { name = "uv" }, { name = "websockets" }, ] @@ -360,6 +361,7 @@ requires-dist = [ { name = "taplo", marker = "extra == 'dev'", specifier = "~=0.9.3" }, { name = "tbparse", specifier = "~=0.0.9" }, { name = "toml", specifier = "~=0.10.2" }, + { name = "uv", specifier = "~=0.12.10" }, { name = "vulture", marker = "extra == 'dev'", specifier = "==2.14" }, { name = "websockets", specifier = "~=16.0" }, ] @@ -2630,6 +2632,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "uv" +version = "0.12.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/92/167ad4b0795530383a84560dc8bc8c0afab6b6fb10baba34b2a257fa6894/uv-0.12.10.tar.gz", hash = "sha256:27e8350faffb35d8ecaa5ed39236709809bb9a883f1d11d13c0964d6051b2384", size = 7144864, upload-time = "2026-09-04T23:14:21.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d1/b3752f00e67e5bf141adec0225c62a17c1cf3fb524db463c541c080f1237/uv-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:efbbc586d40014b9dd3bc37a2ad5803d12919458f0f9d16f122db5d91626f5ab", size = 22182923, upload-time = "2026-09-04T23:13:31.502Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4d/ef846ec0a4dfa8d4f47b96a87a1e331143be5a045c28c8b4b0374946455f/uv-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7e3a70d9dff95481afccd7d41e7ec42fdc230d53487b3e7e633f9b35610401b5", size = 20494379, upload-time = "2026-09-04T23:13:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/04/2e/5614e9a940fe1bf291342410b2cde9fe35a1430135ae7434ea8fd67033f8/uv-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5dc26c73826d2119292d49d71c5a9d5ba9dd97dd02459d816b8226f47f3dc6bd", size = 17238363, upload-time = "2026-09-04T23:13:36.905Z" }, + { url = "https://files.pythonhosted.org/packages/d1/03/e71936935b8ac26390b280e102b9db823e90adb1134f7e226538054fdece/uv-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bd0afae6918795c6a61a649e64d735e339f3d62f8310235da6f19eb3f66f323b", size = 21590025, upload-time = "2026-09-04T23:13:39.588Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/c6388a722fb9db8adf8ec8eca762547b5a06ccbd1176571dc584636af5f1/uv-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:c1fd0fcabc18dccaf541ca44a061c245045c937bec75c0491e94d59cd33ca97f", size = 21677222, upload-time = "2026-09-04T23:13:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c6/96698aed29f25ff289b28b6312c72e260c56f7e1de2e3f690788bddea19e/uv-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de4b06934bd025ceeeb48af5c6f9a82407a2fe2b5b35e456cfb3198ef5029de0", size = 21712683, upload-time = "2026-09-04T23:13:44.927Z" }, + { url = "https://files.pythonhosted.org/packages/df/98/5871a2bff93bd6118ae877b874a09202d39e2831af37f56e2070d5c4bc87/uv-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b610e8cf7c6d51dc119d352e699b674534d0de327b4d2a67eb5d71ec364b8970", size = 22331081, upload-time = "2026-09-04T23:13:47.833Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/d690f34901bdc5094645cf75a00381cfb8f1f28ee87b7089af761094bb6f/uv-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f43c06b19137bb05877b0ec59c4932878db5d1aeb6f7b5c33259c22e0359c4fe", size = 23785643, upload-time = "2026-09-04T23:13:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/90/da/168a8c69f60b9a2c90afb0b9211911ca42a81300c098d545da911bd4ef8f/uv-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0334577671a65b621faddfdf5f9e9dd4eb08eaa7231c4405c4a7f4f17d224d9f", size = 23337759, upload-time = "2026-09-04T23:13:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/f0e14ba1f881f7126152dcaf04119b3a2bd7566ba1cac1d22834bd134cbf/uv-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7d6248ad9f2d282fea795f248da8fa666ab382df50d8385bd98e01049440a0c", size = 19978237, upload-time = "2026-09-04T23:13:56.994Z" }, + { url = "https://files.pythonhosted.org/packages/7a/20/acebfb5fc01b29cce5e68b4824fa451d5fb1e569bb2579bcc7233583a894/uv-0.12.10-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:5c0bcf0598742eb417f1aa41ec82f0d89bd7b7f42d0a519bea495a9419d8d8ee", size = 19402684, upload-time = "2026-09-04T23:13:59.579Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/c40dc8b7b143a6f7843994b1b8a13aa8859d8404973704cd6bad7a26ac8d/uv-0.12.10-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:0ac914e9750bb1de2518d444cc1cff084f4eff3eefe9cdd5814b775443a16df1", size = 22450726, upload-time = "2026-09-04T23:14:02.316Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/b5df49f571b58e694ca8c3ddadd89939036ac0fe7ddaadc7600225ea8585/uv-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:46ca83d33bf87a67d0c5aa5b64f5576fc5c8e700a0ecf4900d409f8a1a1c9810", size = 22571602, upload-time = "2026-09-04T23:14:05.088Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3e/f0335142e8a4c6ddc5117dcc9b5fd55c83cdd8e20a7f3be9584282dbfb64/uv-0.12.10-py3-none-musllinux_1_1_i686.whl", hash = "sha256:c49cd1f6e5ded1bd844119da8b184d5c044ae51441c476af2382fdb8af99edb8", size = 21499938, upload-time = "2026-09-04T23:14:08.109Z" }, + { url = "https://files.pythonhosted.org/packages/98/3e/991f08fcab1706175ae30eda6ef483223511679c56fe1032b919ca3ea391/uv-0.12.10-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:a34e1be296e6e30fdbfee9b84a45d082ce7df4dd4e588ebcfc44249505c10f02", size = 22879435, upload-time = "2026-09-04T23:14:11.1Z" }, + { url = "https://files.pythonhosted.org/packages/c4/80/8db01aabf9937cbc228003392ee9fe542c4dad5c3c5b26f2e1f136d8e463/uv-0.12.10-py3-none-win32.whl", hash = "sha256:f7f2bc56fbd367b1066e8461a82e51e8886bbab52b1f8d1793af8ce52232348c", size = 19873629, upload-time = "2026-09-04T23:14:13.755Z" }, + { url = "https://files.pythonhosted.org/packages/a6/88/e980985d93283c546374b6b9dfa486a28091c4c056226711e31bb0f722d1/uv-0.12.10-py3-none-win_amd64.whl", hash = "sha256:2ad71395c31b5db20c56327f62ee0299f75833308d4413284aba87324d092afa", size = 18084435, upload-time = "2026-09-04T23:14:16.323Z" }, + { url = "https://files.pythonhosted.org/packages/84/10/fa542546044f783060d09a624f9964b595c0a0713cf2d89ade69c6667d8b/uv-0.12.10-py3-none-win_arm64.whl", hash = "sha256:4f097c6b7f63eceb3faf5102009f9a8cc22fd7d8a341858d2fd540f46a592f7a", size = 19598033, upload-time = "2026-09-04T23:14:18.829Z" }, +] + [[package]] name = "uvicorn" version = "0.40.0"