From fb0a469bb6561ad1ef56ef8c7145617f2a32b55d Mon Sep 17 00:00:00 2001 From: Aayush jaiswal Date: Wed, 29 Jul 2026 16:34:38 -0500 Subject: [PATCH 1/4] feat(sandboxes): add harness evals and serverless sandboxes tutorial notebooks --- pyproject.toml | 4 +- sandboxes/harness-evals.py | 1485 ++++++++++++++++++++ sandboxes/serverless-sandboxes-tutorial.py | 916 ++++++++++++ uv.lock | 701 ++++++++- 4 files changed, 3077 insertions(+), 29 deletions(-) create mode 100644 sandboxes/harness-evals.py create mode 100644 sandboxes/serverless-sandboxes-tutorial.py diff --git a/pyproject.toml b/pyproject.toml index a44d0c6..312b641 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,13 +13,15 @@ dependencies = [ "kubernetes>=35.0.0", "marimo>=0.23.6", "moutils>=0.3.12", + "openai>=2.46.0", "ray>=2.53.0", "ruamel-yaml>=0.19.1", "statistics>=1.0.3.5", "torch>=2.10.0", "transformers>=5.0.0", "typing-extensions>=4.15.0", - "wandb>=0.24.2", + "wandb[sandbox]>=0.28.1", + "weave>=0.53.2", ] [dependency-groups] diff --git a/sandboxes/harness-evals.py b/sandboxes/harness-evals.py new file mode 100644 index 0000000..4b206b9 --- /dev/null +++ b/sandboxes/harness-evals.py @@ -0,0 +1,1485 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "cwsandbox==0.25.0", +# "marimo>=0.23.8", +# "pydantic==2.13.4", +# "wandb[sandbox]==0.27.2", +# "weave==0.52.38", +# ] +# /// + +import marimo + +__generated_with = "0.23.14" +app = marimo.App( + width="medium", + app_title="Agent Harness Evals", + css_file="/usr/local/_marimo/custom.css", + auto_download=["html"], +) + + +@app.cell +def _(): + import os + import time + import json + import uuid + import weave + import marimo as mo + from pydantic import PrivateAttr + from wandb.sandbox import ( + Sandbox, + SandboxDefaults, + ResourceOptions, + NetworkOptions, + SandboxTimeoutError, + SandboxExecutionError, + SandboxError, + ) + + return ( + NetworkOptions, + PrivateAttr, + ResourceOptions, + Sandbox, + SandboxDefaults, + SandboxError, + SandboxExecutionError, + SandboxTimeoutError, + json, + mo, + os, + time, + uuid, + weave, + ) + + +@app.cell(hide_code=True) +def _(mo): + mo.vstack( + [ + mo.md( + r""" + # Evaluating Coding Agents Inside Serverless Sandboxes + + /// admonition | About This Notebook + type: info + + This notebook is the agent-centric full coding-agent CLI that lives inside its own sandbox**: + + - The sandbox is provisioned the moment the `weave.Model` is initialized. + - The agent binary is installed and authenticated **headlessly**, no + user-led onboarding, just a W&B API key is needed. + - Every `predict` drives the agent *inside that sandbox* to write a solution, + which is then scored safely in a separate, network-isolated sandbox. + + Four harnesses are wired up: **Codex**, **Claude Code**, **OpenClaw**, and + **Nous Hermes**, and you can score them on the built-in demo tasks or the + **HumanEval** / **MBPP** benchmarks (selected below). + + _If you are running this notebook in edit mode, make sure you start by running all cells._ + /// + """ + ), + mo.md( + r""" + /// details | Prerequisites + type: info + + - **A W&B account + API key** — from [wandb.ai/authorize](https://wandb.ai/authorize). + - **Provider API keys** — an OpenAI key for Codex, OpenClaw, Hermes and/or an Anthropic key for + Claude. Paste whichever you need into the Connect form below; an agent is only + runnable if its key is present. + /// + """ + ), + mo.md( + r""" + /// details | Table of Contents + type: info + + - [**Connect W&B services and provider keys**](#1-connect-wb-services-and-provider-keys) - Authenticate agents headlessly + - [**Define the agent backends**](#2-define-the-agent-backends) - The `AgentBackend` extensibility seam + - [**Score generated code safely in a separate sandbox**](#3-score-generated-code-safely-in-a-separate-sandbox) - Isolated verification + - [**Benchmark tasks**](#4-benchmark-tasks) - Demo, HumanEval, and MBPP + - [**Pick agents and launch an evaluation**](#5-pick-agents-and-launch-an-evaluation) - Run per-agent `weave.Evaluation` + - [**Lifecycle, discovery, and cleanup**](#6-lifecycle-discovery-and-cleanup) - Find and stop sandboxes + /// + """ + ), + ] + ) + return + + +@app.cell(hide_code=True) +def _(mo): + # ---------- 1. Setup W&B + provider keys ---------- + wandb_connect_form = ( + mo.md(""" + - W&B entity *(team or username)*: {entity} + - W&B project: {project} + - W&B API key *(required)*: {api_key} + - OpenAI API key *(for Codex, OpenClaw, Hermes)*: {openai_api_key} + - Anthropic API key *(for Claude)*: {anthropic_api_key} + """) + .batch( + entity=mo.ui.text(value="wandb-smle", full_width=True), + project=mo.ui.text(value="agent-sandbox-eval", full_width=True), + api_key=mo.ui.text(kind="password", placeholder="from wandb.ai/authorize", full_width=True), + openai_api_key=mo.ui.text(kind="password", placeholder="sk-...", full_width=True), + anthropic_api_key=mo.ui.text(kind="password", placeholder="sk-ant-...", full_width=True), + ) + .form(submit_button_label="Connect", bordered=False) + ) + wandb_connect_form + return (wandb_connect_form,) + + +@app.cell(hide_code=True) +def _(mo, os, wandb_connect_form, weave): + _v = wandb_connect_form.value or {} + ENTITY = _v.get("entity") + PROJECT = _v.get("project") + API_KEY = _v.get("api_key") + mo.stop( + not (ENTITY and PROJECT and API_KEY), + mo.md("_Fill in the W&B fields above and press **Connect**._"), + ) + + os.environ["WANDB_API_KEY"] = API_KEY + weave.init(f"{ENTITY}/{PROJECT}") + weave_url = f"https://wandb.ai/{ENTITY}/{PROJECT}/weave" + + PROVIDER_KEYS = { + "openai_api_key": _v.get("openai_api_key") or "", + "anthropic_api_key": _v.get("anthropic_api_key") or "", + } + _have = [n for n, k in PROVIDER_KEYS.items() if k] + + mo.callout( + mo.md( + f"✅ **Connected** — logging to `{ENTITY}/{PROJECT}`. " + f"[Open Weave dashboard]({weave_url}) \n" + f"Provider keys provided: `{', '.join(_have) or 'none yet'}`" + ), + kind="success", + ) + return API_KEY, PROVIDER_KEYS + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 1. Connect W&B services and provider keys + + /// admonition | Connect and authenticate agents + type: info + + Fill in your W&B entity, project, and API key, plus the provider key(s) for + the agent(s) you want to run, then press **Connect**. Keys are kept in memory + and injected into each agent's sandbox as environment variables + (`OPENAI_API_KEY` for Codex, `ANTHROPIC_API_KEY` for + Claude). Nothing is written to disk and no interactive login is triggered. + + The form gates the rest of the notebook: the agent definitions, scorer, and + evaluation cells stay paused until you connect. + /// + """) + return + + +@app.cell(hide_code=True) +def _(mo): + wandb_product_tabs = mo.ui.tabs( + { + "Agents": mo.md( + """ + ### Coding agents as sandboxed models + + Each agent is a real terminal coding tool driven headlessly + (`codex exec`, `claude -p`). Inside its sandbox the agent reads the + task, writes Python, and saves a final `solution.py`, exactly the + workflow a developer would run locally, but isolated and reproducible. + """ + ), + "Weave": mo.md( + """ + ### W&B Weave + + Weave traces the full lifecycle: the agent's generation + (`predict`), the returned solution, the scorer's pass/fail verdict, + and latency — making it easy to compare agents side by side. + """ + ), + "Sandbox": mo.md( + """ + ### Serverless Sandbox + + Two sandbox roles here: the **agent sandbox** (one per model, + provisioned at init, internet egress enabled so the agent can call + its model API and `npm install`) and the **scorer sandbox** (a + fresh, network-isolated sandbox per scored solution, so untrusted + generated code can't phone home). + """ + ), + } + ) + + wandb_product_tabs + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 2. Define the agent backends + + /// admonition | AgentBackend + type: info + + `AgentBackend` is the extensibility seam: each concrete backend knows how to + **install** itself, what **auth env** it needs, and how to build the headless + **run command**. + /// + """) + return + + +@app.cell +def _(): + # ---------- 2.1 Agent backend abstraction ---------- + import shlex + from abc import ABC, abstractmethod + + class AgentBackend(ABC): + id: str + display_name: str + provider: str + key_field: str + eval_parallelism: int | None = None + run_timeout_seconds: int | None = None + + @abstractmethod + def install_cmds(self) -> list: + """Commands run once inside the sandbox at provision time.""" + + @abstractmethod + def auth_env(self, api_key: str) -> dict: + """Env vars (baked into the agent sandbox) for non-interactive auth.""" + + @abstractmethod + def run_argv(self, prompt: str, workdir: str, model: str | None) -> list: + """argv to run the agent headlessly for a single task.""" + + class CodexBackend(AgentBackend): + id = "codex" + display_name = "Codex CLI" + provider = "OpenAI" + key_field = "openai_api_key" + + def install_cmds(self) -> list: + return [["npm", "install", "-g", "@openai/codex"]] + + def auth_env(self, api_key: str) -> dict: + return {"CODEX_API_KEY": api_key, "OPENAI_API_KEY": api_key} + + def run_argv(self, prompt: str, workdir: str, model: str | None) -> list: + # We're already inside an isolated W&B sandbox, so disable Codex's OWN + # nested OS sandbox (landlock/seccomp). + argv = [ + "codex", "exec", + "--skip-git-repo-check", + "--sandbox", "danger-full-access", + "--cd", workdir, + ] + if model: + argv += ["-m", model] + argv.append(prompt) + return argv + + class ClaudeCodeBackend(AgentBackend): + id = "claude" + display_name = "Claude Code CLI" + provider = "Anthropic" + key_field = "anthropic_api_key" + + def install_cmds(self) -> list: + return [["npm", "install", "-g", "@anthropic-ai/claude-code"]] + + def auth_env(self, api_key: str) -> dict: + # IS_SANDBOX=1 is Claude Code's documented escape hatch: W&B sandbox + # runs as root + return {"ANTHROPIC_API_KEY": api_key, "IS_SANDBOX": "1"} + + def run_argv(self, prompt: str, workdir: str, model: str | None) -> list: + inner = ["claude", "-p", prompt, "--bare", "--dangerously-skip-permissions"] + if model: + inner += ["--model", model] + return ["bash", "-lc", f"cd {shlex.quote(workdir)} && {shlex.join(inner)}"] + + class OpenClawBackend(AgentBackend): + # OpenClaw is a full agent HARNESS (Gateway + workspace + tools) + id = "openclaw" + display_name = "OpenClaw" + eval_parallelism = 1 + run_timeout_seconds = 300 + provider = "OpenAI" + key_field = "openai_api_key" + + workspace = "/work/openclaw-ws" + + def install_cmds(self) -> list: + onboard = ( + f"mkdir -p {shlex.quote(self.workspace)} && " + "openclaw onboard --non-interactive --mode local " + f"--workspace {shlex.quote(self.workspace)} " + "--auth-choice openai-api-key --secret-input-mode ref " + "--accept-risk --skip-skills --skip-bootstrap --skip-health" + ) + return [ + ["npm", "install", "-g", "openclaw@latest"], + ["bash", "-lc", onboard], + ] + + def auth_env(self, api_key: str) -> dict: + return {"OPENAI_API_KEY": api_key} + + def run_argv(self, prompt: str, workdir: str, model: str | None) -> list: + return [ + "bash", "-lc", + f"openclaw agent --local --agent main --message {shlex.quote(prompt)}", + ] + + class HermesBackend(AgentBackend): + # Hermes (Nous Research) is a self-improving agent harness + id = "hermes" + display_name = "Hermes (Nous)" + provider = "OpenAI" + key_field = "openai_api_key" + hermes_provider = "openai-api" + default_model = "gpt-5.5" + + def install_cmds(self) -> list: + return [[ + "bash", "-lc", + "curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash", + ]] + + def auth_env(self, api_key: str) -> dict: + return { + "OPENAI_API_KEY": api_key, + "HERMES_YOLO_MODE": "1", # bypass dangerous-command approval + "HERMES_ACCEPT_HOOKS": "1", # auto-approve shell hooks (no TTY) + } + + def run_argv(self, prompt: str, workdir: str, model: str | None) -> list: + m = model or self.default_model + cmd = ( + f"hermes -z {shlex.quote(prompt)} " + f"--provider {self.hermes_provider} --model {shlex.quote(m)}" + ) + return [ + "bash", "-lc", + 'export PATH="$HOME/.local/bin:$PATH" && ' + f"cd {shlex.quote(workdir)} && {cmd}", + ] + + BACKENDS = { + b.id: b + for b in (CodexBackend(), ClaudeCodeBackend(), OpenClawBackend(), HermesBackend()) + } + return (BACKENDS,) + + +@app.cell +def _(ResourceOptions): + # Agent sandboxes need Node (for the CLIs) and git; node:22-bookworm has both. + # They get more CPU/memory than the scorer because they run a full agent loop. + # ONE sandbox per model serves all concurrent predicts (see DEFAULT_PARALLELISM), + # and each `claude`/`codex`/`openclaw`/`hermes` run is a heavy Node/Rust process, so the + # LIMITS are generous to avoid OOM-killing concurrent agents (which manifests as + # empty-output timeouts). Requests stay small so provisioning isn't rejected. + AGENT_IMAGE = "node:22-bookworm" + AGENT_RESOURCES = ResourceOptions( + requests={"cpu": "500m", "memory": "512Mi"}, + limits={"cpu": "4", "memory": "6Gi"}, + ) + return AGENT_IMAGE, AGENT_RESOURCES + + +@app.cell +def _( + AGENT_IMAGE, + AGENT_RESOURCES, + BACKENDS, + NetworkOptions, + PrivateAttr, + Sandbox, + SandboxDefaults, + SandboxTimeoutError, + uuid, + weave, +): + # ---------- 2.2 Sandboxed-agent weave.Model ---------- + import hashlib + import threading + + _AGENT_SANDBOXES: dict = {} + _PROVISION_LOCK = threading.Lock() + + AGENT_MAX_LIFETIME_SECONDS = 3600 + + class SandboxAgentModel(weave.Model): + agent_id: str + model_name: str | None = None + # Two independent budgets: a one-time CLI install vs. each agent task run. + # Codex/Claude return in seconds on these tasks, so 180s is a generous run + # ceiling that fails fast if one hangs. + install_timeout_seconds: int = 900 + run_timeout_seconds: int = 180 + # Sandbox self-termination backstop. Defaults to the floor; the run loop + # raises it for serial agents so the sandbox outlives the full eval. + max_lifetime_seconds: int = AGENT_MAX_LIFETIME_SECONDS + # Serialized link: the sandbox this agent is bound to. Set during setup(); + # lets us re-attach (Sandbox.from_id) and stop() the exact sandbox at any + # time. (It changes per run, so each run logs a new model version.) + sandbox_id: str | None = None + + # Private (not serialized): the provider key, kept out of the trace/version. + _api_key: str = PrivateAttr(default="") + + # ---- lifecycle ---- + def _cache_key(self) -> tuple: + digest = ( + hashlib.sha256(self._api_key.encode()).hexdigest()[:8] + if self._api_key + else "nokey" + ) + return (self.agent_id, self.model_name, digest) + + def _provision(self): + """Spin up the agent's sandbox and install + authenticate the CLI once.""" + backend = BACKENDS[self.agent_id] + defaults = SandboxDefaults( + container_image=AGENT_IMAGE, + tags=("agent-sandbox-eval", self.agent_id), + environment_variables={ + **backend.auth_env(self._api_key), + "NODE_NO_WARNINGS": "1", + "CI": "1", + }, + resources=AGENT_RESOURCES, + ) + # Internet egress: required for npm install AND the agent's model API. + sb = Sandbox.run( + defaults=defaults, + network=NetworkOptions(egress_mode="internet"), + max_lifetime_seconds=self.max_lifetime_seconds, + ) + for cmd in backend.install_cmds(): + proc = sb.exec(cmd, timeout_seconds=self.install_timeout_seconds).result() + if proc.returncode != 0: + try: + sb.stop(missing_ok=True).result() + except Exception: + pass + raise RuntimeError( + f"`{' '.join(cmd)}` failed (rc={proc.returncode}): " + f"{(proc.stderr or '')[-400:]}" + ) + return sb + + @weave.op(name="agent_setup") + def setup(self) -> dict: + """Lazily spin up + install + authenticate the agent's sandbox once, then + cache the handle and bind self.sandbox_id to it. Wrapped as a weave.op so + provisioning (and any failure) is traced as OUTPUT rather than raising and + crashing the run. Idempotent: reuses a cached/known sandbox if present.""" + key = self._cache_key() + sb = _AGENT_SANDBOXES.get(key) + if sb is None: + with _PROVISION_LOCK: + sb = _AGENT_SANDBOXES.get(key) # re-check after acquiring the lock + if sb is None: + try: + if self.sandbox_id is not None: + # Known id, no cached handle (fresh process) -> reattach. + sb = Sandbox.from_id(self.sandbox_id).result() + else: + sb = self._provision() + except Exception as e: + return { + "agent_id": self.agent_id, + "model_name": self.model_name, + "status": "error", + "error": f"{type(e).__name__}: {e}", + } + _AGENT_SANDBOXES[key] = sb + self.sandbox_id = sb.sandbox_id + return { + "agent_id": self.agent_id, + "model_name": self.model_name, + "sandbox_id": self.sandbox_id, + "status": "ready", + } + + def _get_sandbox(self): + """Return (live_handle_or_None, setup_status). Provisions via setup() on + first use; predict relies on this so a setup failure becomes a failed row + instead of an exception.""" + sb = _AGENT_SANDBOXES.get(self._cache_key()) + if sb is not None: + self.sandbox_id = sb.sandbox_id + return sb, {"status": "ready", "sandbox_id": self.sandbox_id} + status = self.setup() + return _AGENT_SANDBOXES.get(self._cache_key()), status + + def ensure_ready(self) -> dict: + """Public hook for the run loop to provision before evaluate (clean error + reporting + keeps the first row's latency from absorbing the install). + Returns the setup() status dict.""" + return self.setup() + + @weave.op(name="agent_generate_solution", kind="llm") + def predict(self, name: str, spec: str, tests: list) -> str: + import shlex as _shlex + + backend = BACKENDS[self.agent_id] + sb, status = self._get_sandbox() + if sb is None: + return f"failed: setup error: {status.get('error', 'unknown')}" + + run_id = uuid.uuid4().hex + workdir = f"/work/{run_id}" + target = f"{workdir}/solution.py" + # Logs live OUTSIDE workdir so the agent doesn't see/touch them. + out_log, err_log = f"/tmp/{run_id}.out", f"/tmp/{run_id}.err" + try: + sb.exec(["mkdir", "-p", workdir]).result() + except Exception as e: # noqa: BLE001 + # The sandbox can be GONE here — e.g. it hit max_lifetime_seconds + # mid-eval (serial agents) and the gRPC call returns NOT_FOUND. + # Record a failed row instead of letting it crash the eval thread. + return ( + f"failed: agent sandbox unavailable ({type(e).__name__}); it may " + f"have exceeded its lifetime. {str(e)[-200:]}" + ) + + prompt = ( + f"You are solving a coding task. Write a Python function " + f"named `{name}` that satisfies the specification below.\n\n" + f"Specification:\n{spec}\n\n" + f"Requirements:\n" + f"- Use the exact function name and signature requested.\n" + f"- Save the complete solution — the function definition plus any " + f"imports it needs (no markdown fences, no prose) — to the file: " + f"{target}\n" + f"- Overwrite the file if it already exists.\n" + ) + argv = backend.run_argv(prompt, workdir, self.model_name) + shell = ( + f"{_shlex.join(argv)} < /dev/null " + f"> {_shlex.quote(out_log)} 2> {_shlex.quote(err_log)}" + ) + + def _tail(path: str, n: int = 500) -> str: + try: + return sb.read_file(path).result().decode()[-n:].strip() + except Exception: + return "" + + try: + sb.exec( + ["bash", "-lc", shell], timeout_seconds=self.run_timeout_seconds + ).result() + except SandboxTimeoutError: + # Catch the PARENT timeout (covers SandboxCommandTimeoutError too) so a + # slow/hung agent records a failed row (with diagnostics) instead of + # crashing the eval. Capture BOTH streams + return ( + f"failed: agent run timed out after {self.run_timeout_seconds}s. " + f"stdout tail: {_tail(out_log)} | stderr tail: {_tail(err_log)}" + ) + except Exception as e: # noqa: BLE001 + # Sandbox vanished during the run (e.g. lifetime exceeded -> NOT_FOUND). + return ( + f"failed: agent sandbox error during run ({type(e).__name__}); it " + f"may have exceeded its lifetime. {str(e)[-200:]}" + ) + + # Prefer the file the agent wrote; surface both streams otherwise. + try: + code = sb.read_file(target).result().decode() + if code.strip(): + return code.strip() + except Exception: + pass + # Show what (if anything) the agent left in the workdir — distinguishes + # "wrote nothing" from "wrote a differently-named file" at a glance. + try: + _ls = ( + sb.exec(["bash", "-lc", f"ls -la {_shlex.quote(workdir)}"]) + .result() + .stdout + or "" + ).strip()[-200:] + except Exception: + _ls = "" + return ( + f"failed: no solution.py written. workdir: {_ls} | " + f"stdout tail: {_tail(out_log)} | stderr tail: {_tail(err_log)}" + ) + + def stop(self) -> None: + """Spin down the sandbox bound to this agent — via the cached handle or, + if that's gone, by re-attaching with the serialized sandbox_id.""" + sb = _AGENT_SANDBOXES.pop(self._cache_key(), None) + if sb is None and self.sandbox_id: + try: + sb = Sandbox.from_id(self.sandbox_id).result() + except Exception: + sb = None + if sb is not None: + try: + sb.stop(missing_ok=True).result() + except Exception: + pass + + + return (SandboxAgentModel,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 3. Score generated code safely in a separate sandbox + + /// admonition | CodeScorer + type: info + + The agent's output is verified by the **`CodeScorer`**: it wraps each task's `(input, expected)` pairs in a + harness, runs the solution in a **fresh, network-isolated** sandbox, and + returns `{passed, error, sandbox_latency}`. Keeping scoring in its own + egress-free sandbox means untrusted generated code can't reach the network, + even though the agent sandbox can. + /// + """) + return + + +@app.cell +def _(ResourceOptions, SandboxDefaults): + # Scorer sandboxes: small, no special networking (isolated by default). + SANDBOX_DEFAULTS = SandboxDefaults( + container_image="python:3.11", + tags=("agent-sandbox-eval", "code-eval"), + environment_variables={"PYTHONUNBUFFERED": "1"}, + resources=ResourceOptions( + requests={"cpu": "250m", "memory": "256Mi"}, + limits={"cpu": "1", "memory": "512Mi"}, + ), + ) + return (SANDBOX_DEFAULTS,) + + +@app.cell +def _( + SANDBOX_DEFAULTS, + Sandbox, + SandboxError, + SandboxExecutionError, + SandboxTimeoutError, + json, + time, + weave, +): + # ---------- 3.1 Sandbox-based scorer ---------- + class CodeScorer(weave.Scorer): + + @weave.op(name="code_scorer", kind="scorer") + def score( + self, + name: str, + spec: str, + tests: list, + output: str, + test_program: str | None = None, + entry_point: str | None = None, + ) -> dict: + start_time = time.time() + _out = (output or "").strip() + if not _out or _out.startswith("failed:"): + return { + "passed": False, + "error": (_out or "empty generation")[-300:], + "sandbox_latency": time.time() - start_time, + } + + + if test_program: + _ep = entry_point or name + script = ( + f"{output}\n\n" + f"{test_program}\n\n" + "import json\n" + "_passed, _error = True, ''\n" + "try:\n" + f" check({_ep})\n" + "except Exception as _e:\n" + " _passed, _error = False, repr(_e)\n" + "with open('/tmp/result.json', 'w') as _f:\n" + " json.dump({'passed': _passed, 'error': _error}, _f)\n" + ) + else: + asserts = "\n".join( + f" assert {name}(*{args!r}) == {expected!r}, {f'failed on input {args!r}'!r}" + for args, expected in tests + ) + script = ( + f"{output}\n\n" + "import json\n" + "_passed, _error = True, ''\n" + "try:\n" + f"{asserts}\n" + "except Exception as _e:\n" + " _passed, _error = False, repr(_e)\n" + "with open('/tmp/result.json', 'w') as _f:\n" + " json.dump({'passed': _passed, 'error': _error}, _f)\n" + ) + + last_exc = None + for attempt in range(3): + sb = None + try: + sb = Sandbox.run(defaults=SANDBOX_DEFAULTS) + sb.write_file("/tmp/t.py", script.encode()).result() + proc = sb.exec( + ["python", "/tmp/t.py"], timeout_seconds=10 + ).result() + try: + verdict = json.loads( + sb.read_file("/tmp/result.json").result().decode() + ) + passed = bool(verdict.get("passed")) + error = str(verdict.get("error", "")) + except Exception: + passed = False + error = (proc.stderr or "no result file written") + end_time = time.time() + return { + "passed": passed, + "error": error[-300:], + "sandbox_latency": (end_time - start_time), + } + except SandboxTimeoutError: + # Parent timeout (caught before SandboxError below) + end_time = time.time() + return { + "passed": False, + "error": "execution timed out after 10s", + "sandbox_latency": (end_time - start_time), + } + except (SandboxExecutionError, SandboxError) as e: + last_exc = e + time.sleep(2 ** attempt) + finally: + if sb is not None: + try: + sb.stop(missing_ok=True).result() + except Exception: + pass + end_time = time.time() + return {"passed": False, "error": f"Sandbox unavailable after 3 attempts: {last_exc}", "sandbox_latency": (end_time - start_time)} + + return (CodeScorer,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 4. Benchmark tasks + + /// admonition | Build a versioned Weave Dataset + type: info + + Pick the problem set the agents solve. Two are wired up behind a small + `BENCHMARKS` registry (the data sibling of the `AgentBackend` seam): + + - **Built-in demo** — the LeetCode-style easy/medium/hard prompts with + deterministic `(input, expected)` tuple tests. + - **HumanEval** — the canonical 164-problem coding benchmark, downloaded on + demand. Each problem is a function signature + docstring; a hidden + `check()` program verifies the agent's solution. + - **MBPP** — the 974-problem "Mostly Basic Python Problems" set. Each problem + is a short natural-language task; the agent writes the named function and a + hidden batch of asserts (wrapped as a `check()`) verifies it. + + All are normalized to the same row schema, so the same `predict` and + `CodeScorer` handle either. The difference from the code-gen tutorial is + *who* solves them: here a coding agent in a sandbox, not a single API call. + Choose a benchmark and the tasks to include; the selection builds a + versioned Weave `Dataset`. + /// + """) + return + + +@app.cell(hide_code=True) +def _(): + # ---------- 4.1 Ground-truth dataset ---------- + TASKS = [ + # --- Easy --- + { + "name": "add", + "spec": "Write a function `add(a, b)` that returns the sum of two numbers.", + "tests": [((2, 3), 5), ((-1, 1), 0), ((0, 0), 0)], + }, + { + "name": "reverse_string", + "spec": "Write a function `reverse_string(s)` that returns the reverse of string s.", + "tests": [(("hello",), "olleh"), (("",), ""), (("a",), "a")], + }, + { + "name": "fizzbuzz", + "spec": "Write a function `fizzbuzz(n)` returning 'Fizz' if n is divisible by 3, 'Buzz' if by 5, 'FizzBuzz' if by both, else str(n).", + "tests": [((3,), "Fizz"), ((5,), "Buzz"), ((15,), "FizzBuzz"), ((7,), "7")], + }, + # --- Medium --- + { + "name": "is_prime", + "spec": "Write a function `is_prime(n)` that returns True iff n is a prime number. n is a positive integer.", + "tests": [((2,), True), ((4,), False), ((17,), True), ((1,), False)], + }, + { + "name": "second_largest", + "spec": "Write a function `second_largest(nums)` returning the second-largest distinct value in a list of ints. Assume len(nums) >= 2 and at least 2 distinct values.", + "tests": [(([1, 2, 3],), 2), (([5, 5, 4, 4, 3],), 4), (([-1, -2, -3],), -2)], + }, + { + "name": "flatten", + "spec": "Write a function `flatten(lst)` that takes a possibly nested list of ints and returns a flat list of all ints in order.", + "tests": [ + (([1, [2, [3, 4]], 5],), [1, 2, 3, 4, 5]), + (([],), []), + (([[1], [2, [3]]],), [1, 2, 3]), + ], + }, + { + "name": "group_anagrams", + "spec": "Write a function `group_anagrams(words)` that groups a list of strings into lists of anagrams. Each group must be sorted alphabetically internally. The returned list of groups must be sorted by the first element of each group.", + "tests": [ + ((["eat", "tea", "tan", "ate", "nat", "bat"],), [["ate", "eat", "tea"], ["bat"], ["nat", "tan"]]), + (([""],), [[""]]), + ((["a"],), [["a"]]), + ], + }, + { + "name": "longest_common_subsequence", + "spec": "Write a function `longest_common_subsequence(s1, s2)` that returns the length of the longest common subsequence of strings s1 and s2.", + "tests": [ + (("abcde", "ace"), 3), + (("abc", "abc"), 3), + (("abc", "def"), 0), + ], + }, + # --- Hard --- + { + "name": "min_coins", + "spec": "Write a function `min_coins(coins, amount)` that returns the minimum number of coins needed to make up the given amount using the given coin denominations. Return -1 if it is not possible.", + "tests": [ + (([1, 5, 11], 15), 3), + (([2], 3), -1), + (([1, 2, 5], 11), 3), + ], + }, + { + "name": "longest_palindrome", + "spec": "Write a function `longest_palindrome(s)` that returns the longest palindromic substring of s. If there are ties, return the one that starts earliest.", + "tests": [ + (("babad",), "bab"), + (("cbbd",), "bb"), + (("a",), "a"), + (("racecar",), "racecar"), + ], + }, + { + "name": "word_break", + "spec": "Write a function `word_break(s, word_dict)` that returns True if the string s can be segmented into a space-separated sequence of one or more words from word_dict (a list of strings).", + "tests": [ + (("leetcode", ["leet", "code"]), True), + (("applepenapple", ["apple", "pen"]), True), + (("catsandog", ["cats", "dog", "sand", "and", "cat"]), False), + ], + }, + ] + return (TASKS,) + + +@app.cell +def _(TASKS): + # ---------- 4.2 Benchmark loaders ---------- + # Every benchmark, demo or real, is normalized to ONE row schema so the + # weave.Dataset, predict(), and CodeScorer never have to branch on the source: + # name -> the function the agent must write + # spec -> the problem text shown to the agent (tests stay hidden) + # tests -> [(args, expected), ...] tuple asserts (demo benchmark) + # test_program -> a `def check(candidate): assert ...` program (HumanEval, MBPP) + # entry_point -> the function `check` is called with + # source -> a human label/id for the row (difficulty or task_id) + # A row uses EITHER `tests` (tuple asserts) OR `test_program` (a check fn); + # the scorer prefers `test_program` when present. + import gzip + import json as _json + import re as _re + import urllib.request + + # Official HumanEval problem set (164 problems). /raw/ redirects to + # raw.githubusercontent.com, which urllib follows automatically. + HUMANEVAL_URL = ( + "https://github.com/openai/human-eval/raw/master/data/HumanEval.jsonl.gz" + ) + # MBPP "Mostly Basic Python Problems" (974 problems), one JSON object per line. + MBPP_URL = ( + "https://github.com/google-research/google-research/raw/master/mbpp/mbpp.jsonl" + ) + _BENCH_CACHE: dict = {} + + def _difficulty_for(index: int) -> str: + return "Easy" if index < 3 else "Medium" if index < 8 else "Hard" + + def load_demo(limit: int | None = None) -> list: + """The 11 hand-written LeetCode-style tasks, normalized to the schema.""" + rows = [ + { + "name": _t["name"], + "spec": _t["spec"], + "tests": _t["tests"], + "test_program": None, + "entry_point": _t["name"], + "source": _difficulty_for(_i), + } + for _i, _t in enumerate(TASKS) + ] + return rows[:limit] if limit else rows + + def _ssl_context(): + # Standalone/uv-managed interpreters don't always trust the OS keychain, + # which makes a plain urllib HTTPS GET fail with CERTIFICATE_VERIFY_FAILED. + # certifi ships transitively with wandb/weave, so use its CA bundle when + # available and fall back to the default context otherwise. + import ssl + try: + import certifi + return ssl.create_default_context(cafile=certifi.where()) + except Exception: # noqa: BLE001 + return ssl.create_default_context() + + def load_humaneval(limit: int | None = None) -> list: + """HumanEval: download + cache the JSONL, map each problem to the schema. + Runs in the notebook process (which already needs internet), not a sandbox.""" + if "humaneval" not in _BENCH_CACHE: + _req = urllib.request.Request( + HUMANEVAL_URL, headers={"User-Agent": "Mozilla/5.0"} + ) + _raw = urllib.request.urlopen(_req, timeout=60, context=_ssl_context()).read() + _text = gzip.decompress(_raw).decode() + _problems = [ + _json.loads(_line) for _line in _text.splitlines() if _line.strip() + ] + _BENCH_CACHE["humaneval"] = [ + { + "name": _p["entry_point"], + "spec": _p["prompt"], + "tests": [], + "test_program": _p["test"], + "entry_point": _p["entry_point"], + "source": _p["task_id"], + } + for _p in _problems + ] + rows = _BENCH_CACHE["humaneval"] + return rows[:limit] if limit else rows + + def _mbpp_entry_point(code: str, test_list: list) -> str: + # MBPP `code` may define helper functions/globals before the real one, so + # pick the def that's actually CALLED in the tests; fall back to the last def. + defs = _re.findall(r"(?m)^[ \t]*def\s+(\w+)\s*\(", code) + called = " ".join(test_list) + for _d in defs: + if _re.search(rf"\b{_re.escape(_d)}\s*\(", called): + return _d + return defs[-1] if defs else "solution" + + def _mbpp_test_program(test_list: list, setup_code: str) -> str: + # Wrap MBPP's bare assert strings (which reference the function by its real + # name) in a `check(candidate)` so the SAME scorer path as HumanEval runs + # them. `candidate` is unused — the asserts hit the global the agent defined. + body = [] + for _line in (setup_code or "").splitlines(): + body.append((" " + _line) if _line.strip() else "") + for _assert in test_list: + body.extend(" " + _line for _line in _assert.splitlines()) + if not any(_l.strip() for _l in body): + body = [" pass"] + return "def check(candidate):\n" + "\n".join(body) + "\n" + + def load_mbpp(limit: int | None = None) -> list: + """MBPP: download + cache the JSONL, map each problem to the schema. The + function name is derived from the reference code; the agent gets only the + natural-language `text` (tests stay hidden), same as the other benchmarks.""" + if "mbpp" not in _BENCH_CACHE: + _req = urllib.request.Request( + MBPP_URL, headers={"User-Agent": "Mozilla/5.0"} + ) + _text = ( + urllib.request.urlopen(_req, timeout=60, context=_ssl_context()) + .read() + .decode() + ) + _problems = [ + _json.loads(_line) for _line in _text.splitlines() if _line.strip() + ] + _rows = [] + for _p in _problems: + _ep = _mbpp_entry_point(_p["code"], _p["test_list"]) + _rows.append( + { + "name": _ep, + "spec": _p["text"], + "tests": [], + "test_program": _mbpp_test_program( + _p["test_list"], _p.get("test_setup_code", "") + ), + "entry_point": _ep, + "source": f"MBPP/{_p['task_id']}", + } + ) + _BENCH_CACHE["mbpp"] = _rows + rows = _BENCH_CACHE["mbpp"] + return rows[:limit] if limit else rows + + # Registry mirrors BACKENDS: adding BigCodeBench later is one more entry. + BENCHMARKS = { + "demo": {"display": "Built-in demo (11 tasks)", "load": load_demo, "size": 11}, + "humaneval": {"display": "HumanEval (164 problems)", "load": load_humaneval, "size": 164}, + # "mbpp": {"display": "MBPP (974 problems)", "load": load_mbpp, "size": 974}, + } + return (BENCHMARKS,) + + +@app.cell(hide_code=True) +def _(BENCHMARKS, mo): + # ---------- 4.3 Benchmark picker ---------- + benchmark_dropdown = mo.ui.dropdown( + options={_v["display"]: _k for _k, _v in BENCHMARKS.items()}, + value=BENCHMARKS["humaneval"]["display"], + label="Benchmark", + ) + # A benchmark x 4 agents x (agent run + scorer sandbox) is expensive, so cap the + # count by default; raise it for a fuller run. Stop at the largest benchmark. + _max_size = max(_v["size"] for _v in BENCHMARKS.values()) + max_problems = mo.ui.number(start=1, stop=_max_size, step=1, value=20, label="Max problems") + mo.vstack( + [ + mo.md( + """ + ### Choose a benchmark + + Pick the problem set the agents will solve. **Built-in demo** is the + 11 hand-written tasks; **HumanEval** (164) and **MBPP** (974) are the + canonical function-completion benchmarks, downloaded on demand. Use + **Max problems** to cap how many are loaded — each problem is run by + every selected agent and scored in its own sandbox, so a full + benchmark x 4-agent sweep is costly. + """ + ), + mo.hstack([benchmark_dropdown, max_problems], justify="start", gap=2), + ] + ) + return benchmark_dropdown, max_problems + + +@app.cell(hide_code=True) +def _(BENCHMARKS, benchmark_dropdown, max_problems, mo): + # ---------- 4.4 Load the selected benchmark ---------- + benchmark_id = benchmark_dropdown.value or "demo" + _limit = int(max_problems.value) if max_problems.value else None + + try: + loaded_tasks = BENCHMARKS[benchmark_id]["load"](_limit) + _load_error = None + except Exception as _e: # noqa: BLE001 — surface as a callout, never crash the notebook + loaded_tasks = [] + _load_error = f"{type(_e).__name__}: {_e}" + + if _load_error: + _bench_summary = mo.callout( + mo.md( + f"⚠️ Could not load **{BENCHMARKS[benchmark_id]['display']}**: " + f"`{_load_error}`. Check your machine's internet access and retry." + ), + kind="danger", + ) + else: + _bench_summary = mo.callout( + mo.md( + f"**Benchmark:** `{BENCHMARKS[benchmark_id]['display']}` \n" + f"**Loaded problems:** `{len(loaded_tasks)}` " + f"(of `{BENCHMARKS[benchmark_id]['size']}` available)" + ), + kind="success", + ) + _bench_summary + return benchmark_id, loaded_tasks + + +@app.cell(hide_code=True) +def _(loaded_tasks, mo): + _all_task_rows = [ + { + "#": _i, + "source": _t["source"], + "function": _t["name"], + "checks": "check()" if _t.get("test_program") else f"{len(_t['tests'])} asserts", + "spec": (_t["spec"][:120] + "…") if len(_t["spec"]) > 120 else _t["spec"], + } + for _i, _t in enumerate(loaded_tasks) + ] + task_table = mo.ui.table( + _all_task_rows, + selection="multi", + initial_selection=list(range(len(_all_task_rows))), + label="Select the benchmark tasks to include in this evaluation", + page_size=20, + ) + task_table + return (task_table,) + + +@app.cell(hide_code=True) +def _(benchmark_id, loaded_tasks, mo, task_table, weave): + # Map the selected table rows back to their full schema dicts by index. + _selected_idx = sorted( + _row["#"] for _row in (task_table.value or []) if "#" in _row + ) + selected_benchmark_tasks = [loaded_tasks[_i] for _i in _selected_idx] + + dataset_name = f"{benchmark_id}_{len(selected_benchmark_tasks)}tasks" + + dataset = ( + weave.Dataset(name=dataset_name, rows=selected_benchmark_tasks) + if selected_benchmark_tasks + else None + ) + + if selected_benchmark_tasks: + _summary = mo.callout( + mo.md( + f"**Weave dataset:** `{dataset_name}` \n" + f"**Selected tasks:** `{len(selected_benchmark_tasks)}` of " + f"`{len(loaded_tasks)}` loaded" + ), + kind="success", + ) + else: + _summary = mo.callout( + mo.md("No tasks selected — check one or more rows in the table above to build a dataset."), + kind="warn", + ) + _summary + return dataset, dataset_name, selected_benchmark_tasks + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 5. Pick agents and launch an evaluation + + /// admonition | Run a per-agent weave.Evaluation + type: info + + Choose the agent(s) to evaluate, then press **Run selected evaluation**. Each + selected agent is wrapped in a `SandboxAgentModel`, which provisions its own + sandbox (install + auth) before the run, solves every task inside that + sandbox, and is scored by `CodeScorer`. Results are logged as a + `weave.Evaluation` per agent. Agent sandboxes are torn down when the run + completes. + + An agent is only runnable if its provider key was entered in the Connect + form, rows without a key are flagged below. + /// + """) + return + + +@app.cell(hide_code=True) +def _(BACKENDS, PROVIDER_KEYS, mo): + _agent_rows = [ + { + "agent_id": _b.id, + "agent": _b.display_name, + "provider": _b.provider, + "key_status": "✅ provided" if PROVIDER_KEYS.get(_b.key_field) else "❌ missing", + } + for _b in BACKENDS.values() + ] + agent_table = mo.ui.table( + _agent_rows, + selection="multi", + initial_selection=[_i for _i, _r in enumerate(_agent_rows) if _r["key_status"].startswith("✅")], + label="Select the coding agents to evaluate", + page_size=20, + ) + run_eval_button = mo.ui.run_button( + label="Run selected evaluation", + tooltip="Provision a sandbox per agent, solve tasks inside it, score in Weave.", + kind="success", + ) + model_picker_modal = mo.vstack( + [ + mo.md( + """ + ### Evaluation controls + + Check the agents you want to evaluate, then click **Run selected + evaluation**. Agents whose provider key is missing will be skipped. + """ + ), + agent_table, + run_eval_button, + ] + ) + + model_picker_modal + return agent_table, run_eval_button + + +@app.cell(hide_code=True) +def _(agent_table, mo): + selected_agent_ids = [ + _row["agent_id"] for _row in (agent_table.value or []) if _row.get("agent_id") + ] + _label = ", ".join(selected_agent_ids) if selected_agent_ids else "No agents selected" + + mo.md( + f""" + **Selected agent(s):** `{_label}` + **Number of agent runs queued:** `{len(selected_agent_ids)}` + """ + ) + return (selected_agent_ids,) + + +@app.cell(hide_code=True) +async def _( + BACKENDS, + CodeScorer, + PROVIDER_KEYS, + SandboxAgentModel, + dataset, + dataset_name, + mo, + run_eval_button, + selected_agent_ids, + selected_benchmark_tasks, + weave, +): + import os as _os + + # Weave evaluates rows concurrently (WEAVE_PARALLELISM). Unlike a hosted-API + # eval, here every concurrent predict is a FULL agent process (Node/Rust) sharing + # ONE agent sandbox, so the old default of 20 starved CPU/RAM and OOM-killed + # agents — surfacing as empty-output runs and 180s timeouts (notably Claude). + # Cap the default at a sandbox-friendly 4; raise WEAVE_PARALLELISM if your agent + # sandbox has more headroom. Harness agents still pin eval_parallelism=1 (shared + # per-sandbox state deadlocks). We set this PER AGENT right before evaluate(). + _DEFAULT_PARALLELISM = _os.environ.get("WEAVE_PARALLELISM", "4") + + async def _run_evaluations(): + _lines = [] + _created = [] + try: + for _agent_id in selected_agent_ids: + _backend = BACKENDS[_agent_id] + _key = PROVIDER_KEYS.get(_backend.key_field) + if not _key: + _lines.append(f"⚠️ Skipped **{_backend.display_name}** — missing `{_backend.key_field}`.") + continue + + _model = SandboxAgentModel(agent_id=_agent_id) + _model._api_key = _key + + # Per-task budget: harness agents (OpenClaw) override the default. + if _backend.run_timeout_seconds: + _model.run_timeout_seconds = _backend.run_timeout_seconds + # Sandbox lifetime must outlast the WHOLE eval. Parallel agents finish + # fast, but serial agents (eval_parallelism=1) run tasks back-to-back, + # so a fixed cap expires mid-eval and the sandbox vanishes (NOT_FOUND). + # Scale the lifetime by task count for those, keeping the default floor. + if _backend.eval_parallelism == 1: + _per_task = _model.run_timeout_seconds + 60 # + scorer/overhead + _model.max_lifetime_seconds = max( + _model.max_lifetime_seconds, + _model.install_timeout_seconds + + len(selected_benchmark_tasks) * _per_task, + ) + + _created.append(_model) + # Provision now (traced via the agent_setup op) so install errors + # surface here and don't skew the first row's latency. setup() returns + # a status dict instead of raising. + _setup = _model.ensure_ready() + if _setup.get("status") != "ready": + _lines.append( + f"❌ **{_backend.display_name}** setup failed: {_setup.get('error')}" + ) + continue + _lines.append( + f"🟢 **{_backend.display_name}** sandbox `{_setup.get('sandbox_id')}` ready." + ) + + # Pin concurrency for this agent (harness agents -> 1). + _par = _backend.eval_parallelism + _os.environ["WEAVE_PARALLELISM"] = str(_par) if _par else _DEFAULT_PARALLELISM + _lines.append( + f"  ↳ concurrency `{_os.environ['WEAVE_PARALLELISM']}`, " + f"per-task timeout `{_model.run_timeout_seconds}s`, " + f"sandbox lifetime `{_model.max_lifetime_seconds}s`." + ) + + _scorer = CodeScorer(name="code_scorer") + _evaluation = weave.Evaluation( + name="agent-code-eval", + dataset=dataset, + scorers=[_scorer], + evaluation_name=f"{_agent_id}_agent_eval", + ) + _results = await _evaluation.evaluate(model=_model) + print(f"Agent: {_agent_id}") + print(_results) + print("-" * 100) + _lines.append(f"✅ **{_backend.display_name}** evaluation finished.") + finally: + for _m in _created: + _m.stop() + return _lines + + if run_eval_button.value and selected_agent_ids and selected_benchmark_tasks: + _out_lines = await _run_evaluations() + _evaluation_status = mo.md( + "### Run summary\n\n" + "\n\n".join(_out_lines) + + f"\n\nDataset **`{dataset_name}`**. Open W&B Weave to inspect traces and metrics." + ) + elif run_eval_button.value and not selected_agent_ids: + _evaluation_status = mo.md("⚠️ No agents selected. Pick at least one agent above, then run again.") + elif run_eval_button.value and not selected_benchmark_tasks: + _evaluation_status = mo.md("⚠️ No benchmark tasks selected. Pick at least one task above, then run again.") + else: + _evaluation_status = mo.md( + "⏸️ Evaluation is ready but has not been launched. Choose agents and tasks, then click **Run selected evaluation**." + ) + + _evaluation_status + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 6. Lifecycle, discovery, and cleanup + + /// admonition | Find and stop sandboxes + type: info + + Both agent and scorer sandboxes are tagged `agent-sandbox-eval`. The run loop + tears down agent sandboxes automatically, but if a run is interrupted you can + discover and stop any survivors with `Sandbox.list` + `stop`. + + ```python + running = Sandbox.list(tags=["agent-sandbox-eval"], include_stopped=True).result() + sb = Sandbox.from_id(running[0].sandbox_id).result() + print(sb.get_status()) + sb.stop(missing_ok=True).result() + ``` + /// + """) + return + + +@app.cell +def _(mo): + lifecycle_btn = mo.ui.run_button(label="List my agent-eval sandboxes", kind="neutral") + lifecycle_btn + return (lifecycle_btn,) + + +@app.cell(hide_code=True) +def _(API_KEY, Sandbox, lifecycle_btn, mo): + mo.stop(not API_KEY, mo.md("_Connect at the top first._")) + mo.stop(not lifecycle_btn.value, mo.md("_Press the button to list tagged sandboxes._")) + + try: + _sandboxes = Sandbox.list( + tags=["agent-sandbox-eval"], include_stopped=True + ).result() + _rows = [ + f"- `{_s.sandbox_id}` — status `{_s.status}`" + for _s in _sandboxes[:20] + ] + _body = "\n".join(_rows) or "_No tagged sandboxes yet — run an evaluation above first._" + _out = mo.callout( + mo.md( + f"**Sandboxes tagged `agent-sandbox-eval`:** {len(_sandboxes)}\n\n{_body}" + ), + kind="success", + ) + except Exception as _e: + _out = mo.callout( + mo.md(f"⚠️ Listing unavailable here: `{type(_e).__name__}: {_e}`"), + kind="warn", + ) + _out + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## What you just did + + /// admonition | Recap + type: success + + - Connected to W&B and supplied provider keys for headless agent auth + - Defined coding agents (Codex, Claude, OpenClaw, Hermes) behind an `AgentBackend` seam + - Wrapped each agent in a `SandboxAgentModel` that **provisions a sandbox at + init**, installs + authenticates the CLI, and drives it inside the sandbox + on every `predict` + - `CodeScorer` to verify solutions in fresh, network-isolated sandboxes + - Ran a per-agent `weave.Evaluation` and traced every generation + score + - Tore down agent sandboxes and listed survivors for cleanup + /// + + /// details | Where to next + type: info + + - [Serverless Sandboxes docs](https://docs.wandb.ai/sandboxes) + - [W&B Weave docs](https://weave-docs.wandb.ai/) + /// + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/sandboxes/serverless-sandboxes-tutorial.py b/sandboxes/serverless-sandboxes-tutorial.py new file mode 100644 index 0000000..a70040c --- /dev/null +++ b/sandboxes/serverless-sandboxes-tutorial.py @@ -0,0 +1,916 @@ +# /// script +# requires-python = ">=3.13" +# dependencies = [ +# "cwsandbox==0.24.0", +# "marimo>=0.23.8", +# "openai==2.46.0", +# "wandb[sandbox]==0.27.0", +# "weave==0.52.38", +# ] +# /// + +import marimo + +__generated_with = "0.23.6" +app = marimo.App( + width="medium", + app_title="Serverless Sandbox Tutorial", + css_file="/usr/local/_marimo/custom.css", + auto_download=["html"], +) + + +@app.cell +def _(): + import os + import time + import weave + import openai + import marimo as mo + from wandb.sandbox import ( + Sandbox, + SandboxDefaults, + ResourceOptions, + SandboxCommandTimeoutError, + SandboxExecutionError, + SandboxError, + ) + + return ( + ResourceOptions, + Sandbox, + SandboxCommandTimeoutError, + SandboxDefaults, + SandboxError, + SandboxExecutionError, + mo, + openai, + os, + time, + weave, + ) + + +@app.cell(hide_code=True) +def _(mo): + mo.vstack( + [ + mo.md( + r""" + # Evaluating Code-Generation Models with Serverless Sandbox, Inference, and Weave + + /// admonition | About This Notebook + type: info + + This marimo notebook walks through an end-to-end code-evaluation workflow for + hosted LLMs. It uses three complementary products: + + - **Serverless Inference** to call hosted code-generation models through an OpenAI-compatible API. + - **Serverless Sandbox** to execute generated Python safely in isolated environments. + - **W&B Weave** to trace model calls, score outputs, and compare evaluation runs. + + The flow is: a model generates a Python function for each benchmark task, the + function runs against deterministic tests inside a sandbox, the pass/fail result + becomes a score, and every step is traced to Weave for side-by-side comparison. + + _If you are running this notebook in edit mode, make sure you start by running all cells._ + /// + """ + ), + mo.md( + r""" + /// details | Prerequisites + type: info + + Before you begin, make sure you have: + + - **A W&B account** — sign up free at [wandb.ai](https://wandb.ai) if you don't have one. + - **A W&B API key** *(required)* — generate one at [wandb.ai/authorize](https://wandb.ai/authorize) and paste it into the Connect form below. It authenticates both Inference and Weave logging. + - **Inference access** — needed to call the hosted models in the picker. See [Serverless Inference](https://docs.wandb.ai/guides/inference/). + + Use the controls below to connect, choose benchmark difficulties, select one or + more models, and launch a reproducible evaluation run. + /// + """ + ), + mo.md( + r""" + /// details | Table of Contents + type: info + + - [**Connect W&B services**](#1-connect-wb-services) - Authenticate and initialize Weave + - [**Define the code-generation agent**](#2-define-the-code-generation-agent) - Wrap a hosted model as a Weave `Model` + - [**Score generated code safely in Serverless Sandbox**](#3-score-generated-code-safely-in-serverless-sandbox) - Run untrusted code in isolation + - [**Benchmark tasks**](#4-benchmark-tasks) - Build a versioned Weave `Dataset` + - [**Pick models and launch an evaluation**](#5-pick-models-and-launch-an-evaluation) - Run a multi-model `weave.Evaluation` + - [**Lifecycle, discovery, and cleanup**](#6-lifecycle-discovery-and-cleanup) - Find and stop sandboxes + /// + """ + ), + ] + ) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 1. Connect W&B services + + /// admonition | Connect and initialize Weave + type: info + + Fill in the **Connect** form above with your W&B entity (team or username), + a project name, and your API key, then press **Connect**. The entity and + project default to the tutorial values: change them to log into your own + workspace. The key is stored in `WANDB_API_KEY` and used to call + `weave.init("/")`. + + The form gates the rest of the notebook: the agent, scorer, and evaluation + cells stay paused until you connect. Once connected, every model call, + generated solution, scorer result, and evaluation summary is logged under + your project, and a link to the Weave dashboard appears in the success + callout above. + /// + """) + return + + +@app.cell(hide_code=True) +def _(mo): + # ---------- 1. Setup W&B ---------- + wandb_connect_form = ( + mo.md(""" + - W&B entity *(team)*: {entity} + - W&B project: {project} + - W&B API key *(required)*: {api_key} + """) + .batch( + entity=mo.ui.text(placeholder="your-wandb-entity" , full_width=True), + project=mo.ui.text(value="serverless-sandbox-tutorial", full_width=True), + api_key=mo.ui.text(kind="password", placeholder="from wandb.ai/authorize", full_width=True), + ) + .form(submit_button_label="Connect", bordered=False) + ) + wandb_connect_form + return (wandb_connect_form,) + + +@app.cell(hide_code=True) +def _(mo, os, wandb_connect_form, weave): + _v = wandb_connect_form.value or {} + ENTITY = _v.get("entity") + PROJECT = _v.get("project") + API_KEY = _v.get("api_key") + mo.stop( + not (ENTITY and PROJECT and API_KEY), + mo.md("_Fill in the form above and press **Connect**._"), + ) + + os.environ["WANDB_API_KEY"] = API_KEY + weave.init(f"{ENTITY}/{PROJECT}") + weave_url = f"https://wandb.ai/{ENTITY}/{PROJECT}/weave" + + mo.callout( + mo.md( + f"✅ **Connected** — logging to `{ENTITY}/{PROJECT}`. " + f"[Open Weave dashboard]({weave_url})" + ), + kind="success", + ) + return API_KEY, ENTITY, PROJECT + + +@app.cell(hide_code=True) +def _(mo): + wandb_product_tabs = mo.ui.tabs( + { + "Sandbox": mo.md( + f""" + ### Serverless Sandbox + + The sandbox executes each generated Python solution in an isolated environment. This keeps evaluation safer while still letting the scorer run real assertions against generated code. + + It's a full remote-execution platform: file I/O (`write_file`/`read_file`), CPU/memory resource control, env vars and team Secrets, networking/egress, live log streaming, parallel `Session`s with remote functions, and tag-based lifecycle management. Serverless Sandbox runs CPU workloads (no GPU). The code scorer in Step 3 exercises these directly. + """ + ), + "Inference": mo.md( + f""" + ### Serverless Inference + + This notebook uses Serverless Inference through an OpenAI-compatible client. Each benchmark prompt is sent to a hosted model with a low-temperature generation setting so the evaluation is repeatable and easy to compare across model providers. + """ + ), + "Weave": mo.md( + f""" + ### W&B Weave + + Weave tracks the full evaluation lifecycle: model inputs, generated code, scorer outputs, pass/fail metrics, and latency. That makes it easy to inspect individual failures and compare runs across models. + """ + ) + } + ) + + wandb_product_tabs + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 2. Define the code-generation agent + + /// admonition | LeetCodeAgent + type: info + + `LeetCodeAgent` wraps a Serverless Inference model behind a Weave `Model` interface. Given a task name, spec, and tests, its `predict` method returns the model's raw solution string, the output drops straight into the sandbox scorer. `predict` is a `@weave.op`, so each + generation is traced with its inputs, output, and latency. + /// + """) + return + + +@app.cell +def _(API_KEY, ENTITY, PROJECT, openai, weave): + #---------- 1.1 Define Function Agent ---------- + SYSTEM_PROMPT = weave.StringPrompt( + """You are a precise Python coder. + Given a function specification, return ONLY the function definition. + - No markdown code fences. + - No explanation. + - Use the exact function name and signature requested. + """ + ) + class LeetCodeAgent(weave.Model): + model_name: str + system_prompt: weave.StringPrompt = SYSTEM_PROMPT + max_tokens: int = 1000 + temperature: float = 0.0 + + @property + def client(self): + return openai.OpenAI( + base_url="https://api.inference.wandb.ai/v1", + api_key=API_KEY, + project=f"{ENTITY}/{PROJECT}", + ) + + @weave.op(name="generate_solution", kind="llm") + def predict(self, name: str, spec: str, tests: list) -> str: + resp = self.client.chat.completions.create( + model=self.model_name, + messages=[ + {"role": "system", "content": SYSTEM_PROMPT.content}, + {"role": "user", "content": spec}, + ], + temperature=self.temperature, + max_tokens=self.max_tokens, + ) + msg = resp.choices[0].message + + # Standard content + if msg.content: + return msg.content.strip() + + # Reasoning/thinking models (e.g. Qwen, DeepSeek-R1) that put output in reasoning_content + reasoning = getattr(msg, "reasoning_content", None) + if reasoning: + return reasoning.strip() + + # Tool-call responses — extract the first tool call argument + if msg.tool_calls: + return msg.tool_calls[0].function.arguments.strip() + + return "failed" + + return (LeetCodeAgent,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 3. Score generated code safely in Serverless Sandbox + + /// admonition | CodeScorer + type: info + + `CodeScorer` is where the sandbox does its real work. It wraps each task's + `(input, expected)` pairs in a small harness, runs the model's solution inside + a fresh sandbox, and returns a structured `{passed, error, sandbox_latency}` + result for Weave. Running untrusted model output in an isolated sandbox is the + whole point so the evaluation stays safe. + + The scorer sets these Sandbox defaults: + + - **`SandboxDefaults`** (defined just below) — one immutable config applied to + every scorer sandbox: container image, `tags` (used for cleanup in the final + step), injected **environment variables**, and **CPU/memory `ResourceOptions`**. + - **`write_file` + `read_file`** — the harness script is written in with + `write_file`; instead of scraping stdout, the script writes a JSON verdict to + `/tmp/result.json` that the scorer pulls back with `read_file`. + - **Native `timeout_seconds`** — the SDK enforces the 5s budget directly, so + there's no `timeout 5` shell wrapper. + - **Network isolation by default** — sandboxes have no egress unless you ask + for it, so model code can't phone home while being scored. + - **Typed errors** — `SandboxCommandTimeoutError` is a *real* failed completion + (no retry); `SandboxExecutionError` / `SandboxError` are infra issues the + backoff loop retries. + /// + """) + return + + +@app.cell +def _(ResourceOptions, SandboxDefaults): + # Shared sandbox configuration. SandboxDefaults is an immutable config object + # applied to every sandbox the scorer creates — container image, tags (for + # later discovery/cleanup via Sandbox.list), injected env vars, and CPU/memory + # limits. Setting it once here is the idiomatic alternative to passing the same + # arguments on every Sandbox.run() call. + SANDBOX_DEFAULTS = SandboxDefaults( + container_image="python:3.11", + tags=("wandb-sandbox-tutorial", "code-eval"), + environment_variables={"PYTHONUNBUFFERED": "1"}, + resources=ResourceOptions( + requests={"cpu": "250m", "memory": "256Mi"}, + limits={"cpu": "1", "memory": "512Mi"}, + ), + ) + return (SANDBOX_DEFAULTS,) + + +@app.cell +def _( + SANDBOX_DEFAULTS, + Sandbox, + SandboxCommandTimeoutError, + SandboxError, + SandboxExecutionError, + time, + weave, +): + # ---------- 1.3 Define Sandbox-based scorer ---------- + import json + + class CodeScorer(weave.Scorer): + + @weave.op(name="code_scorer", kind="scorer") + def score(self, name: str, spec: str, tests: list, output: str) -> dict: + start_time = time.time() + asserts = "\n".join( + f" assert {name}(*{args!r}) == {expected!r}, 'failed on input {args!r}'" + for args, expected in tests + ) + # The harness runs every assertion, then writes a structured verdict + # to a file, which we pull back out with read_file. + script = ( + f"{output}\n\n" + "import json\n" + "_passed, _error = True, ''\n" + "try:\n" + f"{asserts}\n" + "except Exception as _e:\n" + " _passed, _error = False, repr(_e)\n" + "with open('/tmp/result.json', 'w') as _f:\n" + " json.dump({'passed': _passed, 'error': _error}, _f)\n" + ) + + last_exc = None + for attempt in range(3): + sb = None + try: + sb = Sandbox.run(defaults=SANDBOX_DEFAULTS) + # write_file ships the script in; + # read_file pulls the verdict back out + sb.write_file("/tmp/t.py", script.encode()).result() + proc = sb.exec( + ["python", "/tmp/t.py"], timeout_seconds=5 + ).result() + try: + verdict = json.loads( + sb.read_file("/tmp/result.json").result().decode() + ) + passed = bool(verdict.get("passed")) + error = str(verdict.get("error", "")) + except Exception: + passed = False + error = (proc.stderr or "no result file written") + end_time = time.time() + return { + "passed": passed, + "error": error[-300:], + "sandbox_latency": (end_time - start_time), + } + except SandboxCommandTimeoutError: + end_time = time.time() + return { + "passed": False, + "error": "execution timed out after 5s", + "sandbox_latency": (end_time - start_time), + } + except (SandboxExecutionError, SandboxError) as e: + last_exc = e + time.sleep(2 ** attempt) + finally: + if sb is not None: + try: + sb.stop(missing_ok=True).result() + except Exception: + pass + end_time = time.time() + return {"passed": False, "error": f"Sandbox unavailable after 3 attempts: {last_exc}", "sandbox_latency": (end_time - start_time)} + + return (CodeScorer,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + /// details | Also possible (beyond this eval) + type: info + + A few sandbox capabilities aren't needed to score code, but are one argument + away when your use case calls for them: + + - **Team Secrets** — inject credentials from your W&B `wandb-team-secrets` + store without hardcoding them: `Sandbox.run(..., secrets=[Secret(name="OPENAI_API_KEY", env_var="OPENAI_API_KEY")])`. + - **Outbound egress / serving a port** — the scorer keeps sandboxes isolated, + but a task that needs the network can opt in: + `Sandbox.run(..., network=NetworkOptions(egress_mode="internet"))`, or expose + a port with `ingress_mode` + `exposed_ports` to serve a model. + - **Live log streaming** — for long-running jobs, follow output as it happens + with `for line in sb.stream_logs(follow=True): ...`. + - **Snapshots & lifetime caps** — `sb.stop(snapshot_on_stop=True)` or + `Sandbox.run(..., max_lifetime_seconds=300)` to bound resource usage. + /// + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 4. Benchmark tasks + + /// admonition | Build a versioned Weave Dataset + type: info + + The benchmark mixes easy, medium, and hard LeetCode-style prompts. Each row + has a function name, a natural-language spec, and deterministic + `(input, expected)` tests that the sandbox scorer converts into Python + assertions. Select the exact tasks you want in the table below, the + selection updates live and builds a versioned `Dataset` in Weave, so each evaluation run + is tied to an explicit set of tasks. + /// + """) + return + + +@app.cell(hide_code=True) +def _(): + # ---------- 1.2 Define Ground truth Dataset ---------- + # Each task has a name, a natural-language spec, and a list of (input, expected) pairs. + TASKS = [ + # --- Easy --- + { + "name": "add", + "spec": "Write a function `add(a, b)` that returns the sum of two numbers.", + "tests": [((2, 3), 5), ((-1, 1), 0), ((0, 0), 0)], + }, + { + "name": "reverse_string", + "spec": "Write a function `reverse_string(s)` that returns the reverse of string s.", + "tests": [(("hello",), "olleh"), (("",), ""), (("a",), "a")], + }, + { + "name": "fizzbuzz", + "spec": "Write a function `fizzbuzz(n)` returning 'Fizz' if n is divisible by 3, 'Buzz' if by 5, 'FizzBuzz' if by both, else str(n).", + "tests": [((3,), "Fizz"), ((5,), "Buzz"), ((15,), "FizzBuzz"), ((7,), "7")], + }, + # --- Medium --- + { + "name": "is_prime", + "spec": "Write a function `is_prime(n)` that returns True iff n is a prime number. n is a positive integer.", + "tests": [((2,), True), ((4,), False), ((17,), True), ((1,), False)], + }, + { + "name": "second_largest", + "spec": "Write a function `second_largest(nums)` returning the second-largest distinct value in a list of ints. Assume len(nums) >= 2 and at least 2 distinct values.", + "tests": [(([1, 2, 3],), 2), (([5, 5, 4, 4, 3],), 4), (([-1, -2, -3],), -2)], + }, + { + "name": "flatten", + "spec": "Write a function `flatten(lst)` that takes a possibly nested list of ints and returns a flat list of all ints in order.", + "tests": [ + (([1, [2, [3, 4]], 5],), [1, 2, 3, 4, 5]), + (([],), []), + (([[1], [2, [3]]],), [1, 2, 3]), + ], + }, + { + "name": "group_anagrams", + "spec": "Write a function `group_anagrams(words)` that groups a list of strings into lists of anagrams. Each group must be sorted alphabetically internally. The returned list of groups must be sorted by the first element of each group.", + "tests": [ + ((["eat", "tea", "tan", "ate", "nat", "bat"],), [["ate", "eat", "tea"], ["bat"], ["nat", "tan"]]), + (([""],), [[""]]), + ((["a"],), [["a"]]), + ], + }, + { + "name": "longest_common_subsequence", + "spec": "Write a function `longest_common_subsequence(s1, s2)` that returns the length of the longest common subsequence of strings s1 and s2.", + "tests": [ + (("abcde", "ace"), 3), + (("abc", "abc"), 3), + (("abc", "def"), 0), + ], + }, + # --- Hard --- + { + "name": "min_coins", + "spec": "Write a function `min_coins(coins, amount)` that returns the minimum number of coins needed to make up the given amount using the given coin denominations. Return -1 if it is not possible.", + "tests": [ + (([1, 5, 11], 15), 3), + (([2], 3), -1), + (([1, 2, 5], 11), 3), + ], + }, + { + "name": "longest_palindrome", + "spec": "Write a function `longest_palindrome(s)` that returns the longest palindromic substring of s. If there are ties, return the one that starts earliest.", + "tests": [ + (("babad",), "bab"), + (("cbbd",), "bb"), + (("a",), "a"), + (("racecar",), "racecar"), + ], + }, + { + "name": "word_break", + "spec": "Write a function `word_break(s, word_dict)` that returns True if the string s can be segmented into a space-separated sequence of one or more words from word_dict (a list of strings).", + "tests": [ + (("leetcode", ["leet", "code"]), True), + (("applepenapple", ["apple", "pen"]), True), + (("catsandog", ["cats", "dog", "sand", "and", "cat"]), False), + ], + }, + { + "name": "serialize_tree", + "spec": """Write a function `serialize_tree(root)` that serializes a binary tree to a string and a function `deserialize_tree(data)` that deserializes it back. A tree node is represented as a list [val, left, right] where left and right are either None or another such list. The round-trip must be lossless: deserialize_tree(serialize_tree(root)) == root.""", + "tests": [ + (([1, [2, None, None], [3, [4, None, None], [5, None, None]]],), [1, [2, None, None], [3, [4, None, None], [5, None, None]]]), + ((None,), None), + (([1, None, [2, None, [3, None, None]]],), [1, None, [2, None, [3, None, None]]]), + ], + }, + ] + return (TASKS,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Check the rows you want below. By default every task is selected; uncheck + any you want to exclude, or clear them all and pick a focused subset. The + dataset summary underneath updates as you change the selection. + """) + return + + +@app.cell(hide_code=True) +def _(TASKS, mo): + def _difficulty_for(index: int) -> str: + return "Easy" if index < 3 else "Medium" if index < 8 else "Hard" + + _all_task_rows = [ + { + "difficulty": _difficulty_for(_i), + "function": _t["name"], + "test_count": len(_t["tests"]), + "spec": _t["spec"], + } + for _i, _t in enumerate(TASKS) + ] + task_table = mo.ui.table( + _all_task_rows, + selection="multi", + initial_selection=list(range(len(_all_task_rows))), + label="Select the benchmark tasks to include in this evaluation", + page_size=20, + ) + task_table + return (task_table,) + + +@app.cell(hide_code=True) +def _(TASKS, mo, task_table, weave): + def _difficulty_for(index: int) -> str: + return "Easy" if index < 3 else "Medium" if index < 8 else "Hard" + + _difficulty_order = ["Easy", "Medium", "Hard"] + _task_by_name = {_t["name"]: (_i, _t) for _i, _t in enumerate(TASKS)} + + _selected = sorted( + ( + _task_by_name[_row["function"]] + for _row in (task_table.value or []) + if _row.get("function") in _task_by_name + ), + key=lambda _pair: _pair[0], + ) + selected_benchmark_tasks = [_task for _, _task in _selected] + _present = [ + _d for _d in _difficulty_order + if _d in {_difficulty_for(_i) for _i, _ in _selected} + ] + + if not selected_benchmark_tasks: + _suffix = "none" + elif len(selected_benchmark_tasks) == len(TASKS): + _suffix = "all" + else: + _suffix = "_".join(_d.lower() for _d in _present) + f"_{len(selected_benchmark_tasks)}tasks" + dataset_name = f"tasks_{_suffix}" + + dataset = ( + weave.Dataset(name=dataset_name, rows=selected_benchmark_tasks) + if selected_benchmark_tasks + else None + ) + + if selected_benchmark_tasks: + _summary = mo.callout( + mo.md( + f"**Weave dataset:** `{dataset_name}` \n" + f"**Selected tasks:** `{len(selected_benchmark_tasks)}` of `{len(TASKS)}` " + f"({', '.join(_present)})" + ), + kind="success", + ) + else: + _summary = mo.callout( + mo.md("No tasks selected — check one or more rows in the table above to build a dataset."), + kind="warn", + ) + _summary + return dataset, dataset_name, selected_benchmark_tasks + + +@app.cell(hide_code=True) +def _(mo): + mo.md(f""" + --- + ## 5. Pick models and launch an evaluation + + /// admonition | Run a multi-model weave.Evaluation + type: info + + Pick one hosted model for a fast debug run or the full set for a side-by-side + comparison, then press **Run selected evaluation**. The run button keeps + expensive model calls from firing automatically whenever you open or edit the + notebook. Each selected model is wrapped in a `LeetCodeAgent`, scored by + `CodeScorer` against the dataset, and logged as a `weave.Evaluation` you can + open in the Weave dashboard. + + **Parallel sandboxes:** `weave.Evaluation` scores dataset rows + concurrently, and since `CodeScorer` spins up a sandbox per call, you're + running many sandboxes in parallel. If you ever own the loop yourself + (RL rollouts, a custom harness), a `Session` gives you the same fan-out + explicitly —> `@session.function()` + `.map()` to launch, and + `wandb.sandbox.wait(refs, num_returns=1)` to harvest results as they finish. + /// + """) + return + + +@app.cell +def _(): + models_list = [ + "meta-llama/Llama-3.3-70B-Instruct", + "deepseek-ai/DeepSeek-V4-Flash", + "MiniMaxAI/MiniMax-M2.5", + "moonshotai/Kimi-K2.6", + "Qwen/Qwen3-Coder-480B-A35B-Instruct", + + ] + return (models_list,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Check rows in the table below to choose between a full model comparison and a single-model debug run. + """) + return + + +@app.cell(hide_code=True) +def _(mo, models_list): + _model_rows = [ + {"provider": _m.split("/")[0], "model": _m} + for _m in models_list + ] + model_table = mo.ui.table( + _model_rows, + selection="multi", + initial_selection=list(range(len(_model_rows))), + label="Select the Serverless Inference models to evaluate", + page_size=20, + ) + run_eval_button = mo.ui.run_button( + label="Run selected evaluation", + tooltip="Run the selected Serverless Inference model(s), score generated code in Sandbox, and log results to Weave.", + kind="success", + ) + model_picker_modal = mo.vstack( + [ + mo.md( + f""" + ### Evaluation controls + + Check the models you want to evaluate, then click **Run selected evaluation**. Select a single row for a fast debug run, or all rows for a full side-by-side comparison. + """ + ), + model_table, + run_eval_button, + ] + ) + + model_picker_modal + return model_table, run_eval_button + + +@app.cell(hide_code=True) +def _(mo, model_table): + selected_model_names = [ + _row["model"] for _row in (model_table.value or []) if _row.get("model") + ] + _selected_model_label = ", ".join(selected_model_names) if selected_model_names else "No models selected" + + mo.md( + f""" + **Selected evaluation target(s):** `{_selected_model_label}` + **Number of model runs queued:** `{len(selected_model_names)}` + """ + ) + return (selected_model_names,) + + +@app.cell(hide_code=True) +async def _( + CodeScorer, + LeetCodeAgent, + dataset, + dataset_name, + mo, + run_eval_button, + selected_benchmark_tasks, + selected_model_names, + weave, +): + async def _run_evaluations(): + for name in selected_model_names: + _model = LeetCodeAgent(model_name=name) + _code_scorer = CodeScorer(name="code_scorer") + evaluation = weave.Evaluation( + name="simple-code-eval", + dataset=dataset, + scorers=[_code_scorer], + evaluation_name=f"{name}_code_eval", + ) + results = await evaluation.evaluate(model=_model) + print(f"Model: {name}") + print(results) + print("-" * 100) + + if run_eval_button.value and selected_model_names and selected_benchmark_tasks: + await _run_evaluations() + _evaluation_status = mo.md( + f""" + ✅ Evaluation finished for **{len(selected_model_names)}** model run(s) on dataset **`{dataset_name}`**. Open W&B Weave to inspect traces, scorer outputs, and aggregate metrics. + """ + ) + elif run_eval_button.value and not selected_model_names: + _evaluation_status = mo.md( + f""" + ⚠️ No models selected. Pick at least one model above, then click **Run selected evaluation** again. + """ + ) + elif run_eval_button.value and not selected_benchmark_tasks: + _evaluation_status = mo.md( + f""" + ⚠️ No benchmark tasks selected. Pick at least one difficulty above, then click **Run selected evaluation** again. + """ + ) + else: + _evaluation_status = mo.md( + f""" + ⏸️ Evaluation is ready but has not been launched. Choose one or more models and benchmark difficulties, then click **Run selected evaluation**. + """ + ) + + _evaluation_status + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 6. Lifecycle, discovery, and cleanup + + /// admonition | Find and stop sandboxes + type: info + + Sandboxes are addressable resources. `tags` (set via `SandboxDefaults`) let + you find them later with `Sandbox.list`; `from_id` re-attaches to a running + one; `get_status` inspects state; and `stop` can snapshot or you can set a + hard `max_lifetime_seconds` so nothing leaks. + + ```python + running = Sandbox.list(tags=["wandb-sandbox-tutorial"], include_stopped=True).result() + sb = Sandbox.from_id(running[0].sandbox_id).result() + print(sb.get_status()) + sb.stop(snapshot_on_stop=True).result() # or Sandbox.run(..., max_lifetime_seconds=300) + ``` + /// + """) + return + + +@app.cell +def _(mo): + lifecycle_btn = mo.ui.run_button(label="List my tutorial sandboxes", kind="neutral") + lifecycle_btn + return (lifecycle_btn,) + + +@app.cell(hide_code=True) +def _(API_KEY, Sandbox, lifecycle_btn, mo): + mo.stop(not API_KEY, mo.md("_Connect at the top first._")) + mo.stop(not lifecycle_btn.value, mo.md("_Press the button to list tagged sandboxes._")) + + import traceback as _tb + + try: + _sandboxes = Sandbox.list( + tags=["wandb-sandbox-tutorial"], include_stopped=True + ).result() + _rows = [ + f"- `{_s.sandbox_id}` — status `{_s.status}`" + for _s in _sandboxes[:20] + ] + _body = "\n".join(_rows) or "_No tagged sandboxes yet — run the evaluation above first._" + _out = mo.callout( + mo.md( + f"**Sandboxes tagged `wandb-sandbox-tutorial`:** {len(_sandboxes)}\n\n{_body}" + ), + kind="success", + ) + except Exception as _e: + _out = mo.callout( + mo.md(f"⚠️ Listing unavailable here: `{type(_e).__name__}: {_e}`"), + kind="warn", + ) + _out + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## What you just did + + /// admonition | Recap + type: success + + - Connected to W&B from a single form (entity, project, API key) + - Wrapped a hosted Serverless Inference model in a Weave `Model` + - Scored generated code safely inside Serverless Sandbox using + `SandboxDefaults`, `write_file`, native `timeout_seconds`, and typed errors + - Built a versioned Weave `Dataset` from a selectable task table + - Ran a multi-model `weave.Evaluation` and logged every trace, Weave scored + rows concurrently, so sandboxes ran in parallel + - Exercised the sandbox platform inside the scorer: `SandboxDefaults`, file + I/O (`write_file`/`read_file`), CPU/memory limits, network isolation, and + typed errors, then listed the tagged sandboxes for cleanup + /// + + /// details | Where to next + type: info + + - [Serverless Sandboxes docs](https://docs.wandb.ai/sandboxes) + - [Weave docs](https://weave-docs.wandb.ai/) + - [Weave Evaluations guide](https://weave-docs.wandb.ai/guides/core-types/evaluations) + /// + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/uv.lock b/uv.lock index 9c740cc..658612e 100644 --- a/uv.lock +++ b/uv.lock @@ -16,6 +16,18 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'", ] +[[package]] +name = "abnf" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/f2/7b5fac50ee42e8b8d4a098d76743a394546f938c94125adbb93414e5ae7d/abnf-2.2.0.tar.gz", hash = "sha256:433380fd32855bbc60bc7b3d35d40616e21383a32ed1c9b8893d16d9f4a6c2f4", size = 197507, upload-time = "2023-03-17T18:26:24.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/95/f456ae7928a2f3a913f467d4fd9e662e295dd7349fc58b35f77f6c757a23/abnf-2.2.0-py3-none-any.whl", hash = "sha256:5dc2ae31a84ff454f7de46e08a2a21a442a0e21a092468420587a1590b490d1f", size = 39938, upload-time = "2023-03-17T18:26:22.608Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -212,6 +224,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + [[package]] name = "basedpyright" version = "1.38.2" @@ -320,6 +341,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/76/cab7af7f16c0b09347f2ebe7ffda7101132f786acb767666dce43055faab/botocore_stubs-1.42.41-py3-none-any.whl", hash = "sha256:9423110fb0e391834bd2ed44ae5f879d8cb370a444703d966d30842ce2bcb5f0", size = 66759, upload-time = "2026-02-03T20:46:13.02Z" }, ] +[[package]] +name = "cachetools" +version = "7.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, +] + [[package]] name = "cattrs" version = "25.3.0" @@ -421,6 +451,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, ] +[[package]] +name = "chardet" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/b6/9df434a8eeba2e6628c465a1dfa31034228ef79b26f76f46278f4ef7e49d/chardet-7.4.3.tar.gz", hash = "sha256:cc1d4eb92a4ec1c2df3b490836ffa46922e599d34ce0bb75cf41fd2bf6303d56", size = 784800, upload-time = "2026-04-13T21:33:39.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/52/505c207f334d51e937cbaa27ff95776e16e2d120e13cbe491cd7b3a70b50/chardet-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25a862cddc6a9ac07023e808aedd297115345fbaabc2690479481ddc0f980e09", size = 870747, upload-time = "2026-04-13T21:32:56.916Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/d3c79495dee4831b8bebca2790e72cb90f0c5849c940570a7c7e5b70b952/chardet-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7005c88da26fd95d8abb8acbe6281d833e9a9181b03cf49b4546c4555389bd97", size = 853210, upload-time = "2026-04-13T21:32:58.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/99/f6a822ad1bde25a4c38dc3e770485e78e0893dfd871cd6e18ed3ea3a795e/chardet-7.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc50f28bad067393cce0af9091052c3b8df7a23115afd8ba7b2e0947f0cef1f8", size = 873625, upload-time = "2026-04-13T21:32:59.606Z" }, + { url = "https://files.pythonhosted.org/packages/b1/10/31932775c94a86814f76b41c4a772b52abfb0e6125324f32c6da1196c297/chardet-7.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3da294de1a681097848ab58bd3f2771a674f8039d2d87a5538b28856b815e9", size = 883436, upload-time = "2026-04-13T21:33:01.351Z" }, + { url = "https://files.pythonhosted.org/packages/6c/63/0f43e3acf2c436fdb32a0f904aeb03a2904d2126eed34a042a194d235926/chardet-7.4.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c45e116dd51b66226a53ade3f9f635e870de5399b90e00ce45dcc311093bf4", size = 876589, upload-time = "2026-04-13T21:33:02.636Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a6/e9b8f8a3e99602792b01fa7d0a731737615ab56d8bfd0b52935a0ef88b85/chardet-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:ccc1f83ab4bcfb901cf39e0c4ba6bc6e726fc6264735f10e24ceb5cb47387578", size = 941866, upload-time = "2026-04-13T21:33:04.282Z" }, + { url = "https://files.pythonhosted.org/packages/61/33/29de185079e6675c3f375546e30a559b7ddc75ce972f18d6e566cd9ea4eb/chardet-7.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:75d3c65cc16bddf40b8da1fd25ba84fca5f8070f2b14e86083653c1c85aee971", size = 874870, upload-time = "2026-04-13T21:33:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:29af5999f654e8729d251f1724a62b538b1262d9292cccaefddf8a02aae1ef6a", size = 854859, upload-time = "2026-04-13T21:33:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/21/edb36ad5dfa48d7f8eed97ab43931ecdaa8c15166c21b1d614967e49d681/chardet-7.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:626f00299ad62dfe937058a09572beed442ccc7b58f87aa667949b20fd3db235", size = 875032, upload-time = "2026-04-13T21:33:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/a32a241d861cf180853a11c8e5a67641cb1b2af13c3a5ccce83ec07e2c9f/chardet-7.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a4904dd5f071b7a7d7f50b4a67a86db3c902d243bf31708f1d5cde2f68239cb", size = 888283, upload-time = "2026-04-13T21:33:10.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/e1ee6a77abf3782c00e05b89c4d4328c8353bf9500661c4348df1dd68614/chardet-7.4.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d2879598bc220689e8ce509fe9c3f37ad2fca53a36be9c9bd91abdd91dd364f", size = 879974, upload-time = "2026-04-13T21:33:11.448Z" }, + { url = "https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:4b2799bd58e7245cfa8d4ab2e8ad1d76a5c3a5b1f32318eb6acca4c69a3e7101", size = 943973, upload-time = "2026-04-13T21:33:12.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/79ac9b4db5bc87020c9dbc419125371d80882d1d197e9c4765ba8682b605/chardet-7.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e4486df251b8962e86ea9f139ca235aa6e0542a00f7844c9a04160afb99aa9", size = 873769, upload-time = "2026-04-13T21:33:14.002Z" }, + { url = "https://files.pythonhosted.org/packages/55/5f/25bdec773905bff0ff6cf35ca73b17bd05593b4f87bd8c5fa43705f7167d/chardet-7.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fbff1907925b0c5a1064cffb5e040cd5e338585c9c552625f30de6bc2f3107a", size = 853991, upload-time = "2026-04-13T21:33:15.564Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/a29380ee0b215d23d77733b5ad60c5c0c7969650e080c667acdf9462040d/chardet-7.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:365135eaf37ba65a828f8e668eb0a8c38c479dcbec724dc25f4dfd781049c357", size = 874024, upload-time = "2026-04-13T21:33:16.915Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b1/3338e121cbd4c8a126b8ccb1061170c2ce51a53f678c502793ea49c6fd6d/chardet-7.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfc134b70c846c21ead8e43ada3ae1a805fff732f6922f8abcf2ff27b8f6493d", size = 887410, upload-time = "2026-04-13T21:33:18.368Z" }, + { url = "https://files.pythonhosted.org/packages/63/1c/44a9a9e0c59c185a5d307ceaeee8768afa1558f0a24f7a4b5fa11b67586b/chardet-7.4.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9acd9988a93e09390f3cd231201ea7166c415eb8da1b735928990ffc05cb9fbb", size = 879269, upload-time = "2026-04-13T21:33:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/5d0e77ea774bd3224321c248880ea0c0379000ac5c2bb6d77609549de247/chardet-7.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:e1b98790c284ff813f18f7cf7de5f05ea2435a080030c7f1a8318f3a4f80b131", size = 944155, upload-time = "2026-04-13T21:33:21.694Z" }, + { url = "https://files.pythonhosted.org/packages/70/a8/bf0811d859e13801279a2ae64f37a408027b282f2047bc0001c75dd356ad/chardet-7.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d892d3dcd652fdef53e3d6327d39b17c0df40a899dfc919abaeb64c974497531", size = 872887, upload-time = "2026-04-13T21:33:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/51/ac/b9d68ebddfe1b02c77af5bf81120e12b036b4432dc6af7a303d90e2bc38b/chardet-7.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:acc46d1b8b7d5783216afe15db56d1c179b9a40e5a1558bc13164c4fd20674c4", size = 853964, upload-time = "2026-04-13T21:33:24.724Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/17fa103ea9caf5d325a5e4051ab2ba65996fd66baa60b81ee41af1f54e10/chardet-7.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ac3bf11c645734a1701a3804e43eabd98851838192267d08c353a834ab79fea", size = 876006, upload-time = "2026-04-13T21:33:26.098Z" }, + { url = "https://files.pythonhosted.org/packages/c2/20/193faab46a68ea550587331a698c3dca8099f8901d10937c4443135c7ed9/chardet-7.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e3bd9f936e04bae89c254262af08d9e5b98f805175ba1e29d454e6cba3107b7", size = 887680, upload-time = "2026-04-13T21:33:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/94a3c673327392652ee8bdea9a45bc8a5f5365197a7387d68f0eed007115/chardet-7.4.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:27cc23da03630cdecc9aa81a895aa86629c211f995cd57651f0fbc280717bf93", size = 879865, upload-time = "2026-04-13T21:33:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/cad8b5e3623a987f3c930b68e2bdd06cfc388cd91cd42ed05f1227701b73/chardet-7.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:b95c934b9ad59e2ba8abb9be49df70d3ad1b0d95d864b9fdb7588d4fa8bd921c", size = 939594, upload-time = "2026-04-13T21:33:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/33/e0/d06e42fd6f02a58e5e227e5106587751cb38adcff0aaf949add744b78b6e/chardet-7.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c77867f0c1cb8bd819502249fcdc500364aedb07881e11b743726fa2148e7b6e", size = 889714, upload-time = "2026-04-13T21:33:32.772Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ed/40d091954d48abea037baae6be8fb79905e5f78d34d12ea955132c7d8011/chardet-7.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cf1efeaf65a6ef2f5b9cc3a1df6f08ba2831b369ccaa4c7018eaf90aa757bb11", size = 872319, upload-time = "2026-04-13T21:33:34.427Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/82a46821dbfbdfe062710d2bf2ede13426304e3567a23c57d919c0c31630/chardet-7.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f3504c139a2ad544077dd2d9e412cd08b01786843d76997cd43bb6de311723c", size = 892021, upload-time = "2026-04-13T21:33:35.766Z" }, + { url = "https://files.pythonhosted.org/packages/49/57/42d30c562bda5b4a839766c1aad8d5856b798ad2a1c3247b72a679afec94/chardet-7.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457f619882ba66327d4d8d14c6c342269bdb1e4e1c38e8117df941d14d351b04", size = 902509, upload-time = "2026-04-13T21:33:37.096Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/0a40afdb50a0fe041ab95553b835a8160b6cf0e81edf2ae2fe9f5224cbf9/chardet-7.4.3-py3-none-any.whl", hash = "sha256:1173b74051570cf08099d7429d92e4882d375ad4217f92a6e5240ccfb26f231e", size = 626562, upload-time = "2026-04-13T21:33:38.559Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.4" @@ -494,6 +561,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] +[[package]] +name = "cint" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/c8/3ae22fa142be0bf9eee856e90c314f4144dfae376cc5e3e55b9a169670fb/cint-1.0.0.tar.gz", hash = "sha256:66f026d28c46ef9ea9635be5cb342506c6a1af80d11cb1c881a8898ca429fc91", size = 4641, upload-time = "2019-03-19T01:07:48.723Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/c2/898e59963084e1e2cbd4aad1dee92c5bd7a79d121dcff1e659c2a0c2174e/cint-1.0.0-py3-none-any.whl", hash = "sha256:8aa33028e04015711c0305f918cb278f1dc8c5c9997acdc45efad2c7cb1abf50", size = 5573, upload-time = "2019-03-19T01:07:46.496Z" }, +] + [[package]] name = "click" version = "8.3.1" @@ -524,6 +600,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, ] +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +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 = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { 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]] name = "cuda-bindings" version = "12.9.4" @@ -548,6 +680,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/02/4dbe7568a42e46582248942f54dc64ad094769532adbe21e525e4edf7bc4/cuda_pathfinder-1.3.3-py3-none-any.whl", hash = "sha256:9984b664e404f7c134954a771be8775dfd6180ea1e1aef4a5a37d4be05d9bbb1", size = 27154, upload-time = "2025-12-04T22:35:08.996Z" }, ] +[[package]] +name = "cwsandbox" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/d9/540f20685c878f86f8588bff841618e1a125bfb13c8ba4071fe9e041477d/cwsandbox-0.26.0.tar.gz", hash = "sha256:541fbef6fd5cf7b70e2692bf3f35a2ae8f2a577e5caf29162dececb42e973616", size = 489383, upload-time = "2026-06-11T13:09:00.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/61/ac962e3c59d3c349046d204a67c5f301b10ebab4c413b079862fd234a7d5/cwsandbox-0.26.0-py3-none-any.whl", hash = "sha256:88143a5959a93f1a0145a7897d57caabe9c488051e287f185da1a83451896105", size = 178221, upload-time = "2026-06-11T13:08:58.751Z" }, +] + +[package.optional-dependencies] +cli = [ + { name = "click" }, +] + [[package]] name = "datasets" version = "4.5.0" @@ -591,6 +742,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, ] +[[package]] +name = "diskcache-weave" +version = "5.6.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/52/634e1f43486489fdaded1a7c9bd3524b7e0ca9bcc43af426afa511c541e2/diskcache_weave-5.6.3.post1.tar.gz", hash = "sha256:1fe7e648d1d85d517c05b296f1692e7c425a71714dc31a4b7a584a8f8f5604f2", size = 68297, upload-time = "2026-03-19T14:57:54.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/8d/92887441bc338fb8d0b8ea75eb0392c00e20a85ec0bbe02f273188849568/diskcache_weave-5.6.3.post1-py3-none-any.whl", hash = "sha256:b00e9842b74eeecf314456f9c833a6d4f7792ed12b20297b4d3b9df7859ee66f", size = 45905, upload-time = "2026-03-19T14:57:52.819Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -600,6 +760,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + [[package]] name = "docstring-to-markdown" version = "0.17" @@ -662,6 +831,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fickling" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/20/d3c2bdb9235b777763a4afc7cc3673afcd162a707ec0988ae7141a540802/fickling-0.1.12.tar.gz", hash = "sha256:83f6ccc948e21edb9ebd92795069536b47f481ce6add62598eac608b31576821", size = 357026, upload-time = "2026-06-26T23:55:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/c0/c65003b71abc4ffddbae99ae86952a21ff859d81b0f1331f79239fe5a58e/fickling-0.1.12-py3-none-any.whl", hash = "sha256:6232b72857e6ee9d729922811b681132d49dc39abc896581390dad1c0eada814", size = 58960, upload-time = "2026-06-26T23:55:56.401Z" }, +] + [[package]] name = "filelock" version = "3.20.3" @@ -791,27 +969,104 @@ http = [ ] [[package]] -name = "gitdb" -version = "4.0.12" +name = "googleapis-common-protos" +version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smmap" }, + { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c8/e2645aa8ed02fd4c7a2f59d68783b65b1f3cbdfe39a6308e156509d1fee8/googleapis_common_protos-1.75.0-py3-none-any.whl", hash = "sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed", size = 300631, upload-time = "2026-05-07T08:03:30.345Z" }, ] [[package]] -name = "gitpython" -version = "3.1.46" +name = "gql" +version = "4.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "gitdb" }, + { name = "anyio" }, + { name = "backoff" }, + { name = "graphql-core" }, + { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/9f/cf224a88ed71eb223b7aa0b9ff0aa10d7ecc9a4acdca2279eb046c26d5dc/gql-4.0.0.tar.gz", hash = "sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e", size = 215644, upload-time = "2025-08-17T14:32:35.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/30bbd09e8d45339fa77a48f5778d74d47e9242c11b3cd1093b3d994770a5/gql-4.0.0-py3-none-any.whl", hash = "sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479", size = 89900, upload-time = "2025-08-17T14:32:34.029Z" }, +] + +[package.optional-dependencies] +httpx = [ + { name = "httpx" }, +] + +[[package]] +name = "graphql-core" +version = "3.2.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/90/f2aff026ab4aebd80eb71905106a0885f4cfde85dcf965543f45bed0d9ee/graphql_core-3.2.11.tar.gz", hash = "sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802", size = 528407, upload-time = "2026-06-05T13:45:22.915Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/15/b92b4e1d88d02c6eff9733c9eea21846ab435cc4d813d84ccc5d335955df/graphql_core-3.2.11-py3-none-any.whl", hash = "sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0", size = 214879, upload-time = "2026-06-05T13:45:21.245Z" }, +] + +[[package]] +name = "graphviz" +version = "0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" }, +] + +[[package]] +name = "grpcio" +version = "1.82.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/bc/656b89387d6f4ed7e0686c7b64c2ae7e554a759aa58122c8e5fb99392c32/grpcio-1.82.1.tar.gz", hash = "sha256:707b24abd90fcb1e45bcc080577da1dbf9971d107490589b9539af8e1e77b4b5", size = 13187300, upload-time = "2026-07-08T12:36:16.588Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/5b/e5092af97fa671ca279b3e373251af4bf87d5fbda7dc85f6a616899562a7/grpcio-1.82.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:0ddb18a9a9e1f46692b3567ae4abb3f8d117ce6afea48650f8eca06d8ab5d06f", size = 6181472, upload-time = "2026-07-08T12:34:31.009Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/18053a3a2ca03d0c2a1b8cc7271e705007a16aa5dae84bac00935c5b1a7f/grpcio-1.82.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:cf855b1af246720f567b0ce5d0724d45dfa4188eecc3296a2a69257b11b9e94b", size = 11970995, upload-time = "2026-07-08T12:34:33.603Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/21b1acb052876ad00959ec4d1b05fe08607d650bcfa282073bb164c2703c/grpcio-1.82.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb30cb13e25bc13cea70ffc69d6d90c49d36ea6c1d4549e6912f70177834cac", size = 6760127, upload-time = "2026-07-08T12:34:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/3e/12/25eef9c245c54f0061317d13a302357fe8ea03bac240b2b02ececcf54da4/grpcio-1.82.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1e822b2774f719c017cbe700b6e47173b6ae290fb84906f52a5a3c2c60b62e1e", size = 7484377, upload-time = "2026-07-08T12:34:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/1a348767eb9d9bd7765dc4fa8a01723d3bb386d67f981ee5c6f9c02b8b1c/grpcio-1.82.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5dafb1ece8ed45dee7c738f166ec82e19673221ed5ab8967f72858a4685345b2", size = 6924269, upload-time = "2026-07-08T12:34:40.583Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/3aae7a03d34c86ea27988db859a6087c186f6c3f53f9b551e07afd989bfa/grpcio-1.82.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06503106e7271e0a49fd5a1ac04747f1e47e87d900476db6fe45bc87ee411f4", size = 7531848, upload-time = "2026-07-08T12:34:43.277Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/3c4afa625d0dac9090707966916284c035fc5b2fb3e2c51e156accee6735/grpcio-1.82.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ff99bc8cafb6a952201c37b995f425e641c93ffa6e072258525feab57290141d", size = 8568217, upload-time = "2026-07-08T12:34:45.502Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d8489c628e73e20a3d034e7f66912de7b1acb405f01d388f056a88e47924/grpcio-1.82.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:644ae1b94266ac785330f4590a69e52b6a7eb73029043a02209db81c81397d69", size = 7938771, upload-time = "2026-07-08T12:34:48.323Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b7/0a92cfd1658f3a896d4aa12d4efeb7dd4ddfc723725ae22741a5241ea710/grpcio-1.82.1-cp311-cp311-win32.whl", hash = "sha256:e203d2e19d471630084a16c815616f8211dff21c268ab3c5f5bf38417832e074", size = 4256432, upload-time = "2026-07-08T12:34:50.432Z" }, + { url = "https://files.pythonhosted.org/packages/c7/6a/2872c761b025d9ec74386f22a4a7d59c5a5b00ebf718761b33739ffc45de/grpcio-1.82.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d8299c285fe6cc6a1f56badf8d3bc5078c8d20273ee64bafa3783b4bc29a769", size = 5009633, upload-time = "2026-07-08T12:34:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/dc/88/d1350bf3343a2ed87d801584e40609f6c6bd3087926eeca03de50348cf4a/grpcio-1.82.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:c09bd5fa0d5b1fbd773ec349fe61441c3e4ebf168c229aa7538a820bdfad6a58", size = 6144689, upload-time = "2026-07-08T12:34:55.567Z" }, + { url = "https://files.pythonhosted.org/packages/e6/33/71875cdecd27c24ac1385d4783a09853f01b84a825a36aec2a2bc7d0d080/grpcio-1.82.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1eae24810720734598e3e6a1a528d5de0f265fe3fc86575e9ecce424b9ec7379", size = 11952034, upload-time = "2026-07-08T12:34:58.128Z" }, + { url = "https://files.pythonhosted.org/packages/82/b2/d9125df3d8a140dec12cc82c05b7deafedeababcff6496f28b2fd5634d10/grpcio-1.82.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6bd5daf5bde7b24d7ad2cbaf8bf9eac620d96222016bb5e7ddde930dec0673f", size = 6710772, upload-time = "2026-07-08T12:35:01.33Z" }, + { url = "https://files.pythonhosted.org/packages/88/9b/69e2d1627398b964f34437dc476a5aff5a2cc8e7f247d26272b5674b5faf/grpcio-1.82.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecfde669cb687ac020d31ff76debe5dc7a62213335f02262eb6625628da1c03", size = 7450677, upload-time = "2026-07-08T12:35:03.926Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e7/8f855ca29c294956122a2a73023655b9b02602d5111dad2b9b00e7631c68/grpcio-1.82.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:011c8badee95734dee8bf05ce3464756a0ac3ebb8d443afd20c0e2b5e4640ad9", size = 6886855, upload-time = "2026-07-08T12:35:06.174Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f0/fa87e85f49925f44c479d07e58b051e69bcfef6b6d5fbc6749d140f6730a/grpcio-1.82.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b85f4564926fb23114d239392bdcae200db1e6179629edd7d7ab0ab89c96a197", size = 7501323, upload-time = "2026-07-08T12:35:08.49Z" }, + { url = "https://files.pythonhosted.org/packages/67/55/2e0b10ae1d3ef9dcc480b91dc2158f4931fc4675d3af0a2836e39b2a744f/grpcio-1.82.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2c0c8270833395644c3fe6b6a806397955a2bc0538000a19a78b90c05a6c16e0", size = 8536899, upload-time = "2026-07-08T12:35:10.975Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/a3d8b0431fa221efc51ee39d73595ede74ba82a43b7c4313192e580face2/grpcio-1.82.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2ba199205ff46c7778290fe1673c91ac8e7e45678dd5c86e9e56fa33ec8788f6", size = 7913892, upload-time = "2026-07-08T12:35:13.944Z" }, + { url = "https://files.pythonhosted.org/packages/b8/92/f2651ec704d9852a56faef394775038afba435b50ce82ab2404d119c3355/grpcio-1.82.1-cp312-cp312-win32.whl", hash = "sha256:06127691866e295c14e84a1fb86356dd962254f6abd0da4ca4b001eea9e89438", size = 4240985, upload-time = "2026-07-08T12:35:16.048Z" }, + { url = "https://files.pythonhosted.org/packages/96/4f/a5fe8bf0d0a1b24855f370293075c931f27de4eb55f0f158786095bf3c11/grpcio-1.82.1-cp312-cp312-win_amd64.whl", hash = "sha256:1fa3223a3a2e1db74f4c2b255189eb7ea875dfba56e221d252ee3fc7b204778e", size = 5001580, upload-time = "2026-07-08T12:35:18.689Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3e/496992d08c0aaa11272eb6228dc8ab947da01fe835de243cd00521bce4c4/grpcio-1.82.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:b454a2d97bfab7565683a02345f86bd182ab69fd7c2bdb7414171e7538f266b1", size = 6146068, upload-time = "2026-07-08T12:35:21.365Z" }, + { url = "https://files.pythonhosted.org/packages/e7/8f/f263d6f14fdba6b56cfadd91fd3e158a52682b72c6016d1f8723d435659f/grpcio-1.82.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3dde70abfc80b3be11de53ba0d601c439e7fb2afd3583ad1788d1146bec92fdc", size = 11948600, upload-time = "2026-07-08T12:35:24.312Z" }, + { url = "https://files.pythonhosted.org/packages/8c/14/3a02e6ee49c2d85bc15eaae321e0e11ab3542cad3c5b2de121ecce0c4296/grpcio-1.82.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5523099c98c292ea1ae08e617249db760c56a78f8deae879027fe7d1ffbcbf6", size = 6714591, upload-time = "2026-07-08T12:35:27.027Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/58e3738696f48ab7645347b98d8a7f93d10e00e6218388fbfcd6c9310e3d/grpcio-1.82.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:5e5c4dc0a59b0f8490a6bdfd6fc8395b9d8ad8a8407c7d67ca7b5bba15c0877f", size = 7454995, upload-time = "2026-07-08T12:35:29.599Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6c/2557c1a889363072fbf2285ecd0e8c44860d4dbd60f017a32537c5b863e2/grpcio-1.82.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c40d94ba820329cc191981bc22fa6f6eed0799c6d921f3c6709521d59d4a2fd7", size = 6888621, upload-time = "2026-07-08T12:35:32.38Z" }, + { url = "https://files.pythonhosted.org/packages/d2/66/907706ccaff1223f1e10fd5b37fc16faead43392fccb4e786e7e390ac141/grpcio-1.82.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c816180e31e273caaec6f8bd86a8392499d5bbb26f41da44e3dce48bde69095", size = 7505069, upload-time = "2026-07-08T12:35:35.072Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/ff97b0d0f635987ee5ec80dfedafa1aad629303745d48e8637d10eec5b80/grpcio-1.82.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e31fd780b261830720cb70b0fd8f0aa51d49e75a66d7464ad2e31d4b765f2580", size = 8535384, upload-time = "2026-07-08T12:35:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/62/9e/a97fddd970a8d1588cade06eca20443761c1858b0ad6590a5c835aa18062/grpcio-1.82.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d76152d7c31d7210d4a106e5d8b64da5bba5d6abf11be30e2f7b0a0c59bbcbf", size = 7910707, upload-time = "2026-07-08T12:35:40.797Z" }, + { url = "https://files.pythonhosted.org/packages/20/e4/eaba1517888af483a88d449eb7566f0f7f63446d46f339c5891798435875/grpcio-1.82.1-cp313-cp313-win32.whl", hash = "sha256:38e9dcb5258226fb3282630b31b16a968df52c8c6ad514af540646e0a4578f8a", size = 4240363, upload-time = "2026-07-08T12:35:43.298Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/66a98d47732e35290bef722f6149fed3709cd4cf61166f6f53a12f417302/grpcio-1.82.1-cp313-cp313-win_amd64.whl", hash = "sha256:3dbfb52c36d9511ac2b8e6c94fdde837b393ae520cc321f52a333a2deedf5a90", size = 5000980, upload-time = "2026-07-08T12:35:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cb/cf9ae9e164c6e6dc8a494faa9771763df9da150eefe19671009624d1559f/grpcio-1.82.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:35f990f7784c8fd2872644f07f96ebb4d9e48e145a190ab80d0280af91a1bfb2", size = 6146901, upload-time = "2026-07-08T12:35:49.261Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/eccf26dbcfb7f7cab8027c5490a16c8937c5aa7a2ec20a3eab2cf7a43165/grpcio-1.82.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:46536a4a1f4434df3c851b9254ff6fc7df5705b273681a15ca277d5921c178a0", size = 11954756, upload-time = "2026-07-08T12:35:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/ad/75/3b3b4a3cc9f084b026af96e1d3e539b1af29ec7f41ed0dfff3cb99cc8626/grpcio-1.82.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d6650a7c1ebb7921c70e12a385439a8118efb99e669fa9ed31cf25db1843937c", size = 6723087, upload-time = "2026-07-08T12:35:54.973Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/b0f0c9b1400a99a4da4c09b114f101b192f8f11192e76f620b8962f5d90b/grpcio-1.82.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8e110c66df5204c0506d6c8787b35d48b8b699ef5aa366d6c4d67325c67fe9a", size = 7454542, upload-time = "2026-07-08T12:35:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bd/428e38868382aa193697a5aa53973f29c58e58ba4268aa0c86a2715ee58b/grpcio-1.82.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f853eae07235a51a27bb5d6a9a175a59ca55dc9b99edc6ce2f76f07332d333ae", size = 6889588, upload-time = "2026-07-08T12:36:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/03e01d5e10259bf5c08ee50570cc94724e79c956f61fd2f09b341af0956c/grpcio-1.82.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:60b0f2c95337694fc094b77d9f60f50566c84b5677393e342eb98daeee242d98", size = 7514166, upload-time = "2026-07-08T12:36:02.693Z" }, + { url = "https://files.pythonhosted.org/packages/ff/59/278b4b600329e2ba3849f3c1ea3c820b3a01b38a7ad184ba09595e8d2733/grpcio-1.82.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b064fc444812bdaa9825d33c26f8d732d63ee6a5d78557c1faf92c98687fed27", size = 8536166, upload-time = "2026-07-08T12:36:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/44/27/7ccf2ef00f27a8e47a79d641c8ceaf7d3028c7a03d9a97b4c8a9a783c086/grpcio-1.82.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d7ede11d747b4e1bd05e3bc0260e155b65a88735a895a10f6521f19b889511e", size = 7912572, upload-time = "2026-07-08T12:36:08.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/be/33742482d2753f2d3a1b7641664b6622262d44f2f3b609f13425dd86d36f/grpcio-1.82.1-cp314-cp314-win32.whl", hash = "sha256:3d21f19838dc255ecbb79321b15ae9b98fbddff4c3d4aedb0a81bdd7f4ab572a", size = 4321856, upload-time = "2026-07-08T12:36:10.899Z" }, + { url = "https://files.pythonhosted.org/packages/cc/67/03329c847172c78ddeb1eb9be6b444fdbc12775a84c958b27e427e7b926d/grpcio-1.82.1-cp314-cp314-win_amd64.whl", hash = "sha256:e20f1edbb15f99e3128ec86433f9785fd5a451d8f115e74fe0056134f092a9d5", size = 5141114, upload-time = "2026-07-08T12:36:13.595Z" }, ] [[package]] @@ -931,6 +1186,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] +[[package]] +name = "intervaltree" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/c3/b2afa612aa0373f3e6bb190e6de35f293b307d1537f109e3e25dbfcdf212/intervaltree-3.2.1.tar.gz", hash = "sha256:f3f7e8baeb7dd75b9f7a6d33cf3ec10025984a8e66e3016d537e52130c73cfe2", size = 1231531, upload-time = "2025-12-24T04:25:06.773Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7f/8a80a1c7c2ed05822b5a2b312d2995f30c533641f8198366ba2e26a7bb03/intervaltree-3.2.1-py2.py3-none-any.whl", hash = "sha256:a8a8381bbd35d48ceebee932c77ffc988492d22fb1d27d0ba1d74a7694eb8f0b", size = 25929, upload-time = "2025-12-24T04:25:05.298Z" }, +] + [[package]] name = "ipython" version = "9.10.0" @@ -1014,6 +1281,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + [[package]] name = "jmespath" version = "1.1.0" @@ -1059,6 +1412,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, ] +[[package]] +name = "kaitaistruct" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/b8/ca7319556912f68832daa4b81425314857ec08dfccd8dbc8c0f65c992108/kaitaistruct-0.11.tar.gz", hash = "sha256:053ee764288e78b8e53acf748e9733268acbd579b8d82a427b1805453625d74b", size = 11519, upload-time = "2025-09-08T15:46:25.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/4a/cf14bf3b1f5ffb13c69cf5f0ea78031247790558ee88984a8bdd22fae60d/kaitaistruct-0.11-py2.py3-none-any.whl", hash = "sha256:5c6ce79177b4e193a577ecd359e26516d1d6d000a0bffd6e1010f2a46a62a561", size = 11372, upload-time = "2025-09-08T15:46:23.635Z" }, +] + [[package]] name = "kubernetes" version = "35.0.0" @@ -1888,6 +2250,106 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] +[[package]] +name = "openai" +version = "2.46.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/ac/f725c4efbda8657d02be684607e5a2e5ce362e4790fdbcbdfb7c15018647/openai-2.46.0.tar.gz", hash = "sha256:0421e0735ac41451cad894af4cddf0435bfbf8cbc538ac0e15b3c062f2ddc06a", size = 1114628, upload-time = "2026-07-17T02:48:06.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/7b/206238ebcb50b235942b1c66dba4974776f2057402a8d91c399be587d66a/openai-2.46.0-py3-none-any.whl", hash = "sha256:672381db55efb3a1e2610f29304c130cccdd0b319bace4d492b2443cb64c1e7c", size = 1637556, upload-time = "2026-07-17T02:48:03.695Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/09/4d717852c1cf3f854b76c7110a5d00883bc3c99288b9b0dbcbeb9e306eb6/opentelemetry_exporter_otlp_proto_common-1.44.0.tar.gz", hash = "sha256:dc87a5a5bc58f149a56d1547e4691588fa12994cdc3bc039a694ccb3375862ac", size = 20202, upload-time = "2026-07-16T15:25:37.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/71/65fd9d54c10b860f87c045ccee1264cab7011268895d3528818a29c1172a/opentelemetry_exporter_otlp_proto_common-1.44.0-py3-none-any.whl", hash = "sha256:9a9fe61bba73d802904bc989f1d6b4a7b1ee40f06c40e98d6f85af65aaebb694", size = 17045, upload-time = "2026-07-16T15:25:18.201Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/87/95e2a5aaa795b4e2260d74e16df2d5541deb2ea9de010bcd615f4dee2654/opentelemetry_exporter_otlp_proto_http-1.44.0.tar.gz", hash = "sha256:c633d7270ad6b57cd4cfbe8b0007a9e2e7c0cb50bd6c50fe2a7b245f721a09d8", size = 25806, upload-time = "2026-07-16T15:25:39.162Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/d0/fdeb1a98d8d3a6205f5f297c51b4a9bfe65126ab60339669bbe3dd54c2e2/opentelemetry_exporter_otlp_proto_http-1.44.0-py3-none-any.whl", hash = "sha256:838592fce774c1c8bb7b9a0a7facbfa82e17be5a8a4e94cef10cb84ae026bae3", size = 21850, upload-time = "2026-07-16T15:25:20.006Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/01/40ac4ae9a149263cc52c2cee200ddd80cb6d8db1a4610abf8eabce0fe771/opentelemetry_proto-1.44.0.tar.gz", hash = "sha256:c547a79c2f8c0c515d31509154682e5921c7cfd5ca67b70e1f9266e2c3e103f3", size = 46488, upload-time = "2026-07-16T15:25:45.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/7c/8be563d68e93bbefa5c8affb82ddcff91b3ad858ce49957ba7b16fd3e0ab/opentelemetry_proto-1.44.0-py3-none-any.whl", hash = "sha256:898b155a0e1557afd867478fb6158e8122a46329ca0bb8dc53cc55e98f017f56", size = 72483, upload-time = "2026-07-16T15:25:28.429Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5d/77/a6592cbc7c8d9bcc9d6757a9df45e04a7c585e3e6e7a13456da522b21109/opentelemetry_sdk-1.44.0.tar.gz", hash = "sha256:cebe7f65dc12f26ead75c6064de12fd2a9052e5060c0272d402cfa203aae123b", size = 208624, upload-time = "2026-07-16T15:25:46.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/23/ff077e61886ee020a17ce9c8b6fa11c601c8d8345b09ea24f605445df62a/opentelemetry_sdk-1.44.0-py3-none-any.whl", hash = "sha256:df081c4c6bcfdb1211e3e86140376792643128a25f8d72d1d27675936e7e96ad", size = 137221, upload-time = "2026-07-16T15:25:29.534Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.65b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/73/0cbdebcb4cf545fdd328da14f5137e37d0770c3f26185e478b0d15d94f50/opentelemetry_semantic_conventions-0.65b0.tar.gz", hash = "sha256:f9b2b81e9d5b64f11bc952075e7e9c7fb0aab075c7fd1c46d597f1b919852d60", size = 148774, upload-time = "2026-07-16T15:25:46.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0e/49df70d9b81fb5cbae4bbf2a49d865b09bcbcbc4eb53f5851b1027738d78/opentelemetry_semantic_conventions-0.65b0-py3-none-any.whl", hash = "sha256:1cacde7b0ad306f84c5ef08c3dbe1bbaf20165bba6f8bff43b670e555a086bcb", size = 204645, upload-time = "2026-07-16T15:25:30.688Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -1975,6 +2437,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] +[[package]] +name = "pdfminer-six" +version = "20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -1987,6 +2462,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, ] +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, +] + [[package]] name = "platformdirs" version = "4.5.1" @@ -2005,6 +2565,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "polyfile-weave" +version = "0.5.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "abnf" }, + { name = "chardet" }, + { name = "cint" }, + { name = "fickling" }, + { name = "filelock" }, + { name = "graphviz" }, + { name = "intervaltree" }, + { name = "jinja2" }, + { name = "kaitaistruct" }, + { name = "networkx" }, + { name = "pdfminer-six" }, + { name = "pillow" }, + { name = "pyreadline3", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/55/e5400762e3884f743d59291e71eaaa9c52dd7e144b75a11911e74ec1bac9/polyfile_weave-0.5.9.tar.gz", hash = "sha256:12341fab03e06ede1bfebbd3627dd24015fde5353ea74ece2da186321b818bdb", size = 6024974, upload-time = "2026-01-22T22:08:48.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/94/215005530a48c5f7d4ec4a31acdb5828f2bfb985cc6e577b0eaa5882c0e2/polyfile_weave-0.5.9-py3-none-any.whl", hash = "sha256:6ae4b1b5eeac9f5bfc862474484d6d3e33655fab31749d93af0b0a91fddabfc7", size = 1700174, upload-time = "2026-01-22T22:08:46.346Z" }, +] + [[package]] name = "pre-commit" version = "4.5.1" @@ -2415,6 +3000,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, ] +[[package]] +name = "pyreadline3" +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2661,13 +3255,15 @@ dependencies = [ { name = "marimo" }, { name = "moutils", version = "0.3.12", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "moutils", version = "0.4.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "openai" }, { name = "ray" }, { name = "ruamel-yaml" }, { name = "statistics" }, { name = "torch" }, { name = "transformers" }, { name = "typing-extensions" }, - { name = "wandb" }, + { name = "wandb", extra = ["sandbox"] }, + { name = "weave" }, ] [package.dev-dependencies] @@ -2691,13 +3287,15 @@ requires-dist = [ { name = "kubernetes", specifier = ">=35.0.0" }, { name = "marimo", specifier = ">=0.23.6" }, { name = "moutils", specifier = ">=0.3.12" }, + { name = "openai", specifier = ">=2.46.0" }, { name = "ray", specifier = ">=2.53.0" }, { name = "ruamel-yaml", specifier = ">=0.19.1" }, { name = "statistics", specifier = ">=1.0.3.5" }, { name = "torch", specifier = ">=2.10.0" }, { name = "transformers", specifier = ">=5.0.0" }, { name = "typing-extensions", specifier = ">=4.15.0" }, - { name = "wandb", specifier = ">=0.24.2" }, + { name = "wandb", extras = ["sandbox"], specifier = ">=0.28.1" }, + { name = "weave", specifier = ">=0.53.2" }, ] [package.metadata.requires-dev] @@ -3073,12 +3671,21 @@ wheels = [ ] [[package]] -name = "smmap" -version = "5.0.2" +name = "sniffio" +version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] @@ -3129,6 +3736,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + [[package]] name = "tokenizers" version = "0.22.2" @@ -3453,11 +4069,10 @@ wheels = [ [[package]] name = "wandb" -version = "0.24.2" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "gitpython" }, { name = "packaging" }, { name = "platformdirs" }, { name = "protobuf" }, @@ -3467,17 +4082,22 @@ dependencies = [ { name = "sentry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/5c/53cf9f74b89e90facc8c7892d1449f7b39527e50e5cd577346baeb97e423/wandb-0.24.2.tar.gz", hash = "sha256:968b5b91d0a164dfb2f8c604cdf69e6fb09de6596b85b9f9d3c916b71ae86198", size = 44237317, upload-time = "2026-02-05T00:12:16.739Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/fb/8d3f96a8b143060d6fa145462d0785981373e04694e4152555ccb5d23939/wandb-0.28.1.tar.gz", hash = "sha256:870ccb1a01238b0ac07c6fd96a0810a1f79090aba04ea29f4ee012ac8327705d", size = 40578119, upload-time = "2026-07-16T18:47:05.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/82/5299fa22faf2dd55f33f05c26bf908b11ea4d25f32ac270d4bf838b0d97e/wandb-0.24.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:755b8a92edd28e15c052dc2bdc4652e26bce379fa7745360249cbfc589ff5f53", size = 21640026, upload-time = "2026-02-05T00:11:55.267Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/33cb321258778c25c00fb7eb578e69ce99428a66d4376eee4058f230a21a/wandb-0.24.2-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:5e6c0ad176792c7c3d1620a2ad65bd9a5f3886c69362af540d3667bfc97b67fb", size = 22894053, upload-time = "2026-02-05T00:11:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/3e/99/33b0281ac9a0b0c251195e6ce6cb310efa2f84ee117a15e9997fc2f9503b/wandb-0.24.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85861f9b3e54a07b84bade0aa5f4caa156028ab959351d98816a45e3b1411d35", size = 21286409, upload-time = "2026-02-05T00:12:00.584Z" }, - { url = "https://files.pythonhosted.org/packages/70/c8/1b758bd903afee000f023cd03f335ff328a21b3914f9f9deda49b1e57723/wandb-0.24.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:38661c666e70d7e1f460fc0a0edab8a393eaaa5f8773c17be534961a7022779d", size = 23026085, upload-time = "2026-02-05T00:12:02.682Z" }, - { url = "https://files.pythonhosted.org/packages/60/87/724583f258aaeb2c368c79d7412167ce628f8a5ca667faed3cd427dd3be2/wandb-0.24.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:656a4272000999569eb8e0773f1259403bc6bd3e7d1c7d2238d3e359874da9c4", size = 21342088, upload-time = "2026-02-05T00:12:05.375Z" }, - { url = "https://files.pythonhosted.org/packages/1e/5c/e9b36ddc9beb2745a4fb1ec67ae7f995c31f7305a6d17837b72b228360ff/wandb-0.24.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:33cba098d95fd46720cc9023bd23e4a38e9b11836a836b4a57b8d41cff8985f2", size = 23120819, upload-time = "2026-02-05T00:12:07.487Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6e/1ad011da4a5c860fdb88645c738a2dae914b1eea2249aa606659ccd1443f/wandb-0.24.2-py3-none-win32.whl", hash = "sha256:70db8680e8d7edb5bd60dfb7f31aeb5af30b31ad72498c47e1aba7471c337bb2", size = 22295643, upload-time = "2026-02-05T00:12:09.85Z" }, - { url = "https://files.pythonhosted.org/packages/38/8b/721c77616bd1fca8963bffef309da09cdff71002f9d4201dfd5bd370591a/wandb-0.24.2-py3-none-win_amd64.whl", hash = "sha256:a78ac1fa116b196cd33250b3d80f4a5c05c141ad949175515c007ec9826e49a6", size = 22295646, upload-time = "2026-02-05T00:12:11.898Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9a/f3919d7ee7ba99dabf0aac7e299c6c328f5eae94f9f6b28c76005f882d5d/wandb-0.24.2-py3-none-win_arm64.whl", hash = "sha256:b42614b99f8b9af69f88c15a84283a973c8cd5750e9c4752aa3ce21f13dbac9a", size = 20268261, upload-time = "2026-02-05T00:12:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/8df50164d07623cfcefec19bbf9327d9be84b637a827cea1f0c06db005fd/wandb-0.28.1-py3-none-macosx_12_0_arm64.whl", hash = "sha256:da909a76e65c64c0d93acc485d2a19f66e336f1e3f725f1c98a070883e084943", size = 24277925, upload-time = "2026-07-16T18:46:42.383Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/6c3da7e6cb215ad363324db8dc4d83b93626f5e339822b05b1c38a6097fd/wandb-0.28.1-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:3da3db219c54bfd1082c00e9061c8ea894ba43e42733b5af00bb10c09d7158fe", size = 25480852, upload-time = "2026-07-16T18:46:45.102Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1a/d15bcfb4417fa69edcaa33db8ea012db733da1057e193b047e3f69fdd671/wandb-0.28.1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ae9ae6fb29e2e2b1d097ed8b75c0c0240c778c2a8cad1d996dee870a1e401c2c", size = 24832138, upload-time = "2026-07-16T18:46:47.433Z" }, + { url = "https://files.pythonhosted.org/packages/b3/da/49924c7df2952dfd82c86c3779c339c0c3d6f6439387c03d97d0470c3658/wandb-0.28.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8cfb898b6a6c884d9c9294b02764e88bce65049f027a124d6bee53fe722469b6", size = 26486533, upload-time = "2026-07-16T18:46:49.839Z" }, + { url = "https://files.pythonhosted.org/packages/11/c0/06b23518e29690784f1b3081e39c7679ca076cb0af094cb9b4bb309150f5/wandb-0.28.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cf2b1533945395e4fdbe6182b272bb0ca8a02c10b3086a395e2d57686ae3ed0d", size = 25022635, upload-time = "2026-07-16T18:46:52.376Z" }, + { url = "https://files.pythonhosted.org/packages/23/30/6de2f7995a8a6eecbd03d24c79a139a734c0168f5520cf4c7ccb43c1dbbc/wandb-0.28.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7233061080507a4b4098bed1ccb381ce6f890c60397cd4153d060285bcb267bd", size = 27008895, upload-time = "2026-07-16T18:46:55.025Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/49deab9447687625371435ca21b6da82f223f1c7d014d77386b9cb91833c/wandb-0.28.1-py3-none-win32.whl", hash = "sha256:4bc461cda3ce23a19d8df5e42981a664d95fa3231efb10fd1e85d9d4824c7d29", size = 24418398, upload-time = "2026-07-16T18:46:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/bf/6f/ed6616b11ea15b8ceabedcaa567286c1c9ec65fa50563230a90bfb627cc5/wandb-0.28.1-py3-none-win_amd64.whl", hash = "sha256:d98a10370162b1e970850237114c56e9c4c58f3cb701e4b8cb38f36f6749fd52", size = 24418404, upload-time = "2026-07-16T18:47:00.427Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/75b6827a6665337a715c5347c5edbd84eca660f7a0f48d8d6d24d1f66bee/wandb-0.28.1-py3-none-win_arm64.whl", hash = "sha256:4aa07f13dd3bcac2c0524c8d0f49f76e83ab5c1054fd09f3b1a436cfcde146a6", size = 22299006, upload-time = "2026-07-16T18:47:02.71Z" }, +] + +[package.optional-dependencies] +sandbox = [ + { name = "cwsandbox", extra = ["cli"] }, ] [[package]] @@ -3489,6 +4109,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] +[[package]] +name = "weave" +version = "0.53.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "click" }, + { name = "diskcache-weave" }, + { name = "gql", extra = ["httpx"] }, + { name = "jsonschema" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "packaging" }, + { name = "polyfile-weave" }, + { name = "pydantic" }, + { name = "sentry-sdk" }, + { name = "tenacity" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b1/566cd40fa3455f2601306a4ebc1ea6c3f26161a01cbdeda668da1479f4df/weave-0.53.2.tar.gz", hash = "sha256:2e14e3279ebc62eb529aabf498aa02d526a01f18a189dfcfec19d2f38e75832a", size = 1148019, upload-time = "2026-07-16T23:38:52.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/e9/a22ed23dae0f1231b66afb84bd9460fa9b7c82a81841b7eb0c83593de06f/weave-0.53.2-py3-none-any.whl", hash = "sha256:0e44fcd002ed6e77f759f69a6b257edea8b645612c4b2cb41a8291f697299a3f", size = 1390710, upload-time = "2026-07-16T23:38:50.058Z" }, +] + [[package]] name = "websocket-client" version = "1.9.0" From 39b6f94e5ae103b6feecb634c2a95abf98cfa9cd Mon Sep 17 00:00:00 2001 From: Aayush jaiswal Date: Fri, 7 Aug 2026 15:06:49 -0500 Subject: [PATCH 2/4] feat(sandboxes): add Devin Outpost notebook --- sandboxes/README.md | 26 +++ sandboxes/assets/image.png | Bin 0 -> 111448 bytes sandboxes/devin-outpost.py | 332 +++++++++++++++++++++++++++++++++++++ 3 files changed, 358 insertions(+) create mode 100644 sandboxes/README.md create mode 100644 sandboxes/assets/image.png create mode 100644 sandboxes/devin-outpost.py diff --git a/sandboxes/README.md b/sandboxes/README.md new file mode 100644 index 0000000..9c23a77 --- /dev/null +++ b/sandboxes/README.md @@ -0,0 +1,26 @@ +# CoreWeave Sandboxes Examples + +This directory contains marimo notebooks and scripts demonstrating different use cases for [CoreWeave Sandboxes](https://docs.coreweave.com/products/sandboxes), isolated, on-demand execution environments for agentic workloads. + +## Notebooks + +### 1. [`serverless-sandboxes-tutorial.py`](./serverless-sandboxes-tutorial.py) + +An end-to-end code-evaluation workflow: a hosted model (via Serverless Inference's OpenAI-compatible API) generates Python for benchmark tasks, the generated code runs against deterministic tests inside a Serverless Sandbox, and every step is traced and scored in W&B Weave for side-by-side model comparison. + +**Use Case:** Evaluating hosted code-generation models with safe, isolated code execution. + +### 2. [`harness-evals.py`](./harness-evals.py) + +Evaluates full coding-agent CLIs (Codex, Claude Code, OpenClaw, Nous Hermes) that each live inside their own sandbox. Solutions are scored in a separate, network-isolated sandbox against demo tasks or the HumanEval / MBPP benchmarks, with per-agent `weave.Evaluation` runs. + +**Use Case:** Benchmarking and comparing agent harnesses on coding tasks at scale. + +### 3. [`devin-outpost.py`](./devin-outpost.py) + +Creates a Serverless Sandbox from Devin's official CLI image and connects it as +a single Linux worker for an existing Devin Outpost, with bounded resources and +explicit cleanup. + +**Use Case:** Running Devin sessions inside an isolated, on-demand development +environment. diff --git a/sandboxes/assets/image.png b/sandboxes/assets/image.png new file mode 100644 index 0000000000000000000000000000000000000000..5174ef45b767e8fcd06198ec0cd4a79dc4f48dad GIT binary patch literal 111448 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

2)eRF+cv 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 literal 0 HcmV?d00001 diff --git a/sandboxes/devin-outpost.py b/sandboxes/devin-outpost.py new file mode 100644 index 0000000..a06bfec --- /dev/null +++ b/sandboxes/devin-outpost.py @@ -0,0 +1,332 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "marimo>=0.23.6", +# "wandb[sandbox]>=0.28.1", +# ] +# /// + +import marimo + +__generated_with = "0.23.14" +app = marimo.App( + width="medium", + app_title="Devin Outpost on CW Serverless Sandboxes", + css_file="/usr/local/_marimo/custom.css", + auto_download=["html"], +) + + +@app.cell +def _(): + import os + import time + from pathlib import Path + + import marimo as mo + from wandb.sandbox import ( + NetworkOptions, + ResourceOptions, + Sandbox, + SandboxDefaults, + ) + + return ( + NetworkOptions, + Path, + ResourceOptions, + Sandbox, + SandboxDefaults, + mo, + os, + time, + ) + + +@app.cell(hide_code=True) +def _(mo): + mo.vstack( + [ + mo.md( + r""" + # Run a Devin Outpost on CW Serverless Sandboxes + + /// admonition | About This Notebook + type: info + + This tutorial connects a single + [CW Serverless Sandbox](https://docs.wandb.ai/sandboxes) + to a Devin Outpost. You create the Linux Outpost in Devin Cloud, + paste the token shown at creation, and launch an isolated worker + from Devin's official CLI image. + + _If you are running this notebook in edit mode, start by running all cells._ + /// + """ + ), + mo.md( + r""" + /// details | Table of Contents + type: info + + - [**Create a Devin Outpost**](#1-create-a-devin-outpost) + - [**Connect credentials**](#2-connect-credentials) + - [**Start the worker**](#3-start-the-worker) + - [**Start a Devin session**](#4-start-a-devin-session) + - [**Clean up**](#5-clean-up) + /// + """ + ), + ] + ) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 1. Create a Devin Outpost + + In [Devin Cloud](https://app.devin.ai), open + **Settings → Environment → Outposts**, select **Create Outpost**, give it + a name, and choose **Linux** as the platform. + + Devin shows the Outpost token once. Copy it and keep it secure; the + notebook asks for it in the next section. For the authoritative setup + steps and current prerequisites, see the + [Devin Outposts quickstart](https://docs.devin.ai/cloud/outposts/quickstart). + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 2. Connect credentials + + Enter the W&B key used to create CW Serverless Sandboxes, plus the Outpost + name and token shown when you created it. Both secrets are masked, and + the Outpost token is passed to the worker through an environment variable + instead of being embedded in its command text. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + credentials_form = ( + mo.md( + """ + - W&B API key: {wandb_api_key} + - Devin Outpost name: {outpost_name} + - Devin Outpost token: {outpost_token} + """ + ) + .batch( + wandb_api_key=mo.ui.text( + kind="password", + placeholder="from wandb.ai/authorize", + full_width=True, + ), + outpost_name=mo.ui.text( + placeholder="my-linux-outpost", + full_width=True, + ), + outpost_token=mo.ui.text( + kind="password", + placeholder="shown once when the Outpost is created", + full_width=True, + ), + ) + .form(submit_button_label="Launch worker", bordered=True) + ) + credentials_form + return (credentials_form,) + + +@app.cell(hide_code=True) +def _(credentials_form, mo, os): + credentials = credentials_form.value or {} + WANDB_API_KEY = credentials.get("wandb_api_key") + DEVIN_OUTPOST_NAME = credentials.get("outpost_name") + DEVIN_OUTPOST_TOKEN = credentials.get("outpost_token") + + mo.stop( + not (WANDB_API_KEY and DEVIN_OUTPOST_NAME and DEVIN_OUTPOST_TOKEN), + mo.md("_Fill in all three fields and press **Launch worker**._"), + ) + + os.environ["WANDB_API_KEY"] = WANDB_API_KEY + return DEVIN_OUTPOST_NAME, DEVIN_OUTPOST_TOKEN + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 3. Start the worker + + The sandbox uses Devin's official CLI image and installs `git`, the + required developer tool from Devin's container quickstart. Internet + egress lets the worker reach Devin and external package registries. + + This example keeps one worker alive for at most one hour. For repeated + use, bake your repositories and development tools into a dedicated + image instead of installing them at startup. + """) + return + + +@app.cell +def _( + DEVIN_OUTPOST_NAME, + DEVIN_OUTPOST_TOKEN, + NetworkOptions, + ResourceOptions, + Sandbox, + SandboxDefaults, + mo, + time, +): + defaults = SandboxDefaults( + container_image="public.ecr.aws/e0h8a4b6/devin-cli:stable", + tags=("devin-outpost", "tutorial"), + environment_variables={ + "DEVIN_OUTPOST_NAME": DEVIN_OUTPOST_NAME, + "DEVIN_OUTPOST_TOKEN": DEVIN_OUTPOST_TOKEN, + }, + resources=ResourceOptions( + requests={"cpu": "1", "memory": "2Gi"}, + limits={"cpu": "2", "memory": "4Gi"}, + ), + ) + + sandbox = Sandbox.run( + defaults=defaults, + network=NetworkOptions(egress_mode="internet"), + max_lifetime_seconds=3600, + ) + + setup = sandbox.exec( + [ + "bash", + "-lc", + ( + "apt-get update && " + "apt-get install -y --no-install-recommends ca-certificates git && " + "rm -rf /var/lib/apt/lists/* && " + "mkdir -p /repos" + ), + ], + timeout_seconds=300, + ).result() + mo.stop( + setup.returncode != 0, + mo.callout( + mo.md(f"Worker setup failed with exit code `{setup.returncode}`."), + kind="danger", + ), + ) + + worker_process = sandbox.exec( + [ + "bash", + "-lc", + ( + 'exec devin worker start --outpost="$DEVIN_OUTPOST_NAME" ' + '--token="$DEVIN_OUTPOST_TOKEN"' + ), + ], + cwd="/repos", + ) + time.sleep(2) + worker_returncode = worker_process.poll() + mo.stop( + worker_returncode is not None, + mo.callout( + mo.md( + f"Devin worker exited during startup with code `{worker_returncode}`." + ), + kind="danger", + ), + ) + + mo.callout( + mo.md( + f"🟢 Worker launched in sandbox `{sandbox.sandbox_id}`. " + "In Devin Cloud, start a session and choose this Outpost under " + "**Configuration → Virtual environment**." + ), + kind="success", + ) + return (sandbox,) + + +@app.cell(hide_code=True) +def _(Path, mo): + screenshot_path = "sandboxes/assets/image.png" + mo.vstack( + [ + mo.md(f""" + --- + ## 4. Start a Devin session + + In Devin Cloud, start a + new **Agent** session, open **Virtual environment**, expand + **Outposts**, and select **CW Serverless Sandbox**. Then enter a + small prompt such as `Create a "hello world" python script for me` + to confirm the session is running in the remote environment. + """), + mo.image( + src=screenshot_path, + alt=( + "Devin session composer with Virtual environment open and " + "CW Serverless Sandbox selected under Outposts" + ), + width="50%", + rounded=True, + caption=( + "Choose the CW Serverless Sandbox Outpost before submitting " + "your first prompt." + ), + ), + ] + ) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 5. Clean up + + The worker and sandbox consume resources while they are active. Stop the + sandbox when you finish the tutorial; it will also stop automatically + after its one-hour maximum lifetime. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + stop_button = mo.ui.run_button(label="🛑 Stop sandbox") + stop_button + return (stop_button,) + + +@app.cell +def _(mo, sandbox, stop_button): + mo.stop( + not stop_button.value, + mo.md("_Click **Stop sandbox** when you are finished._"), + ) + sandbox.stop(missing_ok=True).result() + mo.md(f"Sandbox `{sandbox.sandbox_id}` stopped.") + return + + +if __name__ == "__main__": + app.run() From 68766975066dd7794ccd060f00116ca4920be816 Mon Sep 17 00:00:00 2001 From: Aayush jaiswal Date: Fri, 7 Aug 2026 15:07:06 -0500 Subject: [PATCH 3/4] feat(sandboxes): add Claude remote control examples --- sandboxes/README.md | 39 ++ sandboxes/claude-remote-control-script.py | 118 ++++ sandboxes/claude-remote-control-tutorial.py | 653 ++++++++++++++++++++ 3 files changed, 810 insertions(+) create mode 100644 sandboxes/claude-remote-control-script.py create mode 100644 sandboxes/claude-remote-control-tutorial.py diff --git a/sandboxes/README.md b/sandboxes/README.md index 9c23a77..da451c7 100644 --- a/sandboxes/README.md +++ b/sandboxes/README.md @@ -24,3 +24,42 @@ explicit cleanup. **Use Case:** Running Devin sessions inside an isolated, on-demand development environment. + +### 4. [`claude-remote-control-tutorial.py`](./claude-remote-control-tutorial.py) + +A step-by-step, interactive tutorial for turning a Serverless Sandbox into the +remote machine your Claude Code session runs on. Walks through connecting W&B, +creating the sandbox (`Sandbox.run()`) with public ingress on port 8080, +installing Claude Code (`sandbox.exec()`), signing in and launching +[Remote Control](https://code.claude.com/docs/en/remote-control) over a PTY +(`sandbox.shell()`), then having Claude build and serve a live website reachable +at `sandbox.service_address`, and finally cleaning up (`sandbox.stop()`). The +OAuth login and the `Enable Remote Control?` prompt are handled inline in the +notebook. + +**Use Case:** Steering Claude Code from [claude.ai/code](https://claude.ai/code) +or the Claude mobile app while execution stays in a cloud sandbox. + +## Scripts + +### 1. [`claude-remote-control-script.py`](./claude-remote-control-script.py) + +The compact, no-frills version of the tutorial above: provisions a sandbox, +installs Claude Code, pre-trusts `/workspace`, and attaches your local terminal +to a PTY inside the sandbox so you can sign in and run `claude remote-control`. +Stops the sandbox on exit. + +**Use Case:** Launching a remote Claude Code session from your terminal in one +command, without the notebook UI. + +## Getting Started + +1. From the repo root, install dependencies: `uv sync` +2. Open a notebook in the marimo editor: `uv run marimo edit sandboxes/serverless-sandboxes-tutorial.py` +3. When prompted about inlined package dependencies, answer `n` to use the project environment (or `Y` for an isolated venv built from the notebook's inline dependencies). + +Scripts run directly in the project environment: + +```bash +uv run python sandboxes/claude-remote-control-script.py +``` diff --git a/sandboxes/claude-remote-control-script.py b/sandboxes/claude-remote-control-script.py new file mode 100644 index 0000000..db81229 --- /dev/null +++ b/sandboxes/claude-remote-control-script.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Create a sandbox, sign in to Claude Code, and run `claude remote-control`. +""" + +from __future__ import annotations + +import fcntl +import os +import signal +import struct +import sys +import termios +import threading +import tty + +os.environ.setdefault("WANDB_SILENT", "true") + +from wandb.sandbox import NetworkOptions, ResourceOptions, Sandbox, SandboxDefaults # noqa: E402 + +# Pre-seeding ~/.claude.json marks /workspace as trusted and skips first-run onboarding +CLAUDE_JSON = '{"hasCompletedOnboarding": true, "projects": {"/workspace": {"hasTrustDialogAccepted": true}}}' +BOOTSTRAP = ( + "npm install -g @anthropic-ai/claude-code --silent " + "&& mkdir -p /workspace " + f"&& printf '%s' '{CLAUDE_JSON}' > ~/.claude.json" +) +# Sign in first (full-scope claude.ai session, stored in the pod's ~/.claude), +# then serve Remote Control from /workspace. +RUN = "claude auth login && cd /workspace && exec claude remote-control" + + +def terminal_size() -> tuple[int, int]: + try: + rows, cols = struct.unpack("hh", fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, b"\0" * 4)) + return cols or 100, rows or 30 + except (OSError, struct.error): + return 100, 30 + + +def attach(sandbox: Sandbox, command: list[str]) -> int: + """Bridge the local terminal to a PTY inside the sandbox.""" + cols, rows = terminal_size() + session = sandbox.shell(command, width=cols, height=rows) + + def pump_output() -> None: + try: + for chunk in session.output: + sys.stdout.buffer.write(chunk) + sys.stdout.buffer.flush() + except Exception: # noqa: BLE001 - session closing is the normal exit path + pass + + threading.Thread(target=pump_output, daemon=True).start() + + def on_resize(*_: object) -> None: + new_cols, new_rows = terminal_size() + try: + session.resize(new_cols, new_rows) + except Exception: # noqa: BLE001 - resize is cosmetic + pass + + signal.signal(signal.SIGWINCH, on_resize) + + fd = sys.stdin.fileno() + saved = termios.tcgetattr(fd) + try: + tty.setraw(fd) + while True: + data = os.read(fd, 1024) + if not data: + break + try: + session.stdin.write(data).result() + except Exception: # noqa: BLE001 - remote session ended; stop forwarding + break + except (OSError, KeyboardInterrupt): + pass + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, saved) + + try: + return session.wait(timeout=10) + except Exception: # noqa: BLE001 + return 0 + + +def main() -> int: + print("Creating sandbox...", flush=True) + sandbox = Sandbox.run( + defaults=SandboxDefaults( + container_image="node:22", + tags=("claude-code", "remote-control"), + resources=ResourceOptions(requests={"cpu": "2", "memory": "4Gi"}), + ), + network=NetworkOptions(egress_mode="internet"), + max_lifetime_seconds=4 * 3600, + ) + sandbox.wait() + print(f" {sandbox.sandbox_id} (expires in 4h)", flush=True) + + print("Installing Claude Code...", flush=True) + setup = sandbox.exec(["bash", "-lc", BOOTSTRAP], timeout_seconds=900) + setup.wait(timeout=900) + if setup.returncode != 0: + sys.stderr.write(setup.result().stderr_bytes.decode(errors="replace")) + sandbox.stop(missing_ok=True).result() + return 1 + + print("Sign in when prompted, then Remote Control starts. Ctrl-C stops it.\n", flush=True) + code = attach(sandbox, ["bash", "-lc", RUN]) + + print("\nStopping sandbox...", flush=True) + sandbox.stop(missing_ok=True).result() + return code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/sandboxes/claude-remote-control-tutorial.py b/sandboxes/claude-remote-control-tutorial.py new file mode 100644 index 0000000..97e071e --- /dev/null +++ b/sandboxes/claude-remote-control-tutorial.py @@ -0,0 +1,653 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "anywidget>=0.9", +# "marimo>=0.23.6", +# "wandb[sandbox]>=0.28.1", +# ] +# /// + +import marimo + +__generated_with = "0.23.6" +app = marimo.App( + width="medium", + app_title="Claude Code in a Serverless Sandbox", +) + + +@app.cell +def _(): + import os + import re + import threading + import time + + import anywidget + import marimo as mo + import requests + + os.environ.setdefault("WANDB_SILENT", "true") + + from wandb.sandbox import ( + NetworkOptions, + ResourceOptions, + Sandbox, + SandboxDefaults, + ) + + return ( + NetworkOptions, + ResourceOptions, + Sandbox, + SandboxDefaults, + anywidget, + mo, + os, + re, + requests, + threading, + time, + ) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Set Up a Serverless Sandbox as a Remote Environment for Claude Code + + /// admonition | What this notebook does + type: info + + Claude Code runs inside a CoreWeave **Serverless Sandbox**, and you steer it from [claude.ai/code](https://claude.ai/code) or the + Claude mobile app via **Remote Control**. Your laptop is only the interface; + execution stays in the sandbox, inference stays on Anthropic's API. + + You need a [claude.ai](https://claude.ai/) subscription + (Pro/Max/Team/Enterprise) for the sign-in step. + /// + """) + return + + +@app.cell(hide_code=True) +def _(mo): + wandb_key_form = ( + mo.md("{api_key}") + .batch( + api_key=mo.ui.text( + kind="password", + placeholder="W&B API key from wandb.ai/authorize", + full_width=True, + ), + ) + .form(submit_button_label="Connect", bordered=False) + ) + mo.vstack( + [ + mo.md(r""" + --- + ## 1. Connect W&B + + Paste your W&B API key from + [wandb.ai/authorize](https://wandb.ai/authorize). It authenticates + every `Sandbox` call in this notebook — starting with `Sandbox.run()` + in the next step. + """), + wandb_key_form, + ] + ) + return (wandb_key_form,) + + +@app.cell(hide_code=True) +def _(mo, os, requests, wandb_key_form): + form_value = wandb_key_form.value or {} + candidate_key = form_value.get("api_key", "").strip() + mo.stop(not candidate_key, mo.md("_Paste your API key above and press **Connect**._")) + + # Validate against the W&B API before exporting anything. This is the same + # check `wandb login --verify` performs, minus its side effect of writing + # the (possibly wrong) key to ~/.netrc before verifying it. + with mo.status.spinner(title="Validating key with api.wandb.ai..."): + viewer_resp = requests.post( + "https://api.wandb.ai/graphql", + json={"query": "query Viewer { viewer { username } }"}, + auth=("api", candidate_key), + timeout=15, + ) + viewer = (viewer_resp.json().get("data") or {}).get("viewer") if viewer_resp.ok else None + mo.stop( + not (viewer and viewer.get("username")), + mo.callout( + mo.md("❌ api.wandb.ai rejected this key. Copy it again from [wandb.ai/authorize](https://wandb.ai/authorize)."), + kind="danger", + ), + ) + + WANDB_KEY = candidate_key + os.environ["WANDB_API_KEY"] = WANDB_KEY + mo.callout(mo.md(f"✅ Key verified — connected as **{viewer['username']}**."), kind="success") + return (WANDB_KEY,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 2. Create the sandbox + + Pick a lifetime and press **Create sandbox** to call + `Sandbox.run(..., max_lifetime_seconds=...)`. + - The lifetime is a hard cap: it + can't be extended later, and expiry kills the sandbox without preserving files. + - `NetworkOptions(egress_mode="internet", ingress_mode="public", exposed_ports=(8080,))` + gives it internet egress plus public ingress on **port 8080**, so anything + Claude serves there is reachable straight from your browser (used in step 5). + """) + return + + +@app.cell +def _(ResourceOptions, SandboxDefaults): + SANDBOX_DEFAULTS = SandboxDefaults( + container_image="node:22", + tags=("claude-code", "remote-control", "tutorial"), + resources=ResourceOptions(requests={"cpu": "2", "memory": "4Gi"}), + ) + return (SANDBOX_DEFAULTS,) + + +@app.cell(hide_code=True) +def _(mo): + lifetime_slider = mo.ui.slider( + start=1, stop=12, step=1, value=4, + label="Sandbox lifetime (hours)", + show_value=True, + ) + create_btn = mo.ui.run_button(label="Create sandbox", kind="success") + mo.hstack([lifetime_slider, create_btn], justify="start", gap=2) + return create_btn, lifetime_slider + + +@app.cell +def _( + NetworkOptions, + SANDBOX_DEFAULTS, + Sandbox, + WANDB_KEY, + create_btn, + lifetime_slider, + mo, +): + assert WANDB_KEY # step 1 must be done first + mo.stop(not create_btn.value, mo.md("_Press **Create sandbox** to provision the sandbox._")) + + lifetime_hours = lifetime_slider.value + with mo.status.spinner(title="Creating sandbox..."): + sandbox = Sandbox.run( + defaults=SANDBOX_DEFAULTS, + network=NetworkOptions( + egress_mode="internet", + ingress_mode="public", + exposed_ports=(8080,), + ), + max_lifetime_seconds=int(lifetime_hours * 3600), + ) + sandbox.wait() + + service_note = ( + f" Port 8080 is public at **`{sandbox.service_address}`**." + if sandbox.service_address + else "" + ) + mo.callout( + mo.md( + f"✅ Sandbox **`{sandbox.sandbox_id}`** is running — hard expiry in " + f"**{lifetime_hours}h**.{service_note}" + ), + kind="success", + ) + return (sandbox,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 3. Install Claude Code + + `sandbox.exec(...)` runs one command that installs Claude Code via npm and + pre-writes `~/.claude.json` so `/workspace` is already trusted. + """) + return + + +@app.cell +def _(mo, sandbox): + CLAUDE_JSON = '{"hasCompletedOnboarding": true, "projects": {"/workspace": {"hasTrustDialogAccepted": true}}}' + BOOTSTRAP_CMD = ( + "npm install -g @anthropic-ai/claude-code --silent " + "&& mkdir -p /workspace " + f"&& printf '%s' '{CLAUDE_JSON}' > ~/.claude.json" + ) + + with mo.status.spinner(title="Installing Claude Code in the sandbox (~30-60s)..."): + bootstrap_proc = sandbox.exec(["bash", "-lc", BOOTSTRAP_CMD], timeout_seconds=900) + bootstrap_proc.wait(timeout=900) + + bootstrap_ok = bootstrap_proc.returncode == 0 + if bootstrap_ok: + bootstrap_note = mo.callout(mo.md("✅ Claude Code installed and `/workspace` pre-trusted."), kind="success") + else: + bootstrap_stderr = bootstrap_proc.result().stderr_bytes.decode(errors="replace") + bootstrap_note = mo.callout( + mo.md(f"❌ Bootstrap failed:\n\n```\n{bootstrap_stderr[-1500:]}\n```"), + kind="danger", + ) + bootstrap_note + return (bootstrap_ok,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 4. Sign in and start Remote Control + + Remote Control only accepts a full claude.ai login (API keys and setup-tokens + are rejected), so `sandbox.shell(...)` opens a PTY running + `claude auth login && cd /workspace && exec claude remote-control`. + Only two steps need you: + + 1. Open the **authorization link** when it appears in the banner and approve. + 2. Paste the returned code into the box and press **Submit code**. + + When the banner + turns green, open the **session link** on claude.ai/code, or find the session + under **Code** in the Claude mobile app. + + The panel refreshes itself every couple of seconds during sign-in and stops + once Remote Control is up. If something gets stuck, the raw sandbox console and + manual keys are in the collapsible section at the bottom. + """) + return + + +@app.cell(hide_code=True) +def _(anywidget, mo): + class NotificationSilencer(anywidget.AnyWidget): + """Invisible widget that stubs the browser Notification API. + + marimo fires a desktop notification every time a run completes while + the tab is unfocused, and the auto-refresh console below completes a + run every couple of seconds. marimo has no setting to turn this off + (v0.23), but its code bails out when Notification.permission is + "denied" — so this widget replaces window.Notification with a stub + that always reports "denied". Applies to this notebook page only. + """ + + _esm = """ + function render({ el }) { + class SilentNotification { + static get permission() { return "denied"; } + static requestPermission() { return Promise.resolve("denied"); } + } + window.Notification = SilentNotification; + el.style.display = "none"; + } + export default { render }; + """ + + notification_silencer = mo.ui.anywidget(NotificationSilencer()) + notification_silencer + return + + +@app.cell(hide_code=True) +def _(bootstrap_ok, mo): + mo.stop(not bootstrap_ok, mo.md("_Fix the bootstrap above first._")) + launch_btn = mo.ui.run_button(label="Start sign-in + Remote Control", kind="success") + launch_btn + return (launch_btn,) + + +@app.cell(hide_code=True) +def _(launch_btn, mo, re, sandbox, threading): + mo.stop(not launch_btn.value, mo.md("_Press the button to open the PTY session in the sandbox._")) + + RUN_CMD = "claude auth login && cd /workspace && exec claude remote-control" + output_chunks: list[bytes] = [] + # Holds the last response to a submitted code so the callout survives + # panel re-renders. + feedback_store: dict = {} + # Flipped by the pump thread when the PTY closes (e.g. after Ctrl-C), so + # the panel stops writing to a dead stream. + session_state = {"ended": False} + # A very wide PTY keeps long OAuth / session URLs on a single line. + session = sandbox.shell(["bash", "-lc", RUN_CMD], width=500, height=40) + + ANSI_RE = re.compile( + rb"\x1b\[[0-9;?]*[ -/]*[@-~]" # CSI sequences (colors, cursor movement) + rb"|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)" # OSC sequences (titles, links) + rb"|\x1b[@-_]" # other escapes + ) + + def render_output(chunks: list[bytes], max_lines: int = 40) -> str: + text = ANSI_RE.sub(b"", b"".join(chunks)).decode("utf-8", errors="replace") + lines = [] + for raw_line in text.split("\n"): + # PTYs end lines with \r\n: drop the trailing \r, then keep only + # the text after any remaining \r (in-place TUI redraws). + line = raw_line.rstrip("\r") + lines.append(line.rsplit("\r", 1)[-1]) + return "\n".join(lines[-max_lines:]) + + def find_urls(chunks: list[bytes]) -> list[str]: + # Scan the RAW stream: links can arrive inside OSC-8 hyperlink escapes, + # which ANSI stripping would delete. + raw_text = b"".join(chunks).decode("utf-8", errors="replace") + urls: list[str] = [] + for raw_url in re.findall(r"https://[^\s\x1b\x07\"'`]+", raw_text): + url = raw_url.rstrip(").,;|>") + if url not in urls and ("claude.ai" in url or "claude.com" in url): + urls.append(url) + return urls + + def pump_session_output() -> None: + # Both interactive prompts in this flow have fixed answers, so the pump + # answers them itself: Enter picks the (pre-selected) claude.ai option + # at the login menu, and "y" confirms the "Enable Remote Control?" + # prompt. The only human steps left are opening the authorization link + # and pasting the code back. + auto_answered = {"login_menu": False, "enable_rc": False} + try: + for chunk in session.output: + output_chunks.append(chunk) + text = b"".join(output_chunks).decode("utf-8", errors="replace") + if not auto_answered["login_menu"] and "login method" in text.lower(): + auto_answered["login_menu"] = True + session.stdin.write(b"\r").result() + if not auto_answered["enable_rc"] and "Enable Remote Control?" in text: + auto_answered["enable_rc"] = True + session.stdin.write(b"y\r").result() + except Exception: # noqa: BLE001 - session closing is the normal exit path + pass + finally: + session_state["ended"] = True + + threading.Thread(target=pump_session_output, daemon=True).start() + return ( + feedback_store, + find_urls, + output_chunks, + render_output, + session, + session_state, + ) + + +@app.cell(hide_code=True) +def _(mo, session): + assert session is not None # console pairs with the live session + # Rendered invisibly by the panel below, and only until sign-in completes. + console_refresh = mo.ui.refresh(default_interval="2s") + key_input = mo.ui.text(placeholder="paste the authorization code here", full_width=True) + send_btn = mo.ui.run_button(label="Submit code", kind="success") + enter_btn = mo.ui.run_button(label="Enter") + up_btn = mo.ui.run_button(label="↑") + down_btn = mo.ui.run_button(label="↓") + ctrl_c_btn = mo.ui.run_button(label="Ctrl-C", kind="danger") + return ( + console_refresh, + ctrl_c_btn, + down_btn, + enter_btn, + key_input, + send_btn, + up_btn, + ) + + +@app.cell(hide_code=True) +def _( + console_refresh, + ctrl_c_btn, + down_btn, + enter_btn, + feedback_store: dict, + find_urls, + key_input, + mo, + output_chunks: list[bytes], + render_output, + send_btn, + session, + session_state, + time, + up_btn, +): + console_refresh.value # re-render on each invisible tick (while sign-in runs) + + keystrokes = b"" + if send_btn.value: + keystrokes = key_input.value.encode() + b"\r" + elif enter_btn.value: + keystrokes = b"\r" + elif up_btn.value: + keystrokes = b"\x1b[A" + elif down_btn.value: + keystrokes = b"\x1b[B" + elif ctrl_c_btn.value: + keystrokes = b"\x03" + + if keystrokes and not session_state["ended"]: + chunk_mark = len(output_chunks) + try: + session.stdin.write(keystrokes).result() + except Exception: # noqa: BLE001 - PTY closed between check and write + session_state["ended"] = True + if send_btn.value and not session_state["ended"]: + # Wait for the sandbox to process the pasted code, then persist its + # full response — success or error — so the callout survives + # later re-renders. + with mo.status.spinner(title="Submitting code to the sandbox..."): + time.sleep(6) + response_text = render_output(output_chunks[chunk_mark:], max_lines=200).strip() + response_lower = response_text.lower() + has_error = any(w in response_lower for w in ("error", "invalid", "failed", "expired", "denied")) + has_success = any(w in response_lower for w in ("success", "logged in", "welcome")) + feedback_store["kind"] = "danger" if has_error else ("success" if has_success else "info") + feedback_store["text"] = response_text or "(no output captured — check the console below)" + + console_text = render_output(output_chunks) or "(waiting for output...)" + full_text = b"".join(output_chunks).decode("utf-8", errors="replace") + detected_urls = find_urls(output_chunks) + authorize_url = next((u for u in detected_urls if "oauth" in u or "authorize" in u), None) + rc_session_url = next((u for u in detected_urls if "/code/" in u), None) + rc_policy_blocked = "Remote Control is disabled" in full_text + signed_in = "Login successful" in full_text + session_ended = session_state["ended"] + flow_done = bool(rc_session_url) or rc_policy_blocked or session_ended + + if session_ended: + status = mo.callout( + mo.md( + "⚪ **The sandbox session has ended** (Remote Control stopped). Press " + "**Start sign-in + Remote Control** above to relaunch, or continue to **Clean up**." + ), + kind="warn", + ) + elif rc_session_url: + status = mo.callout( + mo.md( + f"🟢 **Remote Control is live** — [open your session ↗]({rc_session_url}), " + f"or find it under **Code** in the Claude mobile app." + ), + kind="success", + ) + elif rc_policy_blocked: + status = mo.callout( + mo.md( + "❌ **Signed in, but your organization has Remote Control disabled.** " + "On Team and Enterprise plans it's off by default — an Owner must enable the " + "**Remote Control** toggle at " + "[claude.ai/admin-settings/claude-code](https://claude.ai/admin-settings/claude-code), " + "then restart this step." + ), + kind="danger", + ) + elif signed_in: + status = mo.callout( + mo.md("✅ **Signed in** — enabling Remote Control automatically, the session link will appear here in a moment..."), + kind="info", + ) + elif authorize_url: + status = mo.callout( + mo.md( + f"🔑 **Sign in:** [open the authorization page ↗]({authorize_url}), approve, " + f"then paste the returned code below and press **Submit code**." + ), + kind="info", + ) + else: + status = mo.callout( + mo.md("⏳ Starting sign-in — the login menu is answered automatically; the authorization link will appear here."), + kind="neutral", + ) + + panel_items = [status] + if feedback_store.get("text") and not flow_done: + panel_items.append( + mo.callout( + mo.vstack( + [ + mo.md("**Sandbox response to the submitted code:**"), + mo.plain_text(feedback_store["text"]), + ] + ), + kind=feedback_store["kind"], + ) + ) + if authorize_url and not signed_in and not flow_done: + # The code box only matters between "link surfaced" and "signed in". + panel_items.append(mo.hstack([key_input, send_btn], widths=[5, 1], gap=0.5)) + panel_items.append( + mo.accordion( + { + "Raw sandbox console + manual keys": mo.vstack( + [ + mo.plain_text(console_text), + mo.hstack([enter_btn, up_btn, down_btn, ctrl_c_btn], justify="start", gap=0.5), + ] + ) + } + ) + ) + if not flow_done: + # The invisible refresh only renders (and therefore only ticks) while + # sign-in is still in progress; once Remote Control is live or blocked, + # it drops out of the output and the auto-refresh stops for good. + panel_items.append(mo.Html(f"

