From f61327245a17ad134f6c65b6aba557eb0705e427 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:09:51 +0000 Subject: [PATCH 1/2] chore(python-sdk): drop Python 3.10 compatibility shims Co-Authored-By: mish@e2b.dev --- .changeset/quiet-tigers-remove.md | 7 +++++++ .../e2b_code_interpreter/client.py | 4 +--- packages/desktop-python/e2b_desktop/client.py | 4 +--- packages/desktop-python/e2b_desktop/main.py | 14 ++++++++++++-- packages/python-sdk/e2b/client.py | 4 +--- packages/python-sdk/e2b/connection_config.py | 14 +++++++++++--- .../e2b/envd/client_async/__init__.py | 11 +++++------ packages/python-sdk/e2b/paginator.py | 4 +--- packages/python-sdk/e2b/sandbox/_git/types.py | 4 +--- .../e2b/sandbox/filesystem/filesystem.py | 2 +- packages/python-sdk/e2b/sandbox/sandbox_api.py | 4 ++-- .../e2b/sandbox_async/filesystem/filesystem.py | 2 +- packages/python-sdk/e2b/sandbox_async/main.py | 4 +--- .../python-sdk/e2b/sandbox_async/paginator.py | 4 +--- .../python-sdk/e2b/sandbox_async/sandbox_api.py | 4 +--- packages/python-sdk/e2b/sandbox_sync/main.py | 4 +--- .../python-sdk/e2b/sandbox_sync/paginator.py | 4 +--- .../python-sdk/e2b/sandbox_sync/sandbox_api.py | 4 +--- packages/python-sdk/e2b/secret/secret_async.py | 4 +--- packages/python-sdk/e2b/secret/secret_sync.py | 4 +--- packages/python-sdk/e2b/template/types.py | 4 +--- packages/python-sdk/e2b/template_async/main.py | 4 +--- packages/python-sdk/e2b/template_sync/main.py | 4 +--- .../python-sdk/e2b/volume/connection_config.py | 4 +--- packages/python-sdk/e2b/volume/volume_async.py | 17 +++++++++++++---- packages/python-sdk/e2b/volume/volume_sync.py | 15 ++++++++++++--- 26 files changed, 81 insertions(+), 73 deletions(-) create mode 100644 .changeset/quiet-tigers-remove.md diff --git a/.changeset/quiet-tigers-remove.md b/.changeset/quiet-tigers-remove.md new file mode 100644 index 0000000000..e23e260888 --- /dev/null +++ b/.changeset/quiet-tigers-remove.md @@ -0,0 +1,7 @@ +--- +'@e2b/python-sdk': patch +'@e2b/code-interpreter-python': patch +'@e2b/desktop-python': patch +--- + +Remove Python 3.10 compatibility shims (Python >=3.11 is already required). diff --git a/packages/code-interpreter-python/e2b_code_interpreter/client.py b/packages/code-interpreter-python/e2b_code_interpreter/client.py index 47d286e34f..2d43681add 100644 --- a/packages/code-interpreter-python/e2b_code_interpreter/client.py +++ b/packages/code-interpreter-python/e2b_code_interpreter/client.py @@ -1,9 +1,7 @@ -from typing import Dict, Type, TypeVar, cast +from typing import Dict, Type, TypeVar, Unpack, cast from e2b import ApiParams, E2BClientParams from e2b import E2B as CoreE2B -from typing_extensions import Unpack - from e2b_code_interpreter.code_interpreter_async import AsyncSandbox from e2b_code_interpreter.code_interpreter_sync import Sandbox diff --git a/packages/desktop-python/e2b_desktop/client.py b/packages/desktop-python/e2b_desktop/client.py index 55d859a015..e10a061383 100644 --- a/packages/desktop-python/e2b_desktop/client.py +++ b/packages/desktop-python/e2b_desktop/client.py @@ -1,9 +1,7 @@ -from typing import Dict, Type, TypeVar, cast +from typing import Dict, Type, TypeVar, Unpack, cast from e2b import ApiParams, E2BClientParams from e2b import E2B as CoreE2B -from typing_extensions import Unpack - from e2b_desktop.main import Sandbox T = TypeVar("T") diff --git a/packages/desktop-python/e2b_desktop/main.py b/packages/desktop-python/e2b_desktop/main.py index ec81fec965..3ab7415bf4 100644 --- a/packages/desktop-python/e2b_desktop/main.py +++ b/packages/desktop-python/e2b_desktop/main.py @@ -2,7 +2,18 @@ import time from re import search as re_search from shlex import quote as quote_string -from typing import Callable, Dict, Iterator, Literal, Optional, overload, Tuple, Union +from typing import ( + Callable, + Dict, + Iterator, + Literal, + Optional, + Self, + Unpack, + overload, + Tuple, + Union, +) from uuid import uuid4 from e2b import ( @@ -17,7 +28,6 @@ Volume, ) from e2b.connection_config import ApiParams -from typing_extensions import Self, Unpack MOUSE_BUTTONS = {"left": 1, "right": 3, "middle": 2} diff --git a/packages/python-sdk/e2b/client.py b/packages/python-sdk/e2b/client.py index ada3965f0c..d704e48160 100644 --- a/packages/python-sdk/e2b/client.py +++ b/packages/python-sdk/e2b/client.py @@ -1,6 +1,4 @@ -from typing import Dict, Type, TypeVar, cast - -from typing_extensions import Unpack +from typing import Dict, Type, TypeVar, Unpack, cast from e2b.connection_config import ApiParams from e2b.sandbox_async.main import AsyncSandbox diff --git a/packages/python-sdk/e2b/connection_config.py b/packages/python-sdk/e2b/connection_config.py index 6ad35b37f5..efee15ec61 100644 --- a/packages/python-sdk/e2b/connection_config.py +++ b/packages/python-sdk/e2b/connection_config.py @@ -1,11 +1,19 @@ import logging import os -from typing import cast, Literal, Mapping, Optional, Dict, Tuple, TypedDict, Union +from typing import ( + cast, + Literal, + Mapping, + Optional, + Dict, + Tuple, + TypedDict, + Union, + Unpack, +) import httpx -from typing_extensions import Unpack - from e2b.exceptions import InvalidArgumentException from e2b.retry import resolve_max_retries from e2b.api.metadata import package_version diff --git a/packages/python-sdk/e2b/envd/client_async/__init__.py b/packages/python-sdk/e2b/envd/client_async/__init__.py index 8edf17f4ce..416b327cc1 100644 --- a/packages/python-sdk/e2b/envd/client_async/__init__.py +++ b/packages/python-sdk/e2b/envd/client_async/__init__.py @@ -126,7 +126,7 @@ async def first_event( return await events.__anext__() try: return await asyncio.wait_for(events.__anext__(), request_timeout) - except (asyncio.TimeoutError, TimeoutError) as e: + except TimeoutError as e: raise _request_timeout_exception(request_timeout) from e except ConnectError as e: # wait_for's expiry cancels the __anext__ task, but connectrpc @@ -134,14 +134,13 @@ async def first_event( # before wait_for can turn it into TimeoutError. A caller cancelling # the surrounding task surfaces identically, so tell the two apart by # the pending-cancellation count: wait_for uncancels its own expiry, - # an external cancel stays pending. (Python 3.10 has no cancelling(), - # but its wait_for re-raises external cancels before this point, so - # reaching here without it means the timer expired.) - cancelling = getattr(asyncio.current_task(), "cancelling", None) + # an external cancel stays pending. + task = asyncio.current_task() if ( e.code is Code.CANCELED and isinstance(e.__cause__, asyncio.CancelledError) - and (cancelling is None or cancelling() == 0) + and task is not None + and task.cancelling() == 0 ): raise _request_timeout_exception(request_timeout) from e raise diff --git a/packages/python-sdk/e2b/paginator.py b/packages/python-sdk/e2b/paginator.py index a1a2e11309..af507001ed 100644 --- a/packages/python-sdk/e2b/paginator.py +++ b/packages/python-sdk/e2b/paginator.py @@ -1,6 +1,4 @@ -from typing import Generic, Mapping, Optional, TypeVar - -from typing_extensions import Unpack +from typing import Generic, Mapping, Optional, TypeVar, Unpack from e2b.connection_config import ApiParams diff --git a/packages/python-sdk/e2b/sandbox/_git/types.py b/packages/python-sdk/e2b/sandbox/_git/types.py index 55f4875b93..739d6042d3 100644 --- a/packages/python-sdk/e2b/sandbox/_git/types.py +++ b/packages/python-sdk/e2b/sandbox/_git/types.py @@ -1,7 +1,5 @@ from dataclasses import dataclass -from typing import List, Optional - -from typing_extensions import Literal +from typing import List, Literal, Optional GitResetMode = Literal["soft", "mixed", "hard", "merge", "keep"] """Mode for a git reset operation. diff --git a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py index af54390e46..4c0fb0db81 100644 --- a/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox/filesystem/filesystem.py @@ -240,7 +240,7 @@ async def __anext__(self) -> bytes: if self._idle_timeout: return await asyncio.wait_for(read, self._idle_timeout) return await read - except asyncio.TimeoutError as e: + except TimeoutError as e: # `wait_for`'s expiry; keep the documented httpx exception. The # transport's own idle read timeout already arrives as one. await self.aclose() diff --git a/packages/python-sdk/e2b/sandbox/sandbox_api.py b/packages/python-sdk/e2b/sandbox/sandbox_api.py index aaff962885..66199b0160 100644 --- a/packages/python-sdk/e2b/sandbox/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox/sandbox_api.py @@ -9,14 +9,14 @@ List, Literal, Mapping, + NotRequired, Optional, TypedDict, + Unpack, Union, cast, ) -from typing_extensions import NotRequired, Unpack - from e2b.api.client.models import ( ListedSandbox, SandboxDetail, diff --git a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py index e59612f926..6835bccf23 100644 --- a/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py +++ b/packages/python-sdk/e2b/sandbox_async/filesystem/filesystem.py @@ -255,7 +255,7 @@ async def read( raise await ahandle_envd_api_transport_exception_with_health( e, self._envd_health_api ) - except asyncio.TimeoutError as e: + except TimeoutError as e: # wait_for's expiry; keep the httpx exception the # streamed-read contract established. raise httpx.ReadTimeout(str(e)) from e diff --git a/packages/python-sdk/e2b/sandbox_async/main.py b/packages/python-sdk/e2b/sandbox_async/main.py index 3d0a648f50..1d32462650 100644 --- a/packages/python-sdk/e2b/sandbox_async/main.py +++ b/packages/python-sdk/e2b/sandbox_async/main.py @@ -4,12 +4,10 @@ import logging import shlex import uuid -from typing import Dict, List, Optional, Union, overload +from typing import Dict, List, Optional, Self, Unpack, Union, overload import httpx from packaging.version import Version -from typing_extensions import Self, Unpack - from e2b.api.client.types import Unset from e2b.api.client_async import get_envd_api from e2b.connection_config import ApiParams, ConnectionConfig diff --git a/packages/python-sdk/e2b/sandbox_async/paginator.py b/packages/python-sdk/e2b/sandbox_async/paginator.py index ee5af6960e..42c3b250ad 100644 --- a/packages/python-sdk/e2b/sandbox_async/paginator.py +++ b/packages/python-sdk/e2b/sandbox_async/paginator.py @@ -1,7 +1,5 @@ import urllib.parse -from typing import Optional, List - -from typing_extensions import Unpack +from typing import Optional, List, Unpack from e2b.api.client.api.sandboxes import get_v2_sandboxes from e2b.api.client.api.snapshots import get_snapshots diff --git a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py index e41f8ff397..ec30eb6d7a 100644 --- a/packages/python-sdk/e2b/sandbox_async/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_async/sandbox_api.py @@ -1,10 +1,8 @@ import datetime import logging -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Unpack, Union, cast from packaging.version import Version -from typing_extensions import Unpack - from e2b.api import ( SandboxCreateResponse, api_exception_from_code, diff --git a/packages/python-sdk/e2b/sandbox_sync/main.py b/packages/python-sdk/e2b/sandbox_sync/main.py index 32c1b671b7..4e69efacfa 100644 --- a/packages/python-sdk/e2b/sandbox_sync/main.py +++ b/packages/python-sdk/e2b/sandbox_sync/main.py @@ -3,12 +3,10 @@ import logging import shlex import uuid -from typing import Dict, List, Optional, Union, overload +from typing import Dict, List, Optional, Self, Union, Unpack, overload import httpx from packaging.version import Version -from typing_extensions import Self, Unpack - from e2b.api.client.types import Unset from e2b.api.client_sync import get_envd_api from e2b.connection_config import ApiParams, ConnectionConfig diff --git a/packages/python-sdk/e2b/sandbox_sync/paginator.py b/packages/python-sdk/e2b/sandbox_sync/paginator.py index 6538dd6f22..4ccf51c489 100644 --- a/packages/python-sdk/e2b/sandbox_sync/paginator.py +++ b/packages/python-sdk/e2b/sandbox_sync/paginator.py @@ -1,7 +1,5 @@ import urllib.parse -from typing import Optional, List - -from typing_extensions import Unpack +from typing import Optional, List, Unpack from e2b.api import handle_api_exception from e2b.api.client.api.sandboxes import get_v2_sandboxes diff --git a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py index 8f4db3e890..27e658e393 100644 --- a/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py +++ b/packages/python-sdk/e2b/sandbox_sync/sandbox_api.py @@ -1,10 +1,8 @@ import datetime import logging -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union, Unpack, cast from packaging.version import Version -from typing_extensions import Unpack - from e2b.api import ( SandboxCreateResponse, api_exception_from_code, diff --git a/packages/python-sdk/e2b/secret/secret_async.py b/packages/python-sdk/e2b/secret/secret_async.py index f0e3cdd323..718e0e1e31 100644 --- a/packages/python-sdk/e2b/secret/secret_async.py +++ b/packages/python-sdk/e2b/secret/secret_async.py @@ -1,6 +1,4 @@ -from typing import Dict, List, Optional - -from typing_extensions import Unpack +from typing import Dict, List, Optional, Unpack from e2b.api import handle_api_exception from e2b.api.client.api.secrets import ( diff --git a/packages/python-sdk/e2b/secret/secret_sync.py b/packages/python-sdk/e2b/secret/secret_sync.py index 908936112f..9fc1673794 100644 --- a/packages/python-sdk/e2b/secret/secret_sync.py +++ b/packages/python-sdk/e2b/secret/secret_sync.py @@ -1,6 +1,4 @@ -from typing import Dict, List, Optional - -from typing_extensions import Unpack +from typing import Dict, List, Optional, Unpack from e2b.api import handle_api_exception from e2b.api.client.api.secrets import ( diff --git a/packages/python-sdk/e2b/template/types.py b/packages/python-sdk/e2b/template/types.py index ca2c79c73f..a8458fe194 100644 --- a/packages/python-sdk/e2b/template/types.py +++ b/packages/python-sdk/e2b/template/types.py @@ -2,9 +2,7 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import List, Literal, Optional, TypedDict, Union - -from typing_extensions import NotRequired +from typing import List, Literal, NotRequired, Optional, TypedDict, Union from e2b.template.logger import LogEntry diff --git a/packages/python-sdk/e2b/template_async/main.py b/packages/python-sdk/e2b/template_async/main.py index 3672748a3e..a8ef1104eb 100644 --- a/packages/python-sdk/e2b/template_async/main.py +++ b/packages/python-sdk/e2b/template_async/main.py @@ -1,7 +1,5 @@ from datetime import datetime -from typing import Callable, List, Optional, Union - -from typing_extensions import Unpack +from typing import Callable, List, Optional, Union, Unpack from e2b.api.client.client import AuthenticatedClient from e2b.api.client.types import Unset diff --git a/packages/python-sdk/e2b/template_sync/main.py b/packages/python-sdk/e2b/template_sync/main.py index f42bb5c3b8..98e62284da 100644 --- a/packages/python-sdk/e2b/template_sync/main.py +++ b/packages/python-sdk/e2b/template_sync/main.py @@ -1,7 +1,5 @@ from datetime import datetime -from typing import Callable, List, Optional, Union - -from typing_extensions import Unpack +from typing import Callable, List, Optional, Union, Unpack from e2b.api.client.client import AuthenticatedClient from e2b.api.client.types import Unset diff --git a/packages/python-sdk/e2b/volume/connection_config.py b/packages/python-sdk/e2b/volume/connection_config.py index e636a208da..069dffb13d 100644 --- a/packages/python-sdk/e2b/volume/connection_config.py +++ b/packages/python-sdk/e2b/volume/connection_config.py @@ -1,9 +1,7 @@ import logging import os -from typing import Dict, Optional, TypedDict - -from typing_extensions import Unpack +from typing import Dict, Optional, TypedDict, Unpack from e2b.api.metadata import package_version from e2b.connection_config import HttpVersion, ProxyTypes, resolve_http_version diff --git a/packages/python-sdk/e2b/volume/volume_async.py b/packages/python-sdk/e2b/volume/volume_async.py index ec7034a853..5a7a22b734 100644 --- a/packages/python-sdk/e2b/volume/volume_async.py +++ b/packages/python-sdk/e2b/volume/volume_async.py @@ -1,12 +1,21 @@ import asyncio -from typing import AsyncIterator, IO, List, Literal, Optional, Union, cast, overload +from typing import ( + AsyncIterator, + IO, + List, + Literal, + Optional, + Self, + Union, + Unpack, + cast, + overload, +) from http import HTTPStatus import httpx -from typing_extensions import Self, Unpack - from e2b.api import handle_api_exception from e2b.api.client.api.volumes import ( post_volumes, @@ -583,7 +592,7 @@ async def stream_file() -> AsyncIterator[bytes]: yield chunk finally: await stream_cm.__aexit__(None, None, None) - except asyncio.TimeoutError as e: + except TimeoutError as e: raise httpx.ReadTimeout(str(e)) from e return stream_file() diff --git a/packages/python-sdk/e2b/volume/volume_sync.py b/packages/python-sdk/e2b/volume/volume_sync.py index 187fe6aed8..45b95c6546 100644 --- a/packages/python-sdk/e2b/volume/volume_sync.py +++ b/packages/python-sdk/e2b/volume/volume_sync.py @@ -1,11 +1,20 @@ import io -from typing import IO, Iterator, List, Literal, Optional, Union, cast, overload +from typing import ( + IO, + Iterator, + List, + Literal, + Optional, + Self, + Union, + Unpack, + cast, + overload, +) from http import HTTPStatus import httpx -from typing_extensions import Self, Unpack - from e2b.api import handle_api_exception from e2b.api.client.api.volumes import ( post_volumes, From 2fb3e8e1674b24ea076e2752644fd6225db8b90d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:48:05 +0000 Subject: [PATCH 2/2] fix(python-sdk): compare first_event cancellation count against its entry value Co-Authored-By: mish@e2b.dev --- .../python-sdk/e2b/envd/client_async/__init__.py | 13 ++++++++----- .../tests/test_envd_stream_request_timeout.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/python-sdk/e2b/envd/client_async/__init__.py b/packages/python-sdk/e2b/envd/client_async/__init__.py index 416b327cc1..00f2ce4bab 100644 --- a/packages/python-sdk/e2b/envd/client_async/__init__.py +++ b/packages/python-sdk/e2b/envd/client_async/__init__.py @@ -124,6 +124,8 @@ async def first_event( """ if not request_timeout: return await events.__anext__() + task = asyncio.current_task() + cancelling_before = task.cancelling() if task is not None else 0 try: return await asyncio.wait_for(events.__anext__(), request_timeout) except TimeoutError as e: @@ -132,15 +134,16 @@ async def first_event( # wait_for's expiry cancels the __anext__ task, but connectrpc # converts the delivered CancelledError into ConnectError(CANCELED) # before wait_for can turn it into TimeoutError. A caller cancelling - # the surrounding task surfaces identically, so tell the two apart by - # the pending-cancellation count: wait_for uncancels its own expiry, - # an external cancel stays pending. - task = asyncio.current_task() + # the surrounding task surfaces identically, so compare the pending- + # cancellation count with its entry value: wait_for uncancels its own + # expiry back to that value, while an external cancel leaves it above + # it. Comparing against zero would misclassify a timer expiry when the + # caller had an already-pending, swallowed cancellation. if ( e.code is Code.CANCELED and isinstance(e.__cause__, asyncio.CancelledError) and task is not None - and task.cancelling() == 0 + and task.cancelling() == cancelling_before ): raise _request_timeout_exception(request_timeout) from e raise diff --git a/packages/python-sdk/tests/test_envd_stream_request_timeout.py b/packages/python-sdk/tests/test_envd_stream_request_timeout.py index 82c829ef70..64ddc9f32e 100644 --- a/packages/python-sdk/tests/test_envd_stream_request_timeout.py +++ b/packages/python-sdk/tests/test_envd_stream_request_timeout.py @@ -55,6 +55,21 @@ async def test_timeout_raises_timeout_exception(): assert time.monotonic() - start < 5 +async def test_timeout_with_preexisting_pending_cancellation(): + task = asyncio.current_task() + assert task is not None + task.cancel() + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + + start = time.monotonic() + with pytest.raises(TimeoutException, match="request_timeout"): + await first_event(connectrpc_style_stream(30), 0.1) + assert time.monotonic() - start < 5 + + async def test_timeout_when_cancellation_propagates(): # A stream cancelled outside connectrpc's conversion scope (e.g. before # the request is sent) lets wait_for raise TimeoutError itself.