boxlite 0.3.0.post1__cp314-cp314-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,292 @@
1
+ """
2
+ InteractiveBox - Interactive terminal sessions with PTY support.
3
+
4
+ Provides automatic PTY-based interactive sessions, similar to `docker exec -it`.
5
+ """
6
+
7
+ import asyncio
8
+ import logging
9
+ import os
10
+ import sys
11
+ import termios
12
+ import tty
13
+ from typing import Optional, TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from .runtime import Boxlite
17
+
18
+ # Configure logger
19
+ logger = logging.getLogger("boxlite.interactivebox")
20
+
21
+
22
+ class InteractiveBox:
23
+ """
24
+ Interactive box with automatic PTY and terminal forwarding.
25
+
26
+ When used as a context manager, automatically:
27
+ 1. Auto-detects terminal size (like Docker)
28
+ 2. Starts a shell with PTY
29
+ 3. Sets local terminal to cbreak mode
30
+ 4. Forwards stdin/stdout bidirectionally
31
+ 5. Restores terminal mode on exit
32
+
33
+ Example:
34
+ async with InteractiveBox(image="alpine:latest") as box:
35
+ # You're now in an interactive shell!
36
+ # Type commands, see output in real-time
37
+ # Type "exit" to close
38
+ pass
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ image: str,
44
+ shell: str = "/bin/sh",
45
+ tty: Optional[bool] = None,
46
+ memory_mib: Optional[int] = None,
47
+ cpus: Optional[int] = None,
48
+ runtime: Optional['Boxlite'] = None,
49
+ **kwargs
50
+ ):
51
+ """
52
+ Create interactive box.
53
+
54
+ Args:
55
+ image: Container image to use
56
+ shell: Shell to run (default: /bin/sh)
57
+ tty: Control terminal I/O forwarding behavior:
58
+ - None (default): Auto-detect - forward I/O if stdin is a TTY
59
+ - True: Force I/O forwarding (manual interactive mode)
60
+ - False: No I/O forwarding (programmatic control only)
61
+ memory_mib: Memory limit in MiB
62
+ cpus: Number of CPU cores
63
+ runtime: Optional runtime instance (uses global default if None)
64
+ **kwargs: Additional configuration options (working_dir, env)
65
+ """
66
+ try:
67
+ from .boxlite import Boxlite, BoxOptions
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(
82
+ image=image,
83
+ cpus=cpus,
84
+ memory_mib=memory_mib,
85
+ **kwargs
86
+ )
87
+
88
+ # Create box directly (no SimpleBox wrapper)
89
+ self._box = self._runtime.create(box_opts)
90
+
91
+ # Store interactive config
92
+ self._shell = shell
93
+ self._env = kwargs.get('env', [])
94
+
95
+ # Determine TTY mode: None = auto-detect, True = force, False = disable
96
+ if tty is None:
97
+ self._tty = sys.stdin.isatty()
98
+ else:
99
+ self._tty = tty
100
+
101
+ # Interactive state
102
+ self._old_tty_settings = None
103
+ self._io_task = None
104
+ self._execution = None
105
+ self._stdin = None
106
+ self._stdout = None
107
+ self._stderr = None
108
+ self._exited = None # Event to signal process exit
109
+
110
+ @property
111
+ def id(self) -> str:
112
+ """Get the box ID."""
113
+ return self._box.id
114
+
115
+ async def __aenter__(self):
116
+ """Start box and enter interactive TTY session."""
117
+ # Start box directly
118
+ await self._box.__aenter__()
119
+
120
+ # Start shell with PTY
121
+ self._execution = await self._start_interactive_shell()
122
+
123
+ # Get stdin/stdout/stderr ONCE (can only be called once due to .take())
124
+ self._stdin = self._execution.stdin()
125
+ self._stdout = self._execution.stdout()
126
+ self._stderr = self._execution.stderr()
127
+
128
+ # Only set cbreak mode and start forwarding if tty=True
129
+ if self._tty:
130
+ stdin_fd = sys.stdin.fileno()
131
+ self._old_tty_settings = termios.tcgetattr(stdin_fd)
132
+ tty.setraw(sys.stdin.fileno(), when=termios.TCSANOW)
133
+
134
+ # Create exit event for graceful shutdown
135
+ self._exited = asyncio.Event()
136
+
137
+ # Start bidirectional I/O forwarding using gather (more Pythonic)
138
+ self._io_task = asyncio.gather(
139
+ self._forward_stdin(),
140
+ self._forward_output(),
141
+ self._forward_stderr(),
142
+ self._wait_for_exit(),
143
+ return_exceptions=True
144
+ )
145
+ else:
146
+ # No I/O forwarding, just wait for execution
147
+ self._io_task = self._wait_for_exit()
148
+
149
+ return self
150
+
151
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
152
+ # Restore terminal settings
153
+ if self._old_tty_settings is not None:
154
+ try:
155
+ termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_tty_settings)
156
+ except Exception as e:
157
+ logger.error(f"Caught exception on TTY settings: {e}")
158
+
159
+ """Exit interactive session and restore terminal."""
160
+ # Wait for I/O task to complete (or cancel if needed)
161
+ if hasattr(self, '_io_task') and self._io_task is not None:
162
+ try:
163
+ # Give it a moment to finish naturally
164
+ await asyncio.wait_for(self._io_task, timeout=3)
165
+ logger.info("Closing interactive shell (I/O tasks finished).")
166
+ self._io_task = None
167
+ except asyncio.TimeoutError:
168
+ # If it doesn't finish, that's ok - box is shutting down anyway
169
+ logger.error("Timeout waiting for I/O tasks to finish, cancelling...")
170
+ except Exception as e:
171
+ # Ignore other exceptions during cleanup
172
+ logger.error(f"Caught exception on exit: {e}")
173
+
174
+ # Shutdown box directly
175
+ return await self._box.__aexit__(exc_type, exc_val, exc_tb)
176
+
177
+ async def wait(self):
178
+ await self._execution.wait()
179
+
180
+ async def _start_interactive_shell(self):
181
+ """Start shell with PTY (internal)."""
182
+ # Execute shell with PTY using simplified boolean API
183
+ # Terminal size is auto-detected (like Docker)
184
+ execution = await self._box.exec(
185
+ self._shell,
186
+ args=[],
187
+ env=self._env,
188
+ tty=True, # Simple boolean - auto-detects terminal size
189
+ )
190
+
191
+ return execution
192
+
193
+ async def _forward_stdin(self):
194
+ """Forward stdin to PTY (internal)."""
195
+ try:
196
+ if self._stdin is None:
197
+ return
198
+
199
+ # Forward stdin in chunks
200
+ loop = asyncio.get_event_loop()
201
+ while not self._exited.is_set():
202
+ # Read from stdin with timeout to check exit event
203
+ try:
204
+ read_task = loop.run_in_executor(None, os.read, sys.stdin.fileno(), 1024)
205
+ # Wait for either stdin data or exit event
206
+ done, pending = await asyncio.wait(
207
+ [asyncio.ensure_future(read_task),
208
+ asyncio.ensure_future(self._exited.wait())],
209
+ return_when=asyncio.FIRST_COMPLETED
210
+ )
211
+
212
+ # Cancel pending tasks
213
+ for task in pending:
214
+ task.cancel()
215
+
216
+ # Check if we exited
217
+ if self._exited.is_set():
218
+ logger.info("Closing interactive shell (stdin forwarding).")
219
+ break
220
+
221
+ # Get the data from completed read task
222
+ for task in done:
223
+ if task.exception() is None:
224
+ data = task.result()
225
+ if isinstance(data, bytes) and data:
226
+ await self._stdin.send_input(data)
227
+ elif not data:
228
+ # EOF
229
+ return
230
+
231
+ except asyncio.CancelledError:
232
+ break
233
+
234
+ except asyncio.CancelledError:
235
+ logger.info("Cancelling interactive shell (stdin forwarding).")
236
+ except Exception as e:
237
+ logger.error(f"Caught exception on stdin: {e}")
238
+
239
+ async def _forward_output(self):
240
+ """Forward PTY output to stdout (internal)."""
241
+ try:
242
+ if self._stdout is None:
243
+ return
244
+
245
+ # Forward all output to stdout
246
+ async for chunk in self._stdout:
247
+ # Write directly to stdout (bypass print buffering)
248
+ if isinstance(chunk, bytes):
249
+ sys.stdout.buffer.write(chunk)
250
+ else:
251
+ sys.stdout.buffer.write(chunk.encode('utf-8', errors='replace'))
252
+ sys.stdout.buffer.flush()
253
+
254
+ logger.info("\nOutput forwarding ended.")
255
+
256
+ except asyncio.CancelledError:
257
+ logger.error("Cancelling interactive shell (stdout forwarding).")
258
+ except Exception as e:
259
+ logger.error(f"\nError forwarding output: {e}", file=sys.stderr)
260
+
261
+ async def _forward_stderr(self):
262
+ """Forward PTY stderr to stderr (internal)."""
263
+ try:
264
+ if self._stderr is None:
265
+ return
266
+
267
+ # Forward all error output to stderr
268
+ async for chunk in self._stderr:
269
+ # Write directly to stderr (bypass print buffering)
270
+ if isinstance(chunk, bytes):
271
+ sys.stderr.buffer.write(chunk)
272
+ else:
273
+ sys.stderr.buffer.write(chunk.encode('utf-8', errors='replace'))
274
+ sys.stderr.buffer.flush()
275
+
276
+ logger.info("\nStderr forwarding ended.")
277
+
278
+ except asyncio.CancelledError:
279
+ logger.error("Cancelling interactive shell (stderr forwarding).")
280
+ except Exception as e:
281
+ logger.error(f"\nError forwarding stderr: {e}", file=sys.stderr)
282
+
283
+ async def _wait_for_exit(self):
284
+ """Wait for the shell to exit (internal)."""
285
+ try:
286
+ await self._execution.wait()
287
+ except Exception:
288
+ pass # Ignore errors, cleanup will happen in __aexit__
289
+ finally:
290
+ # Signal other tasks to stop
291
+ if self._exited:
292
+ self._exited.set()
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
boxlite/runtime/mke2fs ADDED
Binary file
boxlite/simplebox.py ADDED
@@ -0,0 +1,208 @@
1
+ """
2
+ SimpleBox - Foundation for specialized container types.
3
+
4
+ Provides common functionality for all specialized boxes (CodeBox, BrowserBox, etc.)
5
+ """
6
+
7
+ import logging
8
+ from enum import IntEnum
9
+ from typing import Optional
10
+
11
+ from .exec import ExecResult
12
+
13
+ # Configure logger
14
+ logger = logging.getLogger("boxlite.simplebox")
15
+
16
+ __all__ = ['SimpleBox']
17
+
18
+
19
+ class StreamType(IntEnum):
20
+ """Stream type for command execution output (deprecated, use execution.py)."""
21
+ STDOUT = 1
22
+ STDERR = 2
23
+
24
+
25
+ class SimpleBox:
26
+ """
27
+ Base class for specialized container types.
28
+
29
+ This class encapsulates the common patterns:
30
+ 1. Async context manager support
31
+ 2. Automatic runtime lifecycle management
32
+ 3. Stdio blocking mode restoration
33
+
34
+ Subclasses should override:
35
+ - _create_box_options(): Return BoxOptions for their specific use case
36
+ - Add domain-specific methods (e.g., CodeBox.run(), BrowserBox.navigate())
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ image: str,
42
+ memory_mib: Optional[int] = None,
43
+ cpus: Optional[int] = None,
44
+ runtime: Optional['Boxlite'] = None,
45
+ **kwargs
46
+ ):
47
+ """
48
+ Create a specialized box.
49
+
50
+ Args:
51
+ image: Container images to use
52
+ memory_mib: Memory limit in MiB
53
+ cpus: Number of CPU cores
54
+ runtime: Optional runtime instance (uses global default if None)
55
+ **kwargs: Additional configuration options
56
+ """
57
+ try:
58
+ from .boxlite import Boxlite
59
+ except ImportError as e:
60
+ raise ImportError(
61
+ f"BoxLite native extension not found: {e}. "
62
+ "Please install with: pip install boxlite"
63
+ )
64
+
65
+ # Use provided runtime or get Rust's global default
66
+ if runtime is None:
67
+ self._runtime = Boxlite.default()
68
+ else:
69
+ self._runtime = runtime
70
+
71
+ # Create box using subclass-defined options
72
+ try:
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
+ image=image,
82
+ cpus=cpus,
83
+ memory_mib=memory_mib,
84
+ **kwargs
85
+ )
86
+ self._box = self._runtime.create(box_opts)
87
+
88
+ async def __aenter__(self):
89
+ """Async context manager entry - delegates to Box.__aenter__."""
90
+ await self._box.__aenter__()
91
+ return self
92
+
93
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
94
+ """Async context manager exit - delegates to Box.__aexit__ (returns awaitable)."""
95
+ return await self._box.__aexit__(exc_type, exc_val, exc_tb)
96
+
97
+ @property
98
+ def id(self) -> str:
99
+ """Get the box ID."""
100
+ return self._box.id
101
+
102
+ def info(self):
103
+ """Get box information."""
104
+ return self._box.info()
105
+
106
+ async def exec(
107
+ self,
108
+ cmd: str,
109
+ *args: str,
110
+ env: Optional[dict[str, str]] = None,
111
+ ) -> ExecResult:
112
+ """
113
+ Execute a command in the box and return the result.
114
+
115
+ Args:
116
+ cmd: Command to execute (e.g., 'ls', 'python')
117
+ *args: Arguments to the command (e.g., '-l', '-a')
118
+ env: Environment variables (default: guest's default environment)
119
+
120
+ Returns:
121
+ ExecResult with exit_code and output
122
+
123
+ Examples:
124
+ Simple execution::
125
+
126
+ result = await box.exec('ls', '-l', '-a')
127
+ print(f"Exit code: {result.exit_code}")
128
+ print(f"Stdout: {result.stdout}")
129
+ print(f"Stderr: {result.stderr}")
130
+
131
+ With environment variables::
132
+
133
+ result = await box.exec('env', env={'FOO': 'bar'})
134
+ print(result.stdout)
135
+ """
136
+
137
+ arg_list = list(args) if args else None
138
+ # Convert env dict to list of tuples if provided
139
+ env_list = list(env.items()) if env else None
140
+
141
+ # Execute via Rust (returns PyExecution)
142
+ execution = await self._box.exec(cmd, arg_list, env_list)
143
+
144
+ # Get streams from Rust execution
145
+ try:
146
+ stdout = execution.stdout()
147
+ except Exception as e:
148
+ logger.error(f"take stdout err: {e}")
149
+ stdout = None
150
+
151
+ try:
152
+ stderr = execution.stderr()
153
+ except Exception as e:
154
+ logger.error(f"take stderr err: {e}")
155
+ stderr = None
156
+
157
+ # Collect stdout and stderr separately
158
+ stdout_lines = []
159
+ stderr_lines = []
160
+
161
+ # Read stdout
162
+ if stdout:
163
+ logger.debug("collecting stdout")
164
+ try:
165
+ async for line in stdout:
166
+ if isinstance(line, bytes):
167
+ stdout_lines.append(line.decode('utf-8', errors='replace'))
168
+ else:
169
+ stdout_lines.append(line)
170
+ except Exception as e:
171
+ logger.error(f"collecting stdout err: {e}")
172
+ pass
173
+
174
+ # Read stderr
175
+ if stderr:
176
+ logger.debug("collecting stderr")
177
+ try:
178
+ async for line in stderr:
179
+ if isinstance(line, bytes):
180
+ stderr_lines.append(line.decode('utf-8', errors='replace'))
181
+ else:
182
+ stderr_lines.append(line)
183
+ except Exception as e:
184
+ logger.error(f"collecting stderr err: {e}")
185
+ pass
186
+
187
+ # Combine lines
188
+ stdout = ''.join(stdout_lines)
189
+ stderr = ''.join(stderr_lines)
190
+
191
+ try:
192
+ exec_result = await execution.wait()
193
+ exit_code = exec_result.exit_code
194
+ except Exception as e:
195
+ logger.error(f"failed to wait execution: {e}")
196
+ exit_code = -1
197
+
198
+ logger.debug(f"exec finish, exit_code: {exit_code}")
199
+
200
+ return ExecResult(exit_code=exit_code, stdout=stdout, stderr=stderr)
201
+
202
+ def shutdown(self):
203
+ """
204
+ Shutdown the box and release resources.
205
+
206
+ Note: Usually not needed as context manager handles cleanup.
207
+ """
208
+ self._box.shutdown()
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: boxlite
3
+ Version: 0.3.0.post1
4
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
5
+ Requires-Dist: pytest-asyncio>=0.21 ; extra == 'dev'
6
+ Provides-Extra: dev
7
+ Summary: Python bindings for Boxlite runtime
8
+ Author: Dorian Zheng
9
+ License-Expression: Apache-2.0
10
+ Requires-Python: >=3.10
@@ -0,0 +1,19 @@
1
+ boxlite-0.3.0.post1.dist-info/RECORD,,
2
+ boxlite-0.3.0.post1.dist-info/WHEEL,sha256=6FIqGwdIERCadatTFtHOIr_ebO64RXZ_R6wcyoBVcQE,105
3
+ boxlite-0.3.0.post1.dist-info/METADATA,sha256=yWxfgsV5xxhmS2vnKMXVCHD2BPWv9SZJRJvHavuirvc,295
4
+ boxlite/exec.py,sha256=UWoLBnKUPx93ueHC2E3KVU7evaj06TIb1pHhuShZwbM,507
5
+ boxlite/boxlite.cpython-314-darwin.so,sha256=4tqDy-AZ2mxpr6SGDnm3cEpFHjf2amLWI-6Qpj8Pp7M,12604192
6
+ boxlite/__init__.py,sha256=2wYQEKJc86jXJIwWmIX1s1OiE3FcRs6k29NzCikcm74,1990
7
+ boxlite/computerbox.py,sha256=K2dRQ5452bwoS33LYlu_Avp1rDsRxbH8golRTPZf5Qo,18945
8
+ boxlite/simplebox.py,sha256=S5rz6scUoMgBK8dVZqYqMyTvhj3oCovgVSdhznyjvXY,6269
9
+ boxlite/browserbox.py,sha256=UX4yxZsnbFHgyLhS9-OC58AG1pecHpUwbxlO6leiV98,4477
10
+ boxlite/errors.py,sha256=vuW2uZvkrX9GGvE3SKIogO8r1rBxUplit_3F2CSxglg,957
11
+ boxlite/interactivebox.py,sha256=ooKt2Ux9f5_Be529fM4M-Aqv65eDgosw-NJB322aZNk,10346
12
+ boxlite/codebox.py,sha256=buppZbGh2_jd5syCXx-xFUMQWvo7DRAMXo5unEAgIZY,3677
13
+ boxlite/runtime/mke2fs,sha256=JMZjDSJfvnWdGKNOcXwRoJOxgb7qW99mhAuovMFdbcU,578008
14
+ boxlite/runtime/boxlite-shim,sha256=76CB-rOoQGrEjDwsZRdUFYSlhSA0YxSUCvcv8M1zQsU,3276320
15
+ boxlite/runtime/boxlite-guest,sha256=S3wwDm9a0I--QKYbDhn4FvI3hdsgYMq8duw9M0SofJY,12200864
16
+ boxlite/runtime/libkrunfw.4.dylib,sha256=cRy63u5WT5twkeR2bHrRCZwcw7uidbeSs_bTCaKb-tA,21979472
17
+ boxlite/runtime/libkrun.1.15.1.dylib,sha256=ltZQfWifiQ_VViXeTRywMCd8Vj1WIP66_pR58-vmneU,3994416
18
+ boxlite/runtime/libgvproxy.dylib,sha256=irddG9i5EMEowq0rTIMfdWDdmp_m0qZ2zdHvf5lQH0s,10809824
19
+ boxlite/runtime/debugfs,sha256=P_1GHbPoLfesVH9KDJiSJ1Z6xX1L-utHOkuRnJYOlUY,662328
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.10.2)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-macosx_14_0_arm64