Skip to content

Commit ed08d1b

Browse files
committed
Beta submodule in Python SDK
1 parent fe3d37d commit ed08d1b

23 files changed

Lines changed: 1124 additions & 152 deletions

File tree

packages/python-sdk/e2b/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
NotEnoughSpaceException,
4343
TemplateException,
4444
)
45-
from .sandbox.sandbox_api import SandboxInfo, SandboxMetrics
45+
from .sandbox.sandbox_api import SandboxInfo, SandboxQuery, SandboxState, SandboxMetrics
4646
from .sandbox.commands.main import ProcessInfo
4747
from .sandbox.commands.command_handle import (
4848
CommandResult,
@@ -61,11 +61,13 @@
6161
from .sandbox_sync.main import Sandbox
6262
from .sandbox_sync.filesystem.watch_handle import WatchHandle
6363
from .sandbox_sync.commands.command_handle import CommandHandle
64+
from .sandbox_async.paginator import AsyncSandboxPaginator
6465

6566
from .sandbox_async.utils import OutputHandler
6667
from .sandbox_async.main import AsyncSandbox
6768
from .sandbox_async.filesystem.watch_handle import AsyncWatchHandle
6869
from .sandbox_async.commands.command_handle import AsyncCommandHandle
70+
from .sandbox_sync.paginator import SandboxPaginator
6971

7072
__all__ = [
7173
# API
@@ -86,6 +88,9 @@
8688
"SandboxInfo",
8789
"SandboxMetrics",
8890
"ProcessInfo",
91+
"SandboxQuery",
92+
"SandboxState",
93+
"SandboxMetrics",
8994
# Command handle
9095
"CommandResult",
9196
"Stderr",
@@ -101,10 +106,12 @@
101106
"FileType",
102107
# Sync sandbox
103108
"Sandbox",
109+
"SandboxPaginator",
104110
"WatchHandle",
105111
"CommandHandle",
106112
# Async sandbox
107113
"OutputHandler",
114+
"AsyncSandboxPaginator",
108115
"AsyncSandbox",
109116
"AsyncWatchHandle",
110117
"AsyncCommandHandle",

packages/python-sdk/e2b/api/client/api/sandboxes/get_v2_sandboxes.py

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/python-sdk/e2b/api/client/models/sandbox_detail.py

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/python-sdk/e2b/sandbox/sandbox_api.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from dataclasses import dataclass
2-
from typing import Optional, Dict, Union
2+
from typing import Optional, Dict, Union, Unpack
33
from datetime import datetime
44

5+
from e2b import ConnectionConfig
56
from e2b.api.client.models import SandboxState, SandboxDetail, ListedSandbox
7+
from e2b.connection_config import ApiParams
68

79

810
@dataclass
@@ -84,6 +86,9 @@ class SandboxQuery:
8486
metadata: Optional[dict[str, str]] = None
8587
"""Filter sandboxes by metadata."""
8688

89+
state: Optional[list[SandboxState]] = None
90+
"""Filter sandboxes by state."""
91+
8792

8893
@dataclass
8994
class SandboxMetrics:
@@ -103,3 +108,34 @@ class SandboxMetrics:
103108
"""Memory used in bytes."""
104109
timestamp: datetime
105110
"""Timestamp of the metric entry."""
111+
112+
113+
class SandboxPaginatorBase:
114+
def __init__(
115+
self,
116+
query: Optional[SandboxQuery] = None,
117+
limit: Optional[int] = None,
118+
next_token: Optional[str] = None,
119+
**opts: Unpack[ApiParams],
120+
):
121+
self._config = ConnectionConfig(**opts)
122+
123+
self.query = query
124+
self.limit = limit
125+
126+
self._has_next = True
127+
self._next_token = next_token
128+
129+
@property
130+
def has_next(self) -> bool:
131+
"""
132+
Returns True if there are more items to fetch.
133+
"""
134+
return self._has_next
135+
136+
@property
137+
def next_token(self) -> Optional[str]:
138+
"""
139+
Returns the next token to use for pagination.
140+
"""
141+
return self._next_token

packages/python-sdk/e2b/sandbox_async/main.py

Lines changed: 139 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
import logging
33
import httpx
44

5-
from typing import Dict, Optional, TypedDict, overload, List
5+
from typing import (
6+
Dict,
7+
Optional,
8+
TypedDict,
9+
overload,
10+
List,
11+
Type,
12+
)
613

714
from packaging.version import Version
815
from typing_extensions import Unpack
@@ -16,7 +23,7 @@
1623
from e2b.sandbox_async.filesystem.filesystem import Filesystem
1724
from e2b.sandbox_async.commands.command import Commands
1825
from e2b.sandbox_async.commands.pty import Pty
19-
from e2b.sandbox_async.sandbox_api import SandboxApi, SandboxInfo
26+
from e2b.sandbox_async.sandbox_api import SandboxApi, SandboxInfo, SandboxApiBeta
2027

2128
logger = logging.getLogger(__name__)
2229

@@ -45,7 +52,127 @@ class AsyncSandboxOpts(TypedDict):
4552
connection_config: ConnectionConfig
4653

4754

48-
class AsyncSandbox(SandboxApi):
55+
class _Beta(SandboxApiBeta):
56+
def __init__(self, sandbox: "AsyncSandbox"):
57+
self._sandbox = sandbox
58+
59+
@overload
60+
async def pause(
61+
self,
62+
**opts: Unpack[ApiParams],
63+
) -> str:
64+
"""
65+
Pause the sandbox.
66+
67+
:return: Sandbox ID that can be used to resume the sandbox
68+
"""
69+
...
70+
71+
@overload
72+
@staticmethod
73+
async def pause(
74+
sandbox_id: str,
75+
**opts: Unpack[ApiParams],
76+
) -> str:
77+
"""
78+
Pause the sandbox specified by sandbox ID.
79+
80+
:param sandbox_id: Sandbox ID
81+
82+
:return: Sandbox ID that can be used to resume the sandbox
83+
"""
84+
...
85+
86+
@class_method_variant("_cls_pause")
87+
async def pause(
88+
self,
89+
**opts: Unpack[ApiParams],
90+
) -> str:
91+
"""
92+
Pause the sandbox.
93+
94+
:param request_timeout: Timeout for the request in **seconds**
95+
96+
:return: Sandbox ID that can be used to resume the sandbox
97+
"""
98+
99+
await self._cls_pause(
100+
sandbox_id=self._sandbox.sandbox_id,
101+
**opts,
102+
)
103+
104+
return self._sandbox.sandbox_id
105+
106+
@overload
107+
async def resume(
108+
self,
109+
timeout: Optional[int] = None,
110+
**opts: Unpack[ApiParams],
111+
) -> "AsyncSandbox":
112+
"""
113+
Resume the sandbox.
114+
115+
:return: A running sandbox instance
116+
"""
117+
...
118+
119+
@overload
120+
@staticmethod
121+
async def resume(
122+
sandbox_id: str,
123+
timeout: Optional[int] = None,
124+
**opts: Unpack[ApiParams],
125+
) -> "AsyncSandbox":
126+
"""
127+
Resume the sandbox.
128+
129+
:param sandbox_id: Sandbox ID
130+
:param timeout: Timeout for the sandbox in **seconds**
131+
132+
:return: A running sandbox instance
133+
"""
134+
...
135+
136+
@class_method_variant("_cls_resume")
137+
async def resume(
138+
self,
139+
timeout: Optional[int] = None,
140+
**opts: Unpack[ApiParams],
141+
) -> "AsyncSandbox":
142+
"""
143+
Resume the sandbox.
144+
145+
The **default sandbox timeout of 300 seconds** will be used for the resumed sandbox.
146+
If you pass a custom timeout via the `timeout` parameter, it will be used instead.
147+
148+
:param timeout: Timeout for the sandbox in **seconds**
149+
150+
:return: A running sandbox instance
151+
"""
152+
153+
print(timeout)
154+
await self._cls_resume(
155+
sandbox_id=self._sandbox.sandbox_id,
156+
timeout=timeout,
157+
**opts,
158+
)
159+
160+
return await self._sandbox.connect(
161+
sandbox_id=self._sandbox.sandbox_id,
162+
**opts,
163+
)
164+
165+
166+
class _AsyncSandboxMeta(type):
167+
"""Metaclass for AsyncSandbox to provide class-level beta access."""
168+
169+
@property
170+
def beta(cls) -> Type[_Beta]:
171+
"""Access to beta features at class level."""
172+
return _Beta
173+
174+
175+
class AsyncSandbox(SandboxApi, metaclass=_AsyncSandboxMeta):
49176
"""
50177
E2B cloud sandbox is a secure and isolated cloud environment.
51178
@@ -89,6 +216,13 @@ def pty(self) -> Pty:
89216
"""
90217
return self._pty
91218

219+
@property
220+
def beta(self) -> _Beta:
221+
"""
222+
Module for beta features.
223+
"""
224+
return self._beta
225+
92226
def __init__(self, **opts: Unpack[AsyncSandboxOpts]):
93227
"""
94228
Use `AsyncSandbox.create()` to create a new sandbox instead.
@@ -120,6 +254,7 @@ def __init__(self, **opts: Unpack[AsyncSandboxOpts]):
120254
self.connection_config,
121255
self._transport.pool,
122256
)
257+
self._beta = _Beta(self)
123258

124259
async def is_running(self, request_timeout: Optional[float] = None) -> bool:
125260
"""
@@ -265,7 +400,7 @@ async def connect(
265400
sandbox_id=sandbox_id,
266401
sandbox_domain=response.sandbox_domain,
267402
connection_config=connection_config,
268-
envd_version=response.envd_version,
403+
envd_version=response._envd_version,
269404
envd_access_token=envd_access_token,
270405
)
271406

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import urllib.parse
2+
from typing import Optional, List
3+
4+
from e2b.api.client.api.sandboxes import get_v2_sandboxes
5+
from e2b.api.client.types import UNSET
6+
from e2b.exceptions import SandboxException
7+
from e2b.sandbox.main import SandboxBase
8+
from e2b.sandbox.sandbox_api import SandboxPaginatorBase, SandboxInfo
9+
from e2b.api import AsyncApiClient, handle_api_exception
10+
from e2b.api.client.models.error import Error
11+
12+
13+
class AsyncSandboxPaginator(SandboxPaginatorBase):
14+
"""
15+
Paginator for listing sandboxes.
16+
17+
Example:
18+
```python
19+
paginator = AsyncSandbox.list()
20+
21+
while paginator.has_next:
22+
sandboxes = await paginator.next_items()
23+
print(sandboxes)
24+
```
25+
"""
26+
27+
async def next_items(self) -> List[SandboxInfo]:
28+
"""
29+
Returns the next page of sandboxes.
30+
31+
Call this method only if `has_next` is `True`, otherwise it will raise an exception.
32+
33+
:returns: List of sandboxes
34+
"""
35+
if not self.has_next:
36+
raise Exception("No more items to fetch")
37+
38+
# Convert filters to the format expected by the API
39+
metadata: Optional[str] = None
40+
if self.query and self.query.metadata:
41+
quoted_metadata = {
42+
urllib.parse.quote(k): urllib.parse.quote(v)
43+
for k, v in self.query.metadata.items()
44+
}
45+
metadata = urllib.parse.urlencode(quoted_metadata)
46+
47+
async with AsyncApiClient(
48+
self._config,
49+
limits=SandboxBase._limits,
50+
) as api_client:
51+
res = await get_v2_sandboxes.asyncio_detailed(
52+
client=api_client,
53+
metadata=metadata if metadata else UNSET,
54+
state=self.query.state if self.query and self.query.state else UNSET,
55+
limit=self.limit if self.limit else UNSET,
56+
next_token=self._next_token if self._next_token else UNSET,
57+
)
58+
59+
if res.status_code >= 300:
60+
raise handle_api_exception(res)
61+
62+
self._next_token = res.headers.get("x-next-token")
63+
self._has_next = bool(self._next_token)
64+
65+
if res.parsed is None:
66+
return []
67+
68+
# Check if res.parse is Error
69+
if isinstance(res.parsed, Error):
70+
raise SandboxException(f"{res.parsed.message}: Request failed")
71+
72+
return [SandboxInfo._from_listed_sandbox(sandbox) for sandbox in res.parsed]

0 commit comments

Comments
 (0)