")) + mo.vstack(panel_items) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 5. Try it: have Claude build a live website + + Remote Control is running, so switch to [claude.ai/code](https://claude.ai/code) + (or the **Code** tab in the mobile app), open your session, and paste the + prompt below, it already contains this sandbox's public address (read from + `sandbox.service_address`). Claude will build the site inside the sandbox and + serve it on port 8080; open the link when it reports done. + """) + return + + +@app.cell(hide_code=True) +def _(mo, sandbox, session): + assert session is not None # meaningful only once Remote Control is running + + if sandbox.service_address: + site_url = f"http://{sandbox.service_address}" + demo_prompt = ( + "Build me a sample website and give me a link to access it. Make it a " + "landing page for the concept of running a W&B Serverless Sandbox as a " + "remote environment for Claude Code. Add some cool design elements and " + "animations, and make sure actual logos are present for both Weights & Biases " + "and Claude. Serve it on port 8080, bound to 0.0.0.0, and keep the " + "server running. Port 8080 on this machine is publicly reachable at " + f"{site_url} — once the server is up, give me that direct link to " + "access the site." + ) + demo_out = mo.vstack( + [ + mo.md(f"```text\n{demo_prompt}\n```"), + mo.md(f"Once Claude reports the server is running, the site is at [{site_url}]({site_url})."), + ] + ) + else: + demo_out = mo.callout( + mo.md( + "⚠️ This sandbox has no public service address — the runner may not " + "support `ingress_mode=\"public\"`. Recreate the sandbox in step 2 " + "or ask your W&B admin about ingress support." + ), + kind="warn", + ) + demo_out + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 6. Clean up + + The sandbox bills until you stop it or the lifetime expires. Ctrl-C above stops the Remote Control server; the button below calls + `sandbox.stop()`. Lost sandboxes: `Sandbox.list(tags=["remote-control"]).result()`. + """) + return + + +@app.cell(hide_code=True) +def _(mo, sandbox): + assert sandbox is not None # nothing to stop before creation + stop_btn = mo.ui.run_button(label="Stop sandbox", kind="danger") + stop_btn + return (stop_btn,) + + +@app.cell +def _(mo, sandbox, stop_btn): + mo.stop(not stop_btn.value, mo.md("_Press **Stop sandbox** when you're finished._")) + sandbox.stop(missing_ok=True).result() + mo.callout(mo.md(f"✅ Sandbox `{sandbox.sandbox_id}` stopped."), kind="success") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + /// details | Where to next + type: info + + - [`claude-code-remote-env.py`](./claude-code-remote-env.py) — the terminal + version: attach your real terminal to Claude Code in a sandbox + - [`claude-remote-control-sandbox.py`](./claude-remote-control-sandbox.py) — + the compact script this notebook explains + - [Remote Control docs](https://code.claude.com/docs/en/remote-control) + - [Sandbox environments compared](https://code.claude.com/docs/en/sandbox-environments) + /// + """) + return + + +if __name__ == "__main__": + app.run() From cd86c7e3310894c6a95ef98fe1178b322b656c64 Mon Sep 17 00:00:00 2001 From: Aayush jaiswal Date: Wed, 12 Aug 2026 11:04:36 -0500 Subject: [PATCH 4/4] update claude notebook --- sandboxes/claude-remote-control-tutorial.py | 226 ++++++++++++++++---- 1 file changed, 182 insertions(+), 44 deletions(-) diff --git a/sandboxes/claude-remote-control-tutorial.py b/sandboxes/claude-remote-control-tutorial.py index 97e071e..53016e0 100644 --- a/sandboxes/claude-remote-control-tutorial.py +++ b/sandboxes/claude-remote-control-tutorial.py @@ -9,10 +9,11 @@ import marimo -__generated_with = "0.23.6" +__generated_with = "0.23.15" app = marimo.App( width="medium", - app_title="Claude Code in a Serverless Sandbox", + app_title="Run Claude Code in a Remote Sandbox Environment", + auto_download=["html"], ) @@ -20,7 +21,6 @@ def _(): import os import re - import threading import time import anywidget @@ -46,26 +46,53 @@ def _(): os, re, requests, - threading, time, ) +@app.cell +def _(): + # Survives relaunches of the sign-in step (this cell has no button + # dependency, so it runs once). Holds the previous PTY session and its + # state dict so a second "Start sign-in" press can tear the old one down + # instead of leaking another `claude` process + pump thread into the + # sandbox. That leak made repeated runs flaky. + launch_registry: dict = {"session": None, "state": None} + return (launch_registry,) + + @app.cell(hide_code=True) def _(mo): mo.md(r""" - # Set Up a Serverless Sandbox as a Remote Environment for Claude Code + # Run Claude Code in a Remote Sandbox Environment /// admonition | What this notebook does type: info - Claude Code runs inside a CoreWeave **Serverless Sandbox**, and you steer it from [claude.ai/code](https://claude.ai/code) or the - Claude mobile app via **Remote Control**. Your laptop is only the interface; - execution stays in the sandbox, inference stays on Anthropic's API. + Claude Code runs inside a CoreWeave **Serverless Sandbox**, and you steer it + from [claude.ai/code](https://claude.ai/code) or the Claude mobile app via + **Remote Control**. Your laptop is only the interface; execution stays in the + sandbox, inference stays on Anthropic's API. You need a [claude.ai](https://claude.ai/) subscription (Pro/Max/Team/Enterprise) for the sign-in step. /// + + /// admonition | Why a sandbox instead of the default cloud environment + type: note + + Claude Code's built-in cloud environment is a locked-down, repo-only VM. Your + own sandbox lets Claude do what that environment can't: + + - **Reach your tools and infrastructure.** Run inside your org's network to + hit internal services, private registries, databases, and clusters. + - **Use real compute.** GPUs and large CPU or memory for training, inference, + or heavy builds. + - **Host on a public URL.** Public ingress makes a dev server Claude starts + reachable on the internet. The default environment only produces a PR, it + can't expose a running site. + - **Bring a custom environment.** Your own image, mounted data, and config. + /// """) return @@ -91,7 +118,7 @@ def _(mo): Paste your W&B API key from [wandb.ai/authorize](https://wandb.ai/authorize). It authenticates - every `Sandbox` call in this notebook — starting with `Sandbox.run()` + every `Sandbox` call in this notebook, starting with `Sandbox.run()` in the next step. """), wandb_key_form, @@ -127,7 +154,7 @@ def _(mo, os, requests, wandb_key_form): WANDB_KEY = candidate_key os.environ["WANDB_API_KEY"] = WANDB_KEY - mo.callout(mo.md(f"✅ Key verified — connected as **{viewer['username']}**."), kind="success") + mo.callout(mo.md(f"✅ Key verified. Connected as **{viewer['username']}**."), kind="success") return (WANDB_KEY,) @@ -143,7 +170,7 @@ def _(mo): can't be extended later, and expiry kills the sandbox without preserving files. - `NetworkOptions(egress_mode="internet", ingress_mode="public", exposed_ports=(8080,))` gives it internet egress plus public ingress on **port 8080**, so anything - Claude serves there is reachable straight from your browser (used in step 5). + Claude serves there is reachable straight from your browser (used in step 6). """) return @@ -203,7 +230,7 @@ def _( ) mo.callout( mo.md( - f"✅ Sandbox **`{sandbox.sandbox_id}`** is running — hard expiry in " + f"✅ Sandbox **`{sandbox.sandbox_id}`** is running, hard expiry in " f"**{lifetime_hours}h**.{service_note}" ), kind="success", @@ -257,19 +284,22 @@ def _(mo): Remote Control only accepts a full claude.ai login (API keys and setup-tokens are rejected), so `sandbox.shell(...)` opens a PTY running - `claude auth login && cd /workspace && exec claude remote-control`. - Only two steps need you: + `claude auth login && cd /workspace && exec claude remote-control --name + 'CoreWeave Sandboxes'`. The `--name` flag is what titles the session in + claude.ai/code; change that string to rename it. Only two steps need you: 1. Open the **authorization link** when it appears in the banner and approve. 2. Paste the returned code into the box and press **Submit code**. - When the banner - turns green, open the **session link** on claude.ai/code, or find the session - under **Code** in the Claude mobile app. + When the banner turns green it pins the **session link** and the Remote + Control details right there in the panel, so they stay put instead of + scrolling away. Step 5 explains exactly what to do with them. The panel refreshes itself every couple of seconds during sign-in and stops once Remote Control is up. If something gets stuck, the raw sandbox console and - manual keys are in the collapsible section at the bottom. + manual keys are in the collapsible section at the bottom. Pressing **Start + sign-in + Remote Control** again cleanly restarts the session (it stops the + previous one first). """) return @@ -283,7 +313,7 @@ class NotificationSilencer(anywidget.AnyWidget): the tab is unfocused, and the auto-refresh console below completes a run every couple of seconds. marimo has no setting to turn this off (v0.23), but its code bails out when Notification.permission is - "denied" — so this widget replaces window.Notification with a stub + "denied", so this widget replaces window.Notification with a stub that always reports "denied". Applies to this notebook page only. """ @@ -313,10 +343,25 @@ def _(bootstrap_ok, mo): @app.cell(hide_code=True) -def _(launch_btn, mo, re, sandbox, threading): +def _(launch_btn, launch_registry, mo, re, sandbox): mo.stop(not launch_btn.value, mo.md("_Press the button to open the PTY session in the sandbox._")) - RUN_CMD = "claude auth login && cd /workspace && exec claude remote-control" + # Relaunch cleanup: stop the previous session's Remote Control server and + # signal its pump thread to exit, so we never run two `claude` PTYs at once. + prev_session = launch_registry.get("session") + prev_state = launch_registry.get("state") + if prev_session is not None: + if prev_state is not None: + prev_state["ended"] = True + try: + prev_session.stdin.write(b"\x03").result() # Ctrl-C the old claude + except Exception: # noqa: BLE001 - old PTY may already be gone + pass + + RUN_CMD = ( + "claude auth login && cd /workspace " + "&& exec claude remote-control --name 'CoreWeave Sandboxes'" + ) output_chunks: list[bytes] = [] # Holds the last response to a submitted code so the callout survives # panel re-renders. @@ -360,9 +405,17 @@ def pump_session_output() -> None: # at the login menu, and "y" confirms the "Enable Remote Control?" # prompt. The only human steps left are opening the authorization link # and pasting the code back. + thread = mo.current_thread() auto_answered = {"login_menu": False, "enable_rc": False} try: for chunk in session.output: + # marimo sets should_exit when this cell is re-run, interrupted, + # or the kernel is restarted. Bail so the thread doesn't outlive + # its session and desync the frontend. (The relaunch teardown + # above Ctrl-Cs the old PTY, which unblocks this read so the + # check is reached promptly.) + if thread.should_exit: + break output_chunks.append(chunk) text = b"".join(output_chunks).decode("utf-8", errors="replace") if not auto_answered["login_menu"] and "login method" in text.lower(): @@ -376,7 +429,13 @@ def pump_session_output() -> None: finally: session_state["ended"] = True - threading.Thread(target=pump_session_output, daemon=True).start() + # mo.Thread, not threading.Thread: marimo tracks it across re-runs and kernel + # restarts and signals should_exit on invalidation. A raw thread survives a + # restart orphaned, which is what left the panel broken until a full page + # reload (Cmd+R). Requires marimo >= 0.23. + mo.Thread(target=pump_session_output, daemon=True).start() + launch_registry["session"] = session + launch_registry["state"] = session_state return ( feedback_store, find_urls, @@ -449,7 +508,7 @@ def _( session_state["ended"] = True if send_btn.value and not session_state["ended"]: # Wait for the sandbox to process the pasted code, then persist its - # full response — success or error — so the callout survives + # full response, success or error, so the callout survives # later re-renders. with mo.status.spinner(title="Submitting code to the sandbox..."): time.sleep(6) @@ -458,7 +517,7 @@ def _( has_error = any(w in response_lower for w in ("error", "invalid", "failed", "expired", "denied")) has_success = any(w in response_lower for w in ("success", "logged in", "welcome")) feedback_store["kind"] = "danger" if has_error else ("success" if has_success else "info") - feedback_store["text"] = response_text or "(no output captured — check the console below)" + feedback_store["text"] = response_text or "(no output captured, check the console below)" console_text = render_output(output_chunks) or "(waiting for output...)" full_text = b"".join(output_chunks).decode("utf-8", errors="replace") @@ -468,7 +527,16 @@ def _( rc_policy_blocked = "Remote Control is disabled" in full_text signed_in = "Login successful" in full_text session_ended = session_state["ended"] - flow_done = bool(rc_session_url) or rc_policy_blocked or session_ended + + # Once the session URL appears, keep refreshing a few more ticks so the rest + # of the Remote Control banner (the "how to connect" instructions) finishes + # printing, then snapshot it into feedback_store. Stored there, it survives + # every later re-render instead of flashing once and vanishing. + if rc_session_url: + feedback_store["rc_ticks"] = feedback_store.get("rc_ticks", 0) + 1 + feedback_store["remote_control_output"] = render_output(output_chunks, max_lines=200).strip() + rc_settled = feedback_store.get("rc_ticks", 0) >= 3 + flow_done = (bool(rc_session_url) and rc_settled) or rc_policy_blocked or session_ended if session_ended: status = mo.callout( @@ -481,7 +549,7 @@ def _( elif rc_session_url: status = mo.callout( mo.md( - f"🟢 **Remote Control is live** — [open your session ↗]({rc_session_url}), " + f"🟢 **Remote Control is live.** [Open your session ↗]({rc_session_url}), " f"or find it under **Code** in the Claude mobile app." ), kind="success", @@ -490,7 +558,7 @@ def _( status = mo.callout( mo.md( "❌ **Signed in, but your organization has Remote Control disabled.** " - "On Team and Enterprise plans it's off by default — an Owner must enable the " + "On Team and Enterprise plans it's off by default, an Owner must enable the " "**Remote Control** toggle at " "[claude.ai/admin-settings/claude-code](https://claude.ai/admin-settings/claude-code), " "then restart this step." @@ -499,7 +567,7 @@ def _( ) elif signed_in: status = mo.callout( - mo.md("✅ **Signed in** — enabling Remote Control automatically, the session link will appear here in a moment..."), + mo.md("✅ **Signed in.** Enabling Remote Control automatically, the session link will appear here in a moment..."), kind="info", ) elif authorize_url: @@ -512,12 +580,26 @@ def _( ) else: status = mo.callout( - mo.md("⏳ Starting sign-in — the login menu is answered automatically; the authorization link will appear here."), + mo.md("⏳ Starting sign-in. The login menu is answered automatically; the authorization link will appear here."), kind="neutral", ) panel_items = [status] - if feedback_store.get("text") and not flow_done: + if feedback_store.get("remote_control_output"): + # Persisted so the "how to connect" text stays put after the panel + # freezes. Previously it flashed once and was gone. + panel_items.append( + mo.callout( + mo.vstack( + [ + mo.md("**Remote Control session details (kept here for reference):**"), + mo.plain_text(feedback_store["remote_control_output"]), + ] + ), + kind="success", + ) + ) + if feedback_store.get("text"): panel_items.append( mo.callout( mo.vstack( @@ -557,7 +639,64 @@ def _( def _(mo): mo.md(r""" --- - ## 5. Try it: have Claude build a live website + ## 5. Use your new remote environment + + With the banner green, the sandbox is now hosting a **Remote Control session** + registered to your claude.ai account. It appears anywhere you're signed in, + marked with a computer icon and a green status dot. The sandbox does the work; + every surface below is just a window into it. You can drive it from all three + at once, messages, subagent progress, and files stay in sync. + + ### From the web (claude.ai/code) + + 1. Click the **session link** in the green banner above, or open + [claude.ai/code](https://claude.ai/code) and pick it out of the list by + name. It shows up as **CoreWeave Sandboxes** (set with `--name` on the + `claude remote-control` command in step 4; change that string to rename it, + or run `/rename` in the session). Online sessions show a computer icon with + a green dot. + 2. Type a prompt. It runs **inside the sandbox**, against the sandbox + filesystem, and `@` autocompletes paths from `/workspace`. + + ### From your phone (the Claude app) + + 1. Install the Claude app for [iOS](https://apps.apple.com/us/app/claude-by-anthropic/id6473753684) + or [Android](https://play.google.com/store/apps/details?id=com.anthropic.claude) + and sign in with the **same** account. + 2. Tap **Code** in the bottom navigation to reach the session list, then open + the session with the green dot. (No app yet? Run `/mobile` in a terminal + Claude Code session for a download QR code.) + 3. Approve tool calls and send follow-ups from anywhere. Ask "notify me when + the build finishes" and a long turn will push to your phone. + + This notebook already started the host process for you: inside the sandbox it + ran `claude remote-control` (server mode), which is what registered the + session. There is nothing extra to run to use *this* sandbox, connect from the + web or phone above. + + /// admonition | `/teleport` runs on *your* machine, not the sandbox + type: warning + + claude.ai/code offers an **Open in terminal** button that copies a + `claude --teleport ` command. Running it does **not** attach your + terminal to the sandbox, it forks the conversation into a **new local session + on your laptop**, seeded with a copy of the transcript. Execution and + filesystem then belong to your laptop (ask for `hostname` and you'll see your + laptop, while claude.ai/code still reports the sandbox). The two sessions + diverge from that point, local work won't appear in the app. Only the + transcript travels; the host machine never does. To keep working *in the + sandbox*, steer it from claude.ai/code or mobile, or open another + `sandbox.shell(...)` into it, don't teleport. + /// + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + --- + ## 6. Try it: have Claude build a live website Remote Control is running, so switch to [claude.ai/code](https://claude.ai/code) (or the **Code** tab in the mobile app), open your session, and paste the @@ -575,13 +714,13 @@ def _(mo, sandbox, session): if sandbox.service_address: site_url = f"http://{sandbox.service_address}" demo_prompt = ( - "Build me a sample website and give me a link to access it. Make it a " - "landing page for the concept of running a W&B Serverless Sandbox as a " - "remote environment for Claude Code. Add some cool design elements and " - "animations, and make sure actual logos are present for both Weights & Biases " - "and Claude. Serve it on port 8080, bound to 0.0.0.0, and keep the " - "server running. Port 8080 on this machine is publicly reachable at " - f"{site_url} — once the server is up, give me that direct link to " + "Build me a sample website and give me a link to access it. Make it a \n" + "landing page for the concept of running a W&B Serverless Sandbox as a \n" + "remote environment for Claude Code. Add some cool design elements and \n" + "animations, and make sure actual logos are present for both Weights & Biases \n" + "and Claude. Serve it on port 8080, bound to 0.0.0.0, and keep the \n" + "server running. Port 8080 on this machine is publicly reachable at \n" + f"{site_url}. Once the server is up, give me that direct link to \n" "access the site." ) demo_out = mo.vstack( @@ -593,7 +732,7 @@ def _(mo, sandbox, session): else: demo_out = mo.callout( mo.md( - "⚠️ This sandbox has no public service address — the runner may not " + "⚠️ This sandbox has no public service address, the runner may not " "support `ingress_mode=\"public\"`. Recreate the sandbox in step 2 " "or ask your W&B admin about ingress support." ), @@ -607,7 +746,7 @@ def _(mo, sandbox, session): def _(mo): mo.md(r""" --- - ## 6. Clean up + ## 7. Clean up The sandbox bills until you stop it or the lifetime expires. Ctrl-C above stops the Remote Control server; the button below calls `sandbox.stop()`. Lost sandboxes: `Sandbox.list(tags=["remote-control"]).result()`. @@ -638,10 +777,9 @@ def _(mo): /// details | Where to next type: info - - [`claude-code-remote-env.py`](./claude-code-remote-env.py) — the terminal - version: attach your real terminal to Claude Code in a sandbox - - [`claude-remote-control-sandbox.py`](./claude-remote-control-sandbox.py) — - the compact script this notebook explains + - [`claude-remote-control-script.py`](https://github.com/coreweave/reference-architecture/blob/main/sandboxes/claude-remote-control-script.py): + the compact terminal version of this notebook — attach your real terminal + to a PTY in the sandbox and sign in from there - [Remote Control docs](https://code.claude.com/docs/en/remote-control) - [Sandbox environments compared](https://code.claude.com/docs/en/sandbox-environments) ///