Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/quiet-tigers-remove.md
Original file line number Diff line number Diff line change
@@ -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).
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 1 addition & 3 deletions packages/desktop-python/e2b_desktop/client.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
14 changes: 12 additions & 2 deletions packages/desktop-python/e2b_desktop/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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}

Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 11 additions & 3 deletions packages/python-sdk/e2b/connection_config.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
18 changes: 10 additions & 8 deletions packages/python-sdk/e2b/envd/client_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,24 +124,26 @@ 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 (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
# 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. (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)
# 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 (cancelling is None or cancelling() == 0)
and task is not None
and task.cancelling() == cancelling_before
):
raise _request_timeout_exception(request_timeout) from e
raise
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/paginator.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/sandbox/_git/types.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/python-sdk/e2b/sandbox/filesystem/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions packages/python-sdk/e2b/sandbox/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/sandbox_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/sandbox_async/paginator.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/sandbox_async/sandbox_api.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/sandbox_sync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/sandbox_sync/paginator.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/sandbox_sync/sandbox_api.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/secret/secret_async.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/secret/secret_sync.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/template/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/template_async/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/template_sync/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 1 addition & 3 deletions packages/python-sdk/e2b/volume/connection_config.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
17 changes: 13 additions & 4 deletions packages/python-sdk/e2b/volume/volume_async.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 12 additions & 3 deletions packages/python-sdk/e2b/volume/volume_sync.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
15 changes: 15 additions & 0 deletions packages/python-sdk/tests/test_envd_stream_request_timeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading