blaxel 0.2.36__py3-none-any.whl → 0.2.38__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. blaxel/__init__.py +2 -2
  2. blaxel/core/client/models/create_job_execution_request_env.py +3 -3
  3. blaxel/core/client/models/preview.py +48 -1
  4. blaxel/core/client/models/sandbox.py +10 -0
  5. blaxel/core/jobs/__init__.py +2 -2
  6. blaxel/core/sandbox/__init__.py +12 -0
  7. blaxel/core/sandbox/client/api/system/__init__.py +0 -0
  8. blaxel/core/sandbox/client/api/system/get_health.py +134 -0
  9. blaxel/core/sandbox/client/api/system/post_upgrade.py +196 -0
  10. blaxel/core/sandbox/client/models/__init__.py +8 -0
  11. blaxel/core/sandbox/client/models/content_search_match.py +24 -25
  12. blaxel/core/sandbox/client/models/content_search_response.py +25 -29
  13. blaxel/core/sandbox/client/models/find_match.py +13 -14
  14. blaxel/core/sandbox/client/models/find_response.py +21 -24
  15. blaxel/core/sandbox/client/models/fuzzy_search_match.py +17 -19
  16. blaxel/core/sandbox/client/models/fuzzy_search_response.py +21 -24
  17. blaxel/core/sandbox/client/models/health_response.py +159 -0
  18. blaxel/core/sandbox/client/models/process_upgrade_state.py +20 -0
  19. blaxel/core/sandbox/client/models/upgrade_request.py +71 -0
  20. blaxel/core/sandbox/client/models/upgrade_status.py +125 -0
  21. blaxel/core/sandbox/default/__init__.py +2 -0
  22. blaxel/core/sandbox/default/filesystem.py +20 -6
  23. blaxel/core/sandbox/default/preview.py +48 -1
  24. blaxel/core/sandbox/default/process.py +66 -21
  25. blaxel/core/sandbox/default/sandbox.py +36 -5
  26. blaxel/core/sandbox/default/system.py +71 -0
  27. blaxel/core/sandbox/sync/__init__.py +2 -0
  28. blaxel/core/sandbox/sync/filesystem.py +19 -2
  29. blaxel/core/sandbox/sync/preview.py +50 -3
  30. blaxel/core/sandbox/sync/process.py +38 -15
  31. blaxel/core/sandbox/sync/sandbox.py +29 -4
  32. blaxel/core/sandbox/sync/system.py +71 -0
  33. blaxel/core/sandbox/types.py +212 -5
  34. blaxel/core/tools/__init__.py +4 -0
  35. blaxel/core/volume/volume.py +10 -0
  36. blaxel/crewai/model.py +81 -44
  37. blaxel/crewai/tools.py +85 -2
  38. blaxel/googleadk/model.py +22 -3
  39. blaxel/googleadk/tools.py +25 -6
  40. blaxel/langgraph/custom/gemini.py +19 -12
  41. blaxel/langgraph/model.py +26 -18
  42. blaxel/langgraph/tools.py +6 -12
  43. blaxel/livekit/model.py +7 -2
  44. blaxel/livekit/tools.py +3 -1
  45. blaxel/llamaindex/model.py +145 -84
  46. blaxel/llamaindex/tools.py +6 -4
  47. blaxel/openai/model.py +7 -1
  48. blaxel/openai/tools.py +13 -3
  49. blaxel/pydantic/model.py +38 -24
  50. blaxel/pydantic/tools.py +37 -4
  51. blaxel-0.2.38.dist-info/METADATA +528 -0
  52. {blaxel-0.2.36.dist-info → blaxel-0.2.38.dist-info}/RECORD +54 -45
  53. blaxel-0.2.36.dist-info/METADATA +0 -228
  54. {blaxel-0.2.36.dist-info → blaxel-0.2.38.dist-info}/WHEEL +0 -0
  55. {blaxel-0.2.36.dist-info → blaxel-0.2.38.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,71 @@
1
+ from typing import Union
2
+
3
+ from ...common.settings import settings
4
+ from ..client.api.system.get_health import asyncio as get_health
5
+ from ..client.api.system.post_upgrade import asyncio as post_upgrade
6
+ from ..client.client import Client
7
+ from ..client.models import ErrorResponse, HealthResponse, SuccessResponse, UpgradeRequest
8
+ from ..types import SandboxConfiguration
9
+ from .action import SandboxAction
10
+
11
+
12
+ class SandboxSystem(SandboxAction):
13
+ """System operations for sandbox including upgrade functionality."""
14
+
15
+ def __init__(self, sandbox_config: SandboxConfiguration):
16
+ super().__init__(sandbox_config)
17
+
18
+ async def upgrade(
19
+ self,
20
+ version: str | None = None,
21
+ base_url: str | None = None,
22
+ ) -> SuccessResponse:
23
+ """Upgrade the sandbox-api to a new version.
24
+
25
+ Triggers an upgrade of the sandbox-api process. Returns immediately before upgrading.
26
+ The upgrade will: download the specified binary from GitHub releases, validate it, and restart.
27
+ All running processes will be preserved across the upgrade.
28
+
29
+ Args:
30
+ version: Version to upgrade to - "develop" (default), "main", "latest",
31
+ or specific tag like "v1.0.0"
32
+ base_url: Base URL for releases (useful for forks, defaults to
33
+ https://github.com/blaxel-ai/sandbox/releases)
34
+
35
+ Returns:
36
+ SuccessResponse with status information
37
+ """
38
+ request = UpgradeRequest(version=version, base_url=base_url)
39
+
40
+ client = Client(
41
+ base_url=self.url,
42
+ headers={**settings.headers, **self.sandbox_config.headers},
43
+ )
44
+
45
+ async with client:
46
+ response = await post_upgrade(client=client, body=request)
47
+ if response is None:
48
+ raise Exception("Failed to upgrade sandbox")
49
+ if isinstance(response, ErrorResponse):
50
+ raise Exception(f"Upgrade failed: {response.error}")
51
+ return response
52
+
53
+ async def health(self) -> HealthResponse:
54
+ """Get health status and system information.
55
+
56
+ Returns health status and system information including upgrade count and binary details.
57
+ Also includes last upgrade attempt status with detailed error information if available.
58
+
59
+ Returns:
60
+ HealthResponse with system status information
61
+ """
62
+ client = Client(
63
+ base_url=self.url,
64
+ headers={**settings.headers, **self.sandbox_config.headers},
65
+ )
66
+
67
+ async with client:
68
+ response = await get_health(client=client)
69
+ if response is None:
70
+ raise Exception("Failed to get health status")
71
+ return response
@@ -6,6 +6,7 @@ from .sandbox import (
6
6
  SyncSandboxPreviews,
7
7
  SyncSandboxProcess,
8
8
  )
9
+ from .system import SyncSandboxSystem
9
10
 
10
11
  __all__ = [
11
12
  "SyncSandboxInstance",
@@ -13,5 +14,6 @@ __all__ = [
13
14
  "SyncSandboxPreviews",
14
15
  "SyncSandboxProcess",
15
16
  "SyncSandboxCodegen",
17
+ "SyncSandboxSystem",
16
18
  "SyncCodeInterpreter",
17
19
  ]
@@ -14,6 +14,7 @@ from ..types import (
14
14
  SandboxConfiguration,
15
15
  SandboxFilesystemFile,
16
16
  WatchEvent,
17
+ WatchHandle,
17
18
  )
18
19
  from .action import SyncSandboxAction
19
20
 
@@ -162,7 +163,23 @@ class SyncSandboxFileSystem(SyncSandboxAction):
162
163
  path: str,
163
164
  callback: Callable[[WatchEvent], None],
164
165
  options: Dict[str, Any] | None = None,
165
- ) -> Dict[str, Callable]:
166
+ ) -> WatchHandle:
167
+ """Watch for file system changes.
168
+
169
+ Returns a WatchHandle that can be used as a context manager:
170
+
171
+ with sandbox.fs.watch(path, callback) as handle:
172
+ # do something
173
+ # handle is automatically closed
174
+
175
+ Or manually:
176
+
177
+ handle = sandbox.fs.watch(path, callback)
178
+ try:
179
+ # do something
180
+ finally:
181
+ handle.close()
182
+ """
166
183
  path = self.format_path(path)
