blaxel 0.2.32__py3-none-any.whl → 0.2.33__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 +2 -2
- blaxel/core/common/__init__.py +5 -1
- blaxel/core/common/autoload.py +2 -127
- blaxel/core/common/sentry.py +319 -0
- blaxel/core/sandbox/default/process.py +144 -29
- blaxel/core/sandbox/sync/process.py +150 -24
- blaxel/core/sandbox/types.py +2 -0
- {blaxel-0.2.32.dist-info → blaxel-0.2.33.dist-info}/METADATA +1 -2
- {blaxel-0.2.32.dist-info → blaxel-0.2.33.dist-info}/RECORD +11 -10
- {blaxel-0.2.32.dist-info → blaxel-0.2.33.dist-info}/WHEEL +0 -0
- {blaxel-0.2.32.dist-info → blaxel-0.2.33.dist-info}/licenses/LICENSE +0 -0
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.
|
|
8
|
-
__commit__ = "
|
|
7
|
+
__version__ = "0.2.33"
|
|
8
|
+
__commit__ = "3472a6744215e59d421423f105edb2ec34edebe8"
|
|
9
9
|
__sentry_dsn__ = "https://9711de13cd02b285ca4378c01de8dc30@o4508714045276160.ingest.us.sentry.io/4510461121462272"
|
|
10
10
|
__all__ = ["autoload", "settings", "env"]
|
|
11
11
|
|
blaxel/core/common/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
from .autoload import autoload
|
|
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",
|
blaxel/core/common/autoload.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -184,55 +184,170 @@ class SandboxProcess(SandboxAction):
|
|
|
184
184
|
) -> Union[ProcessResponse, ProcessResponseWithLog]:
|
|
185
185
|
"""Execute a process in the sandbox."""
|
|
186
186
|
on_log = None
|
|
187
|
+
on_stdout = None
|
|
188
|
+
on_stderr = None
|
|
189
|
+
|
|
187
190
|
if isinstance(process, ProcessRequestWithLog):
|
|
188
191
|
on_log = process.on_log
|
|
192
|
+
on_stdout = process.on_stdout
|
|
193
|
+
on_stderr = process.on_stderr
|
|
189
194
|
process = process.to_dict()
|
|
190
195
|
|
|
191
196
|
if isinstance(process, dict):
|
|
192
197
|
if "on_log" in process:
|
|
193
198
|
on_log = process["on_log"]
|
|
194
199
|
del process["on_log"]
|
|
200
|
+
if "on_stdout" in process:
|
|
201
|
+
on_stdout = process["on_stdout"]
|
|
202
|
+
del process["on_stdout"]
|
|
203
|
+
if "on_stderr" in process:
|
|
204
|
+
on_stderr = process["on_stderr"]
|
|
205
|
+
del process["on_stderr"]
|
|
195
206
|
process = ProcessRequest.from_dict(process)
|
|
196
207
|
|
|
197
208
|
# Store original wait_for_completion setting
|
|
198
209
|
should_wait_for_completion = process.wait_for_completion
|
|
199
210
|
|
|
200
|
-
#
|
|
201
|
-
if should_wait_for_completion and on_log
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
self.handle_response_error(response)
|
|
209
|
-
import json
|
|
210
|
-
|
|
211
|
-
response_data = json.loads(content_bytes) if content_bytes else None
|
|
212
|
-
result = ProcessResponse.from_dict(response_data)
|
|
213
|
-
finally:
|
|
214
|
-
await response.aclose()
|
|
215
|
-
|
|
216
|
-
# Handle wait_for_completion with parallel log streaming
|
|
217
|
-
if should_wait_for_completion and on_log is not None:
|
|
218
|
-
stream_control = self._stream_logs(result.pid, {"on_log": on_log})
|
|
211
|
+
# When waiting for completion with streaming callbacks, use streaming endpoint
|
|
212
|
+
if should_wait_for_completion and (on_log or on_stdout or on_stderr):
|
|
213
|
+
return await self._exec_with_streaming(
|
|
214
|
+
process, on_log=on_log, on_stdout=on_stdout, on_stderr=on_stderr
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
client = self.get_client()
|
|
218
|
+
response = await client.post("/process", json=process.to_dict())
|
|
219
219
|
try:
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
content_bytes = await response.aread()
|
|
221
|
+
self.handle_response_error(response)
|
|
222
|
+
import json
|
|
223
|
+
|
|
224
|
+
response_data = json.loads(content_bytes) if content_bytes else None
|
|
225
|
+
result = ProcessResponse.from_dict(response_data)
|
|
222
226
|
finally:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
stream_control = self._stream_logs(result.pid, {"on_log": on_log})
|
|
227
|
+
await response.aclose()
|
|
228
|
+
|
|
229
|
+
if on_log or on_stdout or on_stderr:
|
|
230
|
+
stream_control = self._stream_logs(
|
|
231
|
+
result.pid, {"on_log": on_log, "on_stdout": on_stdout, "on_stderr": on_stderr}
|
|
232
|
+
)
|
|
230
233
|
return ProcessResponseWithLog(
|
|
231
234
|
result,
|
|
232
235
|
lambda: stream_control["close"]() if stream_control else None,
|
|
233
236
|
)
|
|
234
237
|
|
|
235
|
-
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
async def _exec_with_streaming(
|
|
241
|
+
self,
|
|
242
|
+
process_request: ProcessRequest,
|
|
243
|
+
on_log: Callable[[str], None] | None = None,
|
|
244
|
+
on_stdout: Callable[[str], None] | None = None,
|
|
245
|
+
on_stderr: Callable[[str], None] | None = None,
|
|
246
|
+
) -> ProcessResponseWithLog:
|
|
247
|
+
"""Execute a process with streaming response handling for NDJSON."""
|
|
248
|
+
import json
|
|
249
|
+
|
|
250
|
+
headers = (
|
|
251
|
+
self.sandbox_config.headers
|
|
252
|
+
if self.sandbox_config.force_url
|
|
253
|
+
else {**settings.headers, **self.sandbox_config.headers}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async with httpx.AsyncClient() as client_instance:
|
|
257
|
+
async with client_instance.stream(
|
|
258
|
+
"POST",
|
|
259
|
+
f"{self.url}/process",
|
|
260
|
+
headers={
|
|
261
|
+
**headers,
|
|
262
|
+
"Content-Type": "application/json",
|
|
263
|
+
"Accept": "text/event-stream",
|
|
264
|
+
},
|
|
265
|
+
json=process_request.to_dict(),
|
|
266
|
+
timeout=None,
|
|
267
|
+
) as response:
|
|
268
|
+
if response.status_code >= 400:
|
|
269
|
+
error_text = await response.aread()
|
|
270
|
+
raise Exception(f"Failed to execute process: {error_text}")
|
|
271
|
+
|
|
272
|
+
content_type = response.headers.get("Content-Type", "")
|
|
273
|
+
is_streaming = "application/x-ndjson" in content_type
|
|
274
|
+
|
|
275
|
+
# Fallback: server doesn't support streaming, use legacy approach
|
|
276
|
+
if not is_streaming:
|
|
277
|
+
content = await response.aread()
|
|
278
|
+
data = json.loads(content)
|
|
279
|
+
result = ProcessResponse.from_dict(data)
|
|
280
|
+
|
|
281
|
+
# If process already completed (server waited), emit logs through callbacks
|
|
282
|
+
if result.status == "completed" or result.status == "failed":
|
|
283
|
+
if result.stdout:
|
|
284
|
+
for line in result.stdout.split("\n"):
|
|
285
|
+
if line:
|
|
286
|
+
if on_stdout:
|
|
287
|
+
on_stdout(line)
|
|
288
|
+
if result.stderr:
|
|
289
|
+
for line in result.stderr.split("\n"):
|
|
290
|
+
if line:
|
|
291
|
+
if on_stderr:
|
|
292
|
+
on_stderr(line)
|
|
293
|
+
if result.logs:
|
|
294
|
+
for line in result.logs.split("\n"):
|
|
295
|
+
if line:
|
|
296
|
+
if on_log:
|
|
297
|
+
on_log(line)
|
|
298
|
+
|
|
299
|
+
return ProcessResponseWithLog(result, lambda: None)
|
|
300
|
+
|
|
301
|
+
# Streaming response handling
|
|
302
|
+
buffer = ""
|
|
303
|
+
result = None
|
|
304
|
+
|
|
305
|
+
async for chunk in response.aiter_text():
|
|
306
|
+
buffer += chunk
|
|
307
|
+
lines = buffer.split("\n")
|
|
308
|
+
buffer = lines.pop()
|
|
309
|
+
|
|
310
|
+
for line in lines:
|
|
311
|
+
if not line.strip():
|
|
312
|
+
continue
|
|
313
|
+
try:
|
|
314
|
+
parsed = json.loads(line)
|
|
315
|
+
parsed_type = parsed.get("type", "")
|
|
316
|
+
parsed_data = parsed.get("data", "")
|
|
317
|
+
|
|
318
|
+
if parsed_type == "stdout":
|
|
319
|
+
if parsed_data:
|
|
320
|
+
if on_stdout:
|
|
321
|
+
on_stdout(parsed_data)
|
|
322
|
+
if on_log:
|
|
323
|
+
on_log(parsed_data)
|
|
324
|
+
elif parsed_type == "stderr":
|
|
325
|
+
if parsed_data:
|
|
326
|
+
if on_stderr:
|
|
327
|
+
on_stderr(parsed_data)
|
|
328
|
+
if on_log:
|
|
329
|
+
on_log(parsed_data)
|
|
330
|
+
elif parsed_type == "result":
|
|
331
|
+
try:
|
|
332
|
+
result = ProcessResponse.from_dict(json.loads(parsed_data))
|
|
333
|
+
except Exception:
|
|
334
|
+
raise Exception(f"Failed to parse result JSON: {parsed_data}")
|
|
335
|
+
except json.JSONDecodeError:
|
|
336
|
+
continue
|
|
337
|
+
|
|
338
|
+
# Process any remaining buffer
|
|
339
|
+
if buffer.strip():
|
|
340
|
+
if buffer.startswith("result:"):
|
|
341
|
+
json_str = buffer[7:]
|
|
342
|
+
try:
|
|
343
|
+
result = ProcessResponse.from_dict(json.loads(json_str))
|
|
344
|
+
except Exception:
|
|
345
|
+
raise Exception(f"Failed to parse result JSON: {json_str}")
|
|
346
|
+
|
|
347
|
+
if not result:
|
|
348
|
+
raise Exception("No result received from streaming response")
|
|
349
|
+
|
|
350
|
+
return ProcessResponseWithLog(result, lambda: None)
|
|
236
351
|
|
|
237
352
|
async def wait(
|
|
238
353
|
self, identifier: str, max_wait: int = 60000, interval: int = 1000
|
|
@@ -143,43 +143,169 @@ class SyncSandboxProcess(SyncSandboxAction):
|
|
|
143
143
|
process: Union[ProcessRequest, ProcessRequestWithLog, Dict[str, Any]],
|
|
144
144
|
) -> Union[ProcessResponse, ProcessResponseWithLog]:
|
|
145
145
|
on_log = None
|
|
146
|
+
on_stdout = None
|
|
147
|
+
on_stderr = None
|
|
148
|
+
|
|
146
149
|
if isinstance(process, ProcessRequestWithLog):
|
|
147
150
|
on_log = process.on_log
|
|
151
|
+
on_stdout = process.on_stdout
|
|
152
|
+
on_stderr = process.on_stderr
|
|
148
153
|
process = process.to_dict()
|
|
154
|
+
|
|
149
155
|
if isinstance(process, dict):
|
|
150
156
|
if "on_log" in process:
|
|
151
157
|
on_log = process["on_log"]
|
|
152
158
|
del process["on_log"]
|
|
159
|
+
if "on_stdout" in process:
|
|
160
|
+
on_stdout = process["on_stdout"]
|
|
161
|
+
del process["on_stdout"]
|
|
162
|
+
if "on_stderr" in process:
|
|
163
|
+
on_stderr = process["on_stderr"]
|
|
164
|
+
del process["on_stderr"]
|
|
153
165
|
process = ProcessRequest.from_dict(process)
|
|
166
|
+
|
|
154
167
|
should_wait_for_completion = process.wait_for_completion
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
168
|
+
|
|
169
|
+
# When waiting for completion with streaming callbacks, use streaming endpoint
|
|
170
|
+
if should_wait_for_completion and (on_log or on_stdout or on_stderr):
|
|
171
|
+
return self._exec_with_streaming(
|
|
172
|
+
process, on_log=on_log, on_stdout=on_stdout, on_stderr=on_stderr
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
with self.get_client() as client_instance:
|
|
176
|
+
response = client_instance.post("/process", json=process.to_dict())
|
|
177
|
+
response_data = None
|
|
178
|
+
if response.content:
|
|
179
|
+
try:
|
|
180
|
+
response_data = response.json()
|
|
181
|
+
except Exception:
|
|
182
|
+
self.handle_response_error(response)
|
|
183
|
+
raise
|
|
184
|
+
self.handle_response_error(response)
|
|
185
|
+
result = ProcessResponse.from_dict(response_data)
|
|
186
|
+
|
|
187
|
+
if on_log or on_stdout or on_stderr:
|
|
188
|
+
stream_control = self._stream_logs(
|
|
189
|
+
result.pid, {"on_log": on_log, "on_stdout": on_stdout, "on_stderr": on_stderr}
|
|
190
|
+
)
|
|
178
191
|
return ProcessResponseWithLog(
|
|
179
192
|
result,
|
|
180
193
|
lambda: stream_control["close"]() if stream_control else None,
|
|
181
194
|
)
|
|
182
|
-
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def _exec_with_streaming(
|
|
199
|
+
self,
|
|
200
|
+
process_request: ProcessRequest,
|
|
201
|
+
on_log: Callable[[str], None] | None = None,
|
|
202
|
+
on_stdout: Callable[[str], None] | None = None,
|
|
203
|
+
on_stderr: Callable[[str], None] | None = None,
|
|
204
|
+
) -> ProcessResponseWithLog:
|
|
205
|
+
"""Execute a process with streaming response handling for NDJSON."""
|
|
206
|
+
import json
|
|
207
|
+
|
|
208
|
+
headers = (
|
|
209
|
+
self.sandbox_config.headers
|
|
210
|
+
if self.sandbox_config.force_url
|
|
211
|
+
else {**settings.headers, **self.sandbox_config.headers}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
with httpx.Client() as client_instance:
|
|
215
|
+
with client_instance.stream(
|
|
216
|
+
"POST",
|
|
217
|
+
f"{self.url}/process",
|
|
218
|
+
headers={
|
|
219
|
+
**headers,
|
|
220
|
+
"Content-Type": "application/json",
|
|
221
|
+
"Accept": "text/event-stream",
|
|
222
|
+
},
|
|
223
|
+
json=process_request.to_dict(),
|
|
224
|
+
timeout=None,
|
|
225
|
+
) as response:
|
|
226
|
+
if response.status_code >= 400:
|
|
227
|
+
error_text = response.read()
|
|
228
|
+
raise Exception(f"Failed to execute process: {error_text}")
|
|
229
|
+
|
|
230
|
+
content_type = response.headers.get("Content-Type", "")
|
|
231
|
+
is_streaming = "application/x-ndjson" in content_type
|
|
232
|
+
|
|
233
|
+
# Fallback: server doesn't support streaming, use legacy approach
|
|
234
|
+
if not is_streaming:
|
|
235
|
+
content = response.read()
|
|
236
|
+
data = json.loads(content)
|
|
237
|
+
result = ProcessResponse.from_dict(data)
|
|
238
|
+
|
|
239
|
+
# If process already completed (server waited), emit logs through callbacks
|
|
240
|
+
if result.status == "completed" or result.status == "failed":
|
|
241
|
+
if result.stdout:
|
|
242
|
+
for line in result.stdout.split("\n"):
|
|
243
|
+
if line:
|
|
244
|
+
if on_stdout:
|
|
245
|
+
on_stdout(line)
|
|
246
|
+
if result.stderr:
|
|
247
|
+
for line in result.stderr.split("\n"):
|
|
248
|
+
if line:
|
|
249
|
+
if on_stderr:
|
|
250
|
+
on_stderr(line)
|
|
251
|
+
if result.logs:
|
|
252
|
+
for line in result.logs.split("\n"):
|
|
253
|
+
if line:
|
|
254
|
+
if on_log:
|
|
255
|
+
on_log(line)
|
|
256
|
+
|
|
257
|
+
return ProcessResponseWithLog(result, lambda: None)
|
|
258
|
+
|
|
259
|
+
# Streaming response handling
|
|
260
|
+
buffer = ""
|
|
261
|
+
result = None
|
|
262
|
+
|
|
263
|
+
for chunk in response.iter_text():
|
|
264
|
+
buffer += chunk
|
|
265
|
+
lines = buffer.split("\n")
|
|
266
|
+
buffer = lines.pop()
|
|
267
|
+
|
|
268
|
+
for line in lines:
|
|
269
|
+
if not line.strip():
|
|
270
|
+
continue
|
|
271
|
+
try:
|
|
272
|
+
parsed = json.loads(line)
|
|
273
|
+
parsed_type = parsed.get("type", "")
|
|
274
|
+
parsed_data = parsed.get("data", "")
|
|
275
|
+
|
|
276
|
+
if parsed_type == "stdout":
|
|
277
|
+
if parsed_data:
|
|
278
|
+
if on_stdout:
|
|
279
|
+
on_stdout(parsed_data)
|
|
280
|
+
if on_log:
|
|
281
|
+
on_log(parsed_data)
|
|
282
|
+
elif parsed_type == "stderr":
|
|
283
|
+
if parsed_data:
|
|
284
|
+
if on_stderr:
|
|
285
|
+
on_stderr(parsed_data)
|
|
286
|
+
if on_log:
|
|
287
|
+
on_log(parsed_data)
|
|
288
|
+
elif parsed_type == "result":
|
|
289
|
+
try:
|
|
290
|
+
result = ProcessResponse.from_dict(json.loads(parsed_data))
|
|
291
|
+
except Exception:
|
|
292
|
+
raise Exception(f"Failed to parse result JSON: {parsed_data}")
|
|
293
|
+
except json.JSONDecodeError:
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
# Process any remaining buffer
|
|
297
|
+
if buffer.strip():
|
|
298
|
+
if buffer.startswith("result:"):
|
|
299
|
+
json_str = buffer[7:]
|
|
300
|
+
try:
|
|
301
|
+
result = ProcessResponse.from_dict(json.loads(json_str))
|
|
302
|
+
except Exception:
|
|
303
|
+
raise Exception(f"Failed to parse result JSON: {json_str}")
|
|
304
|
+
|
|
305
|
+
if not result:
|
|
306
|
+
raise Exception("No result received from streaming response")
|
|
307
|
+
|
|
308
|
+
return ProcessResponseWithLog(result, lambda: None)
|
|
183
309
|
|
|
184
310
|
def wait(self, identifier: str, max_wait: int = 60000, interval: int = 1000) -> ProcessResponse:
|
|
185
311
|
start_time = time.monotonic() * 1000
|
blaxel/core/sandbox/types.py
CHANGED
|
@@ -282,6 +282,8 @@ class SandboxCreateConfiguration:
|
|
|
282
282
|
@_attrs_define
|
|
283
283
|
class ProcessRequestWithLog(ProcessRequest):
|
|
284
284
|
on_log: Callable[[str], None] | None = None
|
|
285
|
+
on_stdout: Callable[[str], None] | None = None
|
|
286
|
+
on_stderr: Callable[[str], None] | None = None
|
|
285
287
|
|
|
286
288
|
|
|
287
289
|
class ProcessResponseWithLog:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blaxel
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.33
|
|
4
4
|
Summary: Blaxel - AI development platform SDK
|
|
5
5
|
Project-URL: Homepage, https://blaxel.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.blaxel.ai
|
|
@@ -17,7 +17,6 @@ Requires-Dist: pyjwt>=2.0.0
|
|
|
17
17
|
Requires-Dist: python-dateutil>=2.8.0
|
|
18
18
|
Requires-Dist: pyyaml>=6.0.0
|
|
19
19
|
Requires-Dist: requests>=2.32.3
|
|
20
|
-
Requires-Dist: sentry-sdk>=2.46.0
|
|
21
20
|
Requires-Dist: tomli>=2.2.1
|
|
22
21
|
Requires-Dist: websockets<16.0.0
|
|
23
22
|
Provides-Extra: all
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
blaxel/__init__.py,sha256=
|
|
1
|
+
blaxel/__init__.py,sha256=wk69Pg_DfZapO7suCkD3c_pEa3GOrJe2AUqhua2R2WE,413
|
|
2
2
|
blaxel/core/__init__.py,sha256=CKMC7TaCYdOdnwqcJCN9VjBbNg366coZUGTxI1mgFQQ,1710
|
|
3
3
|
blaxel/core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
blaxel/core/agents/__init__.py,sha256=MJZga99lU8JWUUPHd4rmUfdo7ALwWgF7CQq95SfT2OI,4456
|
|
@@ -330,11 +330,12 @@ blaxel/core/client/models/workspace.py,sha256=3Z-bpTYukkbqW46B39Nljr4XZtAV41AqqK
|
|
|
330
330
|
blaxel/core/client/models/workspace_labels.py,sha256=M_ZbpQeVNdgqWv2RmTYO7znBczUJ6zi5_XS1EDin3Gk,1253
|
|
331
331
|
blaxel/core/client/models/workspace_runtime.py,sha256=Pdr_Q-ugs_5famziqGSyBzUkKQ6e-pAFRGza6GhppSA,1672
|
|
332
332
|
blaxel/core/client/models/workspace_user.py,sha256=f28XZuP3V46y2YYbfiIiNbnIMpy3JINLwpy_mX9N1iU,3430
|
|
333
|
-
blaxel/core/common/__init__.py,sha256=
|
|
334
|
-
blaxel/core/common/autoload.py,sha256=
|
|
333
|
+
blaxel/core/common/__init__.py,sha256=A69U94NslPOYjKvja0bN5z98eguJPC6UaGaVN2-FdRY,736
|
|
334
|
+
blaxel/core/common/autoload.py,sha256=Tq6cmmNcOwUy77ACWM-4Ap8DgRY6GWkAOJmIwugRAPU,1481
|
|
335
335
|
blaxel/core/common/env.py,sha256=05Jm2mw0-KIgR7QaNVyvZPP91B9OlxlJ6mcx6Mqfji0,1234
|
|
336
336
|
blaxel/core/common/internal.py,sha256=NDTFh9Duj84os8GkMXjGzn-UVS9zBDyLAcfPxIpoQGA,3218
|
|
337
337
|
blaxel/core/common/logger.py,sha256=Jt0MCJgYDPq36rl7UyKRDJH76a-AwYdfggNeNYJt6N0,4779
|
|
338
|
+
blaxel/core/common/sentry.py,sha256=P_v1vWivlh-usXV1_JeJ603r1OHoWaR5jaYV2JJ6vDM,9759
|
|
338
339
|
blaxel/core/common/settings.py,sha256=f7ZP8VpB8gKESptyb6S82gTHQsbmx6RcaAkDkxy2JpE,4599
|
|
339
340
|
blaxel/core/common/webhook.py,sha256=N1f2bamP7wRyPyCfmAZKMdjeB3aQ6d6pcafHyVZKtPk,5330
|
|
340
341
|
blaxel/core/jobs/__init__.py,sha256=LZTtkOwqUyMjRTdeLv4EXLbVhgebwvY95jd56IGO4MQ,17082
|
|
@@ -343,7 +344,7 @@ blaxel/core/mcp/client.py,sha256=EZ7l5w3bTXaD41nalHzM-byxfQK-JdcmQqxg3zGpVO4,550
|
|
|
343
344
|
blaxel/core/mcp/server.py,sha256=edAztWBlukERw9-dzS2Sk96TP8R3-CSofY1CZDu19ZA,5967
|
|
344
345
|
blaxel/core/models/__init__.py,sha256=ydz1txqIVyOhehItut-AOnLMnGp7AtCD2zku9gkvAsE,1722
|
|
345
346
|
blaxel/core/sandbox/__init__.py,sha256=SQ9YH0I9ruAYscY_W75jqIhNbejbY_92xL7VqM083Mc,1345
|
|
346
|
-
blaxel/core/sandbox/types.py,sha256=
|
|
347
|
+
blaxel/core/sandbox/types.py,sha256=m8yWOXxpLfzeNPhxBwU1VnXFeIHnv95OQuDnST3tYrc,12703
|
|
347
348
|
blaxel/core/sandbox/client/__init__.py,sha256=N26bD5o1jsTb48oExow6Rgivd8ylaU9jaWZfZsVilP8,128
|
|
348
349
|
blaxel/core/sandbox/client/client.py,sha256=EGCYliUHCk4RyIBlydEZyVpc_VUiIIGPuu2E-xYeKFY,7074
|
|
349
350
|
blaxel/core/sandbox/client/errors.py,sha256=gO8GBmKqmSNgAg-E5oT-oOyxztvp7V_6XG7OUTT15q0,546
|
|
@@ -438,7 +439,7 @@ blaxel/core/sandbox/default/filesystem.py,sha256=Gn1G3DpMmDXplUEpXVENXkjUDAEkWAK
|
|
|
438
439
|
blaxel/core/sandbox/default/interpreter.py,sha256=TJSryQvq2rWedyhMU69tOlXVOF1iIpgIbV2z3_mF72E,11316
|
|
439
440
|
blaxel/core/sandbox/default/network.py,sha256=3ZvrJB_9JdZrclNkwifZOIciz2OqzV0LQfbebjZXLIY,358
|
|
440
441
|
blaxel/core/sandbox/default/preview.py,sha256=dV_xuu9Efop5TnzuFJPeLUZ7CEepuYkJedx01fDVMX4,6132
|
|
441
|
-
blaxel/core/sandbox/default/process.py,sha256=
|
|
442
|
+
blaxel/core/sandbox/default/process.py,sha256=7nI1wJXeZWoveBesC13wur-ghIjP5POZ38G8wqCdJTw,16983
|
|
442
443
|
blaxel/core/sandbox/default/sandbox.py,sha256=b641_MHILDdsz9ZkbSIoRAmbB_0grbd3C_SG5JOwe18,12576
|
|
443
444
|
blaxel/core/sandbox/default/session.py,sha256=XzVpPOH_az6T38Opp4Hmj3RIg7QCzA1l5wh1YDh7czc,5313
|
|
444
445
|
blaxel/core/sandbox/sync/__init__.py,sha256=iqTRxQYbJyHTXoA4MHaigeXFxi9wtJ3o9XygZuFe3bM,372
|
|
@@ -448,7 +449,7 @@ blaxel/core/sandbox/sync/filesystem.py,sha256=FoxM9EJ5sXGysf-x22tbt9yrcbbpaunTD3
|
|
|
448
449
|
blaxel/core/sandbox/sync/interpreter.py,sha256=5cAzwnt5BgnByGimagMBotjGW2vMAz4vutBBrrFV9-A,11062
|
|
449
450
|
blaxel/core/sandbox/sync/network.py,sha256=QkCFKfFayvwL1J4JYwOuXPGlYQuX4J9Jj55Kf_kD-ig,283
|
|
450
451
|
blaxel/core/sandbox/sync/preview.py,sha256=w3bC8iA3QecHiLkRvITmQ6LTT9Co_93G24QpZFgEQSE,6379
|
|
451
|
-
blaxel/core/sandbox/sync/process.py,sha256=
|
|
452
|
+
blaxel/core/sandbox/sync/process.py,sha256=W-ZUM6VyFDxTmexHTQn9PI6iRc0QiB9JMOEq__r2bBA,14913
|
|
452
453
|
blaxel/core/sandbox/sync/sandbox.py,sha256=jNqwMJXIpjC8Fs9nn-ujfSpFK2PgCvEePISKQKaMhH0,10515
|
|
453
454
|
blaxel/core/sandbox/sync/session.py,sha256=e0CVbW2LBRYTwm4RL52S0UdNvhNfuFLo6AYE5hk9DH0,4931
|
|
454
455
|
blaxel/core/tools/__init__.py,sha256=OK2TFqeXAIi6CC7xtL8fFl-4DvCB7jjihkhx6RTld_c,13147
|
|
@@ -500,7 +501,7 @@ blaxel/telemetry/instrumentation/map.py,sha256=PCzZJj39yiYVYJrxLBNP-NW-tjjYyTijw
|
|
|
500
501
|
blaxel/telemetry/instrumentation/utils.py,sha256=FGyMY5ZE4f-0JdZpm_R_BCoKLJ18hftz8vsh7ftDwMk,1889
|
|
501
502
|
blaxel/telemetry/log/log.py,sha256=vtzUIFIIj4MTTKUigILDYXN8NHHPOo44OaKukpyIjQg,2407
|
|
502
503
|
blaxel/telemetry/log/logger.py,sha256=IcFWCd1yyWWGAjAd2i0pDYqpZHQ61pmcaQ7Kf4bC8lg,4150
|
|
503
|
-
blaxel-0.2.
|
|
504
|
-
blaxel-0.2.
|
|
505
|
-
blaxel-0.2.
|
|
506
|
-
blaxel-0.2.
|
|
504
|
+
blaxel-0.2.33.dist-info/METADATA,sha256=nWdiqiO6Qc_LIqAz4Tc6AyLaXFzhtyFkL0lDUSwwxQU,10074
|
|
505
|
+
blaxel-0.2.33.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
506
|
+
blaxel-0.2.33.dist-info/licenses/LICENSE,sha256=p5PNQvpvyDT_0aYBDgmV1fFI_vAD2aSV0wWG7VTgRis,1069
|
|
507
|
+
blaxel-0.2.33.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|