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.
@@ -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)