boxlite 0.5.7__cp312-cp312-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.

Potentially problematic release.


This version of boxlite might be problematic. Click here for more details.

@@ -0,0 +1,65 @@
1
+ """
2
+ BoxLite Sync API - Synchronous wrappers using greenlet fiber switching.
3
+
4
+ This module provides synchronous wrappers for BoxLite's async API using
5
+ greenlet fiber switching. This allows sync code to execute async operations
6
+ without blocking the event loop.
7
+
8
+ Architecture:
9
+ - A dispatcher fiber runs the asyncio event loop
10
+ - User code runs in the main fiber
11
+ - _sync() method switches between fibers to execute async operations
12
+
13
+ Usage:
14
+ from boxlite import SyncCodeBox, SyncSimpleBox
15
+
16
+ # Simplest usage - standalone (like async API)
17
+ with SyncCodeBox() as box:
18
+ result = box.run("print('Hello!')")
19
+ print(result)
20
+
21
+ with SyncSimpleBox(image="alpine:latest") as box:
22
+ result = box.exec("echo", "Hello")
23
+ print(result.stdout)
24
+
25
+ # Or with explicit runtime (for multiple boxes)
26
+ from boxlite import SyncBoxlite
27
+
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
+ Requirements:
36
+ - greenlet>=3.0.0 (install with: pip install boxlite[sync])
37
+
38
+ Note:
39
+ This API cannot be used from within an async context (e.g., inside
40
+ an async function or when an event loop is already running).
41
+ Use the async API (CodeBox, SimpleBox) in those cases.
42
+ """
43
+
44
+ from ._boxlite import SyncBoxlite
45
+ from ._sync_base import SyncBase, SyncContextManager
46
+ from ._box import SyncBox
47
+ from ._execution import SyncExecution, SyncExecStdout, SyncExecStderr
48
+ from ._simplebox import SyncSimpleBox
49
+ from ._codebox import SyncCodeBox
50
+
51
+ __all__ = [
52
+ # Entry point
53
+ "SyncBoxlite",
54
+ # Base classes
55
+ "SyncBase",
56
+ "SyncContextManager",
57
+ # Native API mirrors
58
+ "SyncBox",
59
+ "SyncExecution",
60
+ "SyncExecStdout",
61
+ "SyncExecStderr",
62
+ # Convenience wrappers
63
+ "SyncSimpleBox",
64
+ "SyncCodeBox",
65
+ ]
@@ -0,0 +1,133 @@
1
+ """
2
+ SyncBox - Synchronous wrapper for Box.
3
+
4
+ Mirrors the native Box API exactly, but with synchronous methods.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, List, Optional, Tuple
8
+
9
+ if TYPE_CHECKING:
10
+ from ._boxlite import SyncBoxlite
11
+ from ._execution import SyncExecution
12
+ from ..boxlite import Box, BoxInfo, BoxMetrics
13
+
14
+ __all__ = ["SyncBox"]
15
+
16
+
17
+ class SyncBox:
18
+ """
19
+ Synchronous wrapper for Box.
20
+
21
+ Provides the same API as the native Box class, but with synchronous methods.
22
+ Uses greenlet fiber switching internally to bridge async operations.
23
+
24
+ Usage:
25
+ with SyncBoxlite.default() as runtime:
26
+ box = runtime.create(BoxOptions(image="alpine:latest"))
27
+
28
+ execution = box.exec("echo", ["Hello"])
29
+ stdout = execution.stdout()
30
+
31
+ for line in stdout:
32
+ print(line)
33
+
34
+ result = execution.wait()
35
+ box.stop()
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ runtime: "SyncBoxlite",
41
+ box: "Box",
42
+ ) -> None:
43
+ """
44
+ Create a SyncBox wrapper.
45
+
46
+ Args:
47
+ runtime: The SyncBoxlite runtime providing event loop and dispatcher
48
+ box: The native Box object to wrap
49
+ """
50
+ from ._sync_base import SyncBase
51
+
52
+ self._box = box
53
+ self._runtime = runtime
54
+ # Create a SyncBase helper for _sync() method
55
+ self._sync_helper = SyncBase(box, runtime.loop, runtime.dispatcher_fiber)
56
+
57
+ def _sync(self, coro):
58
+ """Run async operation synchronously."""
59
+ return self._sync_helper._sync(coro)
60
+
61
+ @property
62
+ def id(self) -> str:
63
+ """Get the box ID."""
64
+ return self._box.id
65
+
66
+ @property
67
+ def name(self) -> Optional[str]:
68
+ """Get the box name (if set)."""
69
+ return self._box.name
70
+
71
+ def info(self) -> "BoxInfo":
72
+ """Get box information (synchronous, no I/O)."""
73
+ return self._box.info()
74
+
75
+ def start(self) -> None:
76
+ """
77
+ Start the box (initialize VM).
78
+
79
+ For Configured boxes: initializes VM for the first time.
80
+ For Stopped boxes: restarts the VM.
81
+
82
+ This is idempotent - calling start() on a Running box is a no-op.
83
+ Also called implicitly by exec() if the box is not running.
84
+ """
85
+ self._sync(self._box.start())
86
+
87
+ def exec(
88
+ self,
89
+ cmd: str,
90
+ args: Optional[List[str]] = None,
91
+ env: Optional[List[Tuple[str, str]]] = None,
92
+ ) -> "SyncExecution":
93
+ """
94
+ Execute a command in the box.
95
+
96
+ Args:
97
+ cmd: Command to run (e.g., "echo", "python")
98
+ args: Command arguments as list
99
+ env: Environment variables as list of (key, value) tuples
100
+
101
+ Returns:
102
+ SyncExecution handle for streaming output and waiting for completion.
103
+
104
+ Example:
105
+ execution = box.exec("echo", ["Hello, World!"])
106
+ for line in execution.stdout():
107
+ print(line)
108
+ result = execution.wait()
109
+ print(f"Exit code: {result.exit_code}")
110
+ """
111
+ from ._execution import SyncExecution
112
+
113
+ # Run the async exec and get the Execution handle
114
+ execution = self._sync(self._box.exec(cmd, args, env))
115
+ return SyncExecution(self._runtime, execution)
116
+
117
+ def stop(self) -> None:
118
+ """Stop the box (preserves state for potential restart)."""
119
+ self._sync(self._box.stop())
120
+
121
+ def metrics(self) -> "BoxMetrics":
122
+ """Get box metrics (CPU, memory usage, etc.)."""
123
+ return self._sync(self._box.metrics())
124
+
125
+ # Context manager support
126
+ def __enter__(self) -> "SyncBox":
127
+ """Enter context - starts the box."""
128
+ self._sync(self._box.__aenter__())
129
+ return self
130
+
131
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
132
+ """Exit context - stops the box."""
133
+ self._sync(self._box.__aexit__(exc_type, exc_val, exc_tb))
@@ -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})"