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.
- boxlite/__init__.py +123 -0
- boxlite/boxlite.cpython-312-darwin.so +0 -0
- boxlite/browserbox.py +142 -0
- boxlite/codebox.py +120 -0
- boxlite/computerbox.py +302 -0
- boxlite/constants.py +25 -0
- boxlite/errors.py +44 -0
- boxlite/exec.py +27 -0
- boxlite/interactivebox.py +287 -0
- 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.5.dylib +0 -0
- boxlite/runtime/mke2fs +0 -0
- boxlite/simplebox.py +256 -0
- 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.7.dist-info/METADATA +845 -0
- boxlite-0.5.7.dist-info/RECORD +27 -0
- boxlite-0.5.7.dist-info/WHEEL +4 -0
|
@@ -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
|
+
]
|
boxlite/sync_api/_box.py
ADDED
|
@@ -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})"
|