From a0b2fef96d504140b2a169bcf10311adb9b5e88c Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 26 Jul 2026 11:53:47 +0200 Subject: [PATCH 1/4] Introduce ISandboxClient protocol across sandboxes --- code_sandboxes/__init__.py | 3 +++ code_sandboxes/base.py | 6 ++++++ code_sandboxes/colab_sandbox.py | 6 ++++++ code_sandboxes/docker_sandbox.py | 6 ++++++ code_sandboxes/interfaces.py | 24 ++++++++++++++++++++++++ code_sandboxes/jupyter_sandbox.py | 5 +++-- code_sandboxes/kaggle_sandbox.py | 6 ++++++ 7 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 code_sandboxes/interfaces.py diff --git a/code_sandboxes/__init__.py b/code_sandboxes/__init__.py index 834802b..13f31aa 100644 --- a/code_sandboxes/__init__.py +++ b/code_sandboxes/__init__.py @@ -86,6 +86,7 @@ SandboxFileHandle, SandboxFilesystem, ) +from .interfaces import IKernelClient, ISandboxClient from .jupyter_sandbox import JupyterSandbox from .kaggle_sandbox import KaggleSandbox from .modal_sandbox import ModalSandbox @@ -130,6 +131,8 @@ "FileWatchEventType", "GPUType", "JupyterSandbox", + "IKernelClient", + "ISandboxClient", "KaggleSandbox", "Logs", "MIMEType", diff --git a/code_sandboxes/base.py b/code_sandboxes/base.py index dda4b2d..8adddbe 100644 --- a/code_sandboxes/base.py +++ b/code_sandboxes/base.py @@ -13,6 +13,7 @@ from typing import Any, Union from .commands import SandboxCommands +from .interfaces import ISandboxClient from .filesystem import SandboxFilesystem from .models import ( CodeError, @@ -100,6 +101,11 @@ def is_executing(self) -> bool: """Check if the sandbox is currently executing code.""" return self._executing_event.is_set() + @property + def kernel_client(self) -> ISandboxClient | None: + """Expose an optional kernel client interface for kernel-backed variants.""" + return None + def interrupt(self) -> bool: """Request interruption of the currently running code. diff --git a/code_sandboxes/colab_sandbox.py b/code_sandboxes/colab_sandbox.py index d885658..64adaa2 100644 --- a/code_sandboxes/colab_sandbox.py +++ b/code_sandboxes/colab_sandbox.py @@ -21,6 +21,7 @@ from .base import Sandbox from .exceptions import SandboxConfigurationError, SandboxNotStartedError +from .interfaces import ISandboxClient from .models import ( CodeError, Context, @@ -143,6 +144,11 @@ def start(self) -> None: ) 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 diff --git a/code_sandboxes/docker_sandbox.py b/code_sandboxes/docker_sandbox.py index 54cb09d..99f2f77 100644 --- a/code_sandboxes/docker_sandbox.py +++ b/code_sandboxes/docker_sandbox.py @@ -19,6 +19,7 @@ from .base import Sandbox from .exceptions import SandboxConfigurationError, SandboxNotStartedError +from .interfaces import ISandboxClient from .models import ( CodeError, Context, @@ -186,6 +187,11 @@ def start(self) -> None: ) self._started = True + @property + def kernel_client(self) -> ISandboxClient | None: + """The underlying kernel client for this sandbox, if started.""" + return self._client + def stop(self) -> None: if not self._started: return diff --git a/code_sandboxes/interfaces.py b/code_sandboxes/interfaces.py new file mode 100644 index 0000000..e220561 --- /dev/null +++ b/code_sandboxes/interfaces.py @@ -0,0 +1,24 @@ +# Copyright (c) 2025-2026 Datalayer, Inc. +# +# BSD 3-Clause License + +"""Typing protocols for sandbox clients.""" + +from __future__ import annotations + +from typing import Protocol, runtime_checkable + +try: + from jupyter_kernel_client.interfaces import IKernelClient +except Exception: # pragma: no cover - fallback for optional dependency contexts + class IKernelClient(Protocol): + """Fallback protocol when jupyter-kernel-client is unavailable.""" + + +@runtime_checkable +class ISandboxClient(IKernelClient, Protocol): + """Kernel client protocol exposed by sandbox variants. + + This currently matches ``IKernelClient`` exactly and acts as an extension + point for sandbox-specific client capabilities. + """ diff --git a/code_sandboxes/jupyter_sandbox.py b/code_sandboxes/jupyter_sandbox.py index ad9ea36..d945598 100644 --- a/code_sandboxes/jupyter_sandbox.py +++ b/code_sandboxes/jupyter_sandbox.py @@ -27,6 +27,7 @@ from .base import Sandbox from .exceptions import SandboxConfigurationError, SandboxNotStartedError +from .interfaces import ISandboxClient from .models import ( CodeError, Context, @@ -88,7 +89,7 @@ def __init__( self._server_app = None self._server_thread: threading.Thread | None = None self._server_process: subprocess.Popen | None = None - self._client = None + self._client: ISandboxClient | None = None self._sandbox_id = str(uuid.uuid4()) self._workdir: str | None = None self._workdir_tmp: str | None = None @@ -385,7 +386,7 @@ def start(self) -> None: self._started = True @property - def kernel_client(self): + def kernel_client(self) -> ISandboxClient | None: """The underlying ``jupyter_kernel_client.KernelClient``. Exposed so callers that need the full low-level kernel API (for diff --git a/code_sandboxes/kaggle_sandbox.py b/code_sandboxes/kaggle_sandbox.py index f33fb90..a470734 100644 --- a/code_sandboxes/kaggle_sandbox.py +++ b/code_sandboxes/kaggle_sandbox.py @@ -35,6 +35,7 @@ from .base import Sandbox from .exceptions import SandboxConfigurationError, SandboxNotStartedError +from .interfaces import ISandboxClient from .models import ( CodeError, Context, @@ -182,6 +183,11 @@ def start(self) -> None: ) self._started = True + @property + def kernel_client(self) -> ISandboxClient | None: + """The underlying Kaggle kernel client, if started.""" + return self._client + def _setup_tool_caller(self) -> None: """Keep tool calling on the client side for Kaggle sandboxes.""" return From b942a9e88104b0a3547627a77ba161a493db37fa Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 26 Jul 2026 11:57:37 +0200 Subject: [PATCH 2/4] Bump version to 0.16.0 --- code_sandboxes/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code_sandboxes/__version__.py b/code_sandboxes/__version__.py index d9bd733..94543f4 100644 --- a/code_sandboxes/__version__.py +++ b/code_sandboxes/__version__.py @@ -3,4 +3,4 @@ """Code Sandboxes.""" -__version__ = "0.0.21" +__version__ = "0.16.0" From ea7310105e6ba15e2a5d28d967b752e819522d3e Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 26 Jul 2026 12:31:40 +0200 Subject: [PATCH 3/4] refactor: adopt renamed jupyter kernel client interfaces --- code_sandboxes/__init__.py | 4 ++-- code_sandboxes/docker_sandbox.py | 4 ++-- code_sandboxes/interfaces.py | 8 ++++---- code_sandboxes/jupyter_sandbox.py | 8 ++++---- tests/test_jupyter.py | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/code_sandboxes/__init__.py b/code_sandboxes/__init__.py index 13f31aa..0644447 100644 --- a/code_sandboxes/__init__.py +++ b/code_sandboxes/__init__.py @@ -86,7 +86,7 @@ SandboxFileHandle, SandboxFilesystem, ) -from .interfaces import IKernelClient, ISandboxClient +from .interfaces import IJupyterKernelClient, ISandboxClient from .jupyter_sandbox import JupyterSandbox from .kaggle_sandbox import KaggleSandbox from .modal_sandbox import ModalSandbox @@ -131,7 +131,7 @@ "FileWatchEventType", "GPUType", "JupyterSandbox", - "IKernelClient", + "IJupyterKernelClient", "ISandboxClient", "KaggleSandbox", "Logs", diff --git a/code_sandboxes/docker_sandbox.py b/code_sandboxes/docker_sandbox.py index 99f2f77..be03f15 100644 --- a/code_sandboxes/docker_sandbox.py +++ b/code_sandboxes/docker_sandbox.py @@ -125,7 +125,7 @@ def start(self) -> None: self._ensure_docker() try: - from jupyter_kernel_client import KernelClient + from jupyter_kernel_client import JupyterKernelClient except ImportError as exc: # pragma: no cover - optional dependency raise SandboxConfigurationError( "jupyter-kernel-client is required for DockerSandbox. " @@ -168,7 +168,7 @@ def start(self) -> None: self._wait_for_server(timeout=self.config.timeout or 30.0) - self._client = KernelClient(server_url=self._server_url, token=self._token) + self._client = JupyterKernelClient(server_url=self._server_url, token=self._token) self._client.start() self._default_context = self.create_context("default") diff --git a/code_sandboxes/interfaces.py b/code_sandboxes/interfaces.py index e220561..a7ab930 100644 --- a/code_sandboxes/interfaces.py +++ b/code_sandboxes/interfaces.py @@ -9,16 +9,16 @@ from typing import Protocol, runtime_checkable try: - from jupyter_kernel_client.interfaces import IKernelClient + from jupyter_kernel_client.interfaces import IJupyterKernelClient except Exception: # pragma: no cover - fallback for optional dependency contexts - class IKernelClient(Protocol): + class IJupyterKernelClient(Protocol): """Fallback protocol when jupyter-kernel-client is unavailable.""" @runtime_checkable -class ISandboxClient(IKernelClient, Protocol): +class ISandboxClient(IJupyterKernelClient, Protocol): """Kernel client protocol exposed by sandbox variants. - This currently matches ``IKernelClient`` exactly and acts as an extension + This currently matches ``IJupyterKernelClient`` exactly and acts as an extension point for sandbox-specific client capabilities. """ diff --git a/code_sandboxes/jupyter_sandbox.py b/code_sandboxes/jupyter_sandbox.py index d945598..09e998a 100644 --- a/code_sandboxes/jupyter_sandbox.py +++ b/code_sandboxes/jupyter_sandbox.py @@ -340,7 +340,7 @@ def start(self) -> None: return try: - from jupyter_kernel_client import KernelClient + from jupyter_kernel_client import JupyterKernelClient except ImportError as exc: raise SandboxConfigurationError( "jupyter-kernel-client is required for JupyterSandbox. " @@ -364,7 +364,7 @@ def start(self) -> None: else: kernel_id = None - self._client = KernelClient( + self._client = JupyterKernelClient( server_url=self._server_url, token=self._token, kernel_id=kernel_id, @@ -387,7 +387,7 @@ def start(self) -> None: @property def kernel_client(self) -> ISandboxClient | None: - """The underlying ``jupyter_kernel_client.KernelClient``. + """The underlying ``jupyter_kernel_client.JupyterKernelClient``. Exposed so callers that need the full low-level kernel API (for example streaming execution via ``execute_interactive``) can delegate @@ -463,7 +463,7 @@ def _do_interrupt(self) -> bool: if not self._server_url or not self._client: return False try: - # KernelClient exposes the kernel ID as the `.id` property + # JupyterKernelClient exposes the kernel ID as the `.id` property kernel_id = getattr(self._client, "id", None) if kernel_id: resp = requests.post( diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index e7d0e4d..3fba7c1 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -36,7 +36,7 @@ def stop(self): monkeypatch.setitem( sys.modules, "jupyter_kernel_client", - types.SimpleNamespace(KernelClient=_KernelClientStub), + types.SimpleNamespace(JupyterKernelClient=_KernelClientStub), ) sandbox = JupyterSandbox( @@ -77,7 +77,7 @@ def stop(self): monkeypatch.setitem( sys.modules, "jupyter_kernel_client", - types.SimpleNamespace(KernelClient=_KernelClientStub), + types.SimpleNamespace(JupyterKernelClient=_KernelClientStub), ) sandbox = JupyterSandbox( @@ -101,7 +101,7 @@ def _should_not_be_called(): def test_kernel_client_forwards_client_kwargs(monkeypatch, tmp_path: Path): - """JupyterSandbox forwards client_kwargs to KernelClient.""" + """JupyterSandbox forwards client_kwargs to JupyterKernelClient.""" captured: dict[str, object] = {} @@ -121,7 +121,7 @@ def stop(self): monkeypatch.setitem( sys.modules, "jupyter_kernel_client", - types.SimpleNamespace(KernelClient=_KernelClientStub), + types.SimpleNamespace(JupyterKernelClient=_KernelClientStub), ) notebook_path = str(tmp_path / "notebook.ipynb") From 010e7decd280e7c0a64bfdb51da4cc8b9d881a45 Mon Sep 17 00:00:00 2001 From: Eric Charles Date: Sun, 26 Jul 2026 12:47:32 +0200 Subject: [PATCH 4/4] lint --- README.md | 2 ++ code_sandboxes/__init__.py | 2 +- code_sandboxes/base.py | 2 +- code_sandboxes/interfaces.py | 6 +----- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7f4c835..5caa9e0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Code Sandboxes (`code_sandboxes`) is a Python package for running code in isolated sandbox variants through a unified API. Canonical variant names: + - `jupyter` - `docker` - `eval` @@ -37,6 +38,7 @@ The full documentation is the single source of truth: - Comparison: [https://code-sandboxes.datalayer.tech/comparison](https://code-sandboxes.datalayer.tech/comparison) Published site: + - [https://code-sandboxes.datalayer.tech](https://code-sandboxes.datalayer.tech) ## Install diff --git a/code_sandboxes/__init__.py b/code_sandboxes/__init__.py index 0644447..2113a3f 100644 --- a/code_sandboxes/__init__.py +++ b/code_sandboxes/__init__.py @@ -130,9 +130,9 @@ "FileWatchEvent", "FileWatchEventType", "GPUType", - "JupyterSandbox", "IJupyterKernelClient", "ISandboxClient", + "JupyterSandbox", "KaggleSandbox", "Logs", "MIMEType", diff --git a/code_sandboxes/base.py b/code_sandboxes/base.py index 8adddbe..0a99a9d 100644 --- a/code_sandboxes/base.py +++ b/code_sandboxes/base.py @@ -13,8 +13,8 @@ from typing import Any, Union from .commands import SandboxCommands -from .interfaces import ISandboxClient from .filesystem import SandboxFilesystem +from .interfaces import ISandboxClient from .models import ( CodeError, Context, diff --git a/code_sandboxes/interfaces.py b/code_sandboxes/interfaces.py index a7ab930..bc3a1db 100644 --- a/code_sandboxes/interfaces.py +++ b/code_sandboxes/interfaces.py @@ -8,11 +8,7 @@ from typing import Protocol, runtime_checkable -try: - from jupyter_kernel_client.interfaces import IJupyterKernelClient -except Exception: # pragma: no cover - fallback for optional dependency contexts - class IJupyterKernelClient(Protocol): - """Fallback protocol when jupyter-kernel-client is unavailable.""" +from jupyter_kernel_client.interfaces import IJupyterKernelClient @runtime_checkable