167
184
  closed = threading.Event()
168
185
  if options is None:
@@ -226,7 +243,7 @@ class SyncSandboxFileSystem(SyncSandboxAction):
226
243
  def close():
227
244
  closed.set()
228
245
 
229
- return {"close": close}
246
+ return WatchHandle(close)
230
247
 
231
248
  def format_path(self, path: str) -> str:
232
249
  return path
@@ -1,3 +1,4 @@
1
+ import time
1
2
  from dataclasses import dataclass
2
3
  from datetime import datetime
3
4
  from typing import Any, Dict, List, Union
@@ -11,6 +12,9 @@ from ...client.api.compute.delete_sandbox_preview import sync as delete_sandbox_
11
12
  from ...client.api.compute.delete_sandbox_preview_token import (
12
13
  sync as delete_sandbox_preview_token,
13
14
  )
15
+ from ...client.api.compute.get_sandbox_preview import (
16
+ sync_detailed as get_sandbox_preview_detailed,
17
+ )
14
18
  from ...client.api.compute.get_sandbox_preview import sync as get_sandbox_preview
15
19
  from ...client.api.compute.list_sandbox_preview_tokens import (
16
20
  sync as list_sandbox_preview_tokens,
@@ -188,9 +192,52 @@ class SyncSandboxPreviews:
188
192
  preview_name,
189
193
  client=client,
190
194
  )
