diff --git a/.env.example b/.env.example index 584bb4f9..d4994f04 100644 --- a/.env.example +++ b/.env.example @@ -19,5 +19,11 @@ FIREWORKS_API_KEY="your_fireworks_api_key_here" # E2B API Key (if working with E2B code execution features) # E2B_API_KEY="your_e2b_api_key_here" +# Runloop API Key (if hosting remote rollout servers in Runloop Devboxes) +# RUNLOOP_API_KEY="your_runloop_api_key_here" + +# Optional: Runloop blueprint used by examples/runloop_remote_rollout/test_eval.py +# RUNLOOP_BLUEPRINT_ID="your_runloop_blueprint_id_here" + # Other environment variables your custom reward functions might need # MY_CUSTOM_SERVICE_API_KEY="some_other_key" diff --git a/docs/integrations/runloop_remote_rollout.mdx b/docs/integrations/runloop_remote_rollout.mdx new file mode 100644 index 00000000..498e7c2f --- /dev/null +++ b/docs/integrations/runloop_remote_rollout.mdx @@ -0,0 +1,71 @@ +# Runloop Remote Rollouts + +`RunloopRolloutProcessor` runs your remote rollout server inside a Runloop Devbox and then delegates rollout execution to Eval Protocol's existing `RemoteRolloutProcessor`. + +This is useful when your rollout server needs an isolated, reproducible environment but you still want Eval Protocol to use the standard `/init` request and Fireworks tracing metadata flow. + +## Install + +```bash +pip install "eval-protocol[runloop]" +``` + +Set the API keys used by the local evaluator and remote server: + +```bash +export RUNLOOP_API_KEY=... +export FIREWORKS_API_KEY=... +``` + +## Usage + +```python +from eval_protocol.pytest import RunloopRolloutProcessor, evaluation_test + + +@evaluation_test( + rollout_processor=RunloopRolloutProcessor( + blueprint_id="bpt_your_blueprint_id", + server_command=( + "python -m uvicorn examples.runloop_remote_rollout.server:app " + "--host 0.0.0.0 --port 8000" + ), + port=8000, + ), +) +async def test_my_eval(row): + return row +``` + +The server command must bind to `0.0.0.0` on the configured port so the Runloop tunnel can reach it. The server must expose `POST /init` and should use `FireworksTracingHttpHandler` plus `RolloutIdFilter` to publish rollout completion status. + +## Creating A Blueprint + +`blueprint_id` is required when you want `RunloopRolloutProcessor` to create a fresh Devbox for each eval invocation. The blueprint should contain the rollout server code and its Python dependencies. + +The included example can create a blueprint for a new Runloop account: + +```bash +export RUNLOOP_API_KEY=... +eval "$(python examples/runloop_remote_rollout/create_blueprint.py)" +``` + +That helper uploads the current repository as a temporary Runloop build context and builds a Python image with `eval-protocol[runloop]` installed. Use the printed `RUNLOOP_BLUEPRINT_ID` with `examples/runloop_remote_rollout/test_eval.py`. + +## Existing Devboxes + +You can attach to an existing Devbox instead of creating one from a blueprint: + +```python +RunloopRolloutProcessor( + devbox_id="devbox_existing_id", + server_command="python -m uvicorn server:app --host 0.0.0.0 --port 8000", + port=8000, +) +``` + +Eval Protocol only shuts down Devboxes created by `RunloopRolloutProcessor` when `shutdown_on_cleanup=True`. Existing Devboxes are left running. + +## Trace Flow + +`RunloopRolloutProcessor` does not change default rollout behavior. After setup it calls `RemoteRolloutProcessor(remote_base_url=...)`; `RemoteRolloutProcessor` sends `/init`, polls Fireworks tracing status by rollout ID, and backfills the final row from trace data. diff --git a/eval_protocol/__init__.py b/eval_protocol/__init__.py index 84e282af..32f86ce8 100644 --- a/eval_protocol/__init__.py +++ b/eval_protocol/__init__.py @@ -85,6 +85,7 @@ "evaluation_test": (".pytest", "evaluation_test"), "SingleTurnRolloutProcessor": (".pytest", "SingleTurnRolloutProcessor"), "RemoteRolloutProcessor": (".pytest", "RemoteRolloutProcessor"), + "RunloopRolloutProcessor": (".pytest", "RunloopRolloutProcessor"), "GithubActionRolloutProcessor": (".pytest", "GithubActionRolloutProcessor"), # From .pytest.parameterize "DefaultParameterIdGenerator": (".pytest.parameterize", "DefaultParameterIdGenerator"), @@ -174,6 +175,7 @@ def __init__(self, *args, **kwargs): "DataLoaderConfig", "Status", "RemoteRolloutProcessor", + "RunloopRolloutProcessor", "GithubActionRolloutProcessor", "InputMetadata", "EvaluationRow", @@ -278,6 +280,7 @@ def _get_version(): evaluation_test, SingleTurnRolloutProcessor, RemoteRolloutProcessor, + RunloopRolloutProcessor, GithubActionRolloutProcessor, ) from .pytest.parameterize import DefaultParameterIdGenerator diff --git a/eval_protocol/pytest/__init__.py b/eval_protocol/pytest/__init__.py index 1990042e..a4b3a085 100644 --- a/eval_protocol/pytest/__init__.py +++ b/eval_protocol/pytest/__init__.py @@ -19,6 +19,7 @@ "NoOpRolloutProcessor": (".default_no_op_rollout_processor", "NoOpRolloutProcessor"), "SingleTurnRolloutProcessor": (".default_single_turn_rollout_process", "SingleTurnRolloutProcessor"), "RemoteRolloutProcessor": (".remote_rollout_processor", "RemoteRolloutProcessor"), + "RunloopRolloutProcessor": (".runloop_rollout_processor", "RunloopRolloutProcessor"), "GithubActionRolloutProcessor": (".github_action_rollout_processor", "GithubActionRolloutProcessor"), "RolloutProcessor": (".rollout_processor", "RolloutProcessor"), # Dataset adapter @@ -103,6 +104,7 @@ def __dir__(): "RolloutProcessor", "SingleTurnRolloutProcessor", "RemoteRolloutProcessor", + "RunloopRolloutProcessor", "GithubActionRolloutProcessor", "NoOpRolloutProcessor", # Dataset @@ -133,6 +135,7 @@ def __dir__(): from .default_no_op_rollout_processor import NoOpRolloutProcessor as NoOpRolloutProcessor from .default_single_turn_rollout_process import SingleTurnRolloutProcessor as SingleTurnRolloutProcessor from .remote_rollout_processor import RemoteRolloutProcessor as RemoteRolloutProcessor + from .runloop_rollout_processor import RunloopRolloutProcessor as RunloopRolloutProcessor from .github_action_rollout_processor import GithubActionRolloutProcessor as GithubActionRolloutProcessor from .evaluation_test import evaluation_test as evaluation_test from .exception_config import ( diff --git a/eval_protocol/pytest/runloop_rollout_processor.py b/eval_protocol/pytest/runloop_rollout_processor.py new file mode 100644 index 00000000..b76ce9c8 --- /dev/null +++ b/eval_protocol/pytest/runloop_rollout_processor.py @@ -0,0 +1,238 @@ +"""Runloop-backed remote rollout processor.""" + +from __future__ import annotations + +import asyncio +import os +import time +import urllib.error +import urllib.request +from typing import Any + +from eval_protocol.models import EvaluationRow +from eval_protocol.pytest.remote_rollout_processor import RemoteRolloutProcessor +from eval_protocol.pytest.rollout_processor import RolloutProcessor +from eval_protocol.pytest.types import RolloutProcessorConfig + + +def _load_runloop_sdk() -> Any: + try: + from runloop_api_client import RunloopSDK + except ImportError as exc: + raise ImportError( + "RunloopRolloutProcessor requires the optional Runloop dependency. " + "Install it with `pip install 'eval-protocol[runloop]'`." + ) from exc + return RunloopSDK + + +class RunloopRolloutProcessor(RolloutProcessor): + """Host a remote rollout server in a Runloop Devbox. + + This processor only orchestrates Runloop lifecycle. Row processing is delegated + to :class:`RemoteRolloutProcessor`, so completion and trace collection continue + to use Eval Protocol's existing remote rollout contract. + """ + + def __init__( + self, + *, + blueprint_id: str | None = None, + devbox_id: str | None = None, + server_command: str, + port: int = 8000, + model_base_url: str = "https://tracing.fireworks.ai", + poll_interval: float = 1.0, + timeout_seconds: float = 120.0, + startup_timeout_seconds: float = 60.0, + include_payloads: bool = False, + shutdown_on_cleanup: bool = True, + runloop_api_key: str | None = None, + ) -> None: + if not blueprint_id and not devbox_id: + raise ValueError("Either blueprint_id or devbox_id is required for RunloopRolloutProcessor") + if not server_command: + raise ValueError("server_command is required for RunloopRolloutProcessor") + + self._blueprint_id = blueprint_id + self._devbox_id = devbox_id + self._server_command = server_command + self._port = port + self._model_base_url = model_base_url + self._poll_interval = poll_interval + self._timeout_seconds = timeout_seconds + self._startup_timeout_seconds = startup_timeout_seconds + self._include_payloads = include_payloads + self._shutdown_on_cleanup = shutdown_on_cleanup + self._runloop_api_key = runloop_api_key + + self._client: Any | None = None + self._devbox: Any | None = None + self._server_execution: Any | None = None + self._remote_base_url: str | None = None + self._remote_processor: RemoteRolloutProcessor | None = None + self._owns_devbox = False + self._shutdown_complete = False + + @property + def remote_base_url(self) -> str | None: + """The derived public URL for the Runloop-hosted rollout server.""" + return self._remote_base_url + + @property + def devbox_id(self) -> str | None: + """The Devbox ID used by this processor once setup has completed.""" + if self._devbox is not None and hasattr(self._devbox, "id"): + return self._devbox.id + return self._devbox_id + + def setup(self) -> None: + """Create or attach to a Devbox, expose the server port, and start the server.""" + if self._remote_processor is not None: + return + + api_key = self._runloop_api_key or os.getenv("RUNLOOP_API_KEY") + if not api_key: + raise ValueError( + "RUNLOOP_API_KEY is required for RunloopRolloutProcessor. " + "Set the environment variable or pass runloop_api_key explicitly." + ) + + RunloopSDK = _load_runloop_sdk() + client: Any = RunloopSDK(bearer_token=api_key) + self._client = client + + try: + if self._devbox_id: + devbox = client.devbox.from_id(self._devbox_id) + self._owns_devbox = False + else: + assert self._blueprint_id is not None + devbox = client.devbox.create_from_blueprint_id(self._blueprint_id) + self._owns_devbox = True + + self._devbox = devbox + self._await_running() + tunnel = self._create_tunnel() + self._remote_base_url = self._derive_remote_base_url(tunnel) + self._server_execution = devbox.cmd.exec_async(self._server_command) + self._wait_for_server_startup() + self._remote_processor = RemoteRolloutProcessor( + remote_base_url=self._remote_base_url, + model_base_url=self._model_base_url, + poll_interval=self._poll_interval, + timeout_seconds=self._timeout_seconds, + include_payloads=self._include_payloads, + ) + except Exception: + self._cleanup_partial_setup() + raise + + def __call__(self, rows: list[EvaluationRow], config: RolloutProcessorConfig) -> list[asyncio.Task[EvaluationRow]]: + if self._remote_processor is None: + self.setup() + assert self._remote_processor is not None + return self._remote_processor(rows, config) + + async def acleanup(self) -> None: + """Async cleanup for the delegated processor and any owned Devbox.""" + if self._remote_processor is not None: + await self._remote_processor.acleanup() + if self._should_shutdown_devbox(): + await asyncio.to_thread(self._shutdown_devbox) + + def cleanup(self) -> None: + """Best-effort synchronous cleanup.""" + if self._remote_processor is not None: + self._remote_processor.cleanup() + if self._should_shutdown_devbox(): + self._shutdown_devbox() + + def _await_running(self) -> None: + await_running = getattr(self._devbox, "await_running", None) + if await_running is None: + return + await_running() + + def _create_tunnel(self) -> Any: + assert self._devbox is not None + net = self._devbox.net + create_tunnel = getattr(net, "create_tunnel", None) + if create_tunnel is not None: + return create_tunnel(port=self._port) + + enable_tunnel = getattr(net, "enable_tunnel", None) + if enable_tunnel is None: + raise RuntimeError("Runloop Devbox networking API does not expose create_tunnel or enable_tunnel") + return enable_tunnel(auth_mode="open") + + def _derive_remote_base_url(self, tunnel: Any) -> str: + get_tunnel_url = getattr(self._devbox, "get_tunnel_url", None) + if get_tunnel_url is not None: + url = get_tunnel_url(self._port) + if url: + return str(url).rstrip("/") + + for attr in ("url", "base_url", "public_url"): + value = getattr(tunnel, attr, None) + if value: + return str(value).rstrip("/") + + tunnel_key = getattr(tunnel, "tunnel_key", None) + if tunnel_key: + return f"https://{self._port}-{tunnel_key}.tunnel.runloop.ai" + + raise RuntimeError("Could not determine Runloop tunnel URL for the rollout server") + + def _wait_for_server_startup(self) -> None: + if self._startup_timeout_seconds <= 0: + return + assert self._remote_base_url is not None + + deadline = time.monotonic() + self._startup_timeout_seconds + last_error: Exception | None = None + while time.monotonic() < deadline: + try: + request = urllib.request.Request(self._remote_base_url, method="GET") + with urllib.request.urlopen(request, timeout=min(5.0, self._startup_timeout_seconds)) as response: + response.read(1) + return + except urllib.error.HTTPError as exc: + if exc.code < 500: + return + last_error = exc + time.sleep(min(1.0, max(0.0, deadline - time.monotonic()))) + except Exception as exc: + last_error = exc + time.sleep(min(1.0, max(0.0, deadline - time.monotonic()))) + + message = f"Runloop rollout server did not become reachable within {self._startup_timeout_seconds} seconds" + if last_error is not None: + message = f"{message}: {last_error}" + raise TimeoutError(message) + + def _should_shutdown_devbox(self) -> bool: + return ( + self._devbox is not None + and self._owns_devbox + and self._shutdown_on_cleanup + and not self._shutdown_complete + ) + + def _shutdown_devbox(self) -> None: + if self._devbox is None or self._shutdown_complete: + return + self._devbox.shutdown() + self._shutdown_complete = True + + def _cleanup_partial_setup(self) -> None: + if self._remote_processor is not None: + self._remote_processor.cleanup() + self._remote_processor = None + if self._should_shutdown_devbox(): + self._shutdown_devbox() + self._devbox = None + self._server_execution = None + self._remote_base_url = None + self._owns_devbox = False + self._shutdown_complete = False diff --git a/examples/runloop_remote_rollout/README.md b/examples/runloop_remote_rollout/README.md new file mode 100644 index 00000000..0b24c0d7 --- /dev/null +++ b/examples/runloop_remote_rollout/README.md @@ -0,0 +1,28 @@ +# Runloop Remote Rollout Example + +This example hosts an Eval Protocol remote rollout server in a Runloop Devbox. + +## Requirements + +```bash +pip install "eval-protocol[runloop]" +export RUNLOOP_API_KEY=... +export FIREWORKS_API_KEY=... +``` + +Create a Runloop blueprint that contains this repository and its Python dependencies, then set `RUNLOOP_BLUEPRINT_ID`: + +```bash +eval "$(python examples/runloop_remote_rollout/create_blueprint.py)" +pytest examples/runloop_remote_rollout/test_eval.py +``` + +The blueprint ID matters because `RunloopRolloutProcessor` uses it to create a Devbox that already has this repository and `eval-protocol[runloop]` installed. If you already have a suitable running Devbox, you can pass `devbox_id` to `RunloopRolloutProcessor` instead and skip `RUNLOOP_BLUEPRINT_ID`. + +The processor starts: + +```bash +python -m uvicorn examples.runloop_remote_rollout.server:app --host 0.0.0.0 --port 8000 +``` + +The server receives `POST /init`, performs a chat completion through the Fireworks tracing base URL provided by Eval Protocol, and logs rollout completion using Fireworks tracing metadata. diff --git a/examples/runloop_remote_rollout/__init__.py b/examples/runloop_remote_rollout/__init__.py new file mode 100644 index 00000000..07e3ca2d --- /dev/null +++ b/examples/runloop_remote_rollout/__init__.py @@ -0,0 +1 @@ +"""Runloop remote rollout example.""" diff --git a/examples/runloop_remote_rollout/create_blueprint.py b/examples/runloop_remote_rollout/create_blueprint.py new file mode 100644 index 00000000..fd0822be --- /dev/null +++ b/examples/runloop_remote_rollout/create_blueprint.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import argparse +import tarfile +from datetime import timedelta +from pathlib import Path + +from runloop_api_client import RunloopSDK + + +DEFAULT_DOCKERFILE = """\ +FROM python:3.12-slim +WORKDIR /workspace +COPY . /workspace +RUN pip install --no-cache-dir ".[runloop]" +""" + +IGNORED_CONTEXT_DIRS = { + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".tox", + ".venv", + "__pycache__", + "node_modules", +} + + +def _ignore_build_context(member: tarfile.TarInfo) -> tarfile.TarInfo | None: + parts = set(Path(member.name).parts) + if parts & IGNORED_CONTEXT_DIRS: + return None + return member + + +def main() -> None: + parser = argparse.ArgumentParser(description="Create a Runloop blueprint for the remote rollout example.") + parser.add_argument( + "--repo-root", + type=Path, + default=Path(__file__).resolve().parents[2], + help="Path to the eval-protocol repository root.", + ) + parser.add_argument( + "--name", + default="eval-protocol-runloop-remote-rollout", + help="Runloop blueprint name.", + ) + args = parser.parse_args() + + runloop = RunloopSDK() + build_context = runloop.storage_object.upload_from_dir( + args.repo_root, + name=f"{args.name}.tar.gz", + ttl=timedelta(hours=1), + ignore=_ignore_build_context, + ) + blueprint = runloop.blueprint.create( + name=args.name, + dockerfile=DEFAULT_DOCKERFILE, + build_context={"type": "object", "object_id": build_context.id}, + ) + + print(f"export RUNLOOP_BLUEPRINT_ID={blueprint.id}") + + +if __name__ == "__main__": + main() diff --git a/examples/runloop_remote_rollout/server.py b/examples/runloop_remote_rollout/server.py new file mode 100644 index 00000000..27b8f270 --- /dev/null +++ b/examples/runloop_remote_rollout/server.py @@ -0,0 +1,50 @@ +import logging +import os +import threading + +from fastapi import FastAPI +from openai import OpenAI + +from eval_protocol import FireworksTracingHttpHandler, InitRequest, RolloutIdFilter, Status + + +app = FastAPI() + +logging.basicConfig(level=logging.INFO, format="%(name)s - %(levelname)s - %(message)s") +logging.getLogger().addHandler(FireworksTracingHttpHandler()) + + +@app.get("/") +def health() -> dict[str, str]: + return {"status": "ok"} + + +@app.post("/init") +def init(req: InitRequest) -> dict[str, str]: + logger = logging.getLogger(f"{__name__}.{req.metadata.rollout_id}") + logger.addFilter(RolloutIdFilter(req.metadata.rollout_id)) + + def _worker() -> None: + try: + messages = [message.model_dump(exclude_none=True) for message in req.messages or []] + completion_kwargs = { + "messages": messages, + **{k: v for k, v in req.completion_params.items() if k != "base_url"}, + } + if req.tools: + completion_kwargs["tools"] = req.tools + + api_key = req.api_key or os.environ.get("FIREWORKS_API_KEY") + if not api_key: + raise ValueError("FIREWORKS_API_KEY is required locally or in the /init payload") + + client = OpenAI(base_url=req.model_base_url, api_key=api_key) + completion = client.chat.completions.create(**completion_kwargs) + logger.info("Completed rollout response: %s", completion) + except Exception as exc: + logger.error("Rollout failed: %s", exc, extra={"status": Status.rollout_error(str(exc))}) + else: + logger.info("Rollout completed", extra={"status": Status.rollout_finished()}) + + threading.Thread(target=_worker, daemon=True).start() + return {"status": "started"} diff --git a/examples/runloop_remote_rollout/test_eval.py b/examples/runloop_remote_rollout/test_eval.py new file mode 100644 index 00000000..6752c618 --- /dev/null +++ b/examples/runloop_remote_rollout/test_eval.py @@ -0,0 +1,32 @@ +import os + +import pytest + +from eval_protocol.models import EvaluationRow, Message +from eval_protocol.pytest import RunloopRolloutProcessor, evaluation_test + + +BLUEPRINT_ID = os.environ.get("RUNLOOP_BLUEPRINT_ID") +pytestmark = pytest.mark.skipif(BLUEPRINT_ID is None, reason="RUNLOOP_BLUEPRINT_ID is required for live Runloop smoke") + + +def rows() -> list[EvaluationRow]: + return [EvaluationRow(messages=[Message(role="user", content="What is the capital of France?")])] + + +@evaluation_test( + completion_params=[{"model": "accounts/fireworks/models/gpt-oss-120b"}], + input_rows=[rows()], + rollout_processor=RunloopRolloutProcessor( + blueprint_id=BLUEPRINT_ID or "bpt_your_blueprint_id", + server_command=( + "python -m uvicorn examples.runloop_remote_rollout.server:app " + "--host 0.0.0.0 --port 8000" + ), + port=8000, + timeout_seconds=180, + ), +) +async def test_runloop_remote_rollout(row: EvaluationRow) -> EvaluationRow: + assert row.messages + return row diff --git a/pyproject.toml b/pyproject.toml index 55dd3289..e9119c14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -138,6 +138,9 @@ dspy = [ klavis = [ "klavis>=2.18.0", ] +runloop = [ + "runloop-api-client>=1.22.1", +] # Optional deps for LangGraph example/tests langgraph = [ diff --git a/tests/pytest/test_runloop_rollout_processor.py b/tests/pytest/test_runloop_rollout_processor.py new file mode 100644 index 00000000..4ef4c8e1 --- /dev/null +++ b/tests/pytest/test_runloop_rollout_processor.py @@ -0,0 +1,325 @@ +import asyncio +from types import SimpleNamespace +import urllib.error +from email.message import Message + +import pytest + +from eval_protocol.models import EvaluationRow +from eval_protocol.pytest.runloop_rollout_processor import RunloopRolloutProcessor +from eval_protocol.pytest.types import RolloutProcessorConfig +import eval_protocol.pytest.runloop_rollout_processor as runloop_rollout_processor_module + + +class FakeRemoteRolloutProcessor: + instances = [] + + def __init__(self, **kwargs): + self.kwargs = kwargs + self.calls = [] + self.cleanup_called = False + self.acleanup_called = False + FakeRemoteRolloutProcessor.instances.append(self) + + def __call__(self, rows, config): + self.calls.append((rows, config)) + + async def _return_row(row): + return row + + return [asyncio.create_task(_return_row(row)) for row in rows] + + async def acleanup(self): + self.acleanup_called = True + + def cleanup(self): + self.cleanup_called = True + + +class FakeCommandInterface: + def __init__(self, state): + self._state = state + + def exec_async(self, command): + self._state.server_commands.append(command) + return SimpleNamespace(execution_id="exec-1") + + +class FakeNetworkInterface: + def __init__(self, state): + self._state = state + + def create_tunnel(self, *, port): + self._state.tunnel_ports.append(port) + return SimpleNamespace(tunnel_key=self._state.tunnel_key) + + +class FakeDevbox: + def __init__(self, state, devbox_id): + self._state = state + self.id = devbox_id + self.cmd = FakeCommandInterface(state) + self.net = FakeNetworkInterface(state) + + def await_running(self): + self._state.await_running_calls += 1 + + def get_tunnel_url(self, port): + return f"https://{port}-{self._state.tunnel_key}.tunnel.runloop.ai" + + def shutdown(self): + self._state.shutdown_calls.append(self.id) + + +class FakeDevboxOps: + def __init__(self, state): + self._state = state + + def create_from_blueprint_id(self, blueprint_id): + self._state.created_blueprints.append(blueprint_id) + return FakeDevbox(self._state, "devbox-created") + + def from_id(self, devbox_id): + self._state.attached_devboxes.append(devbox_id) + return FakeDevbox(self._state, devbox_id) + + +class FakeRunloopSDK: + def __init__(self, state, bearer_token): + self._state = state + self._state.bearer_tokens.append(bearer_token) + self.devbox = FakeDevboxOps(state) + + +@pytest.fixture +def fake_runloop(monkeypatch): + state = SimpleNamespace( + bearer_tokens=[], + created_blueprints=[], + attached_devboxes=[], + tunnel_ports=[], + server_commands=[], + shutdown_calls=[], + await_running_calls=0, + tunnel_key="test-tunnel-key", + ) + + def _load_sdk(): + class BoundFakeRunloopSDK(FakeRunloopSDK): + def __init__(self, bearer_token): + super().__init__(state, bearer_token) + + return BoundFakeRunloopSDK + + FakeRemoteRolloutProcessor.instances.clear() + monkeypatch.setattr(runloop_rollout_processor_module, "_load_runloop_sdk", _load_sdk) + monkeypatch.setattr(runloop_rollout_processor_module, "RemoteRolloutProcessor", FakeRemoteRolloutProcessor) + monkeypatch.setenv("RUNLOOP_API_KEY", "runloop-key") + return state + + +def _config(): + return RolloutProcessorConfig(completion_params={}, mcp_config_path="", semaphore=asyncio.Semaphore(10)) + + +def test_setup_creates_devbox_from_blueprint_and_starts_server(fake_runloop): + processor = RunloopRolloutProcessor( + blueprint_id="bp-123", + server_command="python -m uvicorn server:app --host 0.0.0.0 --port 9000", + port=9000, + startup_timeout_seconds=0, + ) + + processor.setup() + + assert fake_runloop.bearer_tokens == ["runloop-key"] + assert fake_runloop.created_blueprints == ["bp-123"] + assert fake_runloop.attached_devboxes == [] + assert fake_runloop.await_running_calls == 1 + assert fake_runloop.tunnel_ports == [9000] + assert fake_runloop.server_commands == ["python -m uvicorn server:app --host 0.0.0.0 --port 9000"] + assert processor.devbox_id == "devbox-created" + assert processor.remote_base_url == "https://9000-test-tunnel-key.tunnel.runloop.ai" + + +def test_setup_uses_existing_devbox_without_shutting_it_down(fake_runloop): + processor = RunloopRolloutProcessor( + devbox_id="devbox-existing", + server_command="python server.py", + startup_timeout_seconds=0, + ) + + processor.setup() + processor.cleanup() + + assert fake_runloop.created_blueprints == [] + assert fake_runloop.attached_devboxes == ["devbox-existing"] + assert fake_runloop.shutdown_calls == [] + + +@pytest.mark.asyncio +async def test_delegates_rows_and_config_to_remote_rollout_processor(fake_runloop): + processor = RunloopRolloutProcessor( + blueprint_id="bp-123", + server_command="python server.py", + port=7000, + model_base_url="https://example.test/tracing", + poll_interval=2.5, + timeout_seconds=300, + include_payloads=True, + startup_timeout_seconds=0, + ) + processor.setup() + + rows = [EvaluationRow()] + config = _config() + tasks = processor(rows, config) + results = await asyncio.gather(*tasks) + + remote = FakeRemoteRolloutProcessor.instances[0] + assert results == rows + assert remote.kwargs == { + "remote_base_url": "https://7000-test-tunnel-key.tunnel.runloop.ai", + "model_base_url": "https://example.test/tracing", + "poll_interval": 2.5, + "timeout_seconds": 300, + "include_payloads": True, + } + assert remote.calls == [(rows, config)] + + +def test_setup_requires_runloop_api_key(monkeypatch): + monkeypatch.delenv("RUNLOOP_API_KEY", raising=False) + + processor = RunloopRolloutProcessor(blueprint_id="bp-123", server_command="python server.py") + + with pytest.raises(ValueError, match="RUNLOOP_API_KEY"): + processor.setup() + + +def test_setup_reports_missing_runloop_dependency(monkeypatch): + monkeypatch.setenv("RUNLOOP_API_KEY", "runloop-key") + + def _raise_missing_dependency(): + raise ImportError( + "RunloopRolloutProcessor requires the optional Runloop dependency. " + "Install it with `pip install 'eval-protocol[runloop]'`." + ) + + monkeypatch.setattr(runloop_rollout_processor_module, "_load_runloop_sdk", _raise_missing_dependency) + processor = RunloopRolloutProcessor(blueprint_id="bp-123", server_command="python server.py") + + with pytest.raises(ImportError, match="eval-protocol\\[runloop\\]"): + processor.setup() + + +def test_setup_cleans_up_owned_devbox_after_startup_failure(fake_runloop, monkeypatch): + processor = RunloopRolloutProcessor( + blueprint_id="bp-123", + server_command="python server.py", + startup_timeout_seconds=0, + ) + + def _fail_startup(): + raise TimeoutError("server did not start") + + monkeypatch.setattr(processor, "_wait_for_server_startup", _fail_startup) + + with pytest.raises(TimeoutError, match="server did not start"): + processor.setup() + + assert fake_runloop.shutdown_calls == ["devbox-created"] + assert processor.remote_base_url is None + assert FakeRemoteRolloutProcessor.instances == [] + + +def test_setup_does_not_shutdown_existing_devbox_after_startup_failure(fake_runloop, monkeypatch): + processor = RunloopRolloutProcessor( + devbox_id="devbox-existing", + server_command="python server.py", + startup_timeout_seconds=0, + ) + + def _fail_startup(): + raise TimeoutError("server did not start") + + monkeypatch.setattr(processor, "_wait_for_server_startup", _fail_startup) + + with pytest.raises(TimeoutError, match="server did not start"): + processor.setup() + + assert fake_runloop.shutdown_calls == [] + + +def test_startup_wait_retries_5xx_http_errors(monkeypatch): + processor = RunloopRolloutProcessor( + devbox_id="devbox-existing", + server_command="python server.py", + startup_timeout_seconds=5, + ) + processor._remote_base_url = "https://8000-test-tunnel-key.tunnel.runloop.ai" + + calls = [] + + class ReadyResponse: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, traceback): + return None + + def read(self, size): + return b"o" + + def _urlopen(request, timeout): + calls.append((request.full_url, timeout)) + if len(calls) == 1: + raise urllib.error.HTTPError( + request.full_url, 503, "Service Unavailable", hdrs=Message(), fp=None + ) + return ReadyResponse() + + monkeypatch.setattr(runloop_rollout_processor_module.urllib.request, "urlopen", _urlopen) + monkeypatch.setattr(runloop_rollout_processor_module.time, "sleep", lambda seconds: None) + + processor._wait_for_server_startup() + + assert len(calls) == 2 + + +def test_startup_wait_accepts_non_5xx_http_errors(monkeypatch): + processor = RunloopRolloutProcessor( + devbox_id="devbox-existing", + server_command="python server.py", + startup_timeout_seconds=5, + ) + processor._remote_base_url = "https://8000-test-tunnel-key.tunnel.runloop.ai" + + calls = [] + + def _urlopen(request, timeout): + calls.append((request.full_url, timeout)) + raise urllib.error.HTTPError(request.full_url, 404, "Not Found", hdrs=Message(), fp=None) + + monkeypatch.setattr(runloop_rollout_processor_module.urllib.request, "urlopen", _urlopen) + + processor._wait_for_server_startup() + + assert len(calls) == 1 + + +@pytest.mark.asyncio +async def test_async_cleanup_closes_remote_processor_and_owned_devbox(fake_runloop): + processor = RunloopRolloutProcessor( + blueprint_id="bp-123", + server_command="python server.py", + startup_timeout_seconds=0, + ) + processor.setup() + + await processor.acleanup() + await processor.acleanup() + + remote = FakeRemoteRolloutProcessor.instances[0] + assert remote.acleanup_called is True + assert fake_runloop.shutdown_calls == ["devbox-created"] diff --git a/uv.lock b/uv.lock index 048760dc..b2f25800 100644 --- a/uv.lock +++ b/uv.lock @@ -1272,6 +1272,9 @@ proxy = [ pydantic = [ { name = "pydantic-ai" }, ] +runloop = [ + { name = "runloop-api-client" }, +] supabase = [ { name = "supabase" }, ] @@ -1363,6 +1366,7 @@ requires-dist = [ { name = "requests", specifier = ">=2.25.0" }, { name = "rich", specifier = ">=12.0.0" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.5.0" }, + { name = "runloop-api-client", marker = "extra == 'runloop'", specifier = ">=1.22.1" }, { name = "selenium", marker = "extra == 'svgbench'", specifier = ">=4.0.0" }, { name = "supabase", marker = "extra == 'supabase'", specifier = ">=2.18.1" }, { name = "swig", marker = "extra == 'box2d'" }, @@ -1385,7 +1389,7 @@ requires-dist = [ { name = "werkzeug", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "zstandard", specifier = ">=0.19.0" }, ] -provides-extras = ["dev", "trl", "openevals", "box2d", "langfuse", "huggingface", "langsmith", "bigquery", "svgbench", "pydantic", "supabase", "chinook", "langchain", "braintrust", "openenv", "dspy", "klavis", "langgraph", "langgraph-tools", "proxy"] +provides-extras = ["dev", "trl", "openevals", "box2d", "langfuse", "huggingface", "langsmith", "bigquery", "svgbench", "pydantic", "supabase", "chinook", "langchain", "braintrust", "openenv", "dspy", "klavis", "runloop", "langgraph", "langgraph-tools", "proxy"] [package.metadata.requires-dev] dev = [ @@ -6033,6 +6037,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892, upload-time = "2025-03-07T15:27:41.687Z" }, ] +[[package]] +name = "runloop-api-client" +version = "1.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/3c/d9f05a360599d4eb374f11e4922f7f4e43388c290df9cba21e28438a84df/runloop_api_client-1.22.1.tar.gz", hash = "sha256:a2885eeb8752fc7adcabfe452ab0418b2b6075030f4621feeec4b1522811eb75", size = 648993, upload-time = "2026-06-01T19:23:28.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/0e/65e37bb055d007b969362ba1c42915ac4ab24839bcc1d77f43a5eb1b62de/runloop_api_client-1.22.1-py3-none-any.whl", hash = "sha256:e12de00602f9dd16a5db78610c1bb5cd2fc1a8c955c0e44f005e989b3c2f9a86", size = 398046, upload-time = "2026-06-01T19:23:27.56Z" }, +] + [[package]] name = "s3transfer" version = "0.13.1" @@ -6909,6 +6931,119 @@ socks = [ { name = "pysocks" }, ] +[[package]] +name = "uuid-utils" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/a1/822ceef22d1c139cffebe4b1b660cfaa10253d5c770aa2598dc8e9497593/uuid_utils-0.16.0.tar.gz", hash = "sha256:d6902d4375dfba4c9902c736bb82d3c040417b67f7d0fa48910ddfdb1ac95de7", size = 42596, upload-time = "2026-05-19T07:44:23.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/78/fc830a25597001586770f0436a4917aac21fcdaf7ac2824bbe168ccdc724/uuid_utils-0.16.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a632fead2a6505a8df3318d5e95503739b9aa1c518521cd93d83ce00699b78f8", size = 566691, upload-time = "2026-05-19T07:45:14.2Z" }, + { url = "https://files.pythonhosted.org/packages/10/39/3f1eee6d3c3c33d6dd75441bdb49ac246de57f97f67faa7ff04cdb5e4ffe/uuid_utils-0.16.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:d716e5b35266400d2a2cd349697868179825f113c543e55c9d2ac304991f8d4f", size = 291039, upload-time = "2026-05-19T07:45:52.28Z" }, + { url = "https://files.pythonhosted.org/packages/c6/85/f7fb16eed216fd8085d62d4ce7179e2a81ac7649e043f34168e7700b6df4/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:207c2a98ca8b065cc93378a3a59744efb88a68e9ecc2c3afefe43d59c864280a", size = 327880, upload-time = "2026-05-19T07:44:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/06/ea/b2b629d29c8234677850e1ae47add9c8866dfb3864af257542989a13ba1b/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79824850330e450c7b2fa933572e32192240060937426052fa3fc05134ed3faa", size = 334090, upload-time = "2026-05-19T07:44:57.354Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/a6871c6231244bb80be06a2babf3ca34396b29d893103d84ddfd3654e6e4/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d89927c47e1a55509e90b7f2fd3e7ff89908c77b61f8f0deda97a89d8854e0f8", size = 448558, upload-time = "2026-05-19T07:45:03.986Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d0/b606a2857f98c20c149044e80f276ff7966c9f679fc7b25f6d608bd8d48b/uuid_utils-0.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ae4168e1ca0ae69d24207645a8b3cd2b641a0ad15058eda17d2c9898aa89d3", size = 327733, upload-time = "2026-05-19T07:43:40.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/7951dd47b6717b6ebb340e673d31d539be928d280a697fab4dd233bcc7fa/uuid_utils-0.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d363017a3223de3a57eb6fca135df6ffcef7c534836bff2e71354dce7d10987c", size = 353659, upload-time = "2026-05-19T07:44:03.551Z" }, + { url = "https://files.pythonhosted.org/packages/a2/5d/f46e91fad5f049c7bd12701293c1ac31b4460ec83606c4bdd37c05abef52/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4a87a7433b355eadaa200f150da6bb5b87bb6de0adf260883b26cb637aba0410", size = 504509, upload-time = "2026-05-19T07:44:34.147Z" }, + { url = "https://files.pythonhosted.org/packages/f4/94/ea4f559e5e87da5847ecf78ba68a78e8bb4e537e1169093ea543cab94886/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6da070e75b0e2424728e6f8547647cce36c83f9a6101a08da4849a8ab2b58105", size = 609358, upload-time = "2026-05-19T07:44:39.711Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/60dbac2459426a925b77e08cb8ec492d4bc82caa0f124f498d2e24409cb8/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1baab8966f9e0097cbaf9cc01ad448b38e616e7b4968ca5e49cb53a74ad91a2f", size = 569428, upload-time = "2026-05-19T07:44:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/e8/90/ae39c1e1bff65dfe9c7c70cbd64b8d529a3d1cc836aeaa7accdc44e5c308/uuid_utils-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b42014536943c1a654ff107538c0f7dc39809d8d774ec8dafd19bec05006e568", size = 532465, upload-time = "2026-05-19T07:44:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/03/5c/4dc93017a095c9c314525a9abc4f9983e520d88d7eff9bd52398d81c374e/uuid_utils-0.16.0-cp310-cp310-win32.whl", hash = "sha256:228701ab6f188b6def24f2add6db64f0794adb1f06d0abacdcec40b0cda13cdf", size = 171162, upload-time = "2026-05-19T07:44:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/43/df/1398f5b117d5daa4d757b156728db7aa092a3eff1271c40ec39dbe945327/uuid_utils-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:10d3c5983f770b1b2847ad811c87a1c9e28f8155d1a27cc581abcd5abb386b64", size = 176927, upload-time = "2026-05-19T07:44:54.93Z" }, + { url = "https://files.pythonhosted.org/packages/24/24/0e18177e2fbb0b9f54f90fd48fe3302dfda731e22ad650d6e6f8f4b3d3d3/uuid_utils-0.16.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:04af9966ecd82b78eeba5725e29aa1e86fb8eb84b5443dd6a9935f9fadb6678e", size = 565929, upload-time = "2026-05-19T07:44:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7e/bb91b04b2c8a081a4df2d50f1a50dd85502e2391c6eaed71b339ec9f2524/uuid_utils-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3d86ca394e0ea21bdb53784eb99276d263b93d1586f56678cab1414b7ae1d0f3", size = 290556, upload-time = "2026-05-19T07:43:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/69/2a/47ee18b294af59754ef5acfa96eb027137c98cef7521199b6f70be705de4/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f504efeb20ffd9571621658f7c8093c646d33150406d5742e49ff7cd861615", size = 328059, upload-time = "2026-05-19T07:45:30.533Z" }, + { url = "https://files.pythonhosted.org/packages/89/7c/ed6d8bb48eeecaed6722af1187d722c5243334be750419d10d5f05dffeb2/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57d85f48535dc541060f6b82f277cbcd12b78c04008ccc1039546cfcec027327", size = 334759, upload-time = "2026-05-19T07:45:07.715Z" }, + { url = "https://files.pythonhosted.org/packages/ff/33/371bddf9fd47e045c375df9668eea0d96ce9201ab6a03985b0155498e376/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39453f1ebf4398fbeb71607f3437e2ac469c9e38b5921755c1e17ad0158a8907", size = 448927, upload-time = "2026-05-19T07:45:11.464Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f1/b201d5ee005d4987fc072714fcb9f6e75303520cf19d4deec0b4df44bf40/uuid_utils-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50361aca5c2a770728a6343df85109fe57f89ac026827f34fe0153563cdc9ce7", size = 327178, upload-time = "2026-05-19T07:44:02.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6a/04b4c02ce5c24a3602baa12e59bd3ec853ae73c3e9319b706c4620f47a05/uuid_utils-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:948485c47d8569a8bf6e86f522a2599fa9134674bee9f483898e601e68c3caca", size = 352981, upload-time = "2026-05-19T07:44:25.578Z" }, + { url = "https://files.pythonhosted.org/packages/2c/19/25db019727d14630c75c2a75a8ea66dd712bb468adcf410bac8d01ff19fd/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ceef237cf8467fddbf6d8466cc1f6e2c04605ec919046ef5eba10a895b559fcf", size = 504686, upload-time = "2026-05-19T07:43:46.43Z" }, + { url = "https://files.pythonhosted.org/packages/5d/93/c000cd42ebfdd37cc74981ed31c979a1270156572bdebab8b5d61460e750/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:24e6fa0d0ade7a9ad60a3c296022474983243df5b4e863babb4828a85ef2e52c", size = 610102, upload-time = "2026-05-19T07:45:53.765Z" }, + { url = "https://files.pythonhosted.org/packages/15/1d/7dd239909c82616722b9ee53fa1b4657c6244fb4fd026890300ebf6db22b/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1c2df42314b014c9d23330f92887e21d2fc72fde0beb170c7833cd2d22d845a1", size = 569048, upload-time = "2026-05-19T07:45:41.596Z" }, + { url = "https://files.pythonhosted.org/packages/f1/49/b6a688648368a9cc0137e183657956853a91dc06ef73deda27290d586155/uuid_utils-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e2f369dd734050fe96ae4905c58779b09276d47d5e9a0e5cd33ec7982784341", size = 532255, upload-time = "2026-05-19T07:45:16.936Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fb/34f221ae93d5ea249a0d7056bdf45313b8d267d6aa9c5d0673ac1a4746c7/uuid_utils-0.16.0-cp311-cp311-win32.whl", hash = "sha256:733da81d51ea578862d8b9b754e8968b6da2be2b7840aee868917c23cae84015", size = 171081, upload-time = "2026-05-19T07:45:26.578Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/c2a608a813f655834ee6df4ce53ea46edad4d54f774eac1890be5c7e4e1c/uuid_utils-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:10d21fddb086e69245c4f0f77c7b442471f3a242aa85f62954bff157baa1c5f2", size = 176770, upload-time = "2026-05-19T07:43:49.102Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/8ab4eff328a833c065f280b2e0d9ac873505b5e5282f2bc5133a9843d4dd/uuid_utils-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:98e2404713677070cee9a99a1f1e24afd496c18e833ee1b31a0587659452ff80", size = 175274, upload-time = "2026-05-19T07:44:27.216Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4c/b4cf43a5d22bcdb91727acdf54be0d78e83e595b73c5a9a8a4291875f059/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:727fae3f0682191ec9c8ce1cd0f71e81b471a2e26b7c5fd66712fc0f11640aa0", size = 562183, upload-time = "2026-05-19T07:45:02.683Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fb/4b0d1c4b5e9f8679ca41b9cdbce5749e1d5db3d3d42a07060d6ce61ac583/uuid_utils-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:66a9c8cedf7695c28e700f6a66bde0809c3b2e0d8a70968be7bfd47c908952e5", size = 289018, upload-time = "2026-05-19T07:44:07.726Z" }, + { url = "https://files.pythonhosted.org/packages/de/43/2dc6c7401c8fab86e46b0b33ada6dcfde949b2fd48877ba6f880862be80e/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9152bff801ec2ccf630df06d67389090a2c612dea87fbf9a887ab4b222929f6f", size = 326171, upload-time = "2026-05-19T07:45:25.186Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f5/48f11fb91f36453611ca148bc441436f279870b1ec6b576dc5167fb6e680/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06fc7db470c37e5c1ab3fd2cd159697d6f8b279d7d23b5b96bd418b115f8caa9", size = 332222, upload-time = "2026-05-19T07:45:09.036Z" }, + { url = "https://files.pythonhosted.org/packages/30/cb/b2b49528521e4a097f129e8bf7850a26f00af46afba778832cf3458a5c00/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e1a1f57fe3631e164dad27b24aa81267810e20575f705af3b0fa734f3a21247", size = 444801, upload-time = "2026-05-19T07:45:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b3/a28d9c6f7c701dfe01c8020b30e33899a28eb9e4d056b07e7388f50ebf67/uuid_utils-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ee392fe59808a731b7b6bf4d453fb6e833774921331cceae5f254d1e9c5b97d", size = 325594, upload-time = "2026-05-19T07:44:44.682Z" }, + { url = "https://files.pythonhosted.org/packages/cf/65/e1ff41dc44966e396ead86e104ba21b35ddb07ff7a64bb55013074ee77fe/uuid_utils-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b2e981b1258db444df4cf4bf4c79673570d081d48d35f22d0f86471e0ad795c5", size = 349312, upload-time = "2026-05-19T07:45:15.582Z" }, + { url = "https://files.pythonhosted.org/packages/ed/57/fb19b7951f66a46e03bd1943a61ee9d59c83e994e56e8c97d79aff1f0e47/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbb92feb4db08cd76e27b4d3b1a82bfde708447317150c614eb9f761a43b387e", size = 502115, upload-time = "2026-05-19T07:43:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/2f/8e/9a129c469b7b77afb62da5c6b7e92591073b845bd0c3108c0d0aa65389fb/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c3c5afaaa68b1d6393d653e9fc93a2fde9da1681da01f74b4593f41d31fb5f1", size = 607433, upload-time = "2026-05-19T07:44:11.675Z" }, + { url = "https://files.pythonhosted.org/packages/4a/56/2ef71fad168cc3d894f7094fa458086c093635d7835381c91470b19c9ad3/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:38126b353527c5f001e4b24db9e62351eb768d0367febcd68100a4b39a035109", size = 566076, upload-time = "2026-05-19T07:44:35.453Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/68e60ea053ca30f35df877b96001331398140d5c4983561affa1350331b1/uuid_utils-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41a67e546d9adf11c4e4cb5c8e81f000f8b1f000c17912ced089b499855719a5", size = 530645, upload-time = "2026-05-19T07:45:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/42/19/b521f7d73094fca4c0c44002f4a42bfcbcf0b770fdc3c4b9a596dda25734/uuid_utils-0.16.0-cp312-cp312-win32.whl", hash = "sha256:52d2cc8c12a3466cd1727883e0746d8bad5dddd670369eb553ba17fdc3b565ca", size = 168887, upload-time = "2026-05-19T07:45:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/87/1f/4126c3ccbc2d98a613664e55f6ab6d7bd4b98424a04486e4fcc76549af15/uuid_utils-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97625e5edfda8b118160ce1e88756f92b1635775f836c168be7bf10928d97fa", size = 174607, upload-time = "2026-05-19T07:43:52.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/62/b83ccc8446ae39dcc0bda2cb3b525b6af6a2036383afe1d1d5fe7b234c2c/uuid_utils-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:baf79c8050eb784b252dd34807df73f61130fe8676b61231baccab62530f20ec", size = 173021, upload-time = "2026-05-19T07:45:10.204Z" }, + { url = "https://files.pythonhosted.org/packages/60/9b/74c1f47a9b4f138a254e51528e5ffaeba6bf99ecead9f0c4b6fccccfbfcb/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d34cf9681e8892fad2a63e393068e544505408748cd8bf0c3517d753a01528d4", size = 563166, upload-time = "2026-05-19T07:44:10.494Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1c/009e37b70f1f0ff17e7103a36bafde33d503d9ea7fe739761aa3e3c9fde6/uuid_utils-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0681d1bdb7956e0c6d581e7601dabcfb2b08c25d2a65189f4e9b102c94f5ff46", size = 289529, upload-time = "2026-05-19T07:43:54.466Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5e/e0323d54321166639eb2be5e8a464f5cb0fc04d72d91f3e78944bb6a1da8/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed45fb8732d216426227096b55accbb87cba57febc86a044d90780b090eb99d0", size = 326328, upload-time = "2026-05-19T07:45:31.901Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/046f6cb958467c3bf4a163a8a53b178b64a62e21ed8ad5b2c1dacb3a2cfc/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b617a334bb01ef2ff8c22900f5a14125eb9063f602131494cc9dc59519beaa5b", size = 332322, upload-time = "2026-05-19T07:43:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/67/80/01914e3949744db7acd0006885e5542fbebb6e39114857d007d29b3265c2/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a750d8aeb8ae880aa9a2529606bde0e994bcc7448730c953107f357a28e6102e", size = 445787, upload-time = "2026-05-19T07:45:36.102Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/f6908f41279f205d70c8a0d5dcb25dd6802741d7f88e3f0123453c3584d3/uuid_utils-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a250e111903c4368745fce5ac2aa607bd477c62d3307e45347338fdb64b38e0", size = 324678, upload-time = "2026-05-19T07:45:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/11/4a/bf841ba90f829c7779d82155e0f4b88ef6726ccc25507d064d50ac2cd329/uuid_utils-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:95b7f480010ea98a29ee809857a98aa923008c68129af1b39244adccff7377fb", size = 349704, upload-time = "2026-05-19T07:44:47.172Z" }, + { url = "https://files.pythonhosted.org/packages/e6/31/3b5c60172b8c57bf4ca485484b8e4edef550ca324f9287f1183be97422e2/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:420aa3ca403cedb73490b6ea3aeefeea7e0455f5ce60bbf856390ee872ae3306", size = 502456, upload-time = "2026-05-19T07:45:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/88/bf/3da8d497af80fd51d8bf85551c77ede67f07825924ec5987bf9b6031014a/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b8a9a7b1065a12d40f2cc25b7d705ab34954cc57095034367bca39ebcf4a876b", size = 607727, upload-time = "2026-05-19T07:44:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/7c8cf03ec15cd6f40e4cbab81b2b4a625461327f68c7971e54723280ec3e/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f235ac5827d74ac630cc87f29278cdaa5d2f273613a6e05bbd96df7aa4170776", size = 566204, upload-time = "2026-05-19T07:44:51.225Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5f/af955feae69cce7fd2121ca3f790ff4b85ad2e17b2149546f50753e1a047/uuid_utils-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c8083284488b84ad178e74add64cfd1e74e8be5e30821e5acbc5019281c658b0", size = 529986, upload-time = "2026-05-19T07:45:57.85Z" }, + { url = "https://files.pythonhosted.org/packages/10/cf/3fec757e51bef10eb41ae8075f5442c60e85ff456b42d16a3063f5dc6c80/uuid_utils-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:27a071a899ba46a551d6524dbbc5a98b88be176d0f55ddf72cf71c005326ac10", size = 98683, upload-time = "2026-05-19T07:44:16.369Z" }, + { url = "https://files.pythonhosted.org/packages/40/a7/cd1adbea7ef882a70db064c00cd93b12e11027b4cdd7ffd79e95c35fc3e3/uuid_utils-0.16.0-cp313-cp313-win32.whl", hash = "sha256:924a8de04460e4cf65998ad0b6568084f7c51740ebd3254d07a0bcde35a84af6", size = 168822, upload-time = "2026-05-19T07:44:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/74/99/617ceb9e3a95b23837012740979baf71afad723b70daf34862da3f7c17a1/uuid_utils-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:5279bc7ab3c6683f1c67314695bee14d869015acbbc677bdb0015190fe753d16", size = 174967, upload-time = "2026-05-19T07:44:56.022Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d8/148ae707bfc36d482e39db679c86b81bdce264d4feb9df5d40a03b7687e3/uuid_utils-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:61a9c4c26ad12ac66fa4bfd0fdb8494724fe7a5b98a9fcd43e78e2b388663dbb", size = 173142, upload-time = "2026-05-19T07:43:50.171Z" }, + { url = "https://files.pythonhosted.org/packages/21/05/ca6d60705e71fdeaa3431dad94e279a8213c5573cb2925e1aabf3dc0330a/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73486b6aa3f755a6c97000f5ea67e7ac78d6df89bf22980789a1e943e24b74f0", size = 564408, upload-time = "2026-05-19T07:44:38.351Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8c/b9a0462c38535c1662acb1025768e2d626bee5ce9e1790bad6b5381162ea/uuid_utils-0.16.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f1614572fd9345cdc3dde3f40c237345719fabca1aa87d2d87b321d523cfa34d", size = 289923, upload-time = "2026-05-19T07:45:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/a53afeef1a56051551a0f5a801e4bce411dd73c6a8c99bad16902651256d/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9346ce6eb1fbd8b03a6b331d66016afcb4edcdff6eac708e21391600529a016a", size = 325762, upload-time = "2026-05-19T07:45:18.261Z" }, + { url = "https://files.pythonhosted.org/packages/72/ca/4462a4f36365d7ee72d41e05e6bcfe127e861b073ab37c25b2c8a518317c/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0fc6eb3fd821466fbab69cf356c6ec2b7327266bbbc740a2eb57c77c4bef965", size = 332359, upload-time = "2026-05-19T07:45:34.886Z" }, + { url = "https://files.pythonhosted.org/packages/c5/67/9d3373fa7c5a746fdecc64e30caf915c29eb632203508d87676f9243ed03/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13a797e5e8f0dadc18351a5aa013815ddac25dce6864072a539d510910c95f71", size = 445483, upload-time = "2026-05-19T07:44:49.598Z" }, + { url = "https://files.pythonhosted.org/packages/57/08/ce01aa6d897fc7f875844fe58cad0a542c8ebf089d9242b654b56260ecb8/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57c3583b1f1c00a94f59726a5e2b988fa209221143919a1af5c2fc24e318fc98", size = 326281, upload-time = "2026-05-19T07:44:59.677Z" }, + { url = "https://files.pythonhosted.org/packages/76/ef/2c719b2c26bb5b5e5061a1435c11ad2bd33ac3cd6d4cd0c7c3ac1d3396ed/uuid_utils-0.16.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:caac9c8b1d50e8fbddc76e93bfefbef472978eb45adbfdb6289d578816992953", size = 350809, upload-time = "2026-05-19T07:45:28.076Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9b/c1ed447328b32229cca38ac4c62d309eab006e5e9c4020e2056a175bc607/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:91db59bad97ed2b9d2c6ed25082fe9762b2c422e694fe06786b28cf4e776ac4c", size = 502088, upload-time = "2026-05-19T07:44:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e0/8442f4efe7bde72f0b4ae5f675d0c7fbe209ad0b54718b8ddf43c46c6fae/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:41985e342a30e76366a8becc60bbdb07d72cd1b86ec657b1f31654e9fb1baada", size = 607631, upload-time = "2026-05-19T07:44:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/9a9fa261edf4c972f28ae83421377e3ab8dbd0bd7db58fd316e782d09a3b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1b0dcedf9266bf34a54d5cbe78648eaa627e02352f2a6923ed647530aea2f661", size = 567618, upload-time = "2026-05-19T07:43:58.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/1bcfdb9d539bd42736dd6076470a42fbb5db23f79712c0a06aa0a3752f7b/uuid_utils-0.16.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26fe23ab60f05de4ad70aaa5b6a4c2a7bbd43055e3dd6f6b31efba0532ac9c71", size = 530971, upload-time = "2026-05-19T07:45:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/24/0c/18945f417d6bb4d0dd2b7652fe36c58c4e83bcf593b9b326b83aa40b853a/uuid_utils-0.16.0-cp313-cp313t-win32.whl", hash = "sha256:7f8cf49c05d58523a0f977cb7f11afc05791a0fa164d7303b8365a34750638e7", size = 169369, upload-time = "2026-05-19T07:44:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/c0eb0c3fab2ed80d706369b750029143b53126809b77b36bcbb77da66bab/uuid_utils-0.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e99f9a8b2420b228faba23a637e96efaf5c6a678b2e225870f24431c82707f50", size = 175384, upload-time = "2026-05-19T07:45:56.623Z" }, + { url = "https://files.pythonhosted.org/packages/b7/77/50ac87b6e18b1c686f700aa38c9471a990683c6a955f71ac1a6677ed8145/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6853b627983aa1b4fd95aa52d9e87136eb94a7b3b7de0fbb1db8a498d457eeec", size = 564108, upload-time = "2026-05-19T07:43:55.609Z" }, + { url = "https://files.pythonhosted.org/packages/83/16/65046676de246bb5334d9f58aa96d2feb9fc347fda3556aaff7da1c2fc7a/uuid_utils-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f44b65ae0c329843817d9c90e36a7a3c677b413bf407c99e67db874dac49dad3", size = 289967, upload-time = "2026-05-19T07:45:38.886Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/54fa988606a15dfd2028e925d8eb9c3ee6edbf1eb7692a67b37282880b56/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de8a365795a76f347f5622621c2bee543cffa0c70949f3ee093bdefc9d926dcc", size = 325835, upload-time = "2026-05-19T07:44:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1b/50622f967ceacea1f89fd065d9bfd395b51acb02cfb0a4ddc8fa9ff0c983/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:426a8c9af90242d879706ccf29da56f0b0712e7739fb0bbe16baacabc75596e2", size = 332607, upload-time = "2026-05-19T07:43:42.42Z" }, + { url = "https://files.pythonhosted.org/packages/12/f5/4059706be6617e2787e375ea52994ce3c3fa3920b7d4a9c8ebf7895681a5/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:833bc4b3c3fc24be541f67b01b4a75b6b9942a9b7137395b4eb35435948bd6da", size = 444287, upload-time = "2026-05-19T07:43:37.106Z" }, + { url = "https://files.pythonhosted.org/packages/65/d5/f44b2710563da687a368f0ce4dcbd462dfb6708bcd46439d831991d595c7/uuid_utils-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efb5252d7c00d586077f10e169d6e6d0b0d0f806d8a085073f0d19b4737aef4e", size = 324949, upload-time = "2026-05-19T07:45:33.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a7/a69e859e37d26c5603f0bc0ae481860f691224f140e5a832f325b804770d/uuid_utils-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b3377ce388fd7bf8d231ec9d1d4f58c8e87888ddea93581f60ed6f878a4f722", size = 349651, upload-time = "2026-05-19T07:43:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/db/73/4139cd3ca7b81ea283c1c8769373e9b2008241c0744a8ffb25f0a1b31325/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:12b6310beb38adc173ec5dc89e98812fd7e3d98f87f3ef01d2ea6ecb5d87994f", size = 502326, upload-time = "2026-05-19T07:45:40.292Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8c/858101583fbad1b3fa04da88b1f7170836aa0f00b4cb712063325c44466d/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a49b5a75497643479c919e2e537a4a36224ac3aaa0fada61b75d87024021ac3e", size = 607689, upload-time = "2026-05-19T07:44:48.355Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/8f3d54a4763dd91ebd0f3d7b0c2ec434e4e0b1fc667b03a44d611a465ec6/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:63bfdf00be51b6b3b79275d6767d034ea5c7a0caa067a35d72861284100cb60a", size = 566214, upload-time = "2026-05-19T07:44:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/4c9a8d9baaa243c7902d84dbba4d51b1ab51c379c66d3fd6368ff6933ecf/uuid_utils-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7525bc59ac4579c32317d2493dd42cf134b9bb50cd0bc6a41dd9f77e4740dde6", size = 529989, upload-time = "2026-05-19T07:44:43.141Z" }, + { url = "https://files.pythonhosted.org/packages/6d/13/d32cea997f880cedde415730ce0e872ebfd7a040155ae0bbda70eccd208e/uuid_utils-0.16.0-cp314-cp314-win32.whl", hash = "sha256:fbcac6e6710aa2e4bfbb81762758e01470dc56d5048ba4253acc77c9833568ff", size = 169146, upload-time = "2026-05-19T07:45:46.655Z" }, + { url = "https://files.pythonhosted.org/packages/1c/19/9fc55172d8fe59e1f27a14d598b427fa508a7ebb35fa7b7b99c24fa0ef13/uuid_utils-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:d23fcaf37368a1647319187ef6f8b741bf079f033065899bc2d00a44b0a1214a", size = 175364, upload-time = "2026-05-19T07:45:55.335Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/fcd9226b715c5aa0638fcdd6deaf0de6c6c3c451c692cd76bfca810c6512/uuid_utils-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:ea3265f8e2b452a4870f3298cb1d183dc4e36a3682cbb264dbe46af31267e706", size = 173268, upload-time = "2026-05-19T07:44:31.19Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/97ec9af95e58b8187f2934008ffab26e1604d149e34fe01c388b0543a24f/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:99f8420c3ed59f89a086782ac197e257f4b1debb4545dffa90cf5db23f96c892", size = 564464, upload-time = "2026-05-19T07:44:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6d/e4082f407484ac28923c0bf8e861e71d277118d8b7542d0a350340e45350/uuid_utils-0.16.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:259bab73c241743d684dcc3507feb76f484d720545e4e4805582aeff8e19700b", size = 290087, upload-time = "2026-05-19T07:44:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/43/c5c5f273c0ff889f20f10344784f9197dd00eb81ccc294330d4b949fea7e/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:897e8ef0dc5e4ac0b17cf9cae84bb41e560d806280ec5b93db7475b504022105", size = 325532, upload-time = "2026-05-19T07:43:47.508Z" }, + { url = "https://files.pythonhosted.org/packages/13/7f/669aa899ab5378374d28a28231e6978f739921a1af394c7ebd6cc86e2639/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c5af79cde16a7600dfccb7d431aec0afd3088ff170b6a09887bf3f7ab3cc7c81", size = 332209, upload-time = "2026-05-19T07:43:51.528Z" }, + { url = "https://files.pythonhosted.org/packages/2b/57/a2a32406d79a222794ef98a19254fd9a81a029a0f32d7740fba9873bff1f/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bece1a6f677ca36047442c465d8166643eed9818b9e43e0bf42d3cf73e92dcff", size = 445507, upload-time = "2026-05-19T07:44:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/85459a35bfa7d73e79acbc4eab1cf6aa6e4d9d022c3260ed9dea539c7f0b/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3444498e7b099499c8a607d7771377020fa55f7274e46f54106af19f752d7", size = 326154, upload-time = "2026-05-19T07:45:23.587Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/e965efdbb503ed14d6e57aec1a22b98326ed24cc2fb48e750c4d192267a0/uuid_utils-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:542098f6cb6874aebeff98715f3ab7646fbe0f2ffb24509ca372828c68c4ed0e", size = 350905, upload-time = "2026-05-19T07:44:36.957Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/4321867888a783d03b7c053c0b68ca45d03974d86fcebf44d4ec268db397/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7207b25fe534bcf4d57e0110f90670e61c1c38b6f4598ba855af69ab428fc118", size = 502098, upload-time = "2026-05-19T07:44:17.696Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/914a47bf42479bff0ce3e1fa1cbe3585354708edc928e27687cf91de9c26/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:16dc5c6e439f75b0456114e955983e2156c1f38887733e54d54205d3005223e4", size = 607032, upload-time = "2026-05-19T07:44:22.151Z" }, + { url = "https://files.pythonhosted.org/packages/85/4c/2abacd6badba61a047eaa39c8347656229d12843bd9bbe4906daa6dc752c/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6d3ee32c57898d8415242b08d5dd086bc4f7bcbbb3fc102ef257f3d793eb294", size = 567664, upload-time = "2026-05-19T07:45:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/53/1f/9d1a09521276424da19dc0d74456aed3311170fec181b28fa6acba45d963/uuid_utils-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7555f120a2282d1901c9a632c2398a614101af4fe3f7c8114aa0f1d8c1978855", size = 530996, upload-time = "2026-05-19T07:45:44.229Z" }, + { url = "https://files.pythonhosted.org/packages/b4/22/14dbedb6b61f492d5524077fd10bbfb137583b0f0aafa6cd870ccb43f39a/uuid_utils-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:756575d082ea4cb7d2f923d5b640c0efe7c82573aab49220c4e09b62d13737ff", size = 169358, upload-time = "2026-05-19T07:45:05.146Z" }, + { url = "https://files.pythonhosted.org/packages/25/f4/a636806c98401a1108f2456e9cc3fa39a618145bfb1d0860c57203159cfe/uuid_utils-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:aa50261a83991dbb570a00573741455bd8f3249444f7329e5bdcd494799d1504", size = 174813, upload-time = "2026-05-19T07:45:59.579Z" }, + { url = "https://files.pythonhosted.org/packages/75/12/3823742459d87a100deb24bb6b41692aa961b267abd130fa7739cdf7d409/uuid_utils-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:22a17e93a371d850ffce8fcdbacc2239f890efe73aa3262b6170c1febc08afe1", size = 171733, upload-time = "2026-05-19T07:45:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/d3/89/655408a5485c56bf2c4561eb85f5bca119b1f4020370b4daaeb8d13e46fb/uuid_utils-0.16.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4e35e9a986e86806a61288fac3afbb51317f2580929feefd1661891ffd7b8c24", size = 569295, upload-time = "2026-05-19T07:45:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/24/1c/a7c5506a4e2cf95ac98fec0996c56daa14e41f2ab1858f569b3556a202f9/uuid_utils-0.16.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b35706350cf9bd4813f1811bebe03cac09795a5a379f90cb3616171f4e9ffc9e", size = 292316, upload-time = "2026-05-19T07:43:57.044Z" }, + { url = "https://files.pythonhosted.org/packages/dd/75/4267ab8baa1e6a8ad7c262e204484b44df0fde0920025ea9b43c2b869726/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4fd5c7936a876ba2606ba124603b559a5c2cea458c59b9c31677e6acc3c53cc", size = 329619, upload-time = "2026-05-19T07:44:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/15/77/c794102831e331564f651099cac55006694677938d70f1033b35da451a89/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:130f7452c1b87b7c16d0bdc1f32a1de531ae4cc4220ed4e691402bbcfc39e0a9", size = 335121, upload-time = "2026-05-19T07:45:47.974Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/458a0a2da75c596b151182a6c7550c6c3d30f479e14e40f69c0336579e59/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5ee0bbbd4ca3968422cd8308f0072520bc73dc760cb26c6fa75ca1aca14d210", size = 449631, upload-time = "2026-05-19T07:45:50.645Z" }, + { url = "https://files.pythonhosted.org/packages/ed/15/dd1fab6f7fcd15f2c331d0c1f0f516bb1113a640216460f82be53db3dcf8/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc0824a31898ef46a9d84d748c3abe27cdb615ac3773c53cc1f84fc8e66dc7c4", size = 328418, upload-time = "2026-05-19T07:44:52.38Z" }, + { url = "https://files.pythonhosted.org/packages/96/56/62dcd551b140cbeb0f87522da2015b4b9e5818327b920506ad88d28562b0/uuid_utils-0.16.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abfbf5e0c47fb31b37164a99515104e449a0bee36a071dc8b105457a2b35a5e6", size = 356177, upload-time = "2026-05-19T07:45:42.856Z" }, + { url = "https://files.pythonhosted.org/packages/44/e7/3937b9a9d6745b94dbe7b86531e098db8c53b77c8d07df7daa9577a47b8e/uuid_utils-0.16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:680799a9ade01d69c53cb9d41392ced24919d4f600bfab5060b61fca37510097", size = 178508, upload-time = "2026-05-19T07:43:43.774Z" }, +] + [[package]] name = "uuid6" version = "2025.0.1"