From bbc521694ceb7e09541a62b55ce1a67fc5da700d Mon Sep 17 00:00:00 2001 From: Juan Diaz Date: Thu, 16 Jul 2026 18:13:42 +0000 Subject: [PATCH] Implement local port forwarding Update completion Split network commands as a separated module Split runtime, constants modules from the cli module to simplify the organization --- README.md | 13 ++ docs/network-command-roadmap.md | 17 ++ pyproject.toml | 5 +- sbx.toml.example | 3 + src/sbx/cli.py | 370 +++++++++---------------------- src/sbx/completion.py | 116 +++++++--- src/sbx/constants.py | 9 + src/sbx/image/build_debian.py | 2 - src/sbx/image/ls.py | 2 - src/sbx/network.py | 338 ++++++++++++++++++++++++++++ src/sbx/runtime.py | 165 ++++++++++++++ tests/test_build_debian_image.py | 2 - tests/test_cli.py | 89 +++++--- tests/test_cli_extra.py | 216 +++++++++++------- tests/test_completion.py | 21 +- uv.lock | 273 +---------------------- 16 files changed, 959 insertions(+), 682 deletions(-) create mode 100644 src/sbx/constants.py create mode 100644 src/sbx/network.py create mode 100644 src/sbx/runtime.py diff --git a/README.md b/README.md index 00bca44..b140950 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,11 @@ sbx run my-sbx --mount /home/me/src/tooling:/workspace/tooling # Disable automatic OAuth callback forwarding. sbx run my-sbx --no-auth-port +# Temporarily forward a running guest web server until Ctrl-C. +sbx network forward my-sbx 3000 +sbx network forward 8080:3000 +sbx network forward 0.0.0.0:3000:3000 + # Keep the VM running after the agent/shell exits. sbx run my-sbx --keep-running sbx shell my-sbx --keep-running @@ -111,6 +116,7 @@ sbx run my-sbx --agent claude | `shell [NAME]` | Open a shell in a sandbox. | | `ls` | List running sandboxes. Use `ls -a` / `ls --all` to include stopped ones. | | `network status [NAME]` | Expert helper: show sandbox networking and auth callback tunnel status. | +| `network forward [NAME] SPEC` | Temporarily forward a host TCP port to a running sandbox until Ctrl-C. | | `network auth-port [NAME]` | Expert helper: manually expose the OAuth callback port for an already-running sandbox. | | `network close-auth-port [NAME]` | Expert helper: close the tracked OAuth callback tunnel. | | `image build-debian` | Advanced helper: build a local Debian/Pi image, optionally with `--with-docker`. | @@ -144,6 +150,13 @@ image = "~/.smolvm/images/debian-sbx" run_user = "agent" ``` +Configure durable TCP forwards applied when the VM starts: + +```toml +[sbx] +port_forwards = ["3000", "8080:3000"] +``` + Build with Docker support: ```bash diff --git a/docs/network-command-roadmap.md b/docs/network-command-roadmap.md index fbfe6df..8ad0d22 100644 --- a/docs/network-command-roadmap.md +++ b/docs/network-command-roadmap.md @@ -10,6 +10,23 @@ sbx network ... ## Current commands +### `sbx network forward [NAME] SPEC` + +Forwards a host TCP port to a running sandbox in the foreground. Press Ctrl-C to stop. + +```bash +sbx network forward 3000 +sbx network forward 8080:3000 +sbx network forward 0.0.0.0:3000:3000 +``` + +Configured forwards live in `.sbx.toml` and are applied when the VM starts: + +```toml +[sbx] +port_forwards = ["3000", "8080:3000"] +``` + ### `sbx network status NAME` Shows networking details and auth callback tunnel state for one sandbox. diff --git a/pyproject.toml b/pyproject.toml index 73e7208..bb1848a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,12 +7,11 @@ name = "sbx" version = "0.2.4.dev0" description = "Run Pi/Claude/Codex coding agents in disposable SmolVM sandboxes." readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.12" license = "Apache-2.0" authors = [{ name = "sbx contributors" }] dependencies = [ "smolvm==0.0.26", - "tomli>=2.4.1; python_version < '3.11'", ] [project.optional-dependencies] @@ -31,7 +30,7 @@ packages = ["src/sbx"] [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py312" src = ["src", "tests"] [tool.ruff.lint] diff --git a/sbx.toml.example b/sbx.toml.example index 049e0fb..328915e 100644 --- a/sbx.toml.example +++ b/sbx.toml.example @@ -17,3 +17,6 @@ stop_on_exit = false copy_host_credentials = false env = [] git_config = true + +# Optional host-to-guest TCP forwards, applied when the VM starts. +# port_forwards = ["3000", "8080:3000", "0.0.0.0:3000:3000"] diff --git a/src/sbx/cli.py b/src/sbx/cli.py index 68ab3a6..1409117 100644 --- a/src/sbx/cli.py +++ b/src/sbx/cli.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import argparse import base64 import json @@ -7,32 +5,31 @@ import re import shlex import shutil -import signal -import socket import sqlite3 import subprocess import sys import tempfile -import time +import tomllib from collections.abc import Iterator, Mapping, Sequence from contextlib import contextmanager, suppress from pathlib import Path from typing import Any import sbx.image.ls +import sbx.network as network from sbx import __version__ from sbx.completion import SUPPORTED_SHELLS, completion_script +from sbx.constants import ( + DEFAULT_BACKEND, + DEFAULT_BOOT_TIMEOUT, + SBX_STATE_DIR, + SESSIONS_FILE, + SMOLVM_DB_PATH, +) from sbx.image import build_debian - -try: # Python 3.11+ - import tomllib -except ModuleNotFoundError: # pragma: no cover - exercised on Python 3.10 - import tomli as tomllib # type: ignore[no-redef] - +from sbx.runtime import ConfigError as RuntimeConfigError AGENTS = ("pi", "claude", "codex") -DEFAULT_BACKEND = "qemu" -DEFAULT_BOOT_TIMEOUT = 30.0 MIB = 1024 * 1024 LAUNCH_COMMANDS = {"pi": "pi", "claude": "claude", "codex": "codex"} USERNAME_RE = re.compile(r"^[a-z_][a-z0-9_-]*[$]?$", re.IGNORECASE) @@ -50,15 +47,10 @@ ) DEFAULT_CONFIG_PATHS = (Path.home() / ".config" / "sbx" / "config.toml",) LOCAL_CONFIG_PATHS = (Path.cwd() / ".sbx.toml",) -SBX_STATE_DIR = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) / "sbx" -TUNNELS_FILE = SBX_STATE_DIR / "tunnels.json" -SESSIONS_FILE = SBX_STATE_DIR / "sessions.json" -SMOLVM_DB_PATH = Path.home() / ".local" / "state" / "smolvm" / "smolvm.db" DEBUG = False -class ConfigError(ValueError): - pass +ConfigError = RuntimeConfigError def _debug(message: str) -> None: @@ -261,28 +253,51 @@ def _workspace_mounts_from_specs(mounts: Sequence[str], *, writable: bool) -> li return workspace_mounts -def _sync_existing_vm_mounts_from_config( - vm_name: str, mounts: Sequence[str], *, writable_mounts: bool +def _sync_existing_vm_start_config( + vm_name: str, + mounts: Sequence[str] | None, + *, + writable_mounts: bool, + port_forwards: Sequence[str], ) -> None: db_path = SMOLVM_DB_PATH.expanduser() if not db_path.exists(): return - desired = _workspace_mounts_from_specs(mounts, writable=writable_mounts) + desired_mounts = ( + _workspace_mounts_from_specs(mounts, writable=writable_mounts) + if mounts is not None + else None + ) + desired_forwards = network.port_forwards_from_specs(port_forwards) + updated: list[str] = [] with sqlite3.connect(db_path) as conn: conn.row_factory = sqlite3.Row row = conn.execute("SELECT status, config FROM vms WHERE id = ?", (vm_name,)).fetchone() if row is None or row["status"] == "running": return config = json.loads(row["config"]) - if config.get("workspace_mounts") == desired: + if desired_mounts is not None and config.get("workspace_mounts") != desired_mounts: + config["workspace_mounts"] = desired_mounts + updated.append("mounts") + if config.get("port_forwards", []) != desired_forwards: + config["port_forwards"] = desired_forwards + updated.append("port forwards") + if not updated: return - config["workspace_mounts"] = desired conn.execute( "UPDATE vms SET config = ? WHERE id = ?", (json.dumps(config, separators=(",", ":")), vm_name), ) - print(f"sbx: updated mounts for existing VM '{vm_name}'") + print(f"sbx: updated {', '.join(updated)} for existing VM '{vm_name}'") + + +def _sync_existing_vm_mounts_from_config( + vm_name: str, mounts: Sequence[str], *, writable_mounts: bool +) -> None: + _sync_existing_vm_start_config( + vm_name, mounts, writable_mounts=writable_mounts, port_forwards=[] + ) def _project_guest_cwd(path_value: Any) -> str | None: @@ -848,15 +863,9 @@ def _write_json_object(path: Path, data: Mapping[str, Any]) -> None: path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") -def _load_tunnels() -> dict[str, Any]: - return _read_json_object(TUNNELS_FILE) - - -def _save_tunnels(data: Mapping[str, Any]) -> None: - _write_json_object(TUNNELS_FILE, data) - - def _pid_is_alive(pid: int) -> bool: + if pid <= 0: + return False try: os.kill(pid, 0) except ProcessLookupError: @@ -866,46 +875,6 @@ def _pid_is_alive(pid: int) -> bool: return True -def _tracked_auth_tunnel(vm_id: str) -> dict[str, Any] | None: - tunnel = _load_tunnels().get(vm_id, {}).get("auth_port") - if not isinstance(tunnel, dict): - return None - pid = tunnel.get("pid") - if not isinstance(pid, int) or not _pid_is_alive(pid): - return None - return tunnel - - -def _tracked_auth_tunnel_for_host_port(host_port: int) -> tuple[str, dict[str, Any]] | None: - for vm_id, vm_data in _load_tunnels().items(): - if not isinstance(vm_data, dict): - continue - tunnel = vm_data.get("auth_port") - if not isinstance(tunnel, dict) or tunnel.get("host_port") != host_port: - continue - pid = tunnel.get("pid") - if isinstance(pid, int) and _pid_is_alive(pid): - return str(vm_id), tunnel - return None - - -def _record_auth_tunnel(vm_id: str, *, pid: int, host_port: int, guest_port: int) -> None: - data = _load_tunnels() - vm_data = data.setdefault(vm_id, {}) - vm_data["auth_port"] = {"pid": pid, "host_port": host_port, "guest_port": guest_port} - _save_tunnels(data) - - -def _remove_auth_tunnel_record(vm_id: str) -> None: - data = _load_tunnels() - vm_data = data.get(vm_id) - if isinstance(vm_data, dict): - vm_data.pop("auth_port", None) - if not vm_data: - data.pop(vm_id, None) - _save_tunnels(data) - - def _load_sessions() -> dict[str, Any]: return _read_json_object(SESSIONS_FILE) @@ -972,121 +941,6 @@ def _stop_vm_if_last_session(vm_id: str, *, stop_on_exit: bool) -> None: _run_smolvm(["sandbox", "stop", vm_id]) -def _localhost_port_is_listening(port: int) -> bool: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.settimeout(0.2) - return sock.connect_ex(("127.0.0.1", port)) == 0 - - -def _close_tracked_auth_tunnel(vm_id: str) -> bool: - tracked = _tracked_auth_tunnel(vm_id) - if tracked is None: - _remove_auth_tunnel_record(vm_id) - return False - - pid = int(tracked["pid"]) - with suppress_process_errors(): - os.killpg(pid, signal.SIGTERM) - deadline = time.monotonic() + 3 - while time.monotonic() < deadline and _pid_is_alive(pid): - time.sleep(0.1) - if _pid_is_alive(pid): - with suppress_process_errors(): - os.killpg(pid, signal.SIGKILL) - _remove_auth_tunnel_record(vm_id) - return True - - -def _expose_auth_port(vm_id: str, host_port: int, guest_port: int, *, replace: bool = False) -> int: - _debug(f"expose auth port: vm={vm_id}, host_port={host_port}, guest_port={guest_port}") - tracked = _tracked_auth_tunnel(vm_id) - if ( - tracked - and tracked.get("host_port") == host_port - and tracked.get("guest_port") == guest_port - ): - _debug(f"auth host port {host_port} already tracked with pid {tracked['pid']}") - return 0 - if _localhost_port_is_listening(host_port): - owner = _tracked_auth_tunnel_for_host_port(host_port) - if owner is not None: - owner_vm, _owner_tunnel = owner - if replace: - print( - f"sbx: replacing auth tunnel on localhost:{host_port}: " - f"VM '{owner_vm}' -> VM '{vm_id}'.", - file=sys.stderr, - ) - _close_tracked_auth_tunnel(owner_vm) - else: - print( - f"sbx: warning: localhost:{host_port} is already used by the auth " - f"tunnel for VM '{owner_vm}'. `/login` in VM '{vm_id}' may not work; " - f"run `sbx network auth-port {vm_id} --replace` to switch it.", - file=sys.stderr, - ) - return 0 - else: - print( - f"sbx: warning: localhost:{host_port} is already in use and is not tracked " - "by sbx. `/login` may not work until that port is free.", - file=sys.stderr, - ) - return 0 - - cmd = _ssh_command(vm_id) - forward_args = [ - "-N", - "-L", - f"127.0.0.1:{host_port}:127.0.0.1:{guest_port}", - "-o", - "ExitOnForwardFailure=yes", - "-o", - "BatchMode=yes", - ] - cmd[-1:-1] = forward_args - _debug_command(cmd, None) - try: - proc = subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, - start_new_session=True, - ) - except FileNotFoundError: - print("sbx: command not found: ssh", file=sys.stderr) - return 127 - except OSError as exc: - print(f"sbx: failed to start auth port tunnel: {exc}", file=sys.stderr) - return 1 - - deadline = time.monotonic() + 5 - while time.monotonic() < deadline: - if proc.poll() is not None: - stderr = proc.stderr.read().strip() if proc.stderr is not None else "" - print(f"sbx: auth port tunnel exited before becoming ready: {stderr}", file=sys.stderr) - return proc.returncode or 1 - if _localhost_port_is_listening(host_port): - _record_auth_tunnel(vm_id, pid=proc.pid, host_port=host_port, guest_port=guest_port) - _debug(f"auth port tunnel ready with pid {proc.pid}") - return 0 - time.sleep(0.1) - - with suppress_process_errors(): - os.killpg(proc.pid, signal.SIGTERM) - print(f"sbx: auth port tunnel did not become ready on localhost:{host_port}", file=sys.stderr) - return 1 - - -class suppress_process_errors: - def __enter__(self) -> None: - return None - - def __exit__(self, exc_type: object, exc: object, traceback: object) -> bool: - return isinstance(exc, (ProcessLookupError, PermissionError, OSError)) - - def _attach_as_root(vm_id: str, launch_command: str, cwd: str | None = None) -> int: from smolvm.env import ENV_FILE @@ -1135,7 +989,7 @@ def _post_start_actions( git_config_text: str | None = None, ) -> int: if auth_port: - port_rc = _expose_auth_port(vm_name, auth_host_port, auth_guest_port) + port_rc = network.expose_auth_port(vm_name, auth_host_port, auth_guest_port) if port_rc != 0: return port_rc if not attach: @@ -1322,7 +1176,7 @@ def _start_preset_with_sdk( args: argparse.Namespace, config: Mapping[str, Any], agent: str, - cpus: int, + cpus: int | None, mounts: Sequence[str], writable_mounts: bool, attach: bool, @@ -1336,10 +1190,12 @@ def _start_preset_with_sdk( copy_host_credentials: bool, forward_env: list[str], json_output: bool, + port_forwards: Sequence[str], ) -> int: from smolvm import SmolVM from smolvm.facade import _build_auto_config from smolvm.presets import apply_preset, get_preset + from smolvm.types import PortForwardConfig preset_name = "claude-code" if agent == "claude" else agent preset = get_preset(preset_name) @@ -1363,7 +1219,15 @@ def start() -> str: disk_size_mib=int(disk_size), ssh_key_path=None, ) - config_obj = config_obj.model_copy(update={"vcpu_count": cpus}) + updates: dict[str, Any] = { + "port_forwards": [ + PortForwardConfig(**item) + for item in network.port_forwards_from_specs(port_forwards) + ] + } + if cpus is not None: + updates["vcpu_count"] = cpus + config_obj = config_obj.model_copy(update=updates) try: vm = SmolVM( config_obj, @@ -1437,6 +1301,7 @@ def _start_local_image( cwd: str | None, git_config_text: str | None, forward_env: list[str] | None = None, + port_forwards: Sequence[str] = (), ) -> int: from smolvm import SmolVM, VMConfig from smolvm.utils import ensure_ssh_key @@ -1489,6 +1354,7 @@ def _start_local_image( "backend": DEFAULT_BACKEND, "ssh_capable": True, "ssh_public_key": public_key.read_text(encoding="utf-8").strip(), + "port_forwards": network.port_forwards_from_specs(port_forwards), } if cpus_value is not None: vm_config["vcpu_count"] = _validate_cpus(cpus_value) @@ -1645,6 +1511,12 @@ def cmd_start(args: argparse.Namespace) -> int: git_config_text = _host_git_config() if git_config else None cpus_value = _arg_or_config(args, "cpus", config, "sbx") cpus = _validate_cpus(cpus_value) if cpus_value is not None else None + try: + port_forwards = _list_value(sbx_cfg.get("port_forwards"), key="[sbx].port_forwards") + network.port_forwards_from_specs(port_forwards) + except ConfigError as exc: + print(f"sbx: {exc}", file=sys.stderr) + return 2 requested_name = _arg_or_config(args, "name", config, "sbx") if requested_name: @@ -1654,8 +1526,11 @@ def cmd_start(args: argparse.Namespace) -> int: if existing_status is not None: if existing_status != "running": try: - _sync_existing_vm_mounts_from_config( - str(requested_name), effective_mounts, writable_mounts=writable_mounts + _sync_existing_vm_start_config( + str(requested_name), + effective_mounts, + writable_mounts=writable_mounts, + port_forwards=port_forwards, ) except ConfigError as exc: print(f"sbx: {exc}", file=sys.stderr) @@ -1707,6 +1582,7 @@ def cmd_start(args: argparse.Namespace) -> int: cwd=project_guest_cwd, git_config_text=git_config_text, forward_env=forward_env, + port_forwards=port_forwards, ) except ConfigError as exc: print(f"sbx: {exc}", file=sys.stderr) @@ -1719,7 +1595,7 @@ def cmd_start(args: argparse.Namespace) -> int: print(f"sbx: failed to start image: {exc}", file=sys.stderr) return 1 - if cpus is not None: + if cpus is not None or port_forwards: try: return _start_preset_with_sdk( args=args, @@ -1739,6 +1615,7 @@ def cmd_start(args: argparse.Namespace) -> int: copy_host_credentials=copy_host_credentials, forward_env=forward_env, json_output=bool(args.json), + port_forwards=port_forwards, ) except ConfigError as exc: print(f"sbx: {exc}", file=sys.stderr) @@ -1802,7 +1679,7 @@ def cmd_start(args: argparse.Namespace) -> int: _set_vm_hostname(vm_name) _maybe_write_project_config(args, config, vm_name=vm_name, agent=str(agent), created=True) if auth_port: - port_rc = _expose_auth_port(vm_name, auth_host_port, auth_guest_port) + port_rc = network.expose_auth_port(vm_name, auth_host_port, auth_guest_port) if port_rc != 0: return port_rc @@ -1883,6 +1760,14 @@ def cmd_passthrough(args: argparse.Namespace) -> int: else _cfg(config, "sbx", "git_config", True) ) git_config_text = _host_git_config() if git_config else None + try: + port_forwards = _list_value( + _sbx_config(config).get("port_forwards"), key="[sbx].port_forwards" + ) + network.port_forwards_from_specs(port_forwards) + except ConfigError as exc: + print(f"sbx: {exc}", file=sys.stderr) + return 2 existing_status = _get_existing_vm_status(name) if ( run_user is not None or project_guest_cwd is not None or git_config_text is not None @@ -1890,6 +1775,14 @@ def cmd_passthrough(args: argparse.Namespace) -> int: print(f"sbx: {_missing_vm_message(name)}", file=sys.stderr) return 1 if existing_status is not None: + if existing_status != "running": + try: + _sync_existing_vm_start_config( + name, None, writable_mounts=False, port_forwards=port_forwards + ) + except ConfigError as exc: + print(f"sbx: {exc}", file=sys.stderr) + return 2 start_rc = _start_existing_vm_if_needed( name, existing_status, @@ -1960,71 +1853,6 @@ def _delete_vm(vm_id: str, extra_args: Sequence[str] | None = None) -> int: return completed.returncode -def cmd_auth_port(args: argparse.Namespace) -> int: - name = _vm_name_from_arg_or_config( - args, getattr(args, "config_data", None), "network auth-port" - ) - if name is None: - return 2 - return _expose_auth_port( - name, args.host_port, args.guest_port, replace=bool(getattr(args, "replace", False)) - ) - - -def cmd_close_auth_port(args: argparse.Namespace) -> int: - name = _vm_name_from_arg_or_config( - args, getattr(args, "config_data", None), "network close-auth-port" - ) - if name is None: - return 2 - if not _close_tracked_auth_tunnel(name): - print(f"No tracked auth port tunnel for '{name}'.") - return 0 - print(f"Closed auth port tunnel for '{name}'.") - return 0 - - -def cmd_network_status(args: argparse.Namespace) -> int: - name = _vm_name_from_arg_or_config( - args, getattr(args, "config_data", None), "network status" - ) - if name is None: - return 2 - completed = _run_smolvm_capture(["sandbox", "info", name, "--json"]) - if completed is None: - return 127 - if completed.returncode != 0: - if completed.stdout: - print(completed.stdout, end="") - if completed.stderr: - print(completed.stderr, end="", file=sys.stderr) - return completed.returncode - - payload = json.loads(completed.stdout) - vm = payload["data"]["vm"] - tracked = _tracked_auth_tunnel(name) - auth_status = "inactive" - auth_detail = "-" - if tracked is not None: - auth_status = "active" - auth_detail = ( - f"pid {tracked['pid']}, localhost:{tracked['host_port']} -> " - f"guest:{tracked['guest_port']}" - ) - elif _localhost_port_is_listening(args.host_port): - auth_status = "busy/untracked" - auth_detail = f"localhost:{args.host_port} is listening but is not tracked by sbx" - - print(f"Sandbox: {vm['name']}") - print(f"Status: {vm['status']}") - print(f"Backend: {vm['backend']}") - print(f"Guest IP: {vm['ip_address']}") - print(f"SSH Port: {vm['ssh_port']}") - print(f"Auth callback: {auth_status}") - print(f"Auth detail: {auth_detail}") - return 0 - - def cmd_create(args: argparse.Namespace) -> int: args.attach = False if args.auth_port is None: @@ -2263,8 +2091,20 @@ def build_parser() -> argparse.ArgumentParser: ) ls_p.set_defaults(func=cmd_passthrough) - network = sub.add_parser("network", help="Expert networking helpers.") - network_sub = network.add_subparsers(dest="network_action", required=True) + network_parser = sub.add_parser("network", help="Expert networking helpers.") + network_sub = network_parser.add_subparsers(dest="network_action", required=True) + forward = network_sub.add_parser( + "forward", + help="Forward a host TCP port to a running sandbox until Ctrl-C.", + ) + forward.add_argument( + "forward_args", + nargs="+", + metavar="[NAME] SPEC", + help="Forward spec: GUEST_PORT, HOST_PORT:GUEST_PORT, or BIND_HOST:HOST_PORT:GUEST_PORT.", + ) + forward.set_defaults(func=network.cmd_forward) + auth_port = network_sub.add_parser( "auth-port", help="Expose the Pi OAuth callback port from a sandbox to host localhost.", @@ -2287,7 +2127,7 @@ def build_parser() -> argparse.ArgumentParser: action="store_true", help="Close an existing sbx-tracked auth tunnel on this host port before exposing it.", ) - auth_port.set_defaults(func=cmd_auth_port) + auth_port.set_defaults(func=network.cmd_auth_port) close_auth_port = network_sub.add_parser( "close-auth-port", @@ -2296,7 +2136,7 @@ def build_parser() -> argparse.ArgumentParser: close_auth_port.add_argument( "name", nargs="?", help="Sandbox name or ID. Defaults to [sbx].name." ) - close_auth_port.set_defaults(func=cmd_close_auth_port) + close_auth_port.set_defaults(func=network.cmd_close_auth_port) network_status = network_sub.add_parser( "status", @@ -2311,7 +2151,7 @@ def build_parser() -> argparse.ArgumentParser: default=1455, help="Host auth callback port to inspect (default: 1455).", ) - network_status.set_defaults(func=cmd_network_status) + network_status.set_defaults(func=network.cmd_status) image = sub.add_parser("image", help="Advanced local image helpers.") image_sub = image.add_subparsers(dest="image_action", required=True) diff --git a/src/sbx/completion.py b/src/sbx/completion.py index f2ff24c..c2a5756 100644 --- a/src/sbx/completion.py +++ b/src/sbx/completion.py @@ -1,5 +1,3 @@ -from __future__ import annotations - SUPPORTED_SHELLS = ("bash", "zsh", "fish") COMMANDS = ( @@ -15,7 +13,7 @@ "doctor", "completion", ) -NETWORK_COMMANDS = ("auth-port", "close-auth-port", "status") +NETWORK_COMMANDS = ("forward", "auth-port", "close-auth-port", "status") IMAGE_COMMANDS = ("build-debian", "ls") AGENTS = ("pi", "claude", "codex") @@ -53,6 +51,7 @@ "--help", ) SHELL_OPTIONS = ( + "--force-start", "--keep-running", "--run-user", "--project-path", @@ -64,7 +63,7 @@ LS_OPTIONS = ("--all", "-a", "--help") RM_OPTIONS = ("--force", "--help") STOP_OPTIONS = ("--help",) -RECREATE_EXTRA_OPTIONS = ("--force",) +RUN_OPTIONS = ("--force-start", *START_OPTIONS) AUTH_PORT_OPTIONS = ("--guest-port", "--host-port", "--replace", "--help") NETWORK_STATUS_OPTIONS = ("--host-port", "--help") IMAGE_BUILD_DEBIAN_OPTIONS = ( @@ -92,6 +91,10 @@ def _words(values: tuple[str, ...]) -> str: return " ".join(values) +def _zsh_words(values: tuple[str, ...]) -> str: + return " ".join(f"'{value}'" for value in values) + + def completion_script(shell: str) -> str: if shell == "bash": return bash_completion() @@ -105,8 +108,9 @@ def completion_script(shell: str) -> str: def bash_completion() -> str: commands = _words(COMMANDS) global_options = _words(GLOBAL_OPTIONS) - start_options = _words(START_OPTIONS) - recreate_options = _words((*RECREATE_EXTRA_OPTIONS, *START_OPTIONS)) + run_options = _words(RUN_OPTIONS) + create_options = _words(START_OPTIONS) + recreate_options = _words(("--force", *START_OPTIONS)) shell_options = _words(SHELL_OPTIONS) ls_options = _words(LS_OPTIONS) rm_options = _words(RM_OPTIONS) @@ -159,8 +163,11 @@ def bash_completion() -> str: fi case "$cmd" in - run|create) - COMPREPLY=( $(compgen -W "{start_options}" -- "$cur") ) + run) + COMPREPLY=( $(compgen -W "{run_options}" -- "$cur") ) + ;; + create) + COMPREPLY=( $(compgen -W "{create_options}" -- "$cur") ) ;; recreate) COMPREPLY=( $(compgen -W "{recreate_options}" -- "$cur") ) @@ -187,8 +194,12 @@ def bash_completion() -> str: done if [[ -z "$subcmd" ]]; then COMPREPLY=( $(compgen -W "{network_commands}" -- "$cur") ) + elif [[ "$subcmd" == "forward" ]]; then + COMPREPLY=( $(compgen -W "--help" -- "$cur") ) elif [[ "$subcmd" == "auth-port" ]]; then COMPREPLY=( $(compgen -W "{auth_port_options}" -- "$cur") ) + elif [[ "$subcmd" == "close-auth-port" ]]; then + COMPREPLY=( $(compgen -W "--help" -- "$cur") ) elif [[ "$subcmd" == "status" ]]; then COMPREPLY=( $(compgen -W "{network_status_options}" -- "$cur") ) fi @@ -219,23 +230,39 @@ def bash_completion() -> str: def zsh_completion() -> str: - commands = " ".join(f"'{command}'" for command in COMMANDS) - start_options = " ".join(f"'{option}'" for option in START_OPTIONS) - shell_options = " ".join(f"'{option}'" for option in SHELL_OPTIONS) - network_commands = " ".join(f"'{command}'" for command in NETWORK_COMMANDS) - image_commands = " ".join(f"'{command}'" for command in IMAGE_COMMANDS) - image_build_debian_options = " ".join(f"'{option}'" for option in IMAGE_BUILD_DEBIAN_OPTIONS) - image_ls_options = " ".join(f"'{option}'" for option in IMAGE_LS_OPTIONS) - shells = " ".join(f"'{shell}'" for shell in COMPLETION_SHELLS) + commands = _zsh_words(COMMANDS) + run_options = _zsh_words(RUN_OPTIONS) + create_options = _zsh_words(START_OPTIONS) + recreate_options = _zsh_words(("--force", *START_OPTIONS)) + shell_options = _zsh_words(SHELL_OPTIONS) + ls_options = _zsh_words(LS_OPTIONS) + rm_options = _zsh_words(RM_OPTIONS) + stop_options = _zsh_words(STOP_OPTIONS) + network_commands = _zsh_words(NETWORK_COMMANDS) + auth_port_options = _zsh_words(AUTH_PORT_OPTIONS) + network_status_options = _zsh_words(NETWORK_STATUS_OPTIONS) + image_commands = _zsh_words(IMAGE_COMMANDS) + image_build_debian_options = _zsh_words(IMAGE_BUILD_DEBIAN_OPTIONS) + image_ls_options = _zsh_words(IMAGE_LS_OPTIONS) + shells = _zsh_words(COMPLETION_SHELLS) return f"""#compdef sbx # zsh completion for sbx _sbx() {{ - local -a commands start_options shell_options network_commands + local -a commands run_options create_options recreate_options shell_options + local -a ls_options rm_options stop_options network_commands + local -a auth_port_options network_status_options local -a image_commands image_build_debian_options image_ls_options shells commands=({commands}) - start_options=({start_options}) + run_options=({run_options}) + create_options=({create_options}) + recreate_options=({recreate_options}) shell_options=({shell_options}) + ls_options=({ls_options}) + rm_options=({rm_options}) + stop_options=({stop_options}) network_commands=({network_commands}) + auth_port_options=({auth_port_options}) + network_status_options=({network_status_options}) image_commands=({image_commands}) image_build_debian_options=({image_build_debian_options}) image_ls_options=({image_ls_options}) @@ -247,15 +274,38 @@ def zsh_completion() -> str: ;; *) case $words[2] in - run|create|recreate) - _describe 'option' start_options + run) + _describe 'option' run_options + ;; + create) + _describe 'option' create_options + ;; + recreate) + _describe 'option' recreate_options ;; shell) _describe 'option' shell_options ;; + ls) + _describe 'option' ls_options + ;; + rm) + _describe 'option' rm_options + ;; + stop) + _describe 'option' stop_options + ;; network) if (( CURRENT == 3 )); then _describe 'network command' network_commands + elif [[ $words[3] == "forward" ]]; then + _describe 'option' '(--help)' + elif [[ $words[3] == "auth-port" ]]; then + _describe 'option' auth_port_options + elif [[ $words[3] == "close-auth-port" ]]; then + _describe 'option' '(--help)' + elif [[ $words[3] == "status" ]]; then + _describe 'option' network_status_options else _arguments '*: :->args' fi @@ -300,11 +350,16 @@ def fish_completion() -> str: "complete -c sbx -f -n '__fish_use_subcommand' " f"-a {command}" ) - for option in START_OPTIONS: - lines.append( - "complete -c sbx -f -n '__fish_seen_subcommand_from run create recreate' " - f"{_fish_flag(option)}" - ) + for command, options in ( + ("run", RUN_OPTIONS), + ("create", START_OPTIONS), + ("recreate", ("--force", *START_OPTIONS)), + ): + for option in options: + lines.append( + f"complete -c sbx -f -n '__fish_seen_subcommand_from {command}' " + f"{_fish_flag(option)}" + ) for agent in AGENTS: lines.append( "complete -c sbx -f -n '__fish_seen_argument -l agent' " @@ -330,6 +385,17 @@ def fish_completion() -> str: "complete -c sbx -f -n '__fish_seen_subcommand_from network; " f"and not __fish_seen_subcommand_from {network_subcommands}' -a {command}" ) + for command, options in ( + ("forward", ("--help",)), + ("auth-port", AUTH_PORT_OPTIONS), + ("close-auth-port", ("--help",)), + ("status", NETWORK_STATUS_OPTIONS), + ): + for option in options: + lines.append( + f"complete -c sbx -f -n '__fish_seen_subcommand_from {command}' " + f"{_fish_flag(option)}" + ) image_subcommands = _words(IMAGE_COMMANDS) for command in IMAGE_COMMANDS: lines.append( diff --git a/src/sbx/constants.py b/src/sbx/constants.py new file mode 100644 index 0000000..61a09e6 --- /dev/null +++ b/src/sbx/constants.py @@ -0,0 +1,9 @@ +import os +from pathlib import Path + +DEFAULT_BACKEND = "qemu" +DEFAULT_BOOT_TIMEOUT = 30.0 +SBX_STATE_DIR = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state")) / "sbx" +TUNNELS_FILE = SBX_STATE_DIR / "tunnels.json" +SESSIONS_FILE = SBX_STATE_DIR / "sessions.json" +SMOLVM_DB_PATH = Path.home() / ".local" / "state" / "smolvm" / "smolvm.db" diff --git a/src/sbx/image/build_debian.py b/src/sbx/image/build_debian.py index 124687e..2f2fffc 100644 --- a/src/sbx/image/build_debian.py +++ b/src/sbx/image/build_debian.py @@ -6,8 +6,6 @@ downloads/resolves a SmolVM-compatible kernel, and prints the resulting paths. """ -from __future__ import annotations - import argparse import hashlib import json diff --git a/src/sbx/image/ls.py b/src/sbx/image/ls.py index bbd1683..c4a0441 100644 --- a/src/sbx/image/ls.py +++ b/src/sbx/image/ls.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import argparse import json from pathlib import Path diff --git a/src/sbx/network.py b/src/sbx/network.py new file mode 100644 index 0000000..fd2e37d --- /dev/null +++ b/src/sbx/network.py @@ -0,0 +1,338 @@ +import argparse +import json +import os +import signal +import socket +import subprocess +import sys +import time +from collections.abc import Mapping, Sequence +from typing import Any + +from sbx.constants import SBX_STATE_DIR, TUNNELS_FILE +from sbx.runtime import ( + ConfigError, + debug, + debug_command, + pid_is_alive, + read_json_object, + run, + run_smolvm_capture, + ssh_command, + suppress_process_errors, + vm_name_from_arg_or_config, + write_json_object, +) + + +def _validate_port(value: str, spec: str) -> int: + try: + port = int(value) + except ValueError as exc: + raise ConfigError(f"invalid port forward {spec!r}: ports must be numbers") from exc + if not 1 <= port <= 65535: + raise ConfigError(f"invalid port forward {spec!r}: ports must be between 1 and 65535") + return port + + +def parse_port_forward(spec: str) -> tuple[str, int, int]: + parts = spec.split(":") + if len(parts) == 1: + port = _validate_port(parts[0], spec) + return "127.0.0.1", port, port + if len(parts) in {2, 3}: + host = "127.0.0.1" if len(parts) == 2 else parts[0] + if not host: + raise ConfigError(f"invalid port forward {spec!r}: host address is empty") + host_port, guest_port = (_validate_port(part, spec) for part in parts[-2:]) + return host, host_port, guest_port + raise ConfigError( + f"invalid port forward {spec!r}: use GUEST_PORT, HOST_PORT:GUEST_PORT, " + "or BIND_HOST:HOST_PORT:GUEST_PORT" + ) + + +def port_forwards_from_specs(specs: Sequence[str]) -> list[dict[str, Any]]: + return [ + {"host_address": host, "host_port": host_port, "guest_port": guest_port} + for host, host_port, guest_port in (parse_port_forward(spec) for spec in specs) + ] + + +def _load_tunnels() -> dict[str, Any]: + return read_json_object(TUNNELS_FILE) + + +def _save_tunnels(data: Mapping[str, Any]) -> None: + write_json_object(TUNNELS_FILE, data, state_dir=SBX_STATE_DIR) + + +def _tracked_auth_tunnel(vm_id: str) -> dict[str, Any] | None: + tunnel = _load_tunnels().get(vm_id, {}).get("auth_port") + if not isinstance(tunnel, dict): + return None + pid = tunnel.get("pid") + if not isinstance(pid, int) or not pid_is_alive(pid): + return None + return tunnel + + +def _tracked_auth_tunnel_for_host_port(host_port: int) -> tuple[str, dict[str, Any]] | None: + for vm_id, vm_data in _load_tunnels().items(): + if not isinstance(vm_data, dict): + continue + tunnel = vm_data.get("auth_port") + if not isinstance(tunnel, dict) or tunnel.get("host_port") != host_port: + continue + pid = tunnel.get("pid") + if isinstance(pid, int) and pid_is_alive(pid): + return str(vm_id), tunnel + return None + + +def _record_auth_tunnel(vm_id: str, *, pid: int, host_port: int, guest_port: int) -> None: + data = _load_tunnels() + data.setdefault(vm_id, {})["auth_port"] = { + "pid": pid, + "host_port": host_port, + "guest_port": guest_port, + } + _save_tunnels(data) + + +def _remove_auth_tunnel_record(vm_id: str) -> None: + data = _load_tunnels() + vm_data = data.get(vm_id) + if isinstance(vm_data, dict): + vm_data.pop("auth_port", None) + if not vm_data: + data.pop(vm_id, None) + _save_tunnels(data) + + +def _localhost_port_is_listening(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(0.2) + return sock.connect_ex(("127.0.0.1", port)) == 0 + + +def _close_tracked_auth_tunnel(vm_id: str) -> bool: + tracked = _tracked_auth_tunnel(vm_id) + if tracked is None: + _remove_auth_tunnel_record(vm_id) + return False + + pid = int(tracked["pid"]) + with suppress_process_errors(): + os.killpg(pid, signal.SIGTERM) + deadline = time.monotonic() + 3 + while time.monotonic() < deadline and pid_is_alive(pid): + time.sleep(0.1) + if pid_is_alive(pid): + with suppress_process_errors(): + os.killpg(pid, signal.SIGKILL) + _remove_auth_tunnel_record(vm_id) + return True + + +def expose_auth_port(vm_id: str, host_port: int, guest_port: int, *, replace: bool = False) -> int: + debug(f"expose auth port: vm={vm_id}, host_port={host_port}, guest_port={guest_port}") + tracked = _tracked_auth_tunnel(vm_id) + if ( + tracked + and tracked.get("host_port") == host_port + and tracked.get("guest_port") == guest_port + ): + debug(f"auth host port {host_port} already tracked with pid {tracked['pid']}") + return 0 + if _localhost_port_is_listening(host_port): + owner = _tracked_auth_tunnel_for_host_port(host_port) + if owner is not None: + owner_vm, _owner_tunnel = owner + if replace: + print( + f"sbx: replacing auth tunnel on localhost:{host_port}: " + f"VM '{owner_vm}' -> VM '{vm_id}'.", + file=sys.stderr, + ) + _close_tracked_auth_tunnel(owner_vm) + else: + print( + f"sbx: warning: localhost:{host_port} is already used by the auth " + f"tunnel for VM '{owner_vm}'. `/login` in VM '{vm_id}' may not work; " + f"run `sbx network auth-port {vm_id} --replace` to switch it.", + file=sys.stderr, + ) + return 0 + else: + print( + f"sbx: warning: localhost:{host_port} is already in use and is not tracked " + "by sbx. `/login` may not work until that port is free.", + file=sys.stderr, + ) + return 0 + + cmd = ssh_command(vm_id) + cmd[-1:-1] = [ + "-N", + "-L", + f"127.0.0.1:{host_port}:127.0.0.1:{guest_port}", + "-o", + "ExitOnForwardFailure=yes", + "-o", + "BatchMode=yes", + ] + debug_command(cmd, None) + try: + proc = subprocess.Popen( + cmd, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + start_new_session=True, + ) + except FileNotFoundError: + print("sbx: command not found: ssh", file=sys.stderr) + return 127 + except OSError as exc: + print(f"sbx: failed to start auth port tunnel: {exc}", file=sys.stderr) + return 1 + + deadline = time.monotonic() + 5 + while time.monotonic() < deadline: + if proc.poll() is not None: + stderr = proc.stderr.read().strip() if proc.stderr is not None else "" + print(f"sbx: auth port tunnel exited before becoming ready: {stderr}", file=sys.stderr) + return proc.returncode or 1 + if _localhost_port_is_listening(host_port): + _record_auth_tunnel(vm_id, pid=proc.pid, host_port=host_port, guest_port=guest_port) + debug(f"auth port tunnel ready with pid {proc.pid}") + return 0 + time.sleep(0.1) + + with suppress_process_errors(): + os.killpg(proc.pid, signal.SIGTERM) + print(f"sbx: auth port tunnel did not become ready on localhost:{host_port}", file=sys.stderr) + return 1 + + +def _foreground_port_forward(vm_id: str, forward: tuple[str, int, int]) -> int: + host, host_port, guest_port = forward + cmd = ssh_command(vm_id) + cmd[-1:-1] = [ + "-N", + "-L", + f"{host}:{host_port}:127.0.0.1:{guest_port}", + "-o", + "ExitOnForwardFailure=yes", + ] + print(f"Forwarding {host}:{host_port} -> guest 127.0.0.1:{guest_port}") + print("Press Ctrl-C to stop.") + return run(cmd) + + +def cmd_forward(args: argparse.Namespace) -> int: + if len(args.forward_args) == 1: + spec = args.forward_args[0] + name = vm_name_from_arg_or_config( + args, getattr(args, "config_data", None), "network forward" + ) + elif len(args.forward_args) == 2: + name, spec = args.forward_args + else: + print("sbx: network forward expects [NAME] SPEC", file=sys.stderr) + return 2 + if name is None: + return 2 + try: + return _foreground_port_forward(str(name), parse_port_forward(spec)) + except ConfigError as exc: + print(f"sbx: {exc}", file=sys.stderr) + return 2 + + +def cmd_auth_port(args: argparse.Namespace) -> int: + name = vm_name_from_arg_or_config(args, getattr(args, "config_data", None), "network auth-port") + if name is None: + return 2 + return expose_auth_port( + name, args.host_port, args.guest_port, replace=bool(getattr(args, "replace", False)) + ) + + +def cmd_close_auth_port(args: argparse.Namespace) -> int: + name = vm_name_from_arg_or_config( + args, getattr(args, "config_data", None), "network close-auth-port" + ) + if name is None: + return 2 + if not _close_tracked_auth_tunnel(name): + print(f"No tracked auth port tunnel for '{name}'.") + return 0 + print(f"Closed auth port tunnel for '{name}'.") + return 0 + + +def _port_forward_detail(value: object) -> str | None: + if not isinstance(value, Mapping): + return None + host_address = value.get("host_address", "127.0.0.1") + host_port = value.get("host_port") + guest_port = value.get("guest_port") + if ( + not isinstance(host_address, str) + or not isinstance(host_port, int) + or not isinstance(guest_port, int) + ): + return None + return f"{host_address}:{host_port} -> guest 127.0.0.1:{guest_port}" + + +def _vm_port_forward_details(vm: Mapping[str, Any]) -> list[str]: + raw = vm.get("port_forwards") + if raw is None and isinstance(vm.get("config"), Mapping): + raw = vm["config"].get("port_forwards") + if not isinstance(raw, list): + return [] + return [detail for item in raw if (detail := _port_forward_detail(item)) is not None] + + +def cmd_status(args: argparse.Namespace) -> int: + name = vm_name_from_arg_or_config(args, getattr(args, "config_data", None), "network status") + if name is None: + return 2 + completed = run_smolvm_capture(["sandbox", "info", name, "--json"]) + if completed is None: + return 127 + if completed.returncode != 0: + if completed.stdout: + print(completed.stdout, end="") + if completed.stderr: + print(completed.stderr, end="", file=sys.stderr) + return completed.returncode + + payload = json.loads(completed.stdout) + vm = payload["data"]["vm"] + tracked = _tracked_auth_tunnel(name) + auth_status = "inactive" + auth_detail = "-" + if tracked is not None: + auth_status = "active" + auth_detail = ( + f"pid {tracked['pid']}, localhost:{tracked['host_port']} -> " + f"guest:{tracked['guest_port']}" + ) + elif _localhost_port_is_listening(args.host_port): + auth_status = "busy/untracked" + auth_detail = f"localhost:{args.host_port} is listening but is not tracked by sbx" + + print(f"Sandbox: {vm['name']}") + print(f"Status: {vm['status']}") + print(f"Backend: {vm['backend']}") + print(f"Guest IP: {vm['ip_address']}") + print(f"SSH Port: {vm['ssh_port']}") + port_forwards = _vm_port_forward_details(vm) + print("Port forwards: " + (", ".join(port_forwards) if port_forwards else "-")) + print(f"Auth callback: {auth_status}") + print(f"Auth detail: {auth_detail}") + return 0 diff --git a/src/sbx/runtime.py b/src/sbx/runtime.py new file mode 100644 index 0000000..07b6796 --- /dev/null +++ b/src/sbx/runtime.py @@ -0,0 +1,165 @@ +import argparse +import json +import os +import shlex +import shutil +import subprocess +import sys +from collections.abc import Mapping, Sequence +from pathlib import Path +from typing import Any + +DEBUG = False + + +class ConfigError(ValueError): + pass + + +def vm_name_from_arg_or_config( + args: argparse.Namespace, config: Mapping[str, Any] | None, command: str +) -> str | None: + name = getattr(args, "name", None) + sbx = (config or {}).get("sbx", {}) + if name is None and isinstance(sbx, Mapping): + name = sbx.get("name") + if not name: + print(f"sbx: {command} requires a VM name argument or [sbx].name", file=sys.stderr) + return None + return str(name) + + +def debug(message: str) -> None: + if DEBUG: + print(f"sbx debug: {message}", file=sys.stderr) + + +def debug_command(argv: Sequence[str], env: Mapping[str, str] | None) -> None: + debug(f"run: {shlex.join(list(argv))}") + active_env = env if env is not None else os.environ + env_source = "custom" if env is not None else "current" + interesting = { + key: active_env.get(key) + for key in ("HOME", "SMOLVM_DATA_DIR", "XDG_STATE_HOME") + if active_env.get(key) is not None + } + debug(f"env source: {env_source}; {interesting}") + + +def run(argv: Sequence[str], *, check: bool = False, env: Mapping[str, str] | None = None) -> int: + debug_command(argv, env) + try: + proc = subprocess.run(list(argv), check=check, env=dict(env) if env is not None else None) + except FileNotFoundError: + print(f"sbx: command not found on PATH: {argv[0]}", file=sys.stderr) + return 127 + except subprocess.CalledProcessError as exc: + debug(f"return code: {exc.returncode}") + return exc.returncode + debug(f"return code: {proc.returncode}") + return proc.returncode + + +def run_capture( + argv: Sequence[str], *, env: Mapping[str, str] | None = None +) -> subprocess.CompletedProcess[str] | None: + debug_command(argv, env) + try: + result = subprocess.run( + list(argv), + check=False, + text=True, + capture_output=True, + env=dict(env) if env is not None else None, + ) + debug(f"return code: {result.returncode}") + if result.stdout: + debug(f"stdout: {result.stdout.strip()[:2000]}") + if result.stderr: + debug(f"stderr: {result.stderr.strip()[:2000]}") + return result + except FileNotFoundError: + print(f"sbx: command not found on PATH: {argv[0]}", file=sys.stderr) + return None + + +def smolvm_argv(args: Sequence[str]) -> list[str]: + return [ + sys.executable, + "-c", + "from smolvm.cli.main import main; raise SystemExit(main())", + *args, + ] + + +def run_smolvm(args: Sequence[str], **kwargs: Any) -> int: + return run(smolvm_argv(args), **kwargs) + + +def run_smolvm_capture( + args: Sequence[str], **kwargs: Any +) -> subprocess.CompletedProcess[str] | None: + return run_capture(smolvm_argv(args), **kwargs) + + +def require(command: str, install_hint: str | None = None) -> bool: + if shutil.which(command): + return True + print(f"sbx: required command not found: {command}", file=sys.stderr) + if install_hint: + print(install_hint, file=sys.stderr) + return False + + +def missing_vm_message(vm_id: str) -> str: + return ( + f"VM {vm_id!r} not found. `sbx shell` attaches to an existing sandbox; " + f"create it with `sbx run {vm_id}` or list VMs with `sbx ls -a`." + ) + + +def ssh_command(vm_id: str) -> list[str]: + from smolvm.exceptions import VMNotFoundError + from smolvm.facade import SmolVM + + try: + vm = SmolVM.from_id(vm_id) + except VMNotFoundError as exc: + raise ConfigError(missing_vm_message(vm_id)) from exc + try: + return list(vm._ssh_direct_command()) + finally: + vm.close() + + +def read_json_object(path: Path) -> dict[str, Any]: + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (FileNotFoundError, json.JSONDecodeError): + return {} + return data if isinstance(data, dict) else {} + + +def write_json_object(path: Path, data: Mapping[str, Any], *, state_dir: Path) -> None: + state_dir.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def pid_is_alive(pid: int) -> bool: + if pid <= 0: + return False + try: + os.kill(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + +class suppress_process_errors: + def __enter__(self) -> None: + return None + + def __exit__(self, exc_type: object, exc: object, traceback: object) -> bool: + return isinstance(exc, (ProcessLookupError, PermissionError, OSError)) diff --git a/tests/test_build_debian_image.py b/tests/test_build_debian_image.py index a581662..e234756 100644 --- a/tests/test_build_debian_image.py +++ b/tests/test_build_debian_image.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import importlib import json import subprocess diff --git a/tests/test_cli.py b/tests/test_cli.py index c4cf537..f29a511 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import json import os import subprocess @@ -538,7 +536,7 @@ def fake_attach(vm_id: str, user: str, launch_command: str, cwd: str | None = No return 0 monkeypatch.setattr(cli, "_run_capture", fake_run_capture) - monkeypatch.setattr(cli, "_expose_auth_port", lambda vm_id, host_port, guest_port: 0) + monkeypatch.setattr(cli.network, "expose_auth_port", lambda vm_id, host_port, guest_port: 0) monkeypatch.setattr(cli, "_prepare_run_user", fake_prepare) monkeypatch.setattr(cli, "_attach_as_user", fake_attach) config = tmp_path / "config.toml" @@ -609,7 +607,7 @@ def test_git_config_defaults_on_for_managed_run( install_fake_smolvm(monkeypatch, tmp_path) captured: dict[str, object] = {} monkeypatch.setattr(cli, "_host_git_config", lambda: "[user]\n\tname = Test\n") - monkeypatch.setattr(cli, "_expose_auth_port", lambda vm_id, host_port, guest_port: 0) + monkeypatch.setattr(cli.network, "expose_auth_port", lambda vm_id, host_port, guest_port: 0) monkeypatch.setattr( cli, "_install_git_config", @@ -640,7 +638,7 @@ def test_no_git_config_disables_forwarding( install_fake_smolvm(monkeypatch, tmp_path) captured: dict[str, object] = {} monkeypatch.setattr(cli, "_host_git_config", lambda: "[user]\n\tname = Test\n") - monkeypatch.setattr(cli, "_expose_auth_port", lambda vm_id, host_port, guest_port: 0) + monkeypatch.setattr(cli.network, "expose_auth_port", lambda vm_id, host_port, guest_port: 0) monkeypatch.setattr( cli, "_install_git_config", @@ -1023,7 +1021,7 @@ def fake_attach(vm_id: str, launch_command: str, cwd: str | None = None) -> int: return 0 monkeypatch.setattr(cli, "_run_capture", fake_run_capture) - monkeypatch.setattr(cli, "_expose_auth_port", fake_expose) + monkeypatch.setattr(cli.network, "expose_auth_port", fake_expose) monkeypatch.setattr(cli, "_attach_as_root", fake_attach) rc = cli.main(["run", "--copy-host-credentials"]) @@ -1071,7 +1069,7 @@ def fake_run(argv: list[str], *, check: bool = False, env: dict[str, str] | None monkeypatch.setattr(cli, "_run_capture", fake_run_capture) monkeypatch.setattr(cli, "_run", fake_run) - monkeypatch.setattr(cli, "_expose_auth_port", lambda vm_id, host_port, guest_port: 0) + monkeypatch.setattr(cli.network, "expose_auth_port", lambda vm_id, host_port, guest_port: 0) monkeypatch.setattr(cli, "_attach_as_root", lambda vm_id, launch_command, cwd=None: 0) rc = cli.main(["run", "--name", "vm1"]) @@ -1216,7 +1214,7 @@ def fake_run_capture( ) monkeypatch.setattr(cli, "_run_capture", fake_run_capture) - monkeypatch.setattr(cli, "_expose_auth_port", lambda vm_id, host_port, guest_port: 0) + monkeypatch.setattr(cli.network, "expose_auth_port", lambda vm_id, host_port, guest_port: 0) monkeypatch.setattr(cli, "_attach_as_root", lambda vm_id, launch_command, cwd=None: 0) rc = cli.main(["run", "pi-sbx"]) @@ -1261,7 +1259,7 @@ def fake_run_capture( ) monkeypatch.setattr(cli, "_run_capture", fake_run_capture) - monkeypatch.setattr(cli, "_expose_auth_port", lambda vm_id, host_port, guest_port: 0) + monkeypatch.setattr(cli.network, "expose_auth_port", lambda vm_id, host_port, guest_port: 0) monkeypatch.setattr(cli, "_attach_as_root", lambda vm_id, launch_command, cwd=None: 0) rc = cli.main(["run", "--name", "vm1", "--copy-host-credentials"]) @@ -1319,11 +1317,11 @@ def test_expose_auth_port_warns_when_port_already_listening( def fail_ssh_command(vm_id: str) -> list[str]: raise AssertionError("should not create a second tunnel") - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: True) - monkeypatch.setattr(cli, "_tracked_auth_tunnel_for_host_port", lambda port: None) - monkeypatch.setattr(cli, "_ssh_command", fail_ssh_command) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: True) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel_for_host_port", lambda port: None) + monkeypatch.setattr(cli.network, "ssh_command", fail_ssh_command) - assert cli._expose_auth_port("vm1", 1455, 1455) == 0 + assert cli.network.expose_auth_port("vm1", 1455, 1455) == 0 assert "warning" in capsys.readouterr().err @@ -1351,12 +1349,12 @@ def fake_listening(port: int) -> bool: calls["listening"] += 1 return calls["listening"] > 1 - monkeypatch.setattr(cli, "_localhost_port_is_listening", fake_listening) - monkeypatch.setattr(cli, "_ssh_command", fake_ssh_command) - monkeypatch.setattr(cli.subprocess, "Popen", FakePopen) - monkeypatch.setattr(cli, "_record_auth_tunnel", lambda *args, **kwargs: None) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", fake_listening) + monkeypatch.setattr(cli.network, "ssh_command", fake_ssh_command) + monkeypatch.setattr(cli.network.subprocess, "Popen", FakePopen) + monkeypatch.setattr(cli.network, "_record_auth_tunnel", lambda *args, **kwargs: None) - rc = cli._expose_auth_port("vm1", 1455, 1455) + rc = cli.network.expose_auth_port("vm1", 1455, 1455) assert rc == 0 assert captured["vm_id"] == "vm1" @@ -1388,7 +1386,7 @@ def fake_expose(vm_id: str, host_port: int, guest_port: int, *, replace: bool = captured["expose"] = (vm_id, host_port, guest_port, replace) return 0 - monkeypatch.setattr(cli, "_expose_auth_port", fake_expose) + monkeypatch.setattr(cli.network, "expose_auth_port", fake_expose) rc = cli.main(["network", "auth-port", "vm1"]) @@ -1396,6 +1394,43 @@ def fake_expose(vm_id: str, host_port: int, guest_port: int, *, replace: bool = assert captured["expose"] == ("vm1", 1455, 1455, False) +def test_network_forward_defaults_to_configured_name( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + config = tmp_path / "config.toml" + config.write_text('[sbx]\nname = "vm1"\n', encoding="utf-8") + captured: dict[str, object] = {} + + def fake_forward(vm_id: str, forward: tuple[str, int, int]) -> int: + captured["vm_id"] = vm_id + captured["forward"] = forward + return 0 + + monkeypatch.setattr(cli.network, "_foreground_port_forward", fake_forward) + + assert cli.main(["--config", str(config), "network", "forward", "8080:3000"]) == 0 + assert captured == { + "vm_id": "vm1", + "forward": ("127.0.0.1", 8080, 3000), + } + + +def test_network_forward_accepts_explicit_name(monkeypatch: pytest.MonkeyPatch) -> None: + captured: dict[str, object] = {} + monkeypatch.setattr( + cli.network, + "_foreground_port_forward", + lambda vm_id, forward: captured.update({"vm_id": vm_id, "forward": forward}) or 0, + ) + + assert cli.main(["network", "forward", "vm2", "0.0.0.0:3000:3000"]) == 0 + assert captured == { + "vm_id": "vm2", + "forward": ("0.0.0.0", 3000, 3000), + } + + def test_network_commands_default_to_configured_name( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, @@ -1404,8 +1439,8 @@ def test_network_commands_default_to_configured_name( (tmp_path / ".sbx.toml").write_text('[sbx]\nname = "vm1"\n', encoding="utf-8") captured: dict[str, object] = {} monkeypatch.setattr( - cli, - "_expose_auth_port", + cli.network, + "expose_auth_port", lambda vm_id, host_port, guest_port, *, replace=False: captured.setdefault( "expose", (vm_id, host_port, guest_port, replace) ) @@ -1415,9 +1450,9 @@ def fake_close_auth_tunnel(vm_id: str) -> bool: captured["close"] = vm_id return False - monkeypatch.setattr(cli, "_close_tracked_auth_tunnel", fake_close_auth_tunnel) - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda vm_id: None) - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: False) + monkeypatch.setattr(cli.network, "_close_tracked_auth_tunnel", fake_close_auth_tunnel) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda vm_id: None) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: False) def fake_run_capture(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: captured["status"] = argv @@ -1430,7 +1465,7 @@ def fake_run_capture(argv: list[str], **kwargs: object) -> subprocess.CompletedP ), ) - monkeypatch.setattr(cli, "_run_smolvm_capture", fake_run_capture) + monkeypatch.setattr(cli.network, "run_smolvm_capture", fake_run_capture) assert cli.main(["network", "auth-port"]) == 0 assert cli.main(["network", "close-auth-port"]) == 0 @@ -1447,8 +1482,8 @@ def test_network_close_auth_port_without_tracked_tunnel( tmp_path: Path, capfd: pytest.CaptureFixture[str], ) -> None: - monkeypatch.setattr(cli, "SBX_STATE_DIR", tmp_path / "state") - monkeypatch.setattr(cli, "TUNNELS_FILE", tmp_path / "state" / "tunnels.json") + monkeypatch.setattr(cli.network, "SBX_STATE_DIR", tmp_path / "state") + monkeypatch.setattr(cli.network, "TUNNELS_FILE", tmp_path / "state" / "tunnels.json") rc = cli.main(["network", "close-auth-port", "vm1"]) diff --git a/tests/test_cli_extra.py b/tests/test_cli_extra.py index 0f8bc99..0b9b366 100644 --- a/tests/test_cli_extra.py +++ b/tests/test_cli_extra.py @@ -1,10 +1,9 @@ -from __future__ import annotations - import json import sqlite3 import subprocess from pathlib import Path from types import SimpleNamespace +from typing import Self import pytest import smolvm @@ -20,7 +19,8 @@ def isolated_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: monkeypatch.setattr(cli, "DEFAULT_CONFIG_PATHS", (tmp_path / "home-config.toml",)) monkeypatch.setattr(cli, "LOCAL_CONFIG_PATHS", (tmp_path / ".sbx.toml",)) monkeypatch.setattr(cli, "SBX_STATE_DIR", tmp_path / "state") - monkeypatch.setattr(cli, "TUNNELS_FILE", tmp_path / "state" / "tunnels.json") + monkeypatch.setattr(cli.network, "SBX_STATE_DIR", tmp_path / "state") + monkeypatch.setattr(cli.network, "TUNNELS_FILE", tmp_path / "state" / "tunnels.json") monkeypatch.setattr(cli, "SESSIONS_FILE", tmp_path / "state" / "sessions.json") monkeypatch.setattr(cli, "SMOLVM_DB_PATH", tmp_path / "smolvm.db") monkeypatch.setattr(cli, "_set_vm_hostname", lambda name: None) @@ -174,6 +174,21 @@ def test_cmd_start_syncs_env_before_existing_vm_attach( assert calls == ["start", "sync", "attach"] +def test_sync_existing_vm_start_config_updates_port_forwards( + capsys: pytest.CaptureFixture[str], +) -> None: + _write_vm_row(cli.SMOLVM_DB_PATH, "vm1", status="stopped", config={}) + + cli._sync_existing_vm_start_config( + "vm1", None, writable_mounts=False, port_forwards=["0.0.0.0:3000:3000"] + ) + + assert _read_vm_config(cli.SMOLVM_DB_PATH, "vm1")["port_forwards"] == [ + {"host_address": "0.0.0.0", "host_port": 3000, "guest_port": 3000} + ] + assert capsys.readouterr().out == "sbx: updated port forwards for existing VM 'vm1'\n" + + def test_cmd_start_does_not_sync_running_vm_mounts( monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: @@ -194,6 +209,20 @@ def test_cmd_start_does_not_sync_running_vm_mounts( assert calls == [] +def test_port_forward_specs_parse_forms() -> None: + assert cli.network.port_forwards_from_specs(["3000", "8080:3000", "0.0.0.0:3000:3000"]) == [ + {"host_address": "127.0.0.1", "host_port": 3000, "guest_port": 3000}, + {"host_address": "127.0.0.1", "host_port": 8080, "guest_port": 3000}, + {"host_address": "0.0.0.0", "host_port": 3000, "guest_port": 3000}, + ] + + +@pytest.mark.parametrize("spec", ["0", "65536", "abc", "1:2:3:4", ":3000:3000"]) +def test_port_forward_specs_reject_invalid(spec: str) -> None: + with pytest.raises(cli.ConfigError): + cli.network.parse_port_forward(spec) + + def test_workspace_mount_specs_parse_bare_and_explicit(tmp_path: Path) -> None: bare = tmp_path / "bare" explicit = tmp_path / "explicit" @@ -267,6 +296,7 @@ def test_start_local_image_happy_path( cwd="/workspace", git_config_text="[user]\n", forward_env=["SBX_TOKEN"], + port_forwards=["8080:3000"], ) assert rc == 0 @@ -280,6 +310,9 @@ def test_start_local_image_happy_path( assert vm_config["vm_id"] == "vm-from-cli" assert vm_config["boot_args"] == "boot args" assert vm_config["ssh_public_key"] == "ssh-ed25519 public" + assert vm_config["port_forwards"] == [ + {"host_address": "127.0.0.1", "host_port": 8080, "guest_port": 3000} + ] assert captured["launch_command"] == "custom-pi" assert captured["git_config_text"] == "[user]\n" @@ -621,7 +654,9 @@ def test_attach_commands(monkeypatch: pytest.MonkeyPatch) -> None: def test_post_start_actions_paths(monkeypatch: pytest.MonkeyPatch) -> None: calls: list[tuple[str, object]] = [] - monkeypatch.setattr(cli, "_expose_auth_port", lambda *args: calls.append(("port", args)) or 0) + monkeypatch.setattr( + cli.network, "expose_auth_port", lambda *args: calls.append(("port", args)) or 0 + ) monkeypatch.setattr(cli, "_register_session", lambda *args: calls.append(("register", args))) monkeypatch.setattr( cli, "_unregister_session", lambda *args: calls.append(("unregister", args)) @@ -680,17 +715,22 @@ def test_post_start_actions_paths(monkeypatch: pytest.MonkeyPatch) -> None: def test_tunnel_and_session_state(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(cli, "_pid_is_alive", lambda pid: pid == 123) - cli._record_auth_tunnel("vm1", pid=123, host_port=1455, guest_port=1455) - assert cli._tracked_auth_tunnel("vm1") == {"pid": 123, "host_port": 1455, "guest_port": 1455} - assert cli._tracked_auth_tunnel_for_host_port(1455) == ( + monkeypatch.setattr(cli.network, "pid_is_alive", lambda pid: pid == 123) + cli.network._record_auth_tunnel("vm1", pid=123, host_port=1455, guest_port=1455) + assert cli.network._tracked_auth_tunnel("vm1") == { + "pid": 123, + "host_port": 1455, + "guest_port": 1455, + } + assert cli.network._tracked_auth_tunnel_for_host_port(1455) == ( "vm1", {"pid": 123, "host_port": 1455, "guest_port": 1455}, ) - assert cli._tracked_auth_tunnel_for_host_port(9999) is None - cli._remove_auth_tunnel_record("vm1") - assert cli._tracked_auth_tunnel("vm1") is None - cli._save_tunnels({"bad": [], "dead": {"auth_port": {"pid": 999, "host_port": 1}}}) - assert cli._tracked_auth_tunnel_for_host_port(1) is None + assert cli.network._tracked_auth_tunnel_for_host_port(9999) is None + cli.network._remove_auth_tunnel_record("vm1") + assert cli.network._tracked_auth_tunnel("vm1") is None + cli.network._save_tunnels({"bad": [], "dead": {"auth_port": {"pid": 999, "host_port": 1}}}) + assert cli.network._tracked_auth_tunnel_for_host_port(1) is None cli._save_sessions( {"vm1": {"sessions": [{"pid": 123, "kind": "run"}, {"pid": 999, "kind": "run"}]}} @@ -710,18 +750,20 @@ def test_expose_auth_port_error_paths( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: monkeypatch.setattr( - cli, "_tracked_auth_tunnel", lambda vm_id: {"pid": 123, "host_port": 1, "guest_port": 2} + cli.network, + "_tracked_auth_tunnel", + lambda vm_id: {"pid": 123, "host_port": 1, "guest_port": 2}, ) - assert cli._expose_auth_port("vm1", 1, 2) == 0 + assert cli.network.expose_auth_port("vm1", 1, 2) == 0 - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda vm_id: None) - monkeypatch.setattr(cli, "_tracked_auth_tunnel_for_host_port", lambda port: ("vm2", {})) - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: True) - assert cli._expose_auth_port("vm1", 1, 2) == 0 + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda vm_id: None) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel_for_host_port", lambda port: ("vm2", {})) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: True) + assert cli.network.expose_auth_port("vm1", 1, 2) == 0 assert "VM 'vm2'" in capsys.readouterr().err - monkeypatch.setattr(cli, "_tracked_auth_tunnel_for_host_port", lambda port: None) - assert cli._expose_auth_port("vm1", 1, 2) == 0 + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel_for_host_port", lambda port: None) + assert cli.network.expose_auth_port("vm1", 1, 2) == 0 assert "not tracked by sbx" in capsys.readouterr().err class ExitedProcess: @@ -732,10 +774,10 @@ class ExitedProcess: def poll(self) -> int: return 1 - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: False) - monkeypatch.setattr(cli, "_ssh_command", lambda vm_id: ["ssh", "root@host"]) - monkeypatch.setattr(cli.subprocess, "Popen", lambda *args, **kwargs: ExitedProcess()) - assert cli._expose_auth_port("vm1", 1, 2) == 1 + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: False) + monkeypatch.setattr(cli.network, "ssh_command", lambda vm_id: ["ssh", "root@host"]) + monkeypatch.setattr(cli.network.subprocess, "Popen", lambda *args, **kwargs: ExitedProcess()) + assert cli.network.expose_auth_port("vm1", 1, 2) == 1 def test_delete_vm_error_paths( @@ -768,11 +810,11 @@ def test_network_status_variants( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: monkeypatch.setattr( - cli, - "_run_capture", + cli.network, + "run_smolvm_capture", lambda argv: subprocess.CompletedProcess(argv, 1, stdout="bad out\n", stderr="bad err\n"), ) - assert cli.cmd_network_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 1 + assert cli.network.cmd_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 1 captured = capsys.readouterr() assert "bad out" in captured.out assert "bad err" in captured.err @@ -791,19 +833,21 @@ def test_network_status_variants( } ) monkeypatch.setattr( - cli, - "_run_capture", + cli.network, + "run_smolvm_capture", lambda argv: subprocess.CompletedProcess(argv, 0, stdout=payload, stderr=""), ) monkeypatch.setattr( - cli, "_tracked_auth_tunnel", lambda name: {"pid": 123, "host_port": 1, "guest_port": 2} + cli.network, + "_tracked_auth_tunnel", + lambda name: {"pid": 123, "host_port": 1, "guest_port": 2}, ) - assert cli.cmd_network_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 0 + assert cli.network.cmd_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 0 assert "active" in capsys.readouterr().out - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda name: None) - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: True) - assert cli.cmd_network_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 0 + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda name: None) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: True) + assert cli.network.cmd_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 0 assert "busy/untracked" in capsys.readouterr().out @@ -816,7 +860,7 @@ class FakeSmolVM: _info = SimpleNamespace(config=SimpleNamespace(comm_channel="ssh")) @classmethod - def from_id(cls, vm_id: str, **kwargs: object) -> FakeSmolVM: + def from_id(cls, vm_id: str, **kwargs: object) -> Self: calls.append(("from_id", kwargs)) return cls() @@ -854,7 +898,7 @@ class FakeSmolVM: _info = SimpleNamespace(config=SimpleNamespace(comm_channel=None)) @classmethod - def from_id(cls, vm_id: str, **kwargs: object) -> FakeSmolVM: + def from_id(cls, vm_id: str, **kwargs: object) -> Self: calls.append(("from_id", kwargs)) return cls() @@ -883,7 +927,7 @@ def test_sync_forwarded_env_empty_allowlist_skips_smolvm( ) -> None: class FakeSmolVM: @classmethod - def from_id(cls, vm_id: str) -> FakeSmolVM: + def from_id(cls, vm_id: str) -> Self: raise AssertionError("SmolVM should not be opened") monkeypatch.setattr(smolvm.facade, "SmolVM", FakeSmolVM, raising=False) @@ -948,19 +992,23 @@ def test_main_config_error( def test_expose_auth_port_missing_ssh_oserror_and_timeout( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda vm_id: None) - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: False) - monkeypatch.setattr(cli, "_ssh_command", lambda vm_id: ["ssh", "root@host"]) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda vm_id: None) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: False) + monkeypatch.setattr(cli.network, "ssh_command", lambda vm_id: ["ssh", "root@host"]) monkeypatch.setattr( - cli.subprocess, "Popen", lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError()) + cli.network.subprocess, + "Popen", + lambda *args, **kwargs: (_ for _ in ()).throw(FileNotFoundError()), ) - assert cli._expose_auth_port("vm1", 1, 2) == 127 + assert cli.network.expose_auth_port("vm1", 1, 2) == 127 assert "command not found: ssh" in capsys.readouterr().err monkeypatch.setattr( - cli.subprocess, "Popen", lambda *args, **kwargs: (_ for _ in ()).throw(OSError("nope")) + cli.network.subprocess, + "Popen", + lambda *args, **kwargs: (_ for _ in ()).throw(OSError("nope")), ) - assert cli._expose_auth_port("vm1", 1, 2) == 1 + assert cli.network.expose_auth_port("vm1", 1, 2) == 1 assert "failed to start auth port tunnel" in capsys.readouterr().err class RunningProcess: @@ -971,31 +1019,33 @@ def poll(self) -> None: return None times = iter([0.0, 6.0]) - monkeypatch.setattr(cli.time, "monotonic", lambda: next(times)) - monkeypatch.setattr(cli.time, "sleep", lambda seconds: None) + monkeypatch.setattr(cli.network.time, "monotonic", lambda: next(times)) + monkeypatch.setattr(cli.network.time, "sleep", lambda seconds: None) killed: list[tuple[int, int]] = [] - monkeypatch.setattr(cli.os, "killpg", lambda pid, sig: killed.append((pid, sig))) - monkeypatch.setattr(cli.subprocess, "Popen", lambda *args, **kwargs: RunningProcess()) - assert cli._expose_auth_port("vm1", 1, 2) == 1 - assert killed == [(123, cli.signal.SIGTERM)] + monkeypatch.setattr(cli.network.os, "killpg", lambda pid, sig: killed.append((pid, sig))) + monkeypatch.setattr(cli.network.subprocess, "Popen", lambda *args, **kwargs: RunningProcess()) + assert cli.network.expose_auth_port("vm1", 1, 2) == 1 + assert killed == [(123, cli.network.signal.SIGTERM)] assert "did not become ready" in capsys.readouterr().err def test_close_auth_port_kills_tracked_tunnel( monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda name: {"pid": 123}) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda name: {"pid": 123}) removed: list[str] = [] - monkeypatch.setattr(cli, "_remove_auth_tunnel_record", lambda name: removed.append(name)) + monkeypatch.setattr( + cli.network, "_remove_auth_tunnel_record", lambda name: removed.append(name) + ) states = iter([True, False, False]) - monkeypatch.setattr(cli, "_pid_is_alive", lambda pid: next(states)) - monkeypatch.setattr(cli.time, "monotonic", lambda: 0) - monkeypatch.setattr(cli.time, "sleep", lambda seconds: None) + monkeypatch.setattr(cli.network, "pid_is_alive", lambda pid: next(states)) + monkeypatch.setattr(cli.network.time, "monotonic", lambda: 0) + monkeypatch.setattr(cli.network.time, "sleep", lambda seconds: None) killed: list[tuple[int, int]] = [] - monkeypatch.setattr(cli.os, "killpg", lambda pid, sig: killed.append((pid, sig))) + monkeypatch.setattr(cli.network.os, "killpg", lambda pid, sig: killed.append((pid, sig))) - assert cli.cmd_close_auth_port(type("Args", (), {"name": "vm1"})()) == 0 - assert killed == [(123, cli.signal.SIGTERM)] + assert cli.network.cmd_close_auth_port(type("Args", (), {"name": "vm1"})()) == 0 + assert killed == [(123, cli.network.signal.SIGTERM)] assert removed == ["vm1"] assert "Closed auth port" in capsys.readouterr().out @@ -1017,14 +1067,14 @@ def test_network_status_inactive( } ) monkeypatch.setattr( - cli, - "_run_capture", + cli.network, + "run_smolvm_capture", lambda argv: subprocess.CompletedProcess(argv, 0, stdout=payload, stderr=""), ) - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda name: None) - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: False) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda name: None) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: False) - assert cli.cmd_network_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 0 + assert cli.network.cmd_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 0 assert "Auth callback: inactive" in capsys.readouterr().out @@ -1084,7 +1134,7 @@ def test_read_toml_oserror_and_create_alias( def test_post_start_actions_auth_port_failure_skips_attach(monkeypatch: pytest.MonkeyPatch) -> None: calls: list[str] = [] - monkeypatch.setattr(cli, "_expose_auth_port", lambda *args: calls.append("port") or 9) + monkeypatch.setattr(cli.network, "expose_auth_port", lambda *args: calls.append("port") or 9) monkeypatch.setattr(cli, "_attach_as_root", lambda *args, **kwargs: calls.append("attach") or 0) assert ( @@ -1104,18 +1154,18 @@ def test_post_start_actions_auth_port_failure_skips_attach(monkeypatch: pytest.M def test_close_auth_port_escalates_to_sigkill(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda name: {"pid": 123}) - monkeypatch.setattr(cli, "_remove_auth_tunnel_record", lambda name: None) - monkeypatch.setattr(cli.time, "sleep", lambda seconds: None) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda name: {"pid": 123}) + monkeypatch.setattr(cli.network, "_remove_auth_tunnel_record", lambda name: None) + monkeypatch.setattr(cli.network.time, "sleep", lambda seconds: None) times = iter([0.0, 4.0]) - monkeypatch.setattr(cli.time, "monotonic", lambda: next(times)) + monkeypatch.setattr(cli.network.time, "monotonic", lambda: next(times)) alive = iter([True, True]) - monkeypatch.setattr(cli, "_pid_is_alive", lambda pid: next(alive)) + monkeypatch.setattr(cli.network, "pid_is_alive", lambda pid: next(alive)) killed: list[int] = [] - monkeypatch.setattr(cli.os, "killpg", lambda pid, sig: killed.append(sig)) + monkeypatch.setattr(cli.network.os, "killpg", lambda pid, sig: killed.append(sig)) - assert cli.cmd_close_auth_port(type("Args", (), {"name": "vm1"})()) == 0 - assert killed == [cli.signal.SIGTERM, cli.signal.SIGKILL] + assert cli.network.cmd_close_auth_port(type("Args", (), {"name": "vm1"})()) == 0 + assert killed == [cli.network.signal.SIGTERM, cli.network.signal.SIGKILL] def test_stop_vm_skips_when_other_sessions_active(monkeypatch: pytest.MonkeyPatch) -> None: @@ -1322,9 +1372,9 @@ def test_local_image_qcow2_disk_size_does_not_request_filesystem_growth( def test_network_status_run_capture_missing(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(cli, "_run_capture", lambda argv: None) + monkeypatch.setattr(cli.network, "run_smolvm_capture", lambda argv: None) - assert cli.cmd_network_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 127 + assert cli.network.cmd_status(type("Args", (), {"name": "vm1", "host_port": 1455})()) == 127 def test_confirm_destructive_action_noninteractive_and_yes( @@ -1454,13 +1504,13 @@ def test_start_existing_vm_timeout_hint_when_vm_is_running( def test_auth_tunnel_success_records_when_port_becomes_ready( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.setattr(cli, "_tracked_auth_tunnel", lambda vm_id: None) + monkeypatch.setattr(cli.network, "_tracked_auth_tunnel", lambda vm_id: None) readiness = iter([False, True]) - monkeypatch.setattr(cli, "_localhost_port_is_listening", lambda port: next(readiness)) - monkeypatch.setattr(cli, "_ssh_command", lambda vm_id: ["ssh", "root@host"]) + monkeypatch.setattr(cli.network, "_localhost_port_is_listening", lambda port: next(readiness)) + monkeypatch.setattr(cli.network, "ssh_command", lambda vm_id: ["ssh", "root@host"]) recorded: list[tuple[str, int, int, int]] = [] monkeypatch.setattr( - cli, + cli.network, "_record_auth_tunnel", lambda vm_id, *, pid, host_port, guest_port: recorded.append( (vm_id, pid, host_port, guest_port) @@ -1473,9 +1523,9 @@ class RunningProcess: def poll(self) -> None: return None - monkeypatch.setattr(cli.subprocess, "Popen", lambda *args, **kwargs: RunningProcess()) + monkeypatch.setattr(cli.network.subprocess, "Popen", lambda *args, **kwargs: RunningProcess()) - assert cli._expose_auth_port("vm1", 1455, 1455) == 0 + assert cli.network.expose_auth_port("vm1", 1455, 1455) == 0 assert recorded == [("vm1", 321, 1455, 1455)] @@ -1501,9 +1551,9 @@ def test_unregister_session_keeps_other_active_sessions(monkeypatch: pytest.Monk def test_network_auth_port_wrapper(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( - cli, - "_expose_auth_port", + cli.network, + "expose_auth_port", lambda name, host, guest, *, replace=False: (name, host, guest, replace), ) args = type("Args", (), {"name": "vm1", "host_port": 1, "guest_port": 2, "replace": True})() - assert cli.cmd_auth_port(args) == ("vm1", 1, 2, True) + assert cli.network.cmd_auth_port(args) == ("vm1", 1, 2, True) diff --git a/tests/test_completion.py b/tests/test_completion.py index dc0b11c..71b4675 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import subprocess import pytest @@ -26,6 +24,25 @@ def test_bash_completion_includes_agent_values() -> None: assert "pi claude codex" in script +def test_completion_scripts_include_all_command_option_groups() -> None: + for shell in SUPPORTED_SHELLS: + script = completion_script(shell) + for value in ( + "forward", + "auth-port", + "close-auth-port", + "status", + "build-debian", + "force-start", + "force", + "host-port", + "guest-port", + "replace", + "rootfs-size-mb", + ): + assert value in script + + def test_bash_completion_completes_redirection_targets(tmp_path) -> None: target = tmp_path / "sbx.sh" target.touch() diff --git a/uv.lock b/uv.lock index 9266933..0fa2b48 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.12" [[package]] name = "annotated-types" @@ -75,10 +75,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, - { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, ] [[package]] @@ -99,31 +95,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, - { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, - { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, - { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, - { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, - { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, - { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, - { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, - { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, - { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, - { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, - { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, - { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, @@ -215,32 +186,6 @@ version = "3.4.9" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, - { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, - { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, - { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, - { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, - { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, - { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, - { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, @@ -323,35 +268,6 @@ version = "7.15.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/97/c52dc440c390b6cfa87be9432b141a956e2d56d9b9f5fc8bd71c5f471722/coverage-7.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:50913d4bf5ddafa6ca3693da5e4dd833dd1b772e0283c99ca7f7d287db67331a", size = 220539, upload-time = "2026-07-02T13:08:19.252Z" }, - { url = "https://files.pythonhosted.org/packages/3f/26/602de8c2aec7e2e3e99ebfb8e04ba65598f746275396eea5f6794ff4673f/coverage-7.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:359e141ccd33893ce3f1ad5525f8b96083003677c82182e5907d62d4ea5799fc", size = 221058, upload-time = "2026-07-02T13:08:21.013Z" }, - { url = "https://files.pythonhosted.org/packages/fc/13/ebab0743138891c1d646d61e247ec29639afcbb6c4e1905e6a0f0c75291a/coverage-7.15.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3200b6204935f928c64b2ca1f923ab8c1acb7c9de45ec61569711b34d25cccaf", size = 247797, upload-time = "2026-07-02T13:08:22.474Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b7/b6ffb9e042aa48dc4144a8a65529affaec8dca0685309353614a2a7386ad/coverage-7.15.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be616bf61346883b2cfdc5178669647e03531d81ab761a7e378558b7e8bcb628", size = 249626, upload-time = "2026-07-02T13:08:23.803Z" }, - { url = "https://files.pythonhosted.org/packages/9c/06/243ff05b652333d8e3d060c11223efc2723b19cacf6605e433fa686ab5d4/coverage-7.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7bafc3fe1059463a8fdd97ca79972d6e2bf819d775c7d54991b5b1971201d6", size = 251493, upload-time = "2026-07-02T13:08:25.397Z" }, - { url = "https://files.pythonhosted.org/packages/d3/2b/867faa17030a806114dae388b32a3fa929d8cd4bf39226fbc11f6e6bb705/coverage-7.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b713aa7fcf325a01d4184d848acb46fd84f78fdb0978470c636b23a06a753d91", size = 253406, upload-time = "2026-07-02T13:08:26.842Z" }, - { url = "https://files.pythonhosted.org/packages/94/c0/d789ce18f6605afc4895db75723424be2ef494282f77f61d8e5832923183/coverage-7.15.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e38e6fba2d56652fdfaf0231f8f78aeb805234a912de25dc291ee5cce5b8faa4", size = 248512, upload-time = "2026-07-02T13:08:28.398Z" }, - { url = "https://files.pythonhosted.org/packages/c9/b6/b2673c30739f4a2e06649a0a38ad8b093c4d865462dc7bab0e9524a2c3b1/coverage-7.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:884499f42e382675be80770391983b90e0c0c774d87dbeeebf5f991cf6612b20", size = 249532, upload-time = "2026-07-02T13:08:29.731Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/acd79e9a41beabee92b623afe4f30b549916f48566271475f2907e752828/coverage-7.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:840481b12e083dbcbafab14794a8781a958edf327c8d3d70b4eee42f9b8253aa", size = 247537, upload-time = "2026-07-02T13:08:31.173Z" }, - { url = "https://files.pythonhosted.org/packages/12/d4/2d301c4d1b3238d7c88b70ab9d13fd53ed9505662a7ff1b46ba1e2e4e3c3/coverage-7.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:276646e9481703d09f854f3b2f018f24e19fd7049ae670a92570043eb97203b1", size = 251348, upload-time = "2026-07-02T13:08:32.63Z" }, - { url = "https://files.pythonhosted.org/packages/35/bb/c67708b2bc00f32e12805ec23d5fa677a0a51652f449341a89f9d6b1b715/coverage-7.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4de4b4d3f5545aa6c60dc4efd9c63b5b5dcc3bf00fe83146b2bdfffb8f6613bd", size = 247806, upload-time = "2026-07-02T13:08:33.931Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6c/57c4f653c47a6e917748f8938e389e72fbcae44e3643cd906664f0477a13/coverage-7.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5c504097b2a89b1e85bc6070d920df77daec701337e3aeef2c17775a5dd0ca90", size = 248410, upload-time = "2026-07-02T13:08:35.189Z" }, - { url = "https://files.pythonhosted.org/packages/6c/94/bb083041aef828903668f134273f319f2bd49224962875359c52faa5497f/coverage-7.15.0-cp310-cp310-win32.whl", hash = "sha256:f6e80ed91f98316e86b9c137206b04b2bcfbffccbdff49bd2eb09dddb1cf14e0", size = 222588, upload-time = "2026-07-02T13:08:36.486Z" }, - { url = "https://files.pythonhosted.org/packages/ef/94/a09d8ee618956f626741b0734854bac4425a00e10c0565f5abca64e7e751/coverage-7.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:b3b3e22030f3f6f5e01a5ce69936552a5c0f6992b7698777377b99041961031f", size = 223214, upload-time = "2026-07-02T13:08:37.885Z" }, - { url = "https://files.pythonhosted.org/packages/ae/23/82e910835ef4b8391047025e1d53aa48d66029f444eb8b25373c849bf503/coverage-7.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:003fff99412ea848c0aaebcc78ed2b6ce7d8a1227ed17e68470672770b78a02a", size = 220662, upload-time = "2026-07-02T13:08:39.205Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0d/c7b213dde2f1579de5231062b386d8413f79c11667eb58c39319b25991da/coverage-7.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cbd804bf2784ce7b45114516050f346ecd50f960c4bb630a7ee9e1d78fa2118", size = 221168, upload-time = "2026-07-02T13:08:40.471Z" }, - { url = "https://files.pythonhosted.org/packages/33/77/d000aeedfac085088337b3c7becdad328474b1f8a9e4c9368a0c99605d68/coverage-7.15.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8773e15c23305b58882a4611fb9b2755977eae0dc2e515366a1b6c98866cc4c2", size = 251587, upload-time = "2026-07-02T13:08:42.033Z" }, - { url = "https://files.pythonhosted.org/packages/cc/e0/86787c56b9df17afd370d5e293515dd4d9a107a561d13054873eefad8ecc/coverage-7.15.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f50e40081494c1dc4239ebb202014cbcc3306ea96fb6302a34c8cc0967fc5ae8", size = 253497, upload-time = "2026-07-02T13:08:43.387Z" }, - { url = "https://files.pythonhosted.org/packages/3f/02/181bc917359299c07dead6270f94e411151c8b60cec905c33499da69afe6/coverage-7.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daf96f37f5fc3a7b6c6da862eb4aee61c426bd63da236ed4a73ef0e503b4bca5", size = 255607, upload-time = "2026-07-02T13:08:44.897Z" }, - { url = "https://files.pythonhosted.org/packages/b9/35/ca5e7427699913da6788c4f910e73ab16c5f4b59ec5d3a999dce2a45112f/coverage-7.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:51aa20f6ae2788fd197747766edf4cd8234fd9423309b934257fa6b21a592723", size = 257563, upload-time = "2026-07-02T13:08:46.334Z" }, - { url = "https://files.pythonhosted.org/packages/0b/4d/b8220bacc2fc3c4e9078e27c32e99fb411479a4718a72bdd00036a9891c8/coverage-7.15.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03d1f922757662eb7af586e77834792274cff776bc7b1d1a0b66a49ea9d84735", size = 251726, upload-time = "2026-07-02T13:08:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/c4/e4/2e145da1991d72189b9c3cf7eca05c716ee7080d099aaea6757cfc7df008/coverage-7.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a6d6acc9a7666245e6133dd15144ca038a85a9cd5026bb06d6bbae9e77440dc9", size = 253301, upload-time = "2026-07-02T13:08:49.5Z" }, - { url = "https://files.pythonhosted.org/packages/72/28/d2c841d698bf762e481f08bd4839d370246b6d9b61dab085a7b20b201a08/coverage-7.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1ac2c4c27c7df851dc9a017c2d7de00b69147e84ba3d96f37a530b0b6fb51035", size = 251361, upload-time = "2026-07-02T13:08:51.304Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ed/55d9ffde994fba3897c0c783f77a7d053b0c18787f6892ed5b0aed73f469/coverage-7.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b761a1d504fd4bd1f20f418753964dca9f5862a511fc854dac58296b3b223671", size = 255129, upload-time = "2026-07-02T13:08:52.661Z" }, - { url = "https://files.pythonhosted.org/packages/1d/c0/ecbf33b8c460ea2718aeb813e2df8140d0370e5f67261c31524ceb0a2a8d/coverage-7.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e43b045e11c16e897895758ae90e4a90cf99e93d58549e2f90c0e2272e155695", size = 251081, upload-time = "2026-07-02T13:08:54.188Z" }, - { url = "https://files.pythonhosted.org/packages/a9/de/fb87b4261f54448dd2b9504ef19a58be42cef0d9520595fbfe1219b15234/coverage-7.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:589b54513e901739f4b4582c705ce96b80c96f57641b1464607e2367a270e540", size = 251988, upload-time = "2026-07-02T13:08:55.726Z" }, - { url = "https://files.pythonhosted.org/packages/df/27/3494d5f291b9a4cb868f73c11221a8bd2d5bd761a8f9acea61ff57128dd1/coverage-7.15.0-cp311-cp311-win32.whl", hash = "sha256:106781b8482749162d0b47056937ba0933508e5d9447f65a5e7d5c422f0d6bb4", size = 222754, upload-time = "2026-07-02T13:08:57.091Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ee/cd4847ebc9be6a9c0123d763645a6f1f3be6b8c58c962706368b79cbac07/coverage-7.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:821e92b3631d762a339695824cadbbc73020354eba2a23a551a99ad34938fbe6", size = 223225, upload-time = "2026-07-02T13:08:58.594Z" }, - { url = "https://files.pythonhosted.org/packages/57/37/5011581aa7f2be498b97dcc7c9902192442a42f4f9a748aeadb3d6506b42/coverage-7.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:309990eb5fb8014b9f67cb211f7fd41876ec8a88a88d3ae76de0ed1d611e3640", size = 222774, upload-time = "2026-07-02T13:09:00.074Z" }, { url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, @@ -415,18 +331,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, ] -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - [[package]] name = "cryptography" version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } wheels = [ @@ -469,12 +379,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, - { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, - { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, - { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, ] [[package]] @@ -486,18 +390,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - [[package]] name = "filelock" version = "3.29.7" @@ -673,35 +565,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, - { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, - { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, - { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, - { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, - { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, @@ -762,22 +625,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, - { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, - { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, - { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] @@ -830,12 +681,10 @@ version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -847,7 +696,7 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", extra = ["toml"] }, + { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] @@ -875,24 +724,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, @@ -992,7 +823,6 @@ version = "0.2.4.dev0" source = { editable = "." } dependencies = [ { name = "smolvm" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] [package.optional-dependencies] @@ -1010,7 +840,6 @@ requires-dist = [ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=7.1.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.21" }, { name = "smolvm", specifier = "==0.0.26" }, - { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.4.1" }, ] provides-extras = ["dev"] @@ -1039,14 +868,6 @@ version = "2026.6.24" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/39/e2/0493883fa4a926d66e1e423576e2a3c07ae754df111132e45177db59c9da/smolvm_core-2026.6.24.tar.gz", hash = "sha256:d82cfe28adf01dd3177dfde9b1023da51f4097746fe60d85a3ead7d8535d89c9", size = 34524, upload-time = "2026-06-24T13:52:14.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/49/1b79b16c0c9b03e9e34cc55913e3eb77cf6f094cc0e20e6eac51d08718b7/smolvm_core-2026.6.24-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:18b4f73417b8507bab3dbfc5ca681d0d2ca91f3b791d18b2d0c81eaf79f0f9ea", size = 468757, upload-time = "2026-06-24T13:51:37.418Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a5/a9ee0b0939e439c176b8ba2bab86e35b53b9daf040626bebbf75cf898b7f/smolvm_core-2026.6.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3f98fb2d558146aa60c339dd9ad368c3b922a39996a0126f8664fcbc52c722b", size = 405031, upload-time = "2026-06-24T13:51:38.813Z" }, - { url = "https://files.pythonhosted.org/packages/cd/cc/69b32ff72b6e9a5c2c2d3439e28a2f8bd87157d6366f924fa2e88e940f21/smolvm_core-2026.6.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93a41f0161934c49231695a5575adec02cc8990162db6b618fe8000fdd228212", size = 913580, upload-time = "2026-06-24T13:51:40.222Z" }, - { url = "https://files.pythonhosted.org/packages/f1/1f/1bb40109450e6cd378da8e175d30a717ad7bf1bd32b4d3e75c7552928ea1/smolvm_core-2026.6.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a2c03cfbcfed588886414014b6a0b539249535618270daa62de2866619fc73c", size = 1037583, upload-time = "2026-06-24T13:51:41.563Z" }, - { url = "https://files.pythonhosted.org/packages/18/a8/4b12d25d99e312606d276d8805188fb1bc9bf838e2d4ec19fe9c483c2bb1/smolvm_core-2026.6.24-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:44d077f25d1e926aae58e5a47bc2483bf00ef8e23f7e95d622a9242c4d91bb3e", size = 468652, upload-time = "2026-06-24T13:51:42.849Z" }, - { url = "https://files.pythonhosted.org/packages/02/4a/763d72763334e7c5e41ac0fd9282ab1a33980706f256f660afca456dc5b4/smolvm_core-2026.6.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4df86e175d5d9a73e5ee39e5fd8143f18999dc5d908717693e08a968ea181ba", size = 404962, upload-time = "2026-06-24T13:51:44.151Z" }, - { url = "https://files.pythonhosted.org/packages/72/38/d9ce109b60a4268cd1c5d2818913339d4f0f62d1d4062c695107ed19613c/smolvm_core-2026.6.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b434b55b6f484b36b71a22ae0f566c10e5a23fc743adf7ebd60152ecaf7e022a", size = 913425, upload-time = "2026-06-24T13:51:45.382Z" }, - { url = "https://files.pythonhosted.org/packages/9f/44/bddc8a0778b1d0b59d3154279d6b55012b53644d305404aef9bc658cba22/smolvm_core-2026.6.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ed3685b4a701550dd762d875bb8fa717570a8302f8813eb49f647dd576de37b", size = 1036874, upload-time = "2026-06-24T13:51:46.666Z" }, { url = "https://files.pythonhosted.org/packages/6f/fe/cfa06c0752a47071d88b35f42244a929fc0db1df420b6b5303e683798b86/smolvm_core-2026.6.24-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:cb7e94e27efad193e1238ae42a4bdeac4ab68660f78864c42006bf141b370424", size = 467435, upload-time = "2026-06-24T13:51:48.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/5d/454e5e291604e1e31edbde625b22e52112bf9eae2ceedb0f3999c9fe716f/smolvm_core-2026.6.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a2d912d627e66fe56824716749c13e2e3d124e84e4086f56c6c31c4288ff0bd5", size = 404587, upload-time = "2026-06-24T13:51:49.653Z" }, { url = "https://files.pythonhosted.org/packages/6a/f1/13c6328ba81a7ac32c69163de0fa6a0662407537d7dbd92c66e28f2c5d58/smolvm_core-2026.6.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a45fc30bb5d3ca75837fbce8519132ecae2fc7f0b8e170b1c9dc5f9844a9015b", size = 912502, upload-time = "2026-06-24T13:51:50.897Z" }, @@ -1063,62 +884,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/46/e8255f8b0430377c07f90926436e7f9093e619feeea515a950e51b7b7cd6/smolvm_core-2026.6.24-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39b2dc781c846c2c16ab1ec35523bffa82056899f54b6da9f46f6763f51910f7", size = 1034834, upload-time = "2026-06-24T13:52:07.263Z" }, { url = "https://files.pythonhosted.org/packages/ea/1b/a2d885db9fea593a87f9977dfb0e030f5b24aa6b682fa59fd63b9ee4b68f/smolvm_core-2026.6.24-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e108991fe94a17ecbd37a7663633c882014614e1dc30ad8cffd2e343362ebcd6", size = 1036842, upload-time = "2026-06-24T13:52:08.694Z" }, { url = "https://files.pythonhosted.org/packages/65/5d/677598029baa7e5aabe569f5011002c3e69344555e3fd260a1b816cab7c2/smolvm_core-2026.6.24-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec7b8bbece3cee67cd2efe8f58b79ce6648b5ed83b703e6d4b90be68effc2b5", size = 1034837, upload-time = "2026-06-24T13:52:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/6d/65/3c8f60d7b7d5550f6b69d5a9a2317ee802f34216b9b150294c4ca3aa5283/smolvm_core-2026.6.24-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47472c5d88d3d7d687b8527d7f1cc35998cfcda00e62277ac2fd9217f7b372", size = 914715, upload-time = "2026-06-24T13:52:11.402Z" }, - { url = "https://files.pythonhosted.org/packages/49/fa/13097ab69d42da4a27eb34f0af9607170c8610580cd9ca4316237c2e7c67/smolvm_core-2026.6.24-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b48b4f6a9cd3acf646c9acdf3d2a9a13556500df8604ddce2eb3bcf63d2eb6", size = 1038060, upload-time = "2026-06-24T13:52:12.658Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] @@ -1160,7 +925,6 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/d9/b477fddb68840b570af8b22afe9b035cbc277b5fb7b33dea390617a8b10f/virtualenv-21.6.1.tar.gz", hash = "sha256:15f978b7cd329f24855ff4a0c4b4899cc7678589f49adbdcbbb4d3232e641128", size = 5526620, upload-time = "2026-07-10T19:33:53.312Z" } wheels = [ @@ -1173,39 +937,6 @@ version = "0.25.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, - { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, - { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, - { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, - { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, - { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, - { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, - { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, - { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, - { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, - { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, - { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" },