191
- if response:
192
- return response
193
- raise errors.UnexpectedStatus(400, b"Failed to delete preview")
195
+ if not response:
196
+ raise errors.UnexpectedStatus(400, b"Failed to delete preview")
197
+
198
+ # If the preview is in DELETING state, wait for it to be fully deleted
199
+ if response.status == "DELETING":
200
+ self._wait_for_deletion(preview_name)
201
+
202
+ return response
203
+
204
+ def _wait_for_deletion(self, preview_name: str, timeout_ms: int = 10000) -> None:
205
+ """Wait for a preview to be fully deleted.
206
+
207
+ Args:
208
+ preview_name: Name of the preview to wait for
209
+ timeout_ms: Timeout in milliseconds (default: 10000)
210
+
211
+ Raises:
212
+ Exception: If the preview is still in DELETING state after timeout
213
+ """
214
+ print(f"Waiting for preview deletion: {preview_name}")
215
+ poll_interval = 0.5 # Poll every 500ms
216
+ elapsed = 0.0
217
+ timeout_seconds = timeout_ms / 1000.0
218
+
219
+ while elapsed < timeout_seconds:
220
+ try:
221
+ response = get_sandbox_preview_detailed(
222
+ self.sandbox_name,
223
+ preview_name,
224
+ client=client,
225
+ )
226
+ if response.status_code == 404:
227
+ return
228
+ except errors.UnexpectedStatus as e:
229
+ # 404 means the preview is deleted
230
+ if e.status_code == 404:
231
+ return
232
+ raise
233
+ # Preview still exists, wait and retry
234
+ time.sleep(poll_interval)
235
+ elapsed += poll_interval
236
+
237
+ # Timeout reached, but deletion was initiated
238
+ raise Exception(
239
+ f"Preview deletion timeout: {preview_name} is still in DELETING state after {timeout_ms}ms"
240
+ )
194
241
 
195
242
 
196
243
  def to_utc_z(dt: datetime) -> str:
@@ -7,7 +7,12 @@ import httpx
7
7
  from ...common.settings import settings
8
8
  from ..client.models import ProcessResponse, SuccessResponse
9
9
  from ..client.models.process_request import ProcessRequest
10
- from ..types import ProcessRequestWithLog, ProcessResponseWithLog, SandboxConfiguration
10
+ from ..types import (
11
+ ProcessRequestWithLog,
12
+ ProcessResponseWithLog,
13
+ SandboxConfiguration,
14
+ StreamHandle,
15
+ )
11
16
  from .action import SyncSandboxAction
12
17
 
13
18
 
@@ -19,19 +24,35 @@ class SyncSandboxProcess(SyncSandboxAction):
19
24
  self,
20
25
  process_name: str,
21
26
  options: Dict[str, Callable[[str], None]] | None = None,
22
- ) -> Dict[str, Callable[[], None]]:
27
+ ) -> StreamHandle:
28
+ """Stream logs from a process with automatic reconnection and deduplication.
29
+
30
+ Returns a StreamHandle that can be used as a context manager:
31
+
32
+ with sandbox.process.stream_logs(name, options) as handle:
33
+ # do something
34
+ # handle is automatically closed
35
+
36
+ Or manually:
37
+
38
+ handle = sandbox.process.stream_logs(name, options)
39
+ try:
40
+ # do something
41
+ finally:
42
+ handle.close()
43
+ """
23
44
  if options is None:
24
45
  options = {}
25
46
  reconnect_interval = 30
26
47
  is_running = threading.Event()
27
48
  is_running.set()
28
49
  seen_logs = set()
29
- current_close = {"fn": None}
50
+ current_stream: StreamHandle | None = None
30
51
  timer_lock = threading.Lock()
31
- reconnect_timer = {"t": None}
52
+ reconnect_timer: dict[str, threading.Timer | None] = {"t": None}
32
53
 
