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
boxlite/errors.py
CHANGED
|
@@ -4,11 +4,12 @@ BoxLite error types.
|
|
|
4
4
|
Provides a hierarchy of exceptions for different failure modes.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__all__ = [
|
|
7
|
+
__all__ = ["BoxliteError", "ExecError", "TimeoutError", "ParseError"]
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BoxliteError(Exception):
|
|
11
11
|
"""Base exception for all boxlite errors."""
|
|
12
|
+
|
|
12
13
|
pass
|
|
13
14
|
|
|
14
15
|
|
|
@@ -21,18 +22,23 @@ class ExecError(BoxliteError):
|
|
|
21
22
|
exit_code: The non-zero exit code
|
|
22
23
|
stderr: Standard error output from the command
|
|
23
24
|
"""
|
|
25
|
+
|
|
24
26
|
def __init__(self, command: str, exit_code: int, stderr: str):
|
|
25
27
|
self.command = command
|
|
26
28
|
self.exit_code = exit_code
|
|
27
29
|
self.stderr = stderr
|
|
28
|
-
super().__init__(
|
|
30
|
+
super().__init__(
|
|
31
|
+
f"Command '{command}' failed with exit code {exit_code}: {stderr}"
|
|
32
|
+
)
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
class TimeoutError(BoxliteError):
|
|
32
36
|
"""Raised when an operation times out."""
|
|
37
|
+
|
|
33
38
|
pass
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
class ParseError(BoxliteError):
|
|
37
42
|
"""Raised when output parsing fails."""
|
|
43
|
+
|
|
38
44
|
pass
|
boxlite/exec.py
CHANGED
|
@@ -7,7 +7,7 @@ Provides Docker-like API for executing commands in boxes.
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
|
-
|
|
10
|
+
"ExecResult",
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
|
|
@@ -21,6 +21,7 @@ class ExecResult:
|
|
|
21
21
|
stdout: Standard output as string
|
|
22
22
|
stderr: Standard error as string
|
|
23
23
|
"""
|
|
24
|
+
|
|
24
25
|
exit_code: int
|
|
25
26
|
stdout: str
|
|
26
27
|
stderr: str
|
boxlite/interactivebox.py
CHANGED
|
@@ -12,19 +12,21 @@ import termios
|
|
|
12
12
|
import tty
|
|
13
13
|
from typing import Optional, TYPE_CHECKING
|
|
14
14
|
|
|
15
|
+
from .simplebox import SimpleBox
|
|
16
|
+
|
|
15
17
|
if TYPE_CHECKING:
|
|
16
|
-
from .
|
|
18
|
+
from .boxlite import Boxlite
|
|
17
19
|
|
|
18
20
|
# Configure logger
|
|
19
21
|
logger = logging.getLogger("boxlite.interactivebox")
|
|
20
22
|
|
|
21
23
|
|
|
22
|
-
class InteractiveBox:
|
|
24
|
+
class InteractiveBox(SimpleBox):
|
|
23
25
|
"""
|
|
24
26
|
Interactive box with automatic PTY and terminal forwarding.
|
|
25
27
|
|
|
26
28
|
When used as a context manager, automatically:
|
|
27
|
-
1. Auto-detects terminal size (
|
|
29
|
+
1. Auto-detects terminal size (for PTY)
|
|
28
30
|
2. Starts a shell with PTY
|
|
29
31
|
3. Sets local terminal to cbreak mode
|
|
30
32
|
4. Forwards stdin/stdout bidirectionally
|
|
@@ -39,14 +41,16 @@ class InteractiveBox:
|
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
43
|
def __init__(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
self,
|
|
45
|
+
image: str,
|
|
46
|
+
shell: str = "/bin/sh",
|
|
47
|
+
tty: Optional[bool] = None,
|
|
48
|
+
memory_mib: Optional[int] = None,
|
|
49
|
+
cpus: Optional[int] = None,
|
|
50
|
+
runtime: Optional["Boxlite"] = None,
|
|
51
|
+
name: Optional[str] = None,
|
|
52
|
+
auto_remove: bool = True,
|
|
53
|
+
**kwargs,
|
|
50
54
|
):
|
|
51
55
|
"""
|
|
52
56
|
Create interactive box.
|
|
@@ -61,43 +65,27 @@ class InteractiveBox:
|
|
|
61
65
|
memory_mib: Memory limit in MiB
|
|
62
66
|
cpus: Number of CPU cores
|
|
63
67
|
runtime: Optional runtime instance (uses global default if None)
|
|
68
|
+
name: Optional name for the box (must be unique)
|
|
69
|
+
auto_remove: Remove box when stopped (default: True)
|
|
64
70
|
**kwargs: Additional configuration options (working_dir, env)
|
|
65
71
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
except ImportError as e:
|
|
69
|
-
raise ImportError(
|
|
70
|
-
f"BoxLite native extension not found: {e}. "
|
|
71
|
-
"Please install with: pip install boxlite"
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Use provided runtime or get default
|
|
75
|
-
if runtime is None:
|
|
76
|
-
self._runtime = Boxlite.default()
|
|
77
|
-
else:
|
|
78
|
-
self._runtime = runtime
|
|
79
|
-
|
|
80
|
-
# Create box options
|
|
81
|
-
box_opts = BoxOptions(
|
|
72
|
+
# Initialize base class (handles runtime, BoxOptions, _box, _started)
|
|
73
|
+
super().__init__(
|
|
82
74
|
image=image,
|
|
83
|
-
cpus=cpus,
|
|
84
75
|
memory_mib=memory_mib,
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
cpus=cpus,
|
|
77
|
+
runtime=runtime,
|
|
78
|
+
name=name,
|
|
79
|
+
auto_remove=auto_remove,
|
|
80
|
+
**kwargs,
|
|
87
81
|
)
|
|
88
82
|
|
|
89
|
-
#
|
|
90
|
-
self._box = self._runtime.create(box_opts)
|
|
91
|
-
|
|
92
|
-
# Store interactive config
|
|
83
|
+
# InteractiveBox-specific config
|
|
93
84
|
self._shell = shell
|
|
94
|
-
self._env = kwargs.get(
|
|
85
|
+
self._env = kwargs.get("env", [])
|
|
95
86
|
|
|
96
87
|
# Determine TTY mode: None = auto-detect, True = force, False = disable
|
|
97
|
-
if tty is None
|
|
98
|
-
self._tty = sys.stdin.isatty()
|
|
99
|
-
else:
|
|
100
|
-
self._tty = tty
|
|
88
|
+
self._tty = sys.stdin.isatty() if tty is None else tty
|
|
101
89
|
|
|
102
90
|
# Interactive state
|
|
103
91
|
self._old_tty_settings = None
|
|
@@ -108,15 +96,15 @@ class InteractiveBox:
|
|
|
108
96
|
self._stderr = None
|
|
109
97
|
self._exited = None # Event to signal process exit
|
|
110
98
|
|
|
111
|
-
|
|
112
|
-
def id(self) -> str:
|
|
113
|
-
"""Get the box ID."""
|
|
114
|
-
return self._box.id
|
|
99
|
+
# id property inherited from SimpleBox
|
|
115
100
|
|
|
116
101
|
async def __aenter__(self):
|
|
117
102
|
"""Start box and enter interactive TTY session."""
|
|
118
|
-
|
|
119
|
-
|
|
103
|
+
if self._started:
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
# Create and start box (via parent)
|
|
107
|
+
await super().__aenter__()
|
|
120
108
|
|
|
121
109
|
# Start shell with PTY
|
|
122
110
|
self._execution = await self._start_interactive_shell()
|
|
@@ -141,7 +129,7 @@ class InteractiveBox:
|
|
|
141
129
|
self._forward_output(),
|
|
142
130
|
self._forward_stderr(),
|
|
143
131
|
self._wait_for_exit(),
|
|
144
|
-
return_exceptions=True
|
|
132
|
+
return_exceptions=True,
|
|
145
133
|
)
|
|
146
134
|
else:
|
|
147
135
|
# No I/O forwarding, just wait for execution
|
|
@@ -153,13 +141,15 @@ class InteractiveBox:
|
|
|
153
141
|
# Restore terminal settings
|
|
154
142
|
if self._old_tty_settings is not None:
|
|
155
143
|
try:
|
|
156
|
-
termios.tcsetattr(
|
|
144
|
+
termios.tcsetattr(
|
|
145
|
+
sys.stdin.fileno(), termios.TCSADRAIN, self._old_tty_settings
|
|
146
|
+
)
|
|
157
147
|
except Exception as e:
|
|
158
148
|
logger.error(f"Caught exception on TTY settings: {e}")
|
|
159
149
|
|
|
160
150
|
"""Exit interactive session and restore terminal."""
|
|
161
151
|
# Wait for I/O task to complete (or cancel if needed)
|
|
162
|
-
if hasattr(self,
|
|
152
|
+
if hasattr(self, "_io_task") and self._io_task is not None:
|
|
163
153
|
try:
|
|
164
154
|
# Give it a moment to finish naturally
|
|
165
155
|
await asyncio.wait_for(self._io_task, timeout=3)
|
|
@@ -172,8 +162,8 @@ class InteractiveBox:
|
|
|
172
162
|
# Ignore other exceptions during cleanup
|
|
173
163
|
logger.error(f"Caught exception on exit: {e}")
|
|
174
164
|
|
|
175
|
-
# Shutdown box
|
|
176
|
-
return await
|
|
165
|
+
# Shutdown box (via parent)
|
|
166
|
+
return await super().__aexit__(exc_type, exc_val, exc_tb)
|
|
177
167
|
|
|
178
168
|
async def wait(self):
|
|
179
169
|
await self._execution.wait()
|
|
@@ -202,12 +192,16 @@ class InteractiveBox:
|
|
|
202
192
|
while not self._exited.is_set():
|
|
203
193
|
# Read from stdin with timeout to check exit event
|
|
204
194
|
try:
|
|
205
|
-
read_task = loop.run_in_executor(
|
|
195
|
+
read_task = loop.run_in_executor(
|
|
196
|
+
None, os.read, sys.stdin.fileno(), 1024
|
|
197
|
+
)
|
|
206
198
|
# Wait for either stdin data or exit event
|
|
207
199
|
done, pending = await asyncio.wait(
|
|
208
|
-
[
|
|
209
|
-
|
|
210
|
-
|
|
200
|
+
[
|
|
201
|
+
asyncio.ensure_future(read_task),
|
|
202
|
+
asyncio.ensure_future(self._exited.wait()),
|
|
203
|
+
],
|
|
204
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
211
205
|
)
|
|
212
206
|
|
|
213
207
|
# Cancel pending tasks
|
|
@@ -249,7 +243,7 @@ class InteractiveBox:
|
|
|
249
243
|
if isinstance(chunk, bytes):
|
|
250
244
|
sys.stdout.buffer.write(chunk)
|
|
251
245
|
else:
|
|
252
|
-
sys.stdout.buffer.write(chunk.encode(
|
|
246
|
+
sys.stdout.buffer.write(chunk.encode("utf-8", errors="replace"))
|
|
253
247
|
sys.stdout.buffer.flush()
|
|
254
248
|
|
|
255
249
|
logger.info("\nOutput forwarding ended.")
|
|
@@ -271,7 +265,7 @@ class InteractiveBox:
|
|
|
271
265
|
if isinstance(chunk, bytes):
|
|
272
266
|
sys.stderr.buffer.write(chunk)
|
|
273
267
|
else:
|
|
274
|
-
sys.stderr.buffer.write(chunk.encode(
|
|
268
|
+
sys.stderr.buffer.write(chunk.encode("utf-8", errors="replace"))
|
|
275
269
|
sys.stderr.buffer.flush()
|
|
276
270
|
|
|
277
271
|
logger.info("\nStderr forwarding ended.")
|
boxlite/runtime/boxlite-guest
CHANGED
|
Binary file
|
boxlite/runtime/boxlite-shim
CHANGED
|
Binary file
|
boxlite/runtime/debugfs
ADDED
|
Binary file
|
boxlite/runtime/libgvproxy.dylib
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
boxlite/runtime/mke2fs
ADDED
|
Binary file
|
boxlite/simplebox.py
CHANGED
|
@@ -6,18 +6,21 @@ Provides common functionality for all specialized boxes (CodeBox, BrowserBox, et
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
from enum import IntEnum
|
|
9
|
-
from typing import Optional
|
|
9
|
+
from typing import Optional, TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from .exec import ExecResult
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .boxlite import Boxlite
|
|
15
|
+
|
|
14
16
|
logger = logging.getLogger("boxlite.simplebox")
|
|
15
17
|
|
|
16
|
-
__all__ = [
|
|
18
|
+
__all__ = ["SimpleBox"]
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class StreamType(IntEnum):
|
|
20
|
-
"""Stream type for command execution output
|
|
22
|
+
"""Stream type for command execution output."""
|
|
23
|
+
|
|
21
24
|
STDOUT = 1
|
|
22
25
|
STDERR = 2
|
|
23
26
|
|
|
@@ -37,12 +40,14 @@ class SimpleBox:
|
|
|
37
40
|
"""
|
|
38
41
|
|
|
39
42
|
def __init__(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
self,
|
|
44
|
+
image: str,
|
|
45
|
+
memory_mib: Optional[int] = None,
|
|
46
|
+
cpus: Optional[int] = None,
|
|
47
|
+
runtime: Optional["Boxlite"] = None,
|
|
48
|
+
name: Optional[str] = None,
|
|
49
|
+
auto_remove: bool = True,
|
|
50
|
+
**kwargs,
|
|
46
51
|
):
|
|
47
52
|
"""
|
|
48
53
|
Create a specialized box.
|
|
@@ -52,10 +57,15 @@ class SimpleBox:
|
|
|
52
57
|
memory_mib: Memory limit in MiB
|
|
53
58
|
cpus: Number of CPU cores
|
|
54
59
|
runtime: Optional runtime instance (uses global default if None)
|
|
60
|
+
name: Optional name for the box (must be unique)
|
|
61
|
+
auto_remove: Remove box when stopped (default: True)
|
|
55
62
|
**kwargs: Additional configuration options
|
|
63
|
+
|
|
64
|
+
Note: The box is not actually created until entering the async context manager.
|
|
65
|
+
Use `async with SimpleBox(...) as box:` to create and start the box.
|
|
56
66
|
"""
|
|
57
67
|
try:
|
|
58
|
-
from .boxlite import Boxlite
|
|
68
|
+
from .boxlite import Boxlite, BoxOptions
|
|
59
69
|
except ImportError as e:
|
|
60
70
|
raise ImportError(
|
|
61
71
|
f"BoxLite native extension not found: {e}. "
|
|
@@ -68,29 +78,46 @@ class SimpleBox:
|
|
|
68
78
|
else:
|
|
69
79
|
self._runtime = runtime
|
|
70
80
|
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
from .boxlite import BoxOptions
|
|
74
|
-
except ImportError as e:
|
|
75
|
-
raise ImportError(
|
|
76
|
-
f"BoxLite native extension not found: {e}. "
|
|
77
|
-
"Please install with: pip install boxlite"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
box_opts = BoxOptions(
|
|
81
|
+
# Store box options for deferred creation in __aenter__
|
|
82
|
+
self._box_options = BoxOptions(
|
|
81
83
|
image=image,
|
|
82
84
|
cpus=cpus,
|
|
83
85
|
memory_mib=memory_mib,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
auto_remove=auto_remove,
|
|
87
|
+
**kwargs,
|
|
86
88
|
)
|
|
87
|
-
self.
|
|
89
|
+
self._name = name
|
|
90
|
+
self._box = None
|
|
91
|
+
self._started = False
|
|
88
92
|
|
|
89
93
|
async def __aenter__(self):
|
|
90
|
-
"""Async context manager entry -
|
|
91
|
-
|
|
94
|
+
"""Async context manager entry - creates and starts the box.
|
|
95
|
+
|
|
96
|
+
This method is idempotent - calling it multiple times is safe.
|
|
97
|
+
All initialization logic lives here; start() is just an alias.
|
|
98
|
+
"""
|
|
99
|
+
if self._started:
|
|
100
|
+
return self
|
|
101
|
+
self._box = await self._runtime.create(self._box_options, name=self._name)
|
|
102
|
+
await self._box.__aenter__()
|
|
103
|
+
self._started = True
|
|
92
104
|
return self
|
|
93
105
|
|
|
106
|
+
async def start(self):
|
|
107
|
+
"""
|
|
108
|
+
Explicitly create and start the box.
|
|
109
|
+
|
|
110
|
+
Alternative to using context manager. Allows::
|
|
111
|
+
|
|
112
|
+
box = SimpleBox(image="alpine:latest")
|
|
113
|
+
await box.start()
|
|
114
|
+
await box.exec("echo", "hello")
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
self for method chaining
|
|
118
|
+
"""
|
|
119
|
+
return await self.__aenter__()
|
|
120
|
+
|
|
94
121
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
95
122
|
"""Async context manager exit - delegates to Box.__aexit__ (returns awaitable)."""
|
|
96
123
|
return await self._box.__aexit__(exc_type, exc_val, exc_tb)
|
|
@@ -98,17 +125,27 @@ class SimpleBox:
|
|
|
98
125
|
@property
|
|
99
126
|
def id(self) -> str:
|
|
100
127
|
"""Get the box ID."""
|
|
128
|
+
if not self._started:
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
"Box not started. Use 'async with SimpleBox(...) as box:' "
|
|
131
|
+
"or call 'await box.start()' first."
|
|
132
|
+
)
|
|
101
133
|
return self._box.id
|
|
102
134
|
|
|
103
135
|
def info(self):
|
|
104
136
|
"""Get box information."""
|
|
137
|
+
if not self._started:
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
"Box not started. Use 'async with SimpleBox(...) as box:' "
|
|
140
|
+
"or call 'await box.start()' first."
|
|
141
|
+
)
|
|
105
142
|
return self._box.info()
|
|
106
143
|
|
|
107
144
|
async def exec(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
145
|
+
self,
|
|
146
|
+
cmd: str,
|
|
147
|
+
*args: str,
|
|
148
|
+
env: Optional[dict[str, str]] = None,
|
|
112
149
|
) -> ExecResult:
|
|
113
150
|
"""
|
|
114
151
|
Execute a command in the box and return the result.
|
|
@@ -134,6 +171,11 @@ class SimpleBox:
|
|
|
134
171
|
result = await box.exec('env', env={'FOO': 'bar'})
|
|
135
172
|
print(result.stdout)
|
|
136
173
|
"""
|
|
174
|
+
if not self._started:
|
|
175
|
+
raise RuntimeError(
|
|
176
|
+
"Box not started. Use 'async with SimpleBox(...) as box:' "
|
|
177
|
+
"or call 'await box.start()' first."
|
|
178
|
+
)
|
|
137
179
|
|
|
138
180
|
arg_list = list(args) if args else None
|
|
139
181
|
# Convert env dict to list of tuples if provided
|
|
@@ -165,7 +207,7 @@ class SimpleBox:
|
|
|
165
207
|
try:
|
|
166
208
|
async for line in stdout:
|
|
167
209
|
if isinstance(line, bytes):
|
|
168
|
-
stdout_lines.append(line.decode(
|
|
210
|
+
stdout_lines.append(line.decode("utf-8", errors="replace"))
|
|
169
211
|
else:
|
|
170
212
|
stdout_lines.append(line)
|
|
171
213
|
except Exception as e:
|
|
@@ -178,7 +220,7 @@ class SimpleBox:
|
|
|
178
220
|
try:
|
|
179
221
|
async for line in stderr:
|
|
180
222
|
if isinstance(line, bytes):
|
|
181
|
-
stderr_lines.append(line.decode(
|
|
223
|
+
stderr_lines.append(line.decode("utf-8", errors="replace"))
|
|
182
224
|
else:
|
|
183
225
|
stderr_lines.append(line)
|
|
184
226
|
except Exception as e:
|
|
@@ -186,8 +228,8 @@ class SimpleBox:
|
|
|
186
228
|
pass
|
|
187
229
|
|
|
188
230
|
# Combine lines
|
|
189
|
-
stdout =
|
|
190
|
-
stderr =
|
|
231
|
+
stdout = "".join(stdout_lines)
|
|
232
|
+
stderr = "".join(stderr_lines)
|
|
191
233
|
|
|
192
234
|
try:
|
|
193
235
|
exec_result = await execution.wait()
|
|
@@ -206,4 +248,9 @@ class SimpleBox:
|
|
|
206
248
|
|
|
207
249
|
Note: Usually not needed as context manager handles cleanup.
|
|
208
250
|
"""
|
|
251
|
+
if not self._started:
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
"Box not started. Use 'async with SimpleBox(...) as box:' "
|
|
254
|
+
"or call 'await box.start()' first."
|
|
255
|
+
)
|
|
209
256
|
self._box.shutdown()
|
|
@@ -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))
|