blaxel 0.2.32__py3-none-any.whl → 0.2.34__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.
blaxel/__init__.py CHANGED
@@ -4,8 +4,8 @@ from .core.common.autoload import autoload
4
4
  from .core.common.env import env
5
5
  from .core.common.settings import settings
6
6
 
7
- __version__ = "0.2.32"
8
- __commit__ = "72a43488d87cfae83389f5a928dfa9256649396b"
7
+ __version__ = "0.2.34"
8
+ __commit__ = "84fc1c14e48dec727c7de1966c51022e09f7c80f"
9
9
  __sentry_dsn__ = "https://9711de13cd02b285ca4378c01de8dc30@o4508714045276160.ingest.us.sentry.io/4510461121462272"
10
10
  __all__ = ["autoload", "settings", "env"]
11
11
 
blaxel/core/__init__.py CHANGED
@@ -30,7 +30,7 @@ from .sandbox import (
30
30
  )
31
31
  from .sandbox.types import Sandbox
32
32
  from .tools import BlTools, bl_tools, convert_mcp_tool_to_blaxel_tool
33
- from .volume import VolumeCreateConfiguration, VolumeInstance
33
+ from .volume import SyncVolumeInstance, VolumeCreateConfiguration, VolumeInstance
34
34
 
35
35
  __all__ = [
36
36
  "BlAgent",
@@ -65,6 +65,7 @@ __all__ = [
65
65
  "convert_mcp_tool_to_blaxel_tool",
66
66
  "websocket_client",
67
67
  "VolumeInstance",
68
+ "SyncVolumeInstance",
68
69
  "VolumeCreateConfiguration",
69
70
  "verify_webhook_signature",
70
71
  "verify_webhook_from_request",
@@ -1,5 +1,6 @@
1
- from .autoload import autoload, capture_exception
1
+ from .autoload import autoload
2
2
  from .env import env
3
+ from .sentry import capture_exception, flush_sentry, init_sentry, is_sentry_initialized
3
4
  from .internal import get_alphanumeric_limited_hash, get_global_unique_hash
4
5
  from .settings import Settings, settings
5
6
  from .webhook import (
@@ -12,6 +13,9 @@ from .webhook import (
12
13
  __all__ = [
13
14
  "autoload",
14
15
  "capture_exception",
16
+ "flush_sentry",
17
+ "init_sentry",
18
+ "is_sentry_initialized",
15
19
  "Settings",
16
20
  "settings",
17
21
  "env",
@@ -1,10 +1,4 @@
1
- import atexit
2
1
  import logging
3
- import sys
4
- import threading
5
- from asyncio import CancelledError
6
-
7
- from sentry_sdk import Client, Hub
8
2
 
9
3
  from ..client import client
10
4
  from ..client.response_interceptor import (
@@ -12,130 +6,11 @@ from ..client.response_interceptor import (
12
6
  response_interceptors_sync,
13
7
  )
14
8
  from ..sandbox.client import client as client_sandbox
9
+ from .sentry import init_sentry
15
10
  from .settings import settings
16
11
 
17
12
  logger = logging.getLogger(__name__)
18
13
 
19
- # Isolated Sentry hub for SDK-only error tracking (doesn't interfere with user's Sentry)
20
- _sentry_hub: Hub | None = None
21
- _captured_exceptions: set = set() # Track already captured exceptions to avoid duplicates
22
-
23
- # Exceptions that are part of normal control flow and should not be captured
24
- _IGNORED_EXCEPTIONS = (
25
- StopIteration, # Iterator exhaustion
26
- StopAsyncIteration, # Async iterator exhaustion
27
- GeneratorExit, # Generator cleanup
28
- KeyboardInterrupt, # User interrupt (Ctrl+C)
29
- SystemExit, # Program exit
30
- CancelledError, # Async task cancellation
31
- )
32
-
33
- # Optional dependencies that may not be installed - import errors for these are expected
34
- _OPTIONAL_DEPENDENCIES = ("opentelemetry",)
35
-
36
-
37
- def _get_exception_key(exc_type, exc_value, frame) -> str:
38
- """Generate a unique key for an exception based on type, message, and origin."""
39
- # Use type name + message + original file/line where exception was raised
40
- # This ensures the same logical exception is only captured once
41
- exc_name = exc_type.__name__ if exc_type else "Unknown"
42
- exc_msg = str(exc_value) if exc_value else ""
43
- # Get the original traceback location (where exception was first raised)
44
- tb = getattr(exc_value, "__traceback__", None)
45
- if tb:
46
- # Walk to the deepest frame (origin of exception)
47
- while tb.tb_next:
48
- tb = tb.tb_next
49
- origin = f"{tb.tb_frame.f_code.co_filename}:{tb.tb_lineno}"
50
- else:
51
- origin = f"{frame.f_code.co_filename}:{frame.f_lineno}"
52
- return f"{exc_name}:{exc_msg}:{origin}"
53
-
54
-
55
- def _is_optional_dependency_error(exc_type, exc_value) -> bool:
56
- """Check if the exception is an import error for an optional dependency."""
57
- # ModuleNotFoundError is a subclass of ImportError, so checking ImportError covers both
58
- if exc_type and issubclass(exc_type, ImportError):
59
- msg = str(exc_value).lower()
60
- return any(dep in msg for dep in _OPTIONAL_DEPENDENCIES)
61
- return False
62
-
63
-
64
- def _trace_blaxel_exceptions(frame, event, arg):
65
- """Trace function that captures exceptions from blaxel SDK code."""
66
- if event == "exception":
67
- exc_type, exc_value, exc_tb = arg
68
-
69
- # Skip control flow exceptions (not actual errors)
70
- if exc_type and issubclass(exc_type, _IGNORED_EXCEPTIONS):
71
- return _trace_blaxel_exceptions
72
-
73
- # Skip import errors for optional dependencies (expected when not installed)
74
- if _is_optional_dependency_error(exc_type, exc_value):
75
- return _trace_blaxel_exceptions
76
-
77
- filename = frame.f_code.co_filename
78
-
79
- # Only capture if it's from blaxel in site-packages
80
- if "site-packages/blaxel" in filename:
81
- # Avoid capturing the same exception multiple times using a content-based key
82
- exc_key = _get_exception_key(exc_type, exc_value, frame)
83
- if exc_key not in _captured_exceptions:
84
- _captured_exceptions.add(exc_key)
85
- capture_exception(exc_value)
86
- # Clean up old exception keys to prevent memory leak
87
- if len(_captured_exceptions) > 1000:
88
- _captured_exceptions.clear()
89
-
90
- return _trace_blaxel_exceptions
91
-
92
-
93
- def sentry() -> None:
94
- """Initialize an isolated Sentry client for SDK error tracking."""
95
- global _sentry_hub
96
- try:
97
- dsn = settings.sentry_dsn
98
- if not dsn:
99
- return
100
-
101
- # Create an isolated client that won't interfere with user's Sentry
102
- sentry_client = Client(
103
- dsn=dsn,
104
- environment=settings.env,
105
- release=f"sdk-python@{settings.version}",
106
- default_integrations=False,
107
- auto_enabling_integrations=False,
108
- )
109
- _sentry_hub = Hub(sentry_client)
110
-
111
- # Set SDK-specific tags
112
- with _sentry_hub.configure_scope() as scope:
113
- scope.set_tag("blaxel.workspace", settings.workspace)
114
- scope.set_tag("blaxel.version", settings.version)
115
- scope.set_tag("blaxel.commit", settings.commit)
116
-
117
- # Install trace function to automatically capture SDK exceptions
118
- sys.settrace(_trace_blaxel_exceptions)
119
- threading.settrace(_trace_blaxel_exceptions)
120
-
121
- # Register atexit handler to flush pending events
122
- atexit.register(_flush_sentry)
123
-
124
- except Exception as e:
125
- logger.debug(f"Error initializing Sentry: {e}")
126
-
127
-
128
- def capture_exception(exception: Exception | None = None) -> None:
129
- """Capture an exception to the SDK's isolated Sentry hub."""
130
- if _sentry_hub is not None and _sentry_hub.client is not None:
131
- _sentry_hub.capture_exception(exception)
132
-
133
-
134
- def _flush_sentry():
135
- """Flush pending Sentry events on program exit."""
136
- if _sentry_hub is not None and _sentry_hub.client is not None:
137
- _sentry_hub.client.flush(timeout=2)
138
-
139
14
 
140
15
  def telemetry() -> None:
141
16
  from blaxel.telemetry import telemetry_manager
@@ -164,7 +39,7 @@ def autoload() -> None:
164
39
 
165
40
  if settings.tracking:
166
41
  try:
167
- sentry()
42
+ init_sentry()
168
43
  except Exception:
169
44
  pass
170
45
 
@@ -0,0 +1,319 @@
1
+ import atexit
2
+ import json
3
+ import logging
4
+ import sys
5
+ import threading
6
+ import traceback
7
+ import uuid
8
+ from asyncio import CancelledError
9
+ from datetime import datetime, timezone
10
+ from typing import Any
11
+ from urllib.parse import urlparse
12
+
13
+ import httpx
14
+
15
+ from .settings import settings
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Lightweight Sentry client using httpx - only captures SDK errors
20
+ _sentry_initialized = False
21
+ _captured_exceptions: set = set() # Track already captured exceptions to avoid duplicates
22
+
23
+ # Parsed DSN components
24
+ _sentry_config: dict[str, str] | None = None
25
+
26
+ # Queue for pending events
27
+ _pending_events: list[dict[str, Any]] = []
28
+ _flush_lock = threading.Lock()
29
+ _handlers_registered = False
30
+
31
+ # Exceptions that are part of normal control flow and should not be captured
32
+ _IGNORED_EXCEPTIONS = (
33
+ StopIteration, # Iterator exhaustion
34
+ StopAsyncIteration, # Async iterator exhaustion
35
+ GeneratorExit, # Generator cleanup
36
+ KeyboardInterrupt, # User interrupt (Ctrl+C)
37
+ SystemExit, # Program exit
38
+ CancelledError, # Async task cancellation
39
+ )
40
+
41
+ # Optional dependencies that may not be installed - import errors for these are expected
42
+ _OPTIONAL_DEPENDENCIES = ("opentelemetry",)
43
+
44
+ # SDK path patterns to identify errors originating from our SDK
45
+ _SDK_PATTERNS = [
46
+ "blaxel/",
47
+ "blaxel\\",
48
+ "site-packages/blaxel",
49
+ "site-packages\\blaxel",
50
+ ]
51
+
52
+
53
+ def _is_from_sdk(error: Exception) -> bool:
54
+ """Check if an error originated from SDK code based on stack trace."""
55
+ tb = error.__traceback__
56
+ if not tb:
57
+ return False
58
+
59
+ # Walk through the traceback
60
+ while tb:
61
+ filename = tb.tb_frame.f_code.co_filename
62
+ if any(pattern in filename for pattern in _SDK_PATTERNS):
63
+ return True
64
+ tb = tb.tb_next
65
+
66
+ return False
67
+
68
+
69
+ def _parse_dsn(dsn: str) -> dict[str, str] | None:
70
+ """
71
+ Parse a Sentry DSN into its components.
72
+ DSN format: https://{public_key}@{host}/{project_id}
73
+ """
74
+ try:
75
+ parsed = urlparse(dsn)
76
+ public_key = parsed.username
77
+ host = parsed.hostname
78
+ project_id = parsed.path.lstrip("/")
79
+
80
+ if not public_key or not host or not project_id:
81
+ return None
82
+
83
+ return {"public_key": public_key, "host": host, "project_id": project_id}
84
+ except Exception:
85
+ return None
86
+
87
+
88
+ def _generate_event_id() -> str:
89
+ """Generate a UUID v4 for event ID."""
90
+ return uuid.uuid4().hex
91
+
92
+
93
+ def _parse_stack_trace(exc: Exception) -> list[dict[str, Any]]:
94
+ """Parse exception traceback into Sentry-compatible frames."""
95
+ frames: list[dict[str, Any]] = []
96
+ tb = traceback.extract_tb(exc.__traceback__)
97
+
98
+ for frame in tb:
99
+ frames.append(
100
+ {
101
+ "filename": frame.filename,
102
+ "function": frame.name or "<anonymous>",
103
+ "lineno": frame.lineno,
104
+ "colno": 0,
105
+ }
106
+ )
107
+
108
+ return frames
109
+
110
+
111
+ def _error_to_sentry_event(error: Exception) -> dict[str, Any]:
112
+ """Convert an Exception to a Sentry event payload."""
113
+ frames = _parse_stack_trace(error)
114
+
115
+ return {
116
+ "event_id": _generate_event_id(),
117
+ "timestamp": datetime.now(timezone.utc).timestamp(),
118
+ "platform": "python",
119
+ "level": "error",
120
+ "environment": settings.env,
121
+ "release": f"sdk-python@{settings.version}",
122
+ "tags": {
123
+ "blaxel.workspace": settings.workspace,
124
+ "blaxel.version": settings.version,
125
+ "blaxel.commit": settings.commit,
126
+ },
127
+ "exception": {
128
+ "values": [
129
+ {
130
+ "type": type(error).__name__,
131
+ "value": str(error),
132
+ "stacktrace": {"frames": frames},
133
+ }
134
+ ]
135
+ },
136
+ }
137
+
138
+
139
+ def _send_to_sentry(event: dict[str, Any]) -> None:
140
+ """Send an event to Sentry using httpx."""
141
+ if not _sentry_config:
142
+ return
143
+
144
+ public_key = _sentry_config["public_key"]
145
+ host = _sentry_config["host"]
146
+ project_id = _sentry_config["project_id"]
147
+ envelope_url = f"https://{host}/api/{project_id}/envelope/"
148
+
149
+ # Create envelope header
150
+ envelope_header = json.dumps(
151
+ {
152
+ "event_id": event["event_id"],
153
+ "sent_at": datetime.now(timezone.utc).isoformat(),
154
+ "dsn": f"https://{public_key}@{host}/{project_id}",
155
+ }
156
+ )
157
+
158
+ # Create item header
159
+ item_header = json.dumps({"type": "event", "content_type": "application/json"})
160
+
161
+ # Create envelope body
162
+ envelope = f"{envelope_header}\n{item_header}\n{json.dumps(event)}"
163
+
164
+ try:
165
+ httpx.post(
166
+ envelope_url,
167
+ headers={
168
+ "Content-Type": "application/x-sentry-envelope",
169
+ "X-Sentry-Auth": f"Sentry sentry_version=7, sentry_client=blaxel-sdk/{settings.version}, sentry_key={public_key}",
170
+ },
171
+ content=envelope,
172
+ timeout=5.0,
173
+ )
174
+ except Exception:
175
+ # Silently fail - error reporting should never break the SDK
176
+ pass
177
+
178
+
179
+ def _get_exception_key(exc_type, exc_value, frame) -> str:
180
+ """Generate a unique key for an exception based on type, message, and origin."""
181
+ exc_name = exc_type.__name__ if exc_type else "Unknown"
182
+ exc_msg = str(exc_value) if exc_value else ""
183
+ tb = getattr(exc_value, "__traceback__", None)
184
+ if tb:
185
+ while tb.tb_next:
186
+ tb = tb.tb_next
187
+ origin = f"{tb.tb_frame.f_code.co_filename}:{tb.tb_lineno}"
188
+ else:
189
+ origin = f"{frame.f_code.co_filename}:{frame.f_lineno}"
190
+ return f"{exc_name}:{exc_msg}:{origin}"
191
+
192
+
193
+ def _is_optional_dependency_error(exc_type, exc_value) -> bool:
194
+ """Check if the exception is an import error for an optional dependency."""
195
+ if exc_type and issubclass(exc_type, ImportError):
196
+ msg = str(exc_value).lower()
197
+ return any(dep in msg for dep in _OPTIONAL_DEPENDENCIES)
198
+ return False
199
+
200
+
201
+ def _trace_blaxel_exceptions(frame, event, arg):
202
+ """Trace function that captures exceptions from blaxel SDK code."""
203
+ if event == "exception":
204
+ exc_type, exc_value, exc_tb = arg
205
+
206
+ # Skip control flow exceptions (not actual errors)
207
+ if exc_type and issubclass(exc_type, _IGNORED_EXCEPTIONS):
208
+ return _trace_blaxel_exceptions
209
+
210
+ # Skip import errors for optional dependencies (expected when not installed)
211
+ if _is_optional_dependency_error(exc_type, exc_value):
212
+ return _trace_blaxel_exceptions
213
+
214
+ filename = frame.f_code.co_filename
215
+
216
+ # Only capture if it's from blaxel in site-packages
217
+ if "site-packages/blaxel" in filename:
218
+ # Avoid capturing the same exception multiple times using a content-based key
219
+ exc_key = _get_exception_key(exc_type, exc_value, frame)
220
+ if exc_key not in _captured_exceptions:
221
+ _captured_exceptions.add(exc_key)
222
+ capture_exception(exc_value)
223
+ # Clean up old exception keys to prevent memory leak
224
+ if len(_captured_exceptions) > 1000:
225
+ _captured_exceptions.clear()
226
+
227
+ return _trace_blaxel_exceptions
228
+
229
+
230
+ def init_sentry() -> None:
231
+ """Initialize the lightweight Sentry client for SDK error tracking."""
232
+ global _sentry_initialized, _sentry_config, _handlers_registered
233
+ try:
234
+ dsn = settings.sentry_dsn
235
+ if not dsn:
236
+ return
237
+
238
+ # Parse DSN
239
+ _sentry_config = _parse_dsn(dsn)
240
+ if not _sentry_config:
241
+ return
242
+
243
+ # Only allow dev/prod environments
244
+ if settings.env not in ("dev", "prod"):
245
+ return
246
+
247
+ _sentry_initialized = True
248
+
249
+ # Register handlers only once
250
+ if not _handlers_registered:
251
+ _handlers_registered = True
252
+
253
+ # Install trace function to automatically capture SDK exceptions
254
+ sys.settrace(_trace_blaxel_exceptions)
255
+ threading.settrace(_trace_blaxel_exceptions)
256
+
257
+ # Register atexit handler to flush pending events
258
+ atexit.register(flush_sentry)
259
+
260
+ except Exception as e:
261
+ logger.debug(f"Error initializing Sentry: {e}")
262
+
263
+
264
+ def capture_exception(exception: Exception | None = None) -> None:
265
+ """Capture an exception to Sentry.
266
+ Only errors originating from SDK code will be captured.
267
+ """
268
+ if not _sentry_initialized or not _sentry_config or exception is None:
269
+ return
270
+
271
+ try:
272
+ # Generate unique key to prevent duplicate captures
273
+ exc_key = f"{type(exception).__name__}:{str(exception)}"
274
+ if exc_key in _captured_exceptions:
275
+ return
276
+
277
+ _captured_exceptions.add(exc_key)
278
+
279
+ # Clean up old exception keys to prevent memory leak
280
+ if len(_captured_exceptions) > 1000:
281
+ _captured_exceptions.clear()
282
+
283
+ # Convert error to Sentry event and queue it
284
+ event = _error_to_sentry_event(exception)
285
+ with _flush_lock:
286
+ _pending_events.append(event)
287
+
288
+ # Send immediately (fire and forget)
289
+ _send_to_sentry(event)
290
+
291
+ except Exception:
292
+ # Silently fail - error capturing should never break the SDK
293
+ pass
294
+
295
+
296
+ def flush_sentry(timeout: float = 2.0) -> None:
297
+ """Flush pending Sentry events."""
298
+ if not _sentry_initialized:
299
+ return
300
+
301
+ with _flush_lock:
302
+ if not _pending_events:
303
+ return
304
+
305
+ events_to_send = _pending_events.copy()
306
+ _pending_events.clear()
307
+
308
+ # Send all pending events
309
+ for event in events_to_send:
310
+ try:
311
+ _send_to_sentry(event)
312
+ except Exception:
313
+ # Silently fail
314
+ pass
315
+
316
+
317
+ def is_sentry_initialized() -> bool:
318
+ """Check if Sentry is initialized and available."""
319
+ return _sentry_initialized