33
54
  def start_stream():
34
- nonlocal current_close
55
+ nonlocal current_stream
35
56
  log_counter = [0]
36
57
 
37
58
  def make_dedup(cb_key: str):
@@ -52,9 +73,9 @@ class SyncSandboxProcess(SyncSandboxAction):
52
73
  wrapped_options["on_stdout"] = make_dedup("on_stdout")
53
74
  if "on_stderr" in options:
54
75
  wrapped_options["on_stderr"] = make_dedup("on_stderr")
55
- if current_close["fn"]:
56
- current_close["fn"]()
57
- current_close["fn"] = self._stream_logs(process_name, wrapped_options)["close"]
76
+ if current_stream:
77
+ current_stream.close()
78
+ current_stream = self._stream_logs(process_name, wrapped_options)
58
79
 
59
80
  def schedule():
60
81
  if is_running.is_set():
@@ -71,23 +92,25 @@ class SyncSandboxProcess(SyncSandboxAction):
71
92
  start_stream()
72
93
 
73
94
  def close():
95
+ nonlocal current_stream
74
96
  is_running.clear()
75
97
  with timer_lock:
76
98
  if reconnect_timer["t"]:
77
99
  reconnect_timer["t"].cancel()
78
100
  reconnect_timer["t"] = None
79
- if current_close["fn"]:
80
- current_close["fn"]()
81
- current_close["fn"] = None
101
+ if current_stream:
102
+ current_stream.close()
103
+ current_stream = None
82
104
  seen_logs.clear()
83
105
 
84
- return {"close": close}
106
+ return StreamHandle(close)
85
107
 
86
108
  def _stream_logs(
87
109
  self,
88
110
  identifier: str,
89
111
  options: Dict[str, Callable[[str], None]] | None = None,
90
- ) -> Dict[str, Callable[[], None]]:
112
+ ) -> StreamHandle:
113
+ """Private method to stream logs from a process with callbacks for different output types."""
91
114
  if options is None:
92
115
  options = {}
93
116
  closed = threading.Event()
@@ -136,7 +159,7 @@ class SyncSandboxProcess(SyncSandboxAction):
136
159
  def close():
137
160
  closed.set()
138
161
 
139
- return {"close": close}
162
+ return StreamHandle(close)
140
163
 
141
164
  def exec(
142
165
  self,
@@ -191,7 +214,7 @@ class SyncSandboxProcess(SyncSandboxAction):
191
214
  )
192
215
  return ProcessResponseWithLog(
193
216
  result,
194
- lambda: stream_control["close"]() if stream_control else None,
217
+ lambda: stream_control.close() if stream_control else None,
195
218
  )
196
219
 
197
220
  return result
@@ -8,7 +8,13 @@ from ...client.api.compute.get_sandbox import sync as get_sandbox
8
8
  from ...client.api.compute.list_sandboxes import sync as list_sandboxes
9
9
  from ...client.api.compute.update_sandbox import sync as update_sandbox
10
10
  from ...client.client import client
11
- from ...client.models import Metadata, Sandbox, SandboxLifecycle, SandboxRuntime, SandboxSpec
11
+ from ...client.models import (
12
+ Metadata,
13
+ Sandbox,
14
+ SandboxLifecycle,
15
+ SandboxRuntime,
16
+ SandboxSpec,
17
+ )
12
18
  from ...client.models.error import Error
13
19
  from ...client.models.sandbox_error import SandboxError
14
20
  from ...client.types import UNSET
@@ -26,6 +32,7 @@ from .network import SyncSandboxNetwork
26
32
  from .preview import SyncSandboxPreviews
27
33
  from .process import SyncSandboxProcess
28
34
  from .session import SyncSandboxSessions
35
+ from .system import SyncSandboxSystem
29
36
 
30
37
  logger = logging.getLogger(__name__)
31
38
 
@@ -73,6 +80,7 @@ class SyncSandboxInstance:
73
80
  self.sessions = SyncSandboxSessions(self.config)
74
81
  self.network = SyncSandboxNetwork(self.config)
75
82
  self.codegen = SyncSandboxCodegen(self.config)
83
+ self.system = SyncSandboxSystem(self.config)
76
84
 
77
85
  @property
78
86
  def metadata(self):
@@ -90,6 +98,14 @@ class SyncSandboxInstance:
90
98
  def spec(self):
91
99
  return self.sandbox.spec
92
100
 
