boxlite 0.2.0.dev0__cp310-cp310-macosx_14_0_arm64.whl → 0.5.6__cp310-cp310-macosx_14_0_arm64.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.
- boxlite/__init__.py +54 -23
- boxlite/boxlite.cpython-310-darwin.so +0 -0
- boxlite/browserbox.py +10 -3
- boxlite/codebox.py +7 -7
- boxlite/computerbox.py +129 -336
- boxlite/constants.py +25 -0
- boxlite/errors.py +8 -2
- boxlite/exec.py +2 -1
- boxlite/interactivebox.py +50 -56
- boxlite/runtime/boxlite-guest +0 -0
- boxlite/runtime/boxlite-shim +0 -0
- boxlite/runtime/debugfs +0 -0
- boxlite/runtime/libgvproxy.dylib +0 -0
- boxlite/runtime/libkrun.1.16.0.dylib +0 -0
- boxlite/runtime/{libkrunfw.4.dylib → libkrunfw.5.dylib} +0 -0
- boxlite/runtime/mke2fs +0 -0
- boxlite/simplebox.py +81 -34
- boxlite/sync_api/__init__.py +65 -0
- boxlite/sync_api/_box.py +133 -0
- boxlite/sync_api/_boxlite.py +377 -0
- boxlite/sync_api/_codebox.py +145 -0
- boxlite/sync_api/_execution.py +203 -0
- boxlite/sync_api/_simplebox.py +180 -0
- boxlite/sync_api/_sync_base.py +137 -0
- boxlite-0.5.6.dist-info/METADATA +845 -0
- boxlite-0.5.6.dist-info/RECORD +27 -0
- {boxlite-0.2.0.dev0.dist-info → boxlite-0.5.6.dist-info}/WHEEL +1 -1
- boxlite-0.2.0.dev0.dist-info/METADATA +0 -9
- boxlite-0.2.0.dev0.dist-info/RECORD +0 -17
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncBoxlite - Synchronous wrapper for Boxlite runtime.
|
|
3
|
+
|
|
4
|
+
Provides sync API using greenlet fiber switching. This is the main entry point
|
|
5
|
+
for the synchronous BoxLite API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
10
|
+
|
|
11
|
+
from greenlet import greenlet
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ._box import SyncBox
|
|
15
|
+
from ..boxlite import Boxlite, BoxOptions, BoxInfo, RuntimeMetrics, Options
|
|
16
|
+
|
|
17
|
+
__all__ = ["SyncBoxlite"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncBoxlite:
|
|
21
|
+
"""
|
|
22
|
+
Synchronous wrapper for Boxlite runtime.
|
|
23
|
+
|
|
24
|
+
This class handles both the dispatcher fiber lifecycle AND provides the
|
|
25
|
+
runtime API. API mirrors async Boxlite exactly.
|
|
26
|
+
|
|
27
|
+
Usage (default runtime - preferred):
|
|
28
|
+
with SyncBoxlite.default() as runtime:
|
|
29
|
+
box = runtime.create(BoxOptions(image="alpine:latest"))
|
|
30
|
+
execution = box.exec("echo", ["Hello"])
|
|
31
|
+
for line in execution.stdout():
|
|
32
|
+
print(line)
|
|
33
|
+
box.stop()
|
|
34
|
+
|
|
35
|
+
Usage (with custom options):
|
|
36
|
+
with SyncBoxlite(Options(home_dir="/custom/path")) as runtime:
|
|
37
|
+
box = runtime.create(BoxOptions(image="alpine:latest"))
|
|
38
|
+
# Data stored in /custom/path instead of ~/.boxlite
|
|
39
|
+
box.stop()
|
|
40
|
+
|
|
41
|
+
Usage (manual start/stop - for REPL, test fixtures):
|
|
42
|
+
runtime = SyncBoxlite.default().start()
|
|
43
|
+
box = runtime.create(BoxOptions(image="alpine:latest"))
|
|
44
|
+
# ... use box ...
|
|
45
|
+
runtime.stop()
|
|
46
|
+
|
|
47
|
+
Architecture:
|
|
48
|
+
- Creates a dispatcher greenlet fiber that runs the event loop
|
|
49
|
+
- User code runs in the main fiber
|
|
50
|
+
- When user calls a sync method, it switches to dispatcher
|
|
51
|
+
- Dispatcher processes the async task
|
|
52
|
+
- When task completes, callback switches back to user fiber
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, options: "Options") -> None:
|
|
56
|
+
"""Create a new SyncBoxlite instance.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
options: Runtime options (e.g., custom home_dir).
|
|
60
|
+
Use SyncBoxlite.default() for default runtime.
|
|
61
|
+
"""
|
|
62
|
+
from ..boxlite import Boxlite
|
|
63
|
+
|
|
64
|
+
self._boxlite = Boxlite(options)
|
|
65
|
+
|
|
66
|
+
self._loop: asyncio.AbstractEventLoop = None
|
|
67
|
+
self._dispatcher_fiber: greenlet = None
|
|
68
|
+
self._own_loop = False
|
|
69
|
+
self._sync_helper = None
|
|
70
|
+
self._started = False
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> "SyncBoxlite":
|
|
73
|
+
"""
|
|
74
|
+
Start the sync runtime and enter context.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Self, with dispatcher fiber running and runtime ready.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
RuntimeError: If called from within an async context.
|
|
81
|
+
"""
|
|
82
|
+
# 1. Create event loop
|
|
83
|
+
try:
|
|
84
|
+
self._loop = asyncio.get_running_loop()
|
|
85
|
+
except RuntimeError:
|
|
86
|
+
self._loop = asyncio.new_event_loop()
|
|
87
|
+
self._own_loop = True
|
|
88
|
+
|
|
89
|
+
# 2. Check not in async context (loop shouldn't be running)
|
|
90
|
+
if self._loop.is_running():
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
"Cannot use SyncBoxlite inside an asyncio loop. "
|
|
93
|
+
"Use the async API (CodeBox, SimpleBox) instead."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# 3. Create dispatcher fiber
|
|
97
|
+
def greenlet_main() -> None:
|
|
98
|
+
"""
|
|
99
|
+
Dispatcher fiber entry point.
|
|
100
|
+
|
|
101
|
+
Runs the event loop indefinitely until stop() is called.
|
|
102
|
+
The event loop efficiently waits for I/O events (via OS-level
|
|
103
|
+
epoll/kqueue) and processes tasks scheduled by _sync() calls.
|
|
104
|
+
"""
|
|
105
|
+
self._loop.run_forever()
|
|
106
|
+
|
|
107
|
+
self._dispatcher_fiber = greenlet(greenlet_main)
|
|
108
|
+
|
|
109
|
+
from ._sync_base import SyncBase
|
|
110
|
+
|
|
111
|
+
self._sync_helper = SyncBase(self._boxlite, self._loop, self._dispatcher_fiber)
|
|
112
|
+
|
|
113
|
+
# 5. Start dispatcher fiber
|
|
114
|
+
g_self = greenlet.getcurrent()
|
|
115
|
+
|
|
116
|
+
def on_ready():
|
|
117
|
+
"""Callback to switch back to user fiber after dispatcher starts."""
|
|
118
|
+
g_self.switch()
|
|
119
|
+
|
|
120
|
+
self._loop.call_soon(on_ready)
|
|
121
|
+
self._dispatcher_fiber.switch()
|
|
122
|
+
# Control returns here after dispatcher calls on_ready()
|
|
123
|
+
|
|
124
|
+
self._started = True
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Stop the sync runtime and clean up.
|
|
130
|
+
|
|
131
|
+
This stops the dispatcher fiber and closes the event loop if we own it.
|
|
132
|
+
"""
|
|
133
|
+
self._started = False
|
|
134
|
+
|
|
135
|
+
# Signal the event loop to stop, then switch to let it finish cleanly
|
|
136
|
+
self._loop.call_soon(self._loop.stop)
|
|
137
|
+
self._dispatcher_fiber.switch()
|
|
138
|
+
|
|
139
|
+
if self._own_loop:
|
|
140
|
+
# Cancel pending tasks
|
|
141
|
+
try:
|
|
142
|
+
tasks = asyncio.all_tasks(self._loop)
|
|
143
|
+
for t in [t for t in tasks if not (t.done() or t.cancelled())]:
|
|
144
|
+
t.cancel()
|
|
145
|
+
self._loop.run_until_complete(self._loop.shutdown_asyncgens())
|
|
146
|
+
except Exception:
|
|
147
|
+
pass # Ignore errors during cleanup
|
|
148
|
+
finally:
|
|
149
|
+
self._loop.close()
|
|
150
|
+
|
|
151
|
+
def start(self) -> "SyncBoxlite":
|
|
152
|
+
"""
|
|
153
|
+
Start the sync runtime (non-context-manager usage).
|
|
154
|
+
|
|
155
|
+
This is an alternative to using `with SyncBoxlite.default() as runtime:`.
|
|
156
|
+
Useful for REPL sessions, test fixtures, or class-based lifecycle.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Self, with dispatcher fiber running and runtime ready.
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
runtime = SyncBoxlite.default().start()
|
|
163
|
+
box = runtime.create(BoxOptions(image="alpine:latest"))
|
|
164
|
+
# ... use box ...
|
|
165
|
+
runtime.stop() # Don't forget to stop!
|
|
166
|
+
"""
|
|
167
|
+
return self.__enter__()
|
|
168
|
+
|
|
169
|
+
def stop(self) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Stop the sync runtime (non-context-manager usage).
|
|
172
|
+
|
|
173
|
+
This cleans up the dispatcher fiber and event loop.
|
|
174
|
+
Must be called if you used start() instead of the context manager.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
runtime = SyncBoxlite.default().start()
|
|
178
|
+
try:
|
|
179
|
+
box = runtime.create(BoxOptions(image="alpine:latest"))
|
|
180
|
+
# ... use box ...
|
|
181
|
+
finally:
|
|
182
|
+
runtime.stop()
|
|
183
|
+
"""
|
|
184
|
+
self.__exit__(None, None, None)
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def init_default(options: "Options") -> None:
|
|
188
|
+
"""
|
|
189
|
+
Initialize the global default runtime with custom options.
|
|
190
|
+
|
|
191
|
+
This must be called before any SyncBoxlite.default() usage if you want
|
|
192
|
+
to customize the default runtime (e.g., custom home_dir).
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
options: Runtime options to use for the default runtime.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
SyncBoxlite.init_default(Options(home_dir="/custom/path"))
|
|
199
|
+
with SyncBoxlite.default() as runtime: # Now uses /custom/path
|
|
200
|
+
...
|
|
201
|
+
"""
|
|
202
|
+
from ..boxlite import Boxlite
|
|
203
|
+
|
|
204
|
+
Boxlite.init_default(options)
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def default() -> "SyncBoxlite":
|
|
208
|
+
"""
|
|
209
|
+
Create a SyncBoxlite with default runtime options.
|
|
210
|
+
|
|
211
|
+
This is the recommended way to create a SyncBoxlite instance.
|
|
212
|
+
Mirrors async Boxlite.default().
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A new SyncBoxlite instance using the default runtime (~/.boxlite).
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
with SyncBoxlite.default() as runtime:
|
|
219
|
+
box = runtime.create(BoxOptions(image="alpine:latest"))
|
|
220
|
+
...
|
|
221
|
+
"""
|
|
222
|
+
instance = object.__new__(SyncBoxlite)
|
|
223
|
+
|
|
224
|
+
from ..boxlite import Boxlite
|
|
225
|
+
|
|
226
|
+
instance._boxlite = Boxlite.default()
|
|
227
|
+
|
|
228
|
+
instance._loop = None
|
|
229
|
+
instance._dispatcher_fiber = None
|
|
230
|
+
instance._own_loop = False
|
|
231
|
+
instance._sync_helper = None
|
|
232
|
+
instance._started = False
|
|
233
|
+
|
|
234
|
+
return instance
|
|
235
|
+
|
|
236
|
+
def _require_started(self) -> None:
|
|
237
|
+
"""Raise RuntimeError if runtime not started."""
|
|
238
|
+
if not self._started:
|
|
239
|
+
raise RuntimeError(
|
|
240
|
+
"SyncBoxlite not started. Use 'with SyncBoxlite(...) as runtime:' "
|
|
241
|
+
"or call 'SyncBoxlite.start()' first."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def _sync(self, coro):
|
|
245
|
+
"""Run async operation synchronously."""
|
|
246
|
+
self._require_started()
|
|
247
|
+
return self._sync_helper._sync(coro)
|
|
248
|
+
|
|
249
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
250
|
+
# Runtime API (mirrors Boxlite)
|
|
251
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
def create(
|
|
254
|
+
self,
|
|
255
|
+
options: "BoxOptions",
|
|
256
|
+
name: Optional[str] = None,
|
|
257
|
+
) -> "SyncBox":
|
|
258
|
+
"""
|
|
259
|
+
Create a new box.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
options: BoxOptions specifying image, resources, etc.
|
|
263
|
+
name: Optional unique name for the box.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
SyncBox handle for the created box.
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
with SyncBoxlite.default() as runtime:
|
|
270
|
+
box = runtime.create(BoxOptions(image="alpine:latest"))
|
|
271
|
+
"""
|
|
272
|
+
self._require_started()
|
|
273
|
+
from ._box import SyncBox
|
|
274
|
+
|
|
275
|
+
native_box = self._sync(self._boxlite.create(options, name=name))
|
|
276
|
+
return SyncBox(self, native_box)
|
|
277
|
+
|
|
278
|
+
def get(self, id_or_name: str) -> Optional["SyncBox"]:
|
|
279
|
+
"""
|
|
280
|
+
Get an existing box by ID or name.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
id_or_name: Box ID or name to look up.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
SyncBox if found, None otherwise.
|
|
287
|
+
"""
|
|
288
|
+
self._require_started()
|
|
289
|
+
from ._box import SyncBox
|
|
290
|
+
|
|
291
|
+
native_box = self._sync(self._boxlite.get(id_or_name))
|
|
292
|
+
if native_box is None:
|
|
293
|
+
return None
|
|
294
|
+
return SyncBox(self, native_box)
|
|
295
|
+
|
|
296
|
+
def list_info(self) -> List["BoxInfo"]:
|
|
297
|
+
"""
|
|
298
|
+
List all boxes.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
List of BoxInfo for all boxes.
|
|
302
|
+
"""
|
|
303
|
+
self._require_started()
|
|
304
|
+
return self._sync(self._boxlite.list_info())
|
|
305
|
+
|
|
306
|
+
def get_info(self, id_or_name: str) -> Optional["BoxInfo"]:
|
|
307
|
+
"""
|
|
308
|
+
Get info for a box by ID or name.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
id_or_name: Box ID or name to look up.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
BoxInfo if found, None otherwise.
|
|
315
|
+
"""
|
|
316
|
+
self._require_started()
|
|
317
|
+
return self._sync(self._boxlite.get_info(id_or_name))
|
|
318
|
+
|
|
319
|
+
def metrics(self) -> "RuntimeMetrics":
|
|
320
|
+
"""
|
|
321
|
+
Get runtime metrics.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
RuntimeMetrics with aggregate statistics.
|
|
325
|
+
"""
|
|
326
|
+
self._require_started()
|
|
327
|
+
return self._sync(self._boxlite.metrics())
|
|
328
|
+
|
|
329
|
+
def remove(self, id_or_name: str, force: bool = False) -> None:
|
|
330
|
+
"""
|
|
331
|
+
Remove a box.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
id_or_name: Box ID or name to remove.
|
|
335
|
+
force: Force removal even if box is running.
|
|
336
|
+
"""
|
|
337
|
+
self._sync(self._boxlite.remove(id_or_name, force))
|
|
338
|
+
|
|
339
|
+
def shutdown(self, timeout: Optional[int] = None) -> None:
|
|
340
|
+
"""
|
|
341
|
+
Gracefully shutdown all boxes in this runtime.
|
|
342
|
+
|
|
343
|
+
This method stops all running boxes, waiting up to `timeout` seconds
|
|
344
|
+
for each box to stop gracefully before force-killing it.
|
|
345
|
+
|
|
346
|
+
After calling this method, the runtime is permanently shut down and
|
|
347
|
+
will return errors for any new operations (like `create()`).
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
timeout: Seconds to wait before force-killing each box:
|
|
351
|
+
- None (default) - Use default timeout (10 seconds)
|
|
352
|
+
- Positive integer - Wait that many seconds
|
|
353
|
+
- -1 - Wait indefinitely (no timeout)
|
|
354
|
+
"""
|
|
355
|
+
self._sync(self._boxlite.shutdown(timeout))
|
|
356
|
+
|
|
357
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
358
|
+
# Properties for internal use by SyncBox/SyncExecution
|
|
359
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def loop(self) -> asyncio.AbstractEventLoop:
|
|
363
|
+
"""Get the event loop used by this runtime."""
|
|
364
|
+
return self._loop
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def dispatcher_fiber(self) -> greenlet:
|
|
368
|
+
"""Get the dispatcher greenlet fiber."""
|
|
369
|
+
return self._dispatcher_fiber
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def runtime(self) -> "Boxlite":
|
|
373
|
+
"""Get the underlying native Boxlite runtime (for internal/advanced use)."""
|
|
374
|
+
return self._boxlite
|
|
375
|
+
|
|
376
|
+
def __repr__(self) -> str:
|
|
377
|
+
return f"SyncBoxlite({self._boxlite})"
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncCodeBox - Synchronous wrapper for CodeBox.
|
|
3
|
+
|
|
4
|
+
Provides a synchronous API for Python code execution using greenlet fiber switching.
|
|
5
|
+
API mirrors async CodeBox exactly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
9
|
+
|
|
10
|
+
from ._simplebox import SyncSimpleBox
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._boxlite import SyncBoxlite
|
|
14
|
+
|
|
15
|
+
__all__ = ["SyncCodeBox"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SyncCodeBox(SyncSimpleBox):
|
|
19
|
+
"""
|
|
20
|
+
Synchronous wrapper for CodeBox.
|
|
21
|
+
|
|
22
|
+
Provides synchronous methods for executing Python code in a secure container.
|
|
23
|
+
Built on top of SyncSimpleBox with Python-specific convenience methods.
|
|
24
|
+
API mirrors async CodeBox exactly.
|
|
25
|
+
|
|
26
|
+
Usage (standalone - recommended):
|
|
27
|
+
with SyncCodeBox() as box:
|
|
28
|
+
result = box.run("print('Hello, World!')")
|
|
29
|
+
print(result) # Hello, World!
|
|
30
|
+
|
|
31
|
+
Usage (with explicit runtime):
|
|
32
|
+
with SyncBoxlite.default() as runtime:
|
|
33
|
+
with SyncCodeBox(runtime=runtime) as box:
|
|
34
|
+
result = box.run("print('Hello!')")
|
|
35
|
+
print(result)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
image: str = "python:slim",
|
|
41
|
+
memory_mib: Optional[int] = None,
|
|
42
|
+
cpus: Optional[int] = None,
|
|
43
|
+
runtime: Optional["SyncBoxlite"] = None,
|
|
44
|
+
name: Optional[str] = None,
|
|
45
|
+
auto_remove: bool = True,
|
|
46
|
+
**kwargs,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Create a SyncCodeBox.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
image: Python container image (default: "python:slim")
|
|
53
|
+
memory_mib: Memory limit in MiB (default: system default)
|
|
54
|
+
cpus: Number of CPU cores (default: system default)
|
|
55
|
+
runtime: Optional SyncBoxlite runtime. If None, creates default runtime.
|
|
56
|
+
name: Optional unique name for the box
|
|
57
|
+
auto_remove: Remove box when stopped (default: True)
|
|
58
|
+
**kwargs: Additional BoxOptions parameters
|
|
59
|
+
"""
|
|
60
|
+
super().__init__(
|
|
61
|
+
image=image,
|
|
62
|
+
memory_mib=memory_mib,
|
|
63
|
+
cpus=cpus,
|
|
64
|
+
runtime=runtime,
|
|
65
|
+
name=name,
|
|
66
|
+
auto_remove=auto_remove,
|
|
67
|
+
**kwargs,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def run(self, code: str, timeout: Optional[int] = None) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Execute Python code synchronously.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
code: Python code to execute
|
|
76
|
+
timeout: Execution timeout in seconds (not yet implemented)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Combined stdout and stderr output
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
with SyncCodeBox() as box:
|
|
83
|
+
result = box.run("print('Hello!')")
|
|
84
|
+
print(result) # Hello!
|
|
85
|
+
|
|
86
|
+
# Multi-line code
|
|
87
|
+
result = box.run('''
|
|
88
|
+
import sys
|
|
89
|
+
print(f"Python {sys.version}")
|
|
90
|
+
''')
|
|
91
|
+
"""
|
|
92
|
+
result = self.exec("/usr/local/bin/python", "-c", code)
|
|
93
|
+
return result.stdout + result.stderr
|
|
94
|
+
|
|
95
|
+
def install_package(self, package: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Install a Python package using pip.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
package: Package name (e.g., "requests", "numpy==1.24.0")
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Installation output
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
box.install_package("requests")
|
|
107
|
+
result = box.run("import requests; print(requests.__version__)")
|
|
108
|
+
"""
|
|
109
|
+
result = self.exec("pip", "install", package)
|
|
110
|
+
return result.stdout + result.stderr
|
|
111
|
+
|
|
112
|
+
def install_packages(self, *packages: str) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Install multiple Python packages.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
*packages: Package names to install
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Installation output
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
box.install_packages("requests", "numpy", "pandas")
|
|
124
|
+
"""
|
|
125
|
+
result = self.exec("pip", "install", *packages)
|
|
126
|
+
return result.stdout + result.stderr
|
|
127
|
+
|
|
128
|
+
def run_script(self, script_path: str) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Execute a Python script file.
|
|
131
|
+
|
|
132
|
+
Reads the script from the host filesystem and executes it in the box.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
script_path: Path to the Python script on the host
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Script output (stdout + stderr)
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
result = box.run_script("./my_script.py")
|
|
142
|
+
"""
|
|
143
|
+
with open(script_path, "r") as f:
|
|
144
|
+
code = f.read()
|
|
145
|
+
return self.run(code)
|