From fc9ae4e201ba2656cabfdb7fdd9c982f678d2511 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 10 Aug 2026 08:00:13 +0200 Subject: [PATCH 1/3] refact: google colab --- README.md | 36 ++- code_sandboxes/__init__.py | 14 +- code_sandboxes/__version__.py | 2 +- code_sandboxes/base.py | 10 +- code_sandboxes/cli.py | 11 +- code_sandboxes/colab.py | 216 +------------- code_sandboxes/colab_sandbox.py | 266 +---------------- code_sandboxes/google_colab.py | 213 ++++++++++++++ code_sandboxes/google_colab_sandbox.py | 273 ++++++++++++++++++ code_sandboxes/models.py | 1 + docs/docs/api-reference/index.mdx | 10 +- docs/docs/cli/index.mdx | 4 +- docs/docs/comparison/index.mdx | 2 +- docs/docs/examples/index.mdx | 4 +- docs/docs/index.mdx | 2 +- docs/docs/sandboxes/google-colab.mdx | 22 +- docs/docs/sandboxes/index.mdx | 6 +- examples/README.md | 10 +- examples/exec/Makefile | 8 +- ...ple.py => google_colab_sandbox_example.py} | 2 +- examples/repl/Makefile | 8 +- ...ple.py => google_colab_sandbox_example.py} | 2 +- pyproject.toml | 1 + tests/test_cli_repl.py | 8 +- tests/test_factory.py | 8 +- tests/{test_colab.py => test_google_colab.py} | 26 +- ....py => test_modal_google_colab_sandbox.py} | 4 +- tests/test_models.py | 1 + 28 files changed, 613 insertions(+), 557 deletions(-) create mode 100644 code_sandboxes/google_colab.py create mode 100644 code_sandboxes/google_colab_sandbox.py rename examples/exec/{colab_sandbox_example.py => google_colab_sandbox_example.py} (97%) rename examples/repl/{colab_sandbox_example.py => google_colab_sandbox_example.py} (96%) rename tests/{test_colab.py => test_google_colab.py} (79%) rename tests/{test_modal_colab_sandbox.py => test_modal_google_colab_sandbox.py} (99%) diff --git a/README.md b/README.md index bd5bf13..7152dca 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,16 @@ Code Sandboxes (`code_sandboxes`) is a Python package for running code in isolat Canonical variant names: -- `jupyter` +- `datalayer` - `docker` - `eval` -- `monty` +- `google_colab` +- `jupyter` - `kaggle` -- `colab` - `modal` -- `datalayer` +- `monty` + +CLI also accepts the alias `google-colab`. ## Documentation @@ -70,9 +72,14 @@ with Sandbox.create( print(sandbox.run_code("x + 2").text) # 42 ``` -### CLI REPL: `kaggle` variant +### Kaggle -Kaggle REPL supports both interactive runtime mode and credential-based batch mode. +Kaggle supports both batch execution and interactive connections through the +`kaggle` sandbox. Install its optional dependency first: + +```bash +pip install "code-sandboxes[kaggle]" +``` Required credentials for batch mode: @@ -90,15 +97,6 @@ export KAGGLE_API_KEY="" sandbox repl --variant kaggle ``` -### Kaggle - -Kaggle supports both batch execution and interactive connections through the -`kaggle` sandbox. Install its optional dependency first: - -```bash -pip install "code-sandboxes[kaggle]" -``` - For batch execution, configure Kaggle credentials and create the sandbox without a runtime URL: @@ -148,17 +146,17 @@ directly to the sandbox: ```python from code_sandboxes import Sandbox -with Sandbox.create(variant="colab", channels_url=channels_url) as sandbox: +with Sandbox.create(variant="google_colab", channels_url=channels_url) as sandbox: print(sandbox.run_code("x = 1 + 1; print(x)").stdout) ``` The lower-level client and parser are owned by Code Sandboxes as well: ```python -from code_sandboxes import ColabKernelClient, parse_colab_channels_url +from code_sandboxes import GoogleColabKernelClient, parse_google_colab_channels_url -server_url, kernel_id, proxy_token = parse_colab_channels_url(channels_url) -with ColabKernelClient.from_channels_url(channels_url) as kernel: +server_url, kernel_id, proxy_token = parse_google_colab_channels_url(channels_url) +with GoogleColabKernelClient.from_channels_url(channels_url) as kernel: print(kernel.execute("print('hello from colab')")) ``` diff --git a/code_sandboxes/__init__.py b/code_sandboxes/__init__.py index 550fcf5..ad0e6dd 100644 --- a/code_sandboxes/__init__.py +++ b/code_sandboxes/__init__.py @@ -15,7 +15,7 @@ - DockerSandbox: Docker container based, good isolation - JupyterSandbox: Jupyter Server with persistent kernel state - DatalayerSandbox: Cloud-based Datalayer runtime, full isolation - - ColabSandbox: Google Colab runtime, connects to an assigned kernel + - GoogleColabSandbox: Google Colab runtime, connects to an assigned kernel - KaggleSandbox: Kaggle runtime, connects to an interactive notebook kernel Cloud container sandboxes: @@ -59,8 +59,13 @@ from .base import Sandbox from .client import CodeExecutionOutcome, CodeSandboxClient, execution_result_to_reply -from .colab import ColabKernelClient, parse_colab_channels_url -from .colab_sandbox import ColabSandbox +from .google_colab import ( + ColabKernelClient, + GoogleColabKernelClient, + parse_colab_channels_url, + parse_google_colab_channels_url, +) +from .google_colab_sandbox import ColabSandbox, GoogleColabSandbox from .commands import CommandResult, ProcessHandle, SandboxCommands from .datalayer_sandbox import DatalayerSandbox from .docker_sandbox import DockerSandbox @@ -134,6 +139,8 @@ "FileType", "FileWatchEvent", "FileWatchEventType", + "GoogleColabKernelClient", + "GoogleColabSandbox", "GPUType", "ISandboxClient", "JupyterSandbox", @@ -178,5 +185,6 @@ "VariableNotFoundError", "execution_result_to_reply", "parse_colab_channels_url", + "parse_google_colab_channels_url", "parse_kaggle_channels_url", ] diff --git a/code_sandboxes/__version__.py b/code_sandboxes/__version__.py index 5145413..711c2f6 100644 --- a/code_sandboxes/__version__.py +++ b/code_sandboxes/__version__.py @@ -3,4 +3,4 @@ """Code Sandboxes.""" -__version__ = "1.0.2" +__version__ = "1.0.3" diff --git a/code_sandboxes/base.py b/code_sandboxes/base.py index f589fc5..e164f63 100644 --- a/code_sandboxes/base.py +++ b/code_sandboxes/base.py @@ -253,6 +253,8 @@ def create( # noqa: C901 from .eval_sandbox import EvalSandbox variant_value = variant.value if isinstance(variant, SandboxVariant) else variant + if variant_value in ("colab", "google-colab"): + variant_value = "google_colab" if variant_value == "eval": sandbox = EvalSandbox(config=config, **kwargs) @@ -269,10 +271,10 @@ def create( # noqa: C901 from .datalayer_sandbox import DatalayerSandbox sandbox = DatalayerSandbox(config=config, **kwargs) - elif variant_value == "colab": - from .colab_sandbox import ColabSandbox + elif variant_value == "google_colab": + from .google_colab_sandbox import GoogleColabSandbox - sandbox = ColabSandbox(config=config, **kwargs) + sandbox = GoogleColabSandbox(config=config, **kwargs) elif variant_value == "kaggle": from .kaggle_sandbox import KaggleSandbox @@ -289,7 +291,7 @@ def create( # noqa: C901 raise ValueError( f"Unknown sandbox variant: {variant}. " "Supported variants: eval, docker, jupyter, " - "datalayer, colab, kaggle, monty, modal" + "datalayer, google_colab, google-colab, colab, kaggle, monty, modal" ) # Set tags if provided diff --git a/code_sandboxes/cli.py b/code_sandboxes/cli.py index 015d7f6..ef67fe9 100644 --- a/code_sandboxes/cli.py +++ b/code_sandboxes/cli.py @@ -22,6 +22,8 @@ "eval", "monty", "colab", + "google_colab", + "google-colab", "kaggle", "modal", "datalayer", @@ -80,6 +82,8 @@ def _resolve_variant(variant: str | None) -> str: f"Unsupported variant: {selected}. Supported values: " + ", ".join(sorted(_SUPPORTED_REPL_VARIANTS)) ) + if selected in {"colab", "google-colab"}: + return "google_colab" return selected @@ -98,7 +102,7 @@ def _resolve_variant_kwargs( # Match `jupyter console` behavior by launching local Jupyter on random port. kwargs["port"] = 0 - if variant == "colab": + if variant == "google_colab": kwargs["server_url"] = server_url or typer.prompt("Colab runtime URL (RUNTIME_URL)") kwargs["kernel_id"] = kernel_id or typer.prompt("Colab kernel id (RUNTIME_ID)") kwargs["proxy_token"] = proxy_token or typer.prompt( @@ -208,7 +212,10 @@ def repl( None, "--variant", "-v", - help="Sandbox variant (jupyter, docker, eval, monty, colab, kaggle, modal, datalayer).", + help=( + "Sandbox variant (jupyter, docker, eval, monty, " + "google_colab/google-colab, kaggle, modal, datalayer)." + ), ), timeout: float = typer.Option(60.0, help="Default code execution timeout (seconds)."), environment: str | None = typer.Option( diff --git a/code_sandboxes/colab.py b/code_sandboxes/colab.py index 35f86e5..e102ee8 100644 --- a/code_sandboxes/colab.py +++ b/code_sandboxes/colab.py @@ -2,207 +2,15 @@ # # BSD 3-Clause License -"""Google Colab kernel client. - -This module provides :class:`ColabKernelClient`, a thin specialization of -:class:`~jupyter_kernel_client.client.JupyterKernelClient` that connects to an -**already-running** Google Colab kernel. - -A Colab runtime is reached through a per-session proxy. Compared to a vanilla -Jupyter Server connection, it requires: - -* a ``colab-runtime-proxy-token`` query parameter on the websocket URL, and -* the ``X-Colab-Client-Agent`` / ``X-Colab-Runtime-Proxy-Token`` HTTP headers on - both the REST and websocket requests. - -.. note:: - This client **reuses an existing kernel**; it does not create a Colab runtime - from scratch. Consumer Colab has no public API to provision a runtime from a - standalone process (authentication lives in the browser session). Start a - runtime from the Colab UI, then connect to it here using the values taken - from the websocket *channels* URL. - -The ``server_url``, ``kernel_id`` and ``proxy_token`` are the parts of the -websocket *channels* URL that Colab's own frontend uses to reach your assigned -runtime, for example:: - - wss:///api/kernels//channels?session_id=<...>&colab-runtime-proxy-token=&colab-client-agent=web - -Use :func:`parse_colab_channels_url` (or -:meth:`ColabKernelClient.from_channels_url`) to turn that URL into the -``server_url``, ``kernel_id`` and ``proxy_token`` expected by the client. - -Example: - >>> from code_sandboxes import ColabKernelClient - >>> kernel = ColabKernelClient( - ... server_url="https://", - ... kernel_id="", - ... proxy_token="", - ... ) - >>> kernel.start() - >>> reply = kernel.execute("print('hey')") - >>> print(reply) - >>> kernel.stop(shutdown_kernel=False) # disconnect only -""" - -from __future__ import annotations - -import logging -import re -import typing as t -from urllib.parse import parse_qs, urlsplit - -from jupyter_kernel_client.client import JupyterKernelClient -from jupyter_kernel_client.wsclient import JupyterSubprotocol - -#: HTTP header identifying the client agent to the Colab proxy. -COLAB_CLIENT_AGENT_HEADER = "X-Colab-Client-Agent" -#: HTTP header carrying the Colab runtime proxy token. -COLAB_RUNTIME_PROXY_TOKEN_HEADER = "X-Colab-Runtime-Proxy-Token" # noqa: S105 -#: Websocket query parameter carrying the Colab runtime proxy token. -COLAB_RUNTIME_PROXY_TOKEN_PARAM = "colab-runtime-proxy-token" # noqa: S105 -#: Default value advertised through :data:`COLAB_CLIENT_AGENT_HEADER`. -DEFAULT_COLAB_CLIENT_AGENT = "code-sandboxes" - -#: Regular expression extracting the kernel id from a Colab channels URL. -_COLAB_KERNEL_RE = re.compile(r"/api/kernels/([^/]+)/channels", re.IGNORECASE) - - -def parse_colab_channels_url(channels_url: str) -> tuple[str, str, str]: - """Extract ``server_url``, ``kernel_id`` and ``proxy_token`` from a URL. - - Parses the websocket *channels* URL of a running Colab kernel session, as - seen in the browser network tab. - - Args: - channels_url: The websocket *channels* URL of a running Colab kernel - session, e.g. - ``wss:///api/kernels//channels?...&colab-runtime-proxy-token=``. - - Returns: - A ``(server_url, kernel_id, proxy_token)`` tuple where ``server_url`` is - the HTTP(S) base *before* ``/api/kernels``, ``kernel_id`` is the kernel - identifier, and ``proxy_token`` is the Colab runtime proxy token. - - Raises: - ValueError: If the URL does not look like a Colab channels URL or is - missing the proxy token. - """ - split = urlsplit(channels_url) - if not split.netloc: - raise ValueError( - f"Could not parse a Colab channels URL from: {channels_url!r}. " - "Expected a websocket URL of the form " - "'wss:///api/kernels//channels?...&colab-runtime-proxy-token='." - ) - - scheme = "https" if split.scheme.lower() in ("wss", "https") else "http" - - marker = "/api/kernels/" - idx = split.path.find(marker) - if idx == -1: - raise ValueError( - f"Could not find '/api/kernels/' in: {channels_url!r}. " - "Expected the URL to contain 'api/kernels//channels'." - ) - prefix = split.path[:idx] - server_url = f"{scheme}://{split.netloc}{prefix}" - - kernel_match = _COLAB_KERNEL_RE.search(split.path) - if kernel_match is None: - raise ValueError( - f"Could not parse a Colab kernel id from: {channels_url!r}. " - "Expected the URL to contain 'api/kernels//channels'." - ) - kernel_id = kernel_match.group(1) - - query = parse_qs(split.query) - proxy_tokens = query.get(COLAB_RUNTIME_PROXY_TOKEN_PARAM) - if not proxy_tokens or not proxy_tokens[0]: - raise ValueError( - f"Could not find the '{COLAB_RUNTIME_PROXY_TOKEN_PARAM}' query " - f"parameter in: {channels_url!r}." - ) - proxy_token = proxy_tokens[0] - - return server_url, kernel_id, proxy_token - - -class ColabKernelClient(JupyterKernelClient): - """Kernel client connected to an existing Google Colab runtime. - - This client connects to a kernel that is **already running** on a Colab - runtime. It does not create a runtime from scratch. - - Args: - server_url: The Colab runtime proxy URL (the HTTP(S) base *before* - ``/api/kernels``, derived from the session's channels URL). - proxy_token: The Colab runtime proxy token (the - ``colab-runtime-proxy-token`` value from the channels URL). - kernel_id: The identifier of the existing Colab kernel to connect to. - client_agent: Value sent through the ``X-Colab-Client-Agent`` header. - subprotocol: Websocket subprotocol to use; Colab uses the default one. - log: Optional logger. - **kwargs: Forwarded to :class:`~jupyter_kernel_client.client.JupyterKernelClient`. - ``client_kwargs`` and ``headers`` may be provided and are merged with - the Colab-specific values. - """ - - def __init__( - self, - server_url: str, - proxy_token: str, - *, - kernel_id: str, - client_agent: str = DEFAULT_COLAB_CLIENT_AGENT, - subprotocol: JupyterSubprotocol | None = JupyterSubprotocol.DEFAULT, - log: logging.Logger | None = None, - **kwargs: t.Any, - ) -> None: - client_kwargs: dict[str, t.Any] = dict(kwargs.pop("client_kwargs", None) or {}) - client_kwargs.setdefault("subprotocol", subprotocol) - - extra_params: dict[str, t.Any] = dict(client_kwargs.get("extra_params", None) or {}) - extra_params[COLAB_RUNTIME_PROXY_TOKEN_PARAM] = proxy_token - client_kwargs["extra_params"] = extra_params - - headers: dict[str, t.Any] = dict(kwargs.pop("headers", None) or {}) - headers.setdefault(COLAB_CLIENT_AGENT_HEADER, client_agent) - headers[COLAB_RUNTIME_PROXY_TOKEN_HEADER] = proxy_token - - # Colab authenticates through the proxy token, not the Jupyter token. - # Drop any provided Jupyter token to avoid sending an Authorization - # header and a `token=` query parameter that Colab does not use. - kwargs.pop("token", None) - - super().__init__( - kernel_id=kernel_id, - log=log, - server_url=server_url, - token=None, - client_kwargs=client_kwargs, - headers=headers, - **kwargs, - ) - - @classmethod - def from_channels_url( - cls, - channels_url: str, - **kwargs: t.Any, - ) -> ColabKernelClient: - """Create a client from a Colab kernel session *channels* URL. - - Args: - channels_url: The websocket *channels* URL of a running Colab kernel - session (see :func:`parse_colab_channels_url`). - **kwargs: Forwarded to :class:`ColabKernelClient`. Values provided - here override those parsed from the URL. - - Returns: - A configured :class:`ColabKernelClient` instance. - """ - server_url, kernel_id, proxy_token = parse_colab_channels_url(channels_url) - kwargs.setdefault("kernel_id", kernel_id) - kwargs.setdefault("proxy_token", proxy_token) - return cls(server_url=server_url, **kwargs) +"""Backward-compatible imports for Google Colab client symbols.""" + +from .google_colab import ( # noqa: F401 + COLAB_CLIENT_AGENT_HEADER, + COLAB_RUNTIME_PROXY_TOKEN_HEADER, + COLAB_RUNTIME_PROXY_TOKEN_PARAM, + DEFAULT_COLAB_CLIENT_AGENT, + ColabKernelClient, + GoogleColabKernelClient, + parse_colab_channels_url, + parse_google_colab_channels_url, +) diff --git a/code_sandboxes/colab_sandbox.py b/code_sandboxes/colab_sandbox.py index f9bb934..b28fb24 100644 --- a/code_sandboxes/colab_sandbox.py +++ b/code_sandboxes/colab_sandbox.py @@ -2,268 +2,6 @@ # # BSD 3-Clause License -"""Google Colab sandbox implementation. +"""Backward-compatible imports for Google Colab sandbox symbols.""" -This sandbox connects to an existing Google Colab runtime and executes code in -its kernel using :class:`code_sandboxes.colab.ColabKernelClient`. - -Unlike the Jupyter/Docker sandboxes, this sandbox does **not** provision a -runtime: a Colab runtime must already be running in a browser session. Reuse it -with either explicit ``server_url`` / ``kernel_id`` / ``proxy_token`` values or -by passing a Colab WebSocket ``channels_url``. -""" - -from __future__ import annotations - -import logging -import time -import uuid - -from .base import Sandbox -from .colab import ColabKernelClient, parse_colab_channels_url -from .exceptions import SandboxConfigurationError, SandboxNotStartedError -from .interfaces import ISandboxClient -from .models import ( - CodeError, - Context, - ExecutionResult, - Logs, - OutputHandler, - OutputMessage, - Result, - SandboxConfig, - SandboxEnvironment, - SandboxInfo, - SandboxStatus, -) - -logger = logging.getLogger(__name__) - - -class ColabSandbox(Sandbox): - """Sandbox backed by a Google Colab runtime. - - Args: - config: Optional sandbox configuration. - server_url: The Colab runtime proxy URL. - kernel_id: The Colab kernel identifier to connect to. - proxy_token: The Colab runtime proxy token. - channels_url: Optional Colab channels URL to parse `server_url`, - `kernel_id`, and `proxy_token` from. - client_agent: Value advertised through the ``X-Colab-Client-Agent`` header. - """ - - def __init__( - self, - config: SandboxConfig | None = None, - server_url: str | None = None, - kernel_id: str | None = None, - proxy_token: str | None = None, - channels_url: str | None = None, - client_agent: str = "code-sandboxes", - **kwargs, - ): - super().__init__(config) - # Allow configuration via SandboxConfig extras as a fallback. - extras = getattr(self.config, "model_extra", None) or {} - self._server_url = server_url or extras.get("server_url") - self._kernel_id = kernel_id or extras.get("kernel_id") - self._proxy_token = proxy_token or extras.get("proxy_token") - self._channels_url = channels_url or extras.get("channels_url") - self._client_agent = client_agent - self._client = None - self._sandbox_id = str(uuid.uuid4()) - self._extra_kwargs = kwargs - - @classmethod - def list_environments(cls) -> list[SandboxEnvironment]: - return [ - SandboxEnvironment( - name="colab", - title="Google Colab", - language="python", - owner="google", - visibility="cloud", - burning_rate=0.0, - metadata={"variant": "colab"}, - ) - ] - - def start(self) -> None: - if self._started: - return - - if self._channels_url and ( - not self._server_url or not self._kernel_id or not self._proxy_token - ): - parsed_server_url, parsed_kernel_id, parsed_proxy_token = parse_colab_channels_url( - self._channels_url - ) - self._server_url = self._server_url or parsed_server_url - self._kernel_id = self._kernel_id or parsed_kernel_id - self._proxy_token = self._proxy_token or parsed_proxy_token - - if not self._server_url or not self._kernel_id or not self._proxy_token: - raise SandboxConfigurationError( - "ColabSandbox requires 'server_url', 'kernel_id', and 'proxy_token'. " - "Provide them directly, or pass 'channels_url' from an active Colab session." - ) - - self._client = ColabKernelClient( - server_url=self._server_url, - kernel_id=self._kernel_id, - proxy_token=self._proxy_token, - client_agent=self._client_agent, - ) - self._client.start() - - self._default_context = self.create_context("default") - self._info = SandboxInfo( - id=self._sandbox_id, - variant="colab", - status=SandboxStatus.RUNNING, - created_at=time.time(), - name=self.config.name, - metadata={"server_url": self._server_url, "kernel_id": self._client.id}, - config=self.config, - ) - self._started = True - - @property - def kernel_client(self) -> ISandboxClient | None: - """The underlying Colab kernel client, if started.""" - return self._client - - def _setup_tool_caller(self) -> None: - """Keep tool calling on the client side for Colab sandboxes.""" - return - - def stop(self) -> None: - if not self._started: - return - if self._client is not None: - try: - # Do not shut down the Colab kernel; we only disconnect. - self._client.stop(shutdown_kernel=False) - except Exception: - logger.debug("Ignoring error while stopping Colab client", exc_info=True) - self._client = None - self._started = False - if self._info: - self._info.status = SandboxStatus.STOPPED - - def run_code( # noqa: C901 - self, - code: str, - language: str = "python", - context: Context | None = None, - on_stdout: OutputHandler[OutputMessage] | None = None, - on_stderr: OutputHandler[OutputMessage] | None = None, - on_result: OutputHandler[Result] | None = None, - on_error: OutputHandler[CodeError] | None = None, - envs: dict[str, str] | None = None, - timeout: float | None = None, - ) -> ExecutionResult: - if not self._started or self._client is None: - raise SandboxNotStartedError() - - if language != "python": - raise ValueError(f"ColabSandbox only supports Python, got: {language}") - - started_at = time.time() - self._interrupt_requested.clear() - self._executing_event.set() - - if envs: - env_code = "\n".join(f"import os; os.environ[{k!r}] = {v!r}" for k, v in envs.items()) - code = f"{env_code}\n{code}" - - try: - reply = self._client.execute(code, timeout=timeout or self.config.timeout) - except Exception as e: - self._executing_event.clear() - was_interrupted = self._interrupt_requested.is_set() - self._interrupt_requested.clear() - return ExecutionResult( - execution_ok=False, - execution_error=f"Failed to execute code: {e}" if not was_interrupted else None, - started_at=started_at, - completed_at=time.time(), - context_id=context.id if context else "default", - interrupted=was_interrupted, - ) - - stdout_messages: list[OutputMessage] = [] - stderr_messages: list[OutputMessage] = [] - results: list[Result] = [] - code_error: CodeError | None = None - exit_code: int | None = None - - current_time = time.time() - for output in reply.get("outputs", []): - output_type = output.get("output_type") - if output_type == "stream": - name = output.get("name") - text = output.get("text", "") - for line in text.splitlines(): - msg = OutputMessage(line=line, timestamp=current_time, error=name == "stderr") - if name == "stderr": - stderr_messages.append(msg) - if on_stderr: - on_stderr(msg) - else: - stdout_messages.append(msg) - if on_stdout: - on_stdout(msg) - elif output_type in ("execute_result", "display_data"): - result = Result( - data=output.get("data", {}), - is_main_result=output_type == "execute_result", - extra=output.get("metadata", {}), - ) - results.append(result) - if on_result: - on_result(result) - elif output_type == "error": - ename = output.get("ename", "Error") - evalue = output.get("evalue", "") - if ename == "SystemExit": - try: - exit_code = int(evalue) if evalue else 0 - except (ValueError, TypeError): - exit_code = 1 if evalue else 0 - else: - code_error = CodeError( - name=ename, - value=evalue, - traceback="\n".join(output.get("traceback", [])), - ) - if on_error: - on_error(code_error) - - self._executing_event.clear() - was_interrupted = self._interrupt_requested.is_set() - self._interrupt_requested.clear() - - return ExecutionResult( - results=results, - logs=Logs(stdout=stdout_messages, stderr=stderr_messages), - execution_ok=True, - code_error=code_error, - exit_code=exit_code, - execution_count=reply.get("execution_count", 0), - context_id=context.id if context else "default", - started_at=started_at, - completed_at=time.time(), - interrupted=was_interrupted, - ) - - def _get_internal_variable(self, name: str, context: Context | None = None): - if not self._started or self._client is None: - raise SandboxNotStartedError() - return self._client.get_variable(name) - - def _set_internal_variable(self, name: str, value, context: Context | None = None) -> None: - if not self._started or self._client is None: - raise SandboxNotStartedError() - self._client.set_variable(name, value) +from .google_colab_sandbox import ColabSandbox, GoogleColabSandbox # noqa: F401 diff --git a/code_sandboxes/google_colab.py b/code_sandboxes/google_colab.py new file mode 100644 index 0000000..efaf535 --- /dev/null +++ b/code_sandboxes/google_colab.py @@ -0,0 +1,213 @@ +# Copyright (c) 2025-2026 Datalayer, Inc. +# +# BSD 3-Clause License + +"""Google Colab kernel client. + +This module provides :class:`GoogleColabKernelClient`, a thin specialization of +:class:`~jupyter_kernel_client.client.JupyterKernelClient` that connects to an +**already-running** Google Colab kernel. + +A Colab runtime is reached through a per-session proxy. Compared to a vanilla +Jupyter Server connection, it requires: + +* a ``colab-runtime-proxy-token`` query parameter on the websocket URL, and +* the ``X-Colab-Client-Agent`` / ``X-Colab-Runtime-Proxy-Token`` HTTP headers on + both the REST and websocket requests. + +.. note:: + This client **reuses an existing kernel**; it does not create a Colab runtime + from scratch. Consumer Colab has no public API to provision a runtime from a + standalone process (authentication lives in the browser session). Start a + runtime from the Colab UI, then connect to it here using the values taken + from the websocket *channels* URL. + +The ``server_url``, ``kernel_id`` and ``proxy_token`` are the parts of the +websocket *channels* URL that Colab's own frontend uses to reach your assigned +runtime, for example:: + + wss:///api/kernels//channels?session_id=<...>&colab-runtime-proxy-token=&colab-client-agent=web + +Use :func:`parse_google_colab_channels_url` (or +:meth:`GoogleColabKernelClient.from_channels_url`) to turn that URL into the +``server_url``, ``kernel_id`` and ``proxy_token`` expected by the client. + +Example: + >>> from code_sandboxes import GoogleColabKernelClient + >>> kernel = GoogleColabKernelClient( + ... server_url="https://", + ... kernel_id="", + ... proxy_token="", + ... ) + >>> kernel.start() + >>> reply = kernel.execute("print('hey')") + >>> print(reply) + >>> kernel.stop(shutdown_kernel=False) # disconnect only +""" + +from __future__ import annotations + +import logging +import re +import typing as t +from urllib.parse import parse_qs, urlsplit + +from jupyter_kernel_client.client import JupyterKernelClient +from jupyter_kernel_client.wsclient import JupyterSubprotocol + +#: HTTP header identifying the client agent to the Colab proxy. +COLAB_CLIENT_AGENT_HEADER = "X-Colab-Client-Agent" +#: HTTP header carrying the Colab runtime proxy token. +COLAB_RUNTIME_PROXY_TOKEN_HEADER = "X-Colab-Runtime-Proxy-Token" # noqa: S105 +#: Websocket query parameter carrying the Colab runtime proxy token. +COLAB_RUNTIME_PROXY_TOKEN_PARAM = "colab-runtime-proxy-token" # noqa: S105 +#: Default value advertised through :data:`COLAB_CLIENT_AGENT_HEADER`. +DEFAULT_COLAB_CLIENT_AGENT = "code-sandboxes" + +#: Regular expression extracting the kernel id from a Colab channels URL. +_COLAB_KERNEL_RE = re.compile(r"/api/kernels/([^/]+)/channels", re.IGNORECASE) + + +def parse_google_colab_channels_url(channels_url: str) -> tuple[str, str, str]: + """Extract ``server_url``, ``kernel_id`` and ``proxy_token`` from a URL. + + Parses the websocket *channels* URL of a running Colab kernel session, as + seen in the browser network tab. + + Args: + channels_url: The websocket *channels* URL of a running Colab kernel + session, e.g. + ``wss:///api/kernels//channels?...&colab-runtime-proxy-token=``. + + Returns: + A ``(server_url, kernel_id, proxy_token)`` tuple where ``server_url`` is + the HTTP(S) base *before* ``/api/kernels``, ``kernel_id`` is the kernel + identifier, and ``proxy_token`` is the Colab runtime proxy token. + + Raises: + ValueError: If the URL does not look like a Colab channels URL or is + missing the proxy token. + """ + split = urlsplit(channels_url) + if not split.netloc: + raise ValueError( + f"Could not parse a Colab channels URL from: {channels_url!r}. " + "Expected a websocket URL of the form " + "'wss:///api/kernels//channels?...&colab-runtime-proxy-token='." + ) + + scheme = "https" if split.scheme.lower() in ("wss", "https") else "http" + + marker = "/api/kernels/" + idx = split.path.find(marker) + if idx == -1: + raise ValueError( + f"Could not find '/api/kernels/' in: {channels_url!r}. " + "Expected the URL to contain 'api/kernels//channels'." + ) + prefix = split.path[:idx] + server_url = f"{scheme}://{split.netloc}{prefix}" + + kernel_match = _COLAB_KERNEL_RE.search(split.path) + if kernel_match is None: + raise ValueError( + f"Could not parse a Colab kernel id from: {channels_url!r}. " + "Expected the URL to contain 'api/kernels//channels'." + ) + kernel_id = kernel_match.group(1) + + query = parse_qs(split.query) + proxy_tokens = query.get(COLAB_RUNTIME_PROXY_TOKEN_PARAM) + if not proxy_tokens or not proxy_tokens[0]: + raise ValueError( + f"Could not find the '{COLAB_RUNTIME_PROXY_TOKEN_PARAM}' query " + f"parameter in: {channels_url!r}." + ) + proxy_token = proxy_tokens[0] + + return server_url, kernel_id, proxy_token + + +class GoogleColabKernelClient(JupyterKernelClient): + """Kernel client connected to an existing Google Colab runtime. + + This client connects to a kernel that is **already running** on a Colab + runtime. It does not create a runtime from scratch. + + Args: + server_url: The Colab runtime proxy URL (the HTTP(S) base *before* + ``/api/kernels``, derived from the session's channels URL). + proxy_token: The Colab runtime proxy token (the + ``colab-runtime-proxy-token`` value from the channels URL). + kernel_id: The identifier of the existing Colab kernel to connect to. + client_agent: Value sent through the ``X-Colab-Client-Agent`` header. + subprotocol: Websocket subprotocol to use; Colab uses the default one. + log: Optional logger. + **kwargs: Forwarded to :class:`~jupyter_kernel_client.client.JupyterKernelClient`. + ``client_kwargs`` and ``headers`` may be provided and are merged with + the Colab-specific values. + """ + + def __init__( + self, + server_url: str, + proxy_token: str, + *, + kernel_id: str, + client_agent: str = DEFAULT_COLAB_CLIENT_AGENT, + subprotocol: JupyterSubprotocol | None = JupyterSubprotocol.DEFAULT, + log: logging.Logger | None = None, + **kwargs: t.Any, + ) -> None: + client_kwargs: dict[str, t.Any] = dict(kwargs.pop("client_kwargs", None) or {}) + client_kwargs.setdefault("subprotocol", subprotocol) + + extra_params: dict[str, t.Any] = dict(client_kwargs.get("extra_params", None) or {}) + extra_params[COLAB_RUNTIME_PROXY_TOKEN_PARAM] = proxy_token + client_kwargs["extra_params"] = extra_params + + headers: dict[str, t.Any] = dict(kwargs.pop("headers", None) or {}) + headers.setdefault(COLAB_CLIENT_AGENT_HEADER, client_agent) + headers[COLAB_RUNTIME_PROXY_TOKEN_HEADER] = proxy_token + + # Colab authenticates through the proxy token, not the Jupyter token. + # Drop any provided Jupyter token to avoid sending an Authorization + # header and a `token=` query parameter that Colab does not use. + kwargs.pop("token", None) + + super().__init__( + kernel_id=kernel_id, + log=log, + server_url=server_url, + token=None, + client_kwargs=client_kwargs, + headers=headers, + **kwargs, + ) + + @classmethod + def from_channels_url( + cls, + channels_url: str, + **kwargs: t.Any, + ) -> GoogleColabKernelClient: + """Create a client from a Colab kernel session *channels* URL. + + Args: + channels_url: The websocket *channels* URL of a running Colab kernel + session (see :func:`parse_google_colab_channels_url`). + **kwargs: Forwarded to :class:`GoogleColabKernelClient`. Values provided + here override those parsed from the URL. + + Returns: + A configured :class:`GoogleColabKernelClient` instance. + """ + server_url, kernel_id, proxy_token = parse_google_colab_channels_url(channels_url) + kwargs.setdefault("kernel_id", kernel_id) + kwargs.setdefault("proxy_token", proxy_token) + return cls(server_url=server_url, **kwargs) + + +# Backward-compatible aliases. +parse_colab_channels_url = parse_google_colab_channels_url +ColabKernelClient = GoogleColabKernelClient diff --git a/code_sandboxes/google_colab_sandbox.py b/code_sandboxes/google_colab_sandbox.py new file mode 100644 index 0000000..feeaf0f --- /dev/null +++ b/code_sandboxes/google_colab_sandbox.py @@ -0,0 +1,273 @@ +# Copyright (c) 2025-2026 Datalayer, Inc. +# +# BSD 3-Clause License + +"""Google Colab sandbox implementation. + +This sandbox connects to an existing Google Colab runtime and executes code in +its kernel using :class:`code_sandboxes.google_colab.GoogleColabKernelClient`. + +Unlike the Jupyter/Docker sandboxes, this sandbox does **not** provision a +runtime: a Colab runtime must already be running in a browser session. Reuse it +with either explicit ``server_url`` / ``kernel_id`` / ``proxy_token`` values or +by passing a Colab WebSocket ``channels_url``. +""" + +from __future__ import annotations + +import logging +import time +import uuid + +from .base import Sandbox +from .google_colab import GoogleColabKernelClient, parse_google_colab_channels_url +from .exceptions import SandboxConfigurationError, SandboxNotStartedError +from .interfaces import ISandboxClient +from .models import ( + CodeError, + Context, + ExecutionResult, + Logs, + OutputHandler, + OutputMessage, + Result, + SandboxConfig, + SandboxEnvironment, + SandboxInfo, + SandboxStatus, +) + +logger = logging.getLogger(__name__) + + +class GoogleColabSandbox(Sandbox): + """Sandbox backed by a Google Colab runtime. + + Args: + config: Optional sandbox configuration. + server_url: The Colab runtime proxy URL. + kernel_id: The Colab kernel identifier to connect to. + proxy_token: The Colab runtime proxy token. + channels_url: Optional Colab channels URL to parse `server_url`, + `kernel_id`, and `proxy_token` from. + client_agent: Value advertised through the ``X-Colab-Client-Agent`` header. + """ + + def __init__( + self, + config: SandboxConfig | None = None, + server_url: str | None = None, + kernel_id: str | None = None, + proxy_token: str | None = None, + channels_url: str | None = None, + client_agent: str = "code-sandboxes", + **kwargs, + ): + super().__init__(config) + # Allow configuration via SandboxConfig extras as a fallback. + extras = getattr(self.config, "model_extra", None) or {} + self._server_url = server_url or extras.get("server_url") + self._kernel_id = kernel_id or extras.get("kernel_id") + self._proxy_token = proxy_token or extras.get("proxy_token") + self._channels_url = channels_url or extras.get("channels_url") + self._client_agent = client_agent + self._client = None + self._sandbox_id = str(uuid.uuid4()) + self._extra_kwargs = kwargs + + @classmethod + def list_environments(cls) -> list[SandboxEnvironment]: + return [ + SandboxEnvironment( + name="google_colab", + title="Google Colab", + language="python", + owner="google", + visibility="cloud", + burning_rate=0.0, + metadata={"variant": "google_colab"}, + ) + ] + + def start(self) -> None: + if self._started: + return + + if self._channels_url and ( + not self._server_url or not self._kernel_id or not self._proxy_token + ): + parsed_server_url, parsed_kernel_id, parsed_proxy_token = parse_google_colab_channels_url( + self._channels_url + ) + self._server_url = self._server_url or parsed_server_url + self._kernel_id = self._kernel_id or parsed_kernel_id + self._proxy_token = self._proxy_token or parsed_proxy_token + + if not self._server_url or not self._kernel_id or not self._proxy_token: + raise SandboxConfigurationError( + "GoogleColabSandbox requires 'server_url', 'kernel_id', and 'proxy_token'. " + "Provide them directly, or pass 'channels_url' from an active Colab session." + ) + + self._client = GoogleColabKernelClient( + server_url=self._server_url, + kernel_id=self._kernel_id, + proxy_token=self._proxy_token, + client_agent=self._client_agent, + ) + self._client.start() + + self._default_context = self.create_context("default") + self._info = SandboxInfo( + id=self._sandbox_id, + variant="google_colab", + status=SandboxStatus.RUNNING, + created_at=time.time(), + name=self.config.name, + metadata={"server_url": self._server_url, "kernel_id": self._client.id}, + config=self.config, + ) + self._started = True + + @property + def kernel_client(self) -> ISandboxClient | None: + """The underlying Colab kernel client, if started.""" + return self._client + + def _setup_tool_caller(self) -> None: + """Keep tool calling on the client side for Colab sandboxes.""" + return + + def stop(self) -> None: + if not self._started: + return + if self._client is not None: + try: + # Do not shut down the Colab kernel; we only disconnect. + self._client.stop(shutdown_kernel=False) + except Exception: + logger.debug("Ignoring error while stopping Colab client", exc_info=True) + self._client = None + self._started = False + if self._info: + self._info.status = SandboxStatus.STOPPED + + def run_code( # noqa: C901 + self, + code: str, + language: str = "python", + context: Context | None = None, + on_stdout: OutputHandler[OutputMessage] | None = None, + on_stderr: OutputHandler[OutputMessage] | None = None, + on_result: OutputHandler[Result] | None = None, + on_error: OutputHandler[CodeError] | None = None, + envs: dict[str, str] | None = None, + timeout: float | None = None, + ) -> ExecutionResult: + if not self._started or self._client is None: + raise SandboxNotStartedError() + + if language != "python": + raise ValueError(f"GoogleColabSandbox only supports Python, got: {language}") + + started_at = time.time() + self._interrupt_requested.clear() + self._executing_event.set() + + if envs: + env_code = "\n".join(f"import os; os.environ[{k!r}] = {v!r}" for k, v in envs.items()) + code = f"{env_code}\n{code}" + + try: + reply = self._client.execute(code, timeout=timeout or self.config.timeout) + except Exception as e: + self._executing_event.clear() + was_interrupted = self._interrupt_requested.is_set() + self._interrupt_requested.clear() + return ExecutionResult( + execution_ok=False, + execution_error=f"Failed to execute code: {e}" if not was_interrupted else None, + started_at=started_at, + completed_at=time.time(), + context_id=context.id if context else "default", + interrupted=was_interrupted, + ) + + stdout_messages: list[OutputMessage] = [] + stderr_messages: list[OutputMessage] = [] + results: list[Result] = [] + code_error: CodeError | None = None + exit_code: int | None = None + + current_time = time.time() + for output in reply.get("outputs", []): + output_type = output.get("output_type") + if output_type == "stream": + name = output.get("name") + text = output.get("text", "") + for line in text.splitlines(): + msg = OutputMessage(line=line, timestamp=current_time, error=name == "stderr") + if name == "stderr": + stderr_messages.append(msg) + if on_stderr: + on_stderr(msg) + else: + stdout_messages.append(msg) + if on_stdout: + on_stdout(msg) + elif output_type in ("execute_result", "display_data"): + result = Result( + data=output.get("data", {}), + is_main_result=output_type == "execute_result", + extra=output.get("metadata", {}), + ) + results.append(result) + if on_result: + on_result(result) + elif output_type == "error": + ename = output.get("ename", "Error") + evalue = output.get("evalue", "") + if ename == "SystemExit": + try: + exit_code = int(evalue) if evalue else 0 + except (ValueError, TypeError): + exit_code = 1 if evalue else 0 + else: + code_error = CodeError( + name=ename, + value=evalue, + traceback="\n".join(output.get("traceback", [])), + ) + if on_error: + on_error(code_error) + + self._executing_event.clear() + was_interrupted = self._interrupt_requested.is_set() + self._interrupt_requested.clear() + + return ExecutionResult( + results=results, + logs=Logs(stdout=stdout_messages, stderr=stderr_messages), + execution_ok=True, + code_error=code_error, + exit_code=exit_code, + execution_count=reply.get("execution_count", 0), + context_id=context.id if context else "default", + started_at=started_at, + completed_at=time.time(), + interrupted=was_interrupted, + ) + + def _get_internal_variable(self, name: str, context: Context | None = None): + if not self._started or self._client is None: + raise SandboxNotStartedError() + return self._client.get_variable(name) + + def _set_internal_variable(self, name: str, value, context: Context | None = None) -> None: + if not self._started or self._client is None: + raise SandboxNotStartedError() + self._client.set_variable(name, value) + + +# Backward-compatible alias. +GoogleColabSandbox = GoogleColabSandbox diff --git a/code_sandboxes/models.py b/code_sandboxes/models.py index 88c57d7..3281d87 100644 --- a/code_sandboxes/models.py +++ b/code_sandboxes/models.py @@ -69,6 +69,7 @@ class SandboxVariant(str, Enum): DOCKER = "docker" JUPYTER = "jupyter" DATALAYER = "datalayer" + GOOGLE_COLAB = "google_colab" COLAB = "colab" KAGGLE = "kaggle" MONTY = "monty" diff --git a/docs/docs/api-reference/index.mdx b/docs/docs/api-reference/index.mdx index 9091ee9..34bbd1e 100644 --- a/docs/docs/api-reference/index.mdx +++ b/docs/docs/api-reference/index.mdx @@ -36,7 +36,7 @@ def create( | Parameter | Type | Description | |-----------|------|-------------| -| `variant` | `str` | Sandbox type: `"eval"`, `"docker"`, `"jupyter"`, `"monty"`, `"kaggle"`, `"colab"`, `"modal"`, or `"datalayer"`. Defaults to `"datalayer"`. | +| `variant` | `str` | Sandbox type: `"eval"`, `"docker"`, `"jupyter"`, `"monty"`, `"kaggle"`, `"google_colab"`, `"modal"`, or `"datalayer"`. Defaults to `"datalayer"`. | | `timeout` | `float` | Execution timeout in seconds | | `environment` | `str` | Runtime environment name | | `gpu` | `str` | GPU type (e.g., `"T4"`, `"A100"`, `"H100"`) | @@ -82,7 +82,7 @@ def list_environments( | Parameter | Type | Description | |-----------|------|-------------| -| `variant` | `str` | Sandbox type: `"eval"`, `"docker"`, `"jupyter"`, `"monty"`, `"kaggle"`, `"colab"`, `"modal"`, or `"datalayer"` | +| `variant` | `str` | Sandbox type: `"eval"`, `"docker"`, `"jupyter"`, `"monty"`, `"kaggle"`, `"google_colab"`, `"modal"`, or `"datalayer"` | | `**kwargs` | `dict` | Variant-specific arguments (e.g., credentials, run URL) | Legacy `local-eval`, `local-docker`, and `local-jupyter` variant names are not supported. @@ -293,11 +293,11 @@ variants. They are exported from `code_sandboxes`: ```python from code_sandboxes import ( - ColabKernelClient, + GoogleColabKernelClient, KaggleExecutionResult, KaggleKernelClient, KaggleKernelExecutor, - parse_colab_channels_url, + parse_google_colab_channels_url, parse_kaggle_channels_url, ) ``` @@ -305,7 +305,7 @@ from code_sandboxes import ( - `KaggleKernelClient` connects to an interactive Kaggle notebook kernel. - `KaggleKernelExecutor` submits and monitors Kaggle batch notebook jobs. - `KaggleExecutionResult` normalizes batch output into a Jupyter-like reply. -- `ColabKernelClient` connects to an already-running Google Colab kernel. +- `GoogleColabKernelClient` connects to an already-running Google Colab kernel. - The parser helpers extract connection details from browser channels URLs. See [Kaggle](/sandboxes/kaggle) and [Google Colab](/sandboxes/google-colab) for diff --git a/docs/docs/cli/index.mdx b/docs/docs/cli/index.mdx index 6ec36b5..8924e69 100644 --- a/docs/docs/cli/index.mdx +++ b/docs/docs/cli/index.mdx @@ -29,7 +29,7 @@ Supported variants: - `eval` - `monty` - `kaggle` -- `colab` +- `google-colab` - `modal` - `datalayer` @@ -39,7 +39,7 @@ Supported variants: - `kaggle`: supports either interactive runtime settings or credential-based batch execution. - `monty`: starts a Monty REPL-backed sandbox. - `modal`: starts a Modal sandbox container. -- `colab`: prompts for runtime URL, kernel ID, and proxy token. +- `google-colab`: prompts for runtime URL, kernel ID, and proxy token. ## Usage diff --git a/docs/docs/comparison/index.mdx b/docs/docs/comparison/index.mdx index e399ea9..401c7bb 100644 --- a/docs/docs/comparison/index.mdx +++ b/docs/docs/comparison/index.mdx @@ -110,7 +110,7 @@ Modal is a serverless platform for running Python code in the cloud. It's design ### Code Sandboxes -Code Sandboxes provides a unified API across all supported variants (`eval`, `monty`, `docker`, `jupyter`, `kaggle`, `colab`, `modal`, `datalayer`), with native Jupyter kernel support. +Code Sandboxes provides a unified API across all supported variants (`eval`, `monty`, `docker`, `jupyter`, `kaggle`, `google-colab`, `modal`, `datalayer`), with native Jupyter kernel support. **Pros:** - Open source and self-hostable diff --git a/docs/docs/examples/index.mdx b/docs/docs/examples/index.mdx index 9d7dd79..b55750e 100644 --- a/docs/docs/examples/index.mdx +++ b/docs/docs/examples/index.mdx @@ -53,10 +53,10 @@ make kaggle ## Colab -- Source: https://github.com/datalayer/code-sandboxes/blob/main/examples/exec/colab_sandbox_example.py +- Source: https://github.com/datalayer/code-sandboxes/blob/main/examples/exec/google_colab_sandbox_example.py ```bash -make colab +make google-colab ``` ## Modal diff --git a/docs/docs/index.mdx b/docs/docs/index.mdx index 3520cd8..2a612ea 100644 --- a/docs/docs/index.mdx +++ b/docs/docs/index.mdx @@ -68,7 +68,7 @@ Code Sandboxes supports these execution variants: | `docker` | Container | Isolated execution | | `jupyter` | Process (Jupyter kernel) | Persistent notebook-style state | | `kaggle` | Managed notebook runtime | Interactive and batch runs | -| `colab` | Managed notebook runtime | Interactive Colab-connected runs | +| `google-colab` | Managed notebook runtime | Interactive Colab-connected runs | | `modal` | Managed container runtime | Ephemeral compute tasks | | `datalayer` | Managed VM/runtime | Production and GPU workloads | diff --git a/docs/docs/sandboxes/google-colab.mdx b/docs/docs/sandboxes/google-colab.mdx index 3caf0fc..d859465 100644 --- a/docs/docs/sandboxes/google-colab.mdx +++ b/docs/docs/sandboxes/google-colab.mdx @@ -6,7 +6,7 @@ title: Google Colab # Google Colab Runs code against a Google Colab runtime. Colab exposes a Jupyter-compatible -kernel behind an authenticating proxy, using the `ColabKernelClient` implemented +kernel behind an authenticating proxy, using the `GoogleColabKernelClient` implemented by Code Sandboxes. - **Requirements:** the base `code-sandboxes` installation. @@ -25,7 +25,7 @@ after the runtime is reassigned or reconnected. from code_sandboxes import Sandbox with Sandbox.create( - variant="colab", + variant="google_colab", server_url="https://8080-m-s-kkb-...-d.us-east1-0.prod.colab.dev", kernel_id="c9bba548-3995-4f26-8e1a-7b8fbb10c578", proxy_token="eyJhbGci....", @@ -39,7 +39,7 @@ Or pass a channels URL directly: ```python with Sandbox.create( - variant="colab", + variant="google_colab", channels_url=( "wss:///api/kernels//channels" "?session_id=<...>&colab-runtime-proxy-token=&colab-client-agent=web" @@ -50,7 +50,7 @@ with Sandbox.create( ## Kernel Client -Use `ColabKernelClient` to connect to a kernel that is already running in Colab. +Use `GoogleColabKernelClient` to connect to a kernel that is already running in Colab. This client reuses an existing Colab runtime; it does not create a Colab runtime from scratch. @@ -66,9 +66,9 @@ All three are available in Colab's channels WebSocket URL. ## Option A: Connect With Explicit Values ```python -from code_sandboxes import ColabKernelClient +from code_sandboxes import GoogleColabKernelClient -kernel = ColabKernelClient( +kernel = GoogleColabKernelClient( server_url="https://", kernel_id="", proxy_token="", @@ -83,14 +83,14 @@ kernel.stop(shutdown_kernel=False) ## Option B: Connect From Channels URL ```python -from code_sandboxes import ColabKernelClient +from code_sandboxes import GoogleColabKernelClient channels_url = ( "wss:///api/kernels//channels" "?session_id=<...>&colab-runtime-proxy-token=&colab-client-agent=web" ) -with ColabKernelClient.from_channels_url(channels_url) as kernel: +with GoogleColabKernelClient.from_channels_url(channels_url) as kernel: reply = kernel.execute("x = 1 + 1; print(x)") print(reply) ``` @@ -98,12 +98,12 @@ with ColabKernelClient.from_channels_url(channels_url) as kernel: You can also parse values directly: ```python -from code_sandboxes import parse_colab_channels_url +from code_sandboxes import parse_google_colab_channels_url -server_url, kernel_id, proxy_token = parse_colab_channels_url(channels_url) +server_url, kernel_id, proxy_token = parse_google_colab_channels_url(channels_url) ``` -`ColabKernelClient` forwards the proxy token as both the +`GoogleColabKernelClient` forwards the proxy token as both the `X-Colab-Runtime-Proxy-Token` HTTP header and the `colab-runtime-proxy-token` WebSocket query parameter. diff --git a/docs/docs/sandboxes/index.mdx b/docs/docs/sandboxes/index.mdx index b8e9352..8c0c2d1 100644 --- a/docs/docs/sandboxes/index.mdx +++ b/docs/docs/sandboxes/index.mdx @@ -12,7 +12,9 @@ A sandbox is an isolated environment where code can be executed safely. Code San Use `Sandbox.create()` to create a new sandbox: Canonical variant names are `jupyter`, `docker`, `eval`, `monty`, `kaggle`, -`colab`, `modal`, and `datalayer`. Older `local-*` names are no longer supported. +`google_colab`, `modal`, and `datalayer`. Older `local-*` names are no longer supported. + +The CLI also accepts `google-colab` as an alias for `google_colab`. ```python from code_sandboxes import Sandbox @@ -65,7 +67,7 @@ Each page below explains how to configure each variant. | [`monty`](./monty) | Secure in-process Python subset via Monty | | [`docker`](./docker) | Jupyter execution in a Docker container | | [`kaggle`](./kaggle) | Kaggle runtime (interactive or batch) | -| [`colab`](./google-colab) | Google Colab runtime via runtime proxy | +| [`google_colab`](./google-colab) | Google Colab runtime via runtime proxy | | [`modal`](./modal) | Modal container execution | | [`datalayer`](./datalayer) | Datalayer managed runtime with optional GPU | diff --git a/examples/README.md b/examples/README.md index 0d68452..25be16f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -21,7 +21,7 @@ Supported sandbox variants: - `docker` - `eval` - `monty` -- `colab` +- `google-colab` - `kaggle` - `modal` - `datalayer` @@ -34,7 +34,7 @@ python eval_sandbox_example.py python jupyter_sandbox_example.py python docker_sandbox_example.py python monty_sandbox_example.py -python colab_sandbox_example.py +python google_colab_sandbox_example.py python kaggle_sandbox_example.py python modal_sandbox_example.py python datalayer_sandbox_example.py @@ -48,7 +48,7 @@ make eval make jupyter make docker make monty -make colab +make google-colab make kaggle make modal make datalayer @@ -62,7 +62,7 @@ make eval make jupyter make docker make monty -make colab +make google-colab make kaggle make modal make datalayer @@ -72,7 +72,7 @@ Notes by variant: - `docker`: requires Docker support and a Docker image (for example `code-sandboxes-jupyter:latest`). - `monty`: requires `code-sandboxes[monty]` (`pydantic-monty`). -- `colab`: requires `RUNTIME_URL`, `RUNTIME_ID`, and `RUNTIME_PROXY_TOKEN`. +- `google-colab`: requires `RUNTIME_URL`, `RUNTIME_ID`, and `RUNTIME_PROXY_TOKEN`. - `kaggle`: requires `RUNTIME_CHANNELS_URL`, or `RUNTIME_URL` and `RUNTIME_ID`. - `modal`: requires `MODAL_TOKEN_ID`/`MODAL_TOKEN_SECRET` or `~/.modal.toml`. - `datalayer`: requires Datalayer runtime credentials/config. diff --git a/examples/exec/Makefile b/examples/exec/Makefile index cd77d32..f18c1c8 100644 --- a/examples/exec/Makefile +++ b/examples/exec/Makefile @@ -2,9 +2,9 @@ PYTHON ?= python -.PHONY: all eval docker jupyter monty colab kaggle modal modal-gpu datalayer +.PHONY: all eval docker jupyter monty google-colab kaggle modal modal-gpu datalayer -all: eval docker jupyter monty colab modal datalayer +all: eval docker jupyter monty google-colab modal datalayer eval: $(PYTHON) eval_sandbox_example.py @@ -18,8 +18,8 @@ jupyter: monty: $(PYTHON) monty_sandbox_example.py -colab: - $(PYTHON) colab_sandbox_example.py +google-colab: + $(PYTHON) google_colab_sandbox_example.py kaggle: $(PYTHON) kaggle_sandbox_example.py diff --git a/examples/exec/colab_sandbox_example.py b/examples/exec/google_colab_sandbox_example.py similarity index 97% rename from examples/exec/colab_sandbox_example.py rename to examples/exec/google_colab_sandbox_example.py index 5c768ab..ceeeba9 100644 --- a/examples/exec/colab_sandbox_example.py +++ b/examples/exec/google_colab_sandbox_example.py @@ -29,7 +29,7 @@ def main() -> None: runtime_proxy_token = _require("RUNTIME_PROXY_TOKEN") with Sandbox.create( - variant="colab", + variant="google_colab", timeout=60, server_url=runtime_url, kernel_id=runtime_id, diff --git a/examples/repl/Makefile b/examples/repl/Makefile index 596d718..7afc3b8 100644 --- a/examples/repl/Makefile +++ b/examples/repl/Makefile @@ -2,9 +2,9 @@ PYTHON ?= python -.PHONY: all eval docker jupyter monty colab kaggle modal modal-gpu datalayer +.PHONY: all eval docker jupyter monty google-colab kaggle modal modal-gpu datalayer -all: eval docker jupyter monty colab modal datalayer +all: eval docker jupyter monty google-colab modal datalayer eval: $(PYTHON) eval_sandbox_example.py @@ -18,8 +18,8 @@ jupyter: monty: $(PYTHON) monty_sandbox_example.py -colab: - $(PYTHON) colab_sandbox_example.py +google-colab: + $(PYTHON) google_colab_sandbox_example.py kaggle: $(PYTHON) kaggle_sandbox_example.py diff --git a/examples/repl/colab_sandbox_example.py b/examples/repl/google_colab_sandbox_example.py similarity index 96% rename from examples/repl/colab_sandbox_example.py rename to examples/repl/google_colab_sandbox_example.py index cf534b5..59bf7c4 100644 --- a/examples/repl/colab_sandbox_example.py +++ b/examples/repl/google_colab_sandbox_example.py @@ -24,7 +24,7 @@ def main() -> None: runtime_proxy_token = _require("RUNTIME_PROXY_TOKEN") with Sandbox.create( - variant="colab", + variant="google_colab", timeout=60, server_url=runtime_url, kernel_id=runtime_id, diff --git a/pyproject.toml b/pyproject.toml index 6922b7e..9766d7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ code-sandboxes = "code_sandboxes.cli:main" [project.optional-dependencies] datalayer = ["agent_runtimes>=1.0.16"] docker = ["docker>=6.0"] +google-colab = [] kaggle = ["kaggle>=1.6"] monty = ["pydantic-monty"] modal = ["modal>=0.64"] diff --git a/tests/test_cli_repl.py b/tests/test_cli_repl.py index d21d531..31fe261 100644 --- a/tests/test_cli_repl.py +++ b/tests/test_cli_repl.py @@ -63,10 +63,14 @@ def _fake_create(*args, **kwargs): # Prompts: server_url, kernel_id, proxy_token, then repl command. user_input = "https://colab-host.example\nkernel-abc\nproxy-xyz\n:exit\n" - result = runner.invoke(sandbox_cli.app, ["repl", "--variant", "colab"], input=user_input) + result = runner.invoke( + sandbox_cli.app, + ["repl", "--variant", "google-colab"], + input=user_input, + ) assert result.exit_code == 0 - assert captured["kwargs"]["variant"] == "colab" + assert captured["kwargs"]["variant"] == "google_colab" assert captured["kwargs"]["server_url"] == "https://colab-host.example" assert captured["kwargs"]["kernel_id"] == "kernel-abc" assert captured["kwargs"]["proxy_token"] == "proxy-xyz" # noqa: S105 diff --git a/tests/test_factory.py b/tests/test_factory.py index 5130856..2b670be 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -7,7 +7,7 @@ import pytest from code_sandboxes.base import Sandbox, SandboxVariant -from code_sandboxes.colab_sandbox import ColabSandbox +from code_sandboxes.google_colab_sandbox import GoogleColabSandbox from code_sandboxes.datalayer_sandbox import DatalayerSandbox from code_sandboxes.docker_sandbox import DockerSandbox from code_sandboxes.eval_sandbox import EvalSandbox @@ -70,7 +70,7 @@ def test_create_invalid_variant(self): ("jupyter", JupyterSandbox), ("docker", DockerSandbox), ("datalayer", DatalayerSandbox), - ("colab", ColabSandbox), + ("colab", GoogleColabSandbox), ("kaggle", KaggleSandbox), ("monty", MontySandbox), ("modal", ModalSandbox), @@ -89,7 +89,7 @@ def test_create_default_variant_is_datalayer(self): def test_create_colab_forwards_connection_kwargs(self): """Test that Colab-specific connection kwargs are propagated.""" sandbox = Sandbox.create( - variant="colab", + variant="google_colab", server_url="https://colab-host.example", kernel_id="kernel-id", proxy_token="proxy-token", # noqa: S106 @@ -99,7 +99,7 @@ def test_create_colab_forwards_connection_kwargs(self): ), client_agent="agent-name", ) - assert isinstance(sandbox, ColabSandbox) + assert isinstance(sandbox, GoogleColabSandbox) assert sandbox._server_url == "https://colab-host.example" assert sandbox._kernel_id == "kernel-id" assert sandbox._proxy_token == "proxy-token" # noqa: S105 diff --git a/tests/test_colab.py b/tests/test_google_colab.py similarity index 79% rename from tests/test_colab.py rename to tests/test_google_colab.py index 5604a99..1ee11b2 100644 --- a/tests/test_colab.py +++ b/tests/test_google_colab.py @@ -8,12 +8,12 @@ import pytest -from code_sandboxes.colab import ( +from code_sandboxes.google_colab import ( COLAB_CLIENT_AGENT_HEADER, COLAB_RUNTIME_PROXY_TOKEN_HEADER, COLAB_RUNTIME_PROXY_TOKEN_PARAM, - ColabKernelClient, - parse_colab_channels_url, + GoogleColabKernelClient, + parse_google_colab_channels_url, ) CHANNELS_URL = ( @@ -35,10 +35,10 @@ def fake_kernel_client_init(self, *args, **kwargs): captured.update(kwargs) monkeypatch.setattr( - "code_sandboxes.colab.JupyterKernelClient.__init__", fake_kernel_client_init + "code_sandboxes.google_colab.JupyterKernelClient.__init__", fake_kernel_client_init ) - ColabKernelClient( + GoogleColabKernelClient( server_url="https://colab-host.example", kernel_id="kernel-123", proxy_token="proxy-abc", # noqa: S106 @@ -68,10 +68,10 @@ def fake_kernel_client_init(self, *args, **kwargs): captured.update(kwargs) monkeypatch.setattr( - "code_sandboxes.colab.JupyterKernelClient.__init__", fake_kernel_client_init + "code_sandboxes.google_colab.JupyterKernelClient.__init__", fake_kernel_client_init ) - ColabKernelClient( + GoogleColabKernelClient( server_url="https://colab-host.example", kernel_id="kernel-123", proxy_token="proxy-abc", # noqa: S106 @@ -83,26 +83,26 @@ def fake_kernel_client_init(self, *args, **kwargs): def test_parse_colab_channels_url_extracts_parts(): - server_url, kernel_id, proxy_token = parse_colab_channels_url(CHANNELS_URL) + server_url, kernel_id, proxy_token = parse_google_colab_channels_url(CHANNELS_URL) assert server_url == SERVER_URL assert kernel_id == KERNEL_ID assert proxy_token == PROXY_TOKEN def test_parse_colab_channels_url_maps_ws_to_http(): - server_url, _, _ = parse_colab_channels_url(CHANNELS_URL.replace("wss://", "ws://")) + server_url, _, _ = parse_google_colab_channels_url(CHANNELS_URL.replace("wss://", "ws://")) assert server_url.startswith("http://") def test_parse_colab_channels_url_requires_proxy_token(): without_token = CHANNELS_URL.replace("&colab-runtime-proxy-token=proxy-abc", "") with pytest.raises(ValueError): - parse_colab_channels_url(without_token) + parse_google_colab_channels_url(without_token) def test_parse_colab_channels_url_rejects_invalid_url(): with pytest.raises(ValueError): - parse_colab_channels_url("https://colab.research.google.com/not-a-channels-url") + parse_google_colab_channels_url("https://colab.research.google.com/not-a-channels-url") def test_colab_kernel_client_from_channels_url(monkeypatch): @@ -112,10 +112,10 @@ def fake_kernel_client_init(self, *args, **kwargs): captured.update(kwargs) monkeypatch.setattr( - "code_sandboxes.colab.JupyterKernelClient.__init__", fake_kernel_client_init + "code_sandboxes.google_colab.JupyterKernelClient.__init__", fake_kernel_client_init ) - ColabKernelClient.from_channels_url(CHANNELS_URL) + GoogleColabKernelClient.from_channels_url(CHANNELS_URL) assert captured["server_url"] == SERVER_URL assert captured["kernel_id"] == KERNEL_ID diff --git a/tests/test_modal_colab_sandbox.py b/tests/test_modal_google_colab_sandbox.py similarity index 99% rename from tests/test_modal_colab_sandbox.py rename to tests/test_modal_google_colab_sandbox.py index 06e9ee6..bc7f09b 100644 --- a/tests/test_modal_colab_sandbox.py +++ b/tests/test_modal_google_colab_sandbox.py @@ -12,7 +12,7 @@ import pytest -from code_sandboxes.colab_sandbox import ColabSandbox +from code_sandboxes.google_colab_sandbox import GoogleColabSandbox from code_sandboxes.kaggle_sandbox import KaggleSandbox from code_sandboxes.modal_sandbox import ModalSandbox from code_sandboxes.models import SandboxConfig @@ -89,7 +89,7 @@ def test_modal_nonzero_return_without_stderr_sets_exit_code(): def test_colab_execute_exception_sets_execution_ok_false(): """Infrastructure execute errors must set execution_ok to False.""" - sandbox = ColabSandbox( + sandbox = GoogleColabSandbox( config=SandboxConfig(timeout=10.0), server_url="https://colab-host.example", kernel_id="kernel-id", diff --git a/tests/test_models.py b/tests/test_models.py index a2ddf25..bfe748d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -45,6 +45,7 @@ def test_sandbox_variant_enum(self): assert SandboxVariantEnum.DOCKER.value == "docker" assert SandboxVariantEnum.JUPYTER.value == "jupyter" assert SandboxVariantEnum.DATALAYER.value == "datalayer" + assert SandboxVariantEnum.GOOGLE_COLAB.value == "google_colab" assert SandboxVariantEnum.COLAB.value == "colab" assert SandboxVariantEnum.KAGGLE.value == "kaggle" assert SandboxVariantEnum.MONTY.value == "monty" From 641d00d35a3fb6ef647ff79a10e97277b01d87d2 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 10 Aug 2026 08:04:28 +0200 Subject: [PATCH 2/3] lint --- README.md | 20 +++++--------------- code_sandboxes/__init__.py | 7 +------ code_sandboxes/base.py | 4 +--- code_sandboxes/cli.py | 3 +-- code_sandboxes/colab.py | 16 ---------------- code_sandboxes/colab_sandbox.py | 7 ------- code_sandboxes/google_colab.py | 5 ----- code_sandboxes/google_colab_sandbox.py | 4 ---- code_sandboxes/models.py | 1 - tests/test_factory.py | 2 +- tests/test_models.py | 1 - 11 files changed, 9 insertions(+), 61 deletions(-) delete mode 100644 code_sandboxes/colab.py delete mode 100644 code_sandboxes/colab_sandbox.py diff --git a/README.md b/README.md index 7152dca..b1a5933 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ Canonical variant names: - `modal` - `monty` -CLI also accepts the alias `google-colab`. - ## Documentation The full documentation is the single source of truth: @@ -51,9 +49,7 @@ pip install code-sandboxes For backend-specific extras and credentials, see [https://code-sandboxes.datalayer.tech/installation](https://code-sandboxes.datalayer.tech/installation) and [https://code-sandboxes.datalayer.tech/sandboxes](https://code-sandboxes.datalayer.tech/sandboxes). -## Quick Examples - -### Python: launch a `jupyter` sandbox +### Jupyter Sandbox ```python from code_sandboxes import Sandbox @@ -72,7 +68,7 @@ with Sandbox.create( print(sandbox.run_code("x + 2").text) # 42 ``` -### Kaggle +## Kaggle Sandbox Kaggle supports both batch execution and interactive connections through the `kaggle` sandbox. Install its optional dependency first: @@ -134,10 +130,10 @@ with KaggleKernelClient.from_channels_url(channels_url, token=None) as kernel: print(kernel.execute("x = 1 + 1; print(x)")) ``` -See the [complete Kaggle guide](docs/docs/sandboxes/kaggle.mdx) for authentication, +See the [complete Kaggle guide](https://code-sandboxes.datalayer.tech/sandboxes/kaggle) for authentication, accelerators, channels URL retrieval, and execution options. -### Google Colab +## Google Colab Google Colab exposes an already-running kernel through an authenticating proxy. Copy its WebSocket channels URL from the browser's Network tools, then pass it @@ -160,15 +156,9 @@ with GoogleColabKernelClient.from_channels_url(channels_url) as kernel: print(kernel.execute("print('hello from colab')")) ``` -See the [complete Google Colab guide](docs/docs/sandboxes/google-colab.mdx) for +See the [complete Google Colab guide](https://code-sandboxes.datalayer.tech/sandboxes/google-colab) for proxy authentication, explicit connection values, and channels URL retrieval. -For full setup and parameters for all variants, see: - -- [https://code-sandboxes.datalayer.tech/sandboxes](https://code-sandboxes.datalayer.tech/sandboxes) -- [https://code-sandboxes.datalayer.tech/cli](https://code-sandboxes.datalayer.tech/cli) -- [https://code-sandboxes.datalayer.tech/installation](https://code-sandboxes.datalayer.tech/installation) - ## License BSD 3-Clause License diff --git a/code_sandboxes/__init__.py b/code_sandboxes/__init__.py index ad0e6dd..8ab93c9 100644 --- a/code_sandboxes/__init__.py +++ b/code_sandboxes/__init__.py @@ -60,12 +60,10 @@ from .base import Sandbox from .client import CodeExecutionOutcome, CodeSandboxClient, execution_result_to_reply from .google_colab import ( - ColabKernelClient, GoogleColabKernelClient, - parse_colab_channels_url, parse_google_colab_channels_url, ) -from .google_colab_sandbox import ColabSandbox, GoogleColabSandbox +from .google_colab_sandbox import GoogleColabSandbox from .commands import CommandResult, ProcessHandle, SandboxCommands from .datalayer_sandbox import DatalayerSandbox from .docker_sandbox import DockerSandbox @@ -125,8 +123,6 @@ "CodeError", "CodeExecutionOutcome", "CodeSandboxClient", - "ColabKernelClient", - "ColabSandbox", "CommandResult", "Context", "ContextNotFoundError", @@ -184,7 +180,6 @@ "TunnelInfo", "VariableNotFoundError", "execution_result_to_reply", - "parse_colab_channels_url", "parse_google_colab_channels_url", "parse_kaggle_channels_url", ] diff --git a/code_sandboxes/base.py b/code_sandboxes/base.py index e164f63..a7c93d3 100644 --- a/code_sandboxes/base.py +++ b/code_sandboxes/base.py @@ -253,8 +253,6 @@ def create( # noqa: C901 from .eval_sandbox import EvalSandbox variant_value = variant.value if isinstance(variant, SandboxVariant) else variant - if variant_value in ("colab", "google-colab"): - variant_value = "google_colab" if variant_value == "eval": sandbox = EvalSandbox(config=config, **kwargs) @@ -291,7 +289,7 @@ def create( # noqa: C901 raise ValueError( f"Unknown sandbox variant: {variant}. " "Supported variants: eval, docker, jupyter, " - "datalayer, google_colab, google-colab, colab, kaggle, monty, modal" + "datalayer, google_colab, kaggle, monty, modal" ) # Set tags if provided diff --git a/code_sandboxes/cli.py b/code_sandboxes/cli.py index ef67fe9..3440887 100644 --- a/code_sandboxes/cli.py +++ b/code_sandboxes/cli.py @@ -21,7 +21,6 @@ "docker", "eval", "monty", - "colab", "google_colab", "google-colab", "kaggle", @@ -82,7 +81,7 @@ def _resolve_variant(variant: str | None) -> str: f"Unsupported variant: {selected}. Supported values: " + ", ".join(sorted(_SUPPORTED_REPL_VARIANTS)) ) - if selected in {"colab", "google-colab"}: + if selected == "google-colab": return "google_colab" return selected diff --git a/code_sandboxes/colab.py b/code_sandboxes/colab.py deleted file mode 100644 index e102ee8..0000000 --- a/code_sandboxes/colab.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2025-2026 Datalayer, Inc. -# -# BSD 3-Clause License - -"""Backward-compatible imports for Google Colab client symbols.""" - -from .google_colab import ( # noqa: F401 - COLAB_CLIENT_AGENT_HEADER, - COLAB_RUNTIME_PROXY_TOKEN_HEADER, - COLAB_RUNTIME_PROXY_TOKEN_PARAM, - DEFAULT_COLAB_CLIENT_AGENT, - ColabKernelClient, - GoogleColabKernelClient, - parse_colab_channels_url, - parse_google_colab_channels_url, -) diff --git a/code_sandboxes/colab_sandbox.py b/code_sandboxes/colab_sandbox.py deleted file mode 100644 index b28fb24..0000000 --- a/code_sandboxes/colab_sandbox.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) 2025-2026 Datalayer, Inc. -# -# BSD 3-Clause License - -"""Backward-compatible imports for Google Colab sandbox symbols.""" - -from .google_colab_sandbox import ColabSandbox, GoogleColabSandbox # noqa: F401 diff --git a/code_sandboxes/google_colab.py b/code_sandboxes/google_colab.py index efaf535..54ddd65 100644 --- a/code_sandboxes/google_colab.py +++ b/code_sandboxes/google_colab.py @@ -206,8 +206,3 @@ def from_channels_url( kwargs.setdefault("kernel_id", kernel_id) kwargs.setdefault("proxy_token", proxy_token) return cls(server_url=server_url, **kwargs) - - -# Backward-compatible aliases. -parse_colab_channels_url = parse_google_colab_channels_url -ColabKernelClient = GoogleColabKernelClient diff --git a/code_sandboxes/google_colab_sandbox.py b/code_sandboxes/google_colab_sandbox.py index feeaf0f..3d71012 100644 --- a/code_sandboxes/google_colab_sandbox.py +++ b/code_sandboxes/google_colab_sandbox.py @@ -267,7 +267,3 @@ def _set_internal_variable(self, name: str, value, context: Context | None = Non if not self._started or self._client is None: raise SandboxNotStartedError() self._client.set_variable(name, value) - - -# Backward-compatible alias. -GoogleColabSandbox = GoogleColabSandbox diff --git a/code_sandboxes/models.py b/code_sandboxes/models.py index 3281d87..4fd3984 100644 --- a/code_sandboxes/models.py +++ b/code_sandboxes/models.py @@ -70,7 +70,6 @@ class SandboxVariant(str, Enum): JUPYTER = "jupyter" DATALAYER = "datalayer" GOOGLE_COLAB = "google_colab" - COLAB = "colab" KAGGLE = "kaggle" MONTY = "monty" MODAL = "modal" diff --git a/tests/test_factory.py b/tests/test_factory.py index 2b670be..c7eeb52 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -70,7 +70,7 @@ def test_create_invalid_variant(self): ("jupyter", JupyterSandbox), ("docker", DockerSandbox), ("datalayer", DatalayerSandbox), - ("colab", GoogleColabSandbox), + ("google_colab", GoogleColabSandbox), ("kaggle", KaggleSandbox), ("monty", MontySandbox), ("modal", ModalSandbox), diff --git a/tests/test_models.py b/tests/test_models.py index bfe748d..8c88372 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -46,7 +46,6 @@ def test_sandbox_variant_enum(self): assert SandboxVariantEnum.JUPYTER.value == "jupyter" assert SandboxVariantEnum.DATALAYER.value == "datalayer" assert SandboxVariantEnum.GOOGLE_COLAB.value == "google_colab" - assert SandboxVariantEnum.COLAB.value == "colab" assert SandboxVariantEnum.KAGGLE.value == "kaggle" assert SandboxVariantEnum.MONTY.value == "monty" assert SandboxVariantEnum.MODAL.value == "modal" From 26b839def3fa080f1b2e02bcc5f66325566b2fc3 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Mon, 10 Aug 2026 08:07:13 +0200 Subject: [PATCH 3/3] lint --- README.md | 3 +-- code_sandboxes/__init__.py | 12 ++++++------ code_sandboxes/google_colab_sandbox.py | 10 ++++++---- tests/test_factory.py | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b1a5933..e1c5014 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,10 @@ [![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io) [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=1ABC9C)](https://github.com/sponsors/datalayer) +[![PyPI - Version](https://img.shields.io/pypi/v/code-sandboxes)](https://pypi.org/project/code-sandboxes) # { } 📦 Code Sandboxes -[![PyPI - Version](https://img.shields.io/pypi/v/code-sandboxes)](https://pypi.org/project/code-sandboxes) - Code Sandboxes (`code_sandboxes`) is a Python package for running code in isolated sandbox variants through a unified API. Canonical variant names: diff --git a/code_sandboxes/__init__.py b/code_sandboxes/__init__.py index 8ab93c9..b31dac3 100644 --- a/code_sandboxes/__init__.py +++ b/code_sandboxes/__init__.py @@ -59,11 +59,6 @@ from .base import Sandbox from .client import CodeExecutionOutcome, CodeSandboxClient, execution_result_to_reply -from .google_colab import ( - GoogleColabKernelClient, - parse_google_colab_channels_url, -) -from .google_colab_sandbox import GoogleColabSandbox from .commands import CommandResult, ProcessHandle, SandboxCommands from .datalayer_sandbox import DatalayerSandbox from .docker_sandbox import DockerSandbox @@ -90,6 +85,11 @@ SandboxFileHandle, SandboxFilesystem, ) +from .google_colab import ( + GoogleColabKernelClient, + parse_google_colab_channels_url, +) +from .google_colab_sandbox import GoogleColabSandbox from .interfaces import ISandboxClient from .jupyter_sandbox import JupyterSandbox from .kaggle import KAGGLE_API_TOKEN_ENV, KaggleKernelClient, parse_kaggle_channels_url @@ -135,9 +135,9 @@ "FileType", "FileWatchEvent", "FileWatchEventType", + "GPUType", "GoogleColabKernelClient", "GoogleColabSandbox", - "GPUType", "ISandboxClient", "JupyterSandbox", "KaggleExecutionResult", diff --git a/code_sandboxes/google_colab_sandbox.py b/code_sandboxes/google_colab_sandbox.py index 3d71012..9b58f6b 100644 --- a/code_sandboxes/google_colab_sandbox.py +++ b/code_sandboxes/google_colab_sandbox.py @@ -20,8 +20,8 @@ import uuid from .base import Sandbox -from .google_colab import GoogleColabKernelClient, parse_google_colab_channels_url from .exceptions import SandboxConfigurationError, SandboxNotStartedError +from .google_colab import GoogleColabKernelClient, parse_google_colab_channels_url from .interfaces import ISandboxClient from .models import ( CodeError, @@ -96,9 +96,11 @@ def start(self) -> None: if self._channels_url and ( not self._server_url or not self._kernel_id or not self._proxy_token ): - parsed_server_url, parsed_kernel_id, parsed_proxy_token = parse_google_colab_channels_url( - self._channels_url - ) + ( + parsed_server_url, + parsed_kernel_id, + parsed_proxy_token, + ) = parse_google_colab_channels_url(self._channels_url) self._server_url = self._server_url or parsed_server_url self._kernel_id = self._kernel_id or parsed_kernel_id self._proxy_token = self._proxy_token or parsed_proxy_token diff --git a/tests/test_factory.py b/tests/test_factory.py index c7eeb52..aaca4a0 100644 --- a/tests/test_factory.py +++ b/tests/test_factory.py @@ -7,10 +7,10 @@ import pytest from code_sandboxes.base import Sandbox, SandboxVariant -from code_sandboxes.google_colab_sandbox import GoogleColabSandbox from code_sandboxes.datalayer_sandbox import DatalayerSandbox from code_sandboxes.docker_sandbox import DockerSandbox from code_sandboxes.eval_sandbox import EvalSandbox +from code_sandboxes.google_colab_sandbox import GoogleColabSandbox from code_sandboxes.jupyter_sandbox import JupyterSandbox from code_sandboxes.kaggle_sandbox import KaggleSandbox from code_sandboxes.modal_sandbox import ModalSandbox