101
+ @property
102
+ def last_used_at(self):
103
+ return self.sandbox.last_used_at
104
+
105
+ @property
106
+ def expires_in(self):
107
+ return self.sandbox.expires_in
108
+
93
109
  def wait(self, max_wait: int = 60000, interval: int = 1000) -> "SyncSandboxInstance":
94
110
  logger.warning(
95
111
  "⚠️ Warning: sandbox.wait() is deprecated. You don't need to wait for the sandbox to be deployed anymore."
@@ -159,17 +175,26 @@ class SyncSandboxInstance:
159
175
  volumes=volumes,
160
176
  ),
161
177
  )
162
- if ttl:
178
+ if ttl and sandbox.spec.runtime:
163
179
  sandbox.spec.runtime.ttl = ttl
164
- if expires:
180
+ if expires and sandbox.spec.runtime:
165
181
  sandbox.spec.runtime.expires = expires.isoformat()
166
182
  if region:
167
183
  sandbox.spec.region = region
168
184
  if lifecycle:
169
- sandbox.spec.lifecycle = lifecycle
185
+ if type(lifecycle) is dict:
186
+ lifecycle = SandboxLifecycle.from_dict(lifecycle)
187
+ assert lifecycle is not None
188
+ sandbox.spec.lifecycle = lifecycle
189
+ elif type(lifecycle) is SandboxLifecycle:
190
+ sandbox.spec.lifecycle = lifecycle
191
+ else:
192
+ raise ValueError(f"Invalid lifecycle type: {type(lifecycle)}")
170
193
  else:
171
194
  if isinstance(sandbox, dict):
172
195
  sandbox = Sandbox.from_dict(sandbox)
196
+ assert isinstance(sandbox, Sandbox)
197
+
173
198
  if not sandbox.metadata:
174
199
  sandbox.metadata = Metadata(name=default_name)
175
200
  if not sandbox.spec:
@@ -0,0 +1,71 @@
1
+ from typing import Union
2
+
3
+ from ...common.settings import settings
4
+ from ..client.api.system.get_health import sync as get_health
5
+ from ..client.api.system.post_upgrade import sync as post_upgrade
6
+ from ..client.client import Client
7
+ from ..client.models import ErrorResponse, HealthResponse, SuccessResponse, UpgradeRequest
8
+ from ..types import SandboxConfiguration
9
+ from .action import SyncSandboxAction
10
+
11
+
12
+ class SyncSandboxSystem(SyncSandboxAction):
13
+ """System operations for sandbox including upgrade functionality (sync version)."""
14
+
15
+ def __init__(self, sandbox_config: SandboxConfiguration):
16
+ super().__init__(sandbox_config)
17
+
18
+ def upgrade(
19
+ self,
20
+ version: str | None = None,
21
+ base_url: str | None = None,
22
+ ) -> SuccessResponse:
23
+ """Upgrade the sandbox-api to a new version.
24
+
25
+ Triggers an upgrade of the sandbox-api process. Returns immediately before upgrading.
26
+ The upgrade will: download the specified binary from GitHub releases, validate it, and restart.
27
+ All running processes will be preserved across the upgrade.
28
+
29
+ Args:
30
+ version: Version to upgrade to - "develop" (default), "main", "latest",
31
+ or specific tag like "v1.0.0"
32
+ base_url: Base URL for releases (useful for forks, defaults to
33
+ https://github.com/blaxel-ai/sandbox/releases)
34
+
35
+ Returns:
36
+ SuccessResponse with status information
37
+ """
38
+ request = UpgradeRequest(version=version, base_url=base_url)
39
+
40
+ client = Client(
41
+ base_url=self.url,
42
+ headers={**settings.headers, **self.sandbox_config.headers},
43
+ )
44
+
45
+ with client:
46
+ response = post_upgrade(client=client, body=request)
47
+ if response is None:
48
+ raise Exception("Failed to upgrade sandbox")
49
+ if isinstance(response, ErrorResponse):
50
+ raise Exception(f"Upgrade failed: {response.error}")
51
+ return response
52
+
53
+ def health(self) -> HealthResponse:
54
+ """Get health status and system information.
55
+
56
+ Returns health status and system information including upgrade count and binary details.
57
+ Also includes last upgrade attempt status with detailed error information if available.
58
+
59
+ Returns:
60
+ HealthResponse with system status information
61
+ """
62
+ client = Client(
63
+ base_url=self.url,
64
+ headers={**settings.headers, **self.sandbox_config.headers},
65
+ )
66
+
67
+ with client:
68
+ response = get_health(client=client)
69
+ if response is None:
70
+ raise Exception("Failed to get health status")
71
+ return response