boxlite 0.2.1__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,293 @@
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
+ working_dir=kwargs.get('working_dir'),
86
+ env=kwargs.get('env', [])
87
+ )
88
+
89
+ # Create box directly (no SimpleBox wrapper)
90
+ self._box = self._runtime.create(box_opts)
91
+
92
+ # Store interactive config
93
+ self._shell = shell
94
+ self._env = kwargs.get('env', [])
95
+
96
+ # 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
101
+
102
+ # Interactive state
103
+ self._old_tty_settings = None
104
+ self._io_task = None
105
+ self._execution = None
106
+ self._stdin = None
107
+ self._stdout = None
108
+ self._stderr = None
109
+ self._exited = None # Event to signal process exit
110
+
111
+ @property
112
+ def id(self) -> str:
113
+ """Get the box ID."""
114
+ return self._box.id
115
+
116
+ async def __aenter__(self):
117
+ """Start box and enter interactive TTY session."""
118
+ # Start box directly
119
+ await self._box.__aenter__()
120
+
121
+ # Start shell with PTY
122
+ self._execution = await self._start_interactive_shell()
123
+
124
+ # Get stdin/stdout/stderr ONCE (can only be called once due to .take())
125
+ self._stdin = self._execution.stdin()
126
+ self._stdout = self._execution.stdout()
127
+ self._stderr = self._execution.stderr()
128
+
129
+ # Only set cbreak mode and start forwarding if tty=True
130
+ if self._tty:
131
+ stdin_fd = sys.stdin.fileno()
132
+ self._old_tty_settings = termios.tcgetattr(stdin_fd)
133
+ tty.setraw(sys.stdin.fileno(), when=termios.TCSANOW)
134
+
135
+ # Create exit event for graceful shutdown
136
+ self._exited = asyncio.Event()
137
+
138
+ # Start bidirectional I/O forwarding using gather (more Pythonic)
139
+ self._io_task = asyncio.gather(
140
+ self._forward_stdin(),
141
+ self._forward_output(),
142
+ self._forward_stderr(),
143
+ self._wait_for_exit(),
144
+ return_exceptions=True
145
+ )
146
+ else:
147
+ # No I/O forwarding, just wait for execution
148
+ self._io_task = self._wait_for_exit()
149
+
150
+ return self
151
+
152
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
153
+ # Restore terminal settings
154
+ if self._old_tty_settings is not None:
155
+ try:
156
+ termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_tty_settings)
157
+ except Exception as e:
158
+ logger.error(f"Caught exception on TTY settings: {e}")
159
+
160
+ """Exit interactive session and restore terminal."""
161
+ # Wait for I/O task to complete (or cancel if needed)
162
+ if hasattr(self, '_io_task') and self._io_task is not None:
163
+ try:
164
+ # Give it a moment to finish naturally
165
+ await asyncio.wait_for(self._io_task, timeout=3)
166
+ logger.info("Closing interactive shell (I/O tasks finished).")
167
+ self._io_task = None
168
+ except asyncio.TimeoutError:
169
+ # If it doesn't finish, that's ok - box is shutting down anyway
170
+ logger.error("Timeout waiting for I/O tasks to finish, cancelling...")
171
+ except Exception as e:
172
+ # Ignore other exceptions during cleanup
173
+ logger.error(f"Caught exception on exit: {e}")
174
+
175
+ # Shutdown box directly
176
+ return await self._box.__aexit__(exc_type, exc_val, exc_tb)
177
+
178
+ async def wait(self):
179
+ await self._execution.wait()
180
+
181
+ async def _start_interactive_shell(self):
182
+ """Start shell with PTY (internal)."""
183
+ # Execute shell with PTY using simplified boolean API
184
+ # Terminal size is auto-detected (like Docker)
185
+ execution = await self._box.exec(
186
+ self._shell,
187
+ args=[],
188
+ env=self._env,
189
+ tty=True, # Simple boolean - auto-detects terminal size
190
+ )
191
+
192
+ return execution
193
+
194
+ async def _forward_stdin(self):
195
+ """Forward stdin to PTY (internal)."""
196
+ try:
197
+ if self._stdin is None:
198
+ return
199
+
200
+ # Forward stdin in chunks
201
+ loop = asyncio.get_event_loop()
202
+ while not self._exited.is_set():
203
+ # Read from stdin with timeout to check exit event
204
+ try:
205
+ read_task = loop.run_in_executor(None, os.read, sys.stdin.fileno(), 1024)
206
+ # Wait for either stdin data or exit event
207
+ done, pending = await asyncio.wait(
208
+ [asyncio.ensure_future(read_task),
209
+ asyncio.ensure_future(self._exited.wait())],
210
+ return_when=asyncio.FIRST_COMPLETED
211
+ )
212
+
213
+ # Cancel pending tasks
214
+ for task in pending:
215
+ task.cancel()
216
+
217
+ # Check if we exited
218
+ if self._exited.is_set():
219
+ logger.info("Closing interactive shell (stdin forwarding).")
220
+ break
221
+
222
+ # Get the data from completed read task
223
+ for task in done:
224
+ if task.exception() is None:
225
+ data = task.result()
226
+ if isinstance(data, bytes) and data:
227
+ await self._stdin.send_input(data)
228
+ elif not data:
229
+ # EOF
230
+ return
231
+
232
+ except asyncio.CancelledError:
233
+ break
234
+
235
+ except asyncio.CancelledError:
236
+ logger.info("Cancelling interactive shell (stdin forwarding).")
237
+ except Exception as e:
238
+ logger.error(f"Caught exception on stdin: {e}")
239
+
240
+ async def _forward_output(self):
241
+ """Forward PTY output to stdout (internal)."""
242
+ try:
243
+ if self._stdout is None:
244
+ return
245
+
246
+ # Forward all output to stdout
247
+ async for chunk in self._stdout:
248
+ # Write directly to stdout (bypass print buffering)
249
+ if isinstance(chunk, bytes):
250
+ sys.stdout.buffer.write(chunk)
251
+ else:
252
+ sys.stdout.buffer.write(chunk.encode('utf-8', errors='replace'))
253
+ sys.stdout.buffer.flush()
254
+
255
+ logger.info("\nOutput forwarding ended.")
256
+
257
+ except asyncio.CancelledError:
258
+ logger.error("Cancelling interactive shell (stdout forwarding).")
259
+ except Exception as e:
260
+ logger.error(f"\nError forwarding output: {e}", file=sys.stderr)
261
+
262
+ async def _forward_stderr(self):
263
+ """Forward PTY stderr to stderr (internal)."""
264
+ try:
265
+ if self._stderr is None:
266
+ return
267
+
268
+ # Forward all error output to stderr
269
+ async for chunk in self._stderr:
270
+ # Write directly to stderr (bypass print buffering)
271
+ if isinstance(chunk, bytes):
272
+ sys.stderr.buffer.write(chunk)
273
+ else:
274
+ sys.stderr.buffer.write(chunk.encode('utf-8', errors='replace'))
275
+ sys.stderr.buffer.flush()
276
+
277
+ logger.info("\nStderr forwarding ended.")
278
+
279
+ except asyncio.CancelledError:
280
+ logger.error("Cancelling interactive shell (stderr forwarding).")
281
+ except Exception as e:
282
+ logger.error(f"\nError forwarding stderr: {e}", file=sys.stderr)
283
+
284
+ async def _wait_for_exit(self):
285
+ """Wait for the shell to exit (internal)."""
286
+ try:
287
+ await self._execution.wait()
288
+ except Exception:
289
+ pass # Ignore errors, cleanup will happen in __aexit__
290
+ finally:
291
+ # Signal other tasks to stop
292
+ if self._exited:
293
+ self._exited.set()
Binary file
Binary file
Binary file
Binary file
Binary file
boxlite/simplebox.py ADDED
@@ -0,0 +1,209 @@
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
+ working_dir=kwargs.get('working_dir'),
85
+ env=kwargs.get('env', [])
86
+ )
87
+ self._box = self._runtime.create(box_opts)
88
+
89
+ async def __aenter__(self):
90
+ """Async context manager entry - delegates to Box.__aenter__."""
91
+ await self._box.__aenter__()
92
+ return self
93
+
94
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
95
+ """Async context manager exit - delegates to Box.__aexit__ (returns awaitable)."""
96
+ return await self._box.__aexit__(exc_type, exc_val, exc_tb)
97
+
98
+ @property
99
+ def id(self) -> str:
100
+ """Get the box ID."""
101
+ return self._box.id
102
+
103
+ def info(self):
104
+ """Get box information."""
105
+ return self._box.info()
106
+
107
+ async def exec(
108
+ self,
109
+ cmd: str,
110
+ *args: str,
111
+ env: Optional[dict[str, str]] = None,
112
+ ) -> ExecResult:
113
+ """
114
+ Execute a command in the box and return the result.
115
+
116
+ Args:
117
+ cmd: Command to execute (e.g., 'ls', 'python')
118
+ *args: Arguments to the command (e.g., '-l', '-a')
119
+ env: Environment variables (default: guest's default environment)
120
+
121
+ Returns:
122
+ ExecResult with exit_code and output
123
+
124
+ Examples:
125
+ Simple execution::
126
+
127
+ result = await box.exec('ls', '-l', '-a')
128
+ print(f"Exit code: {result.exit_code}")
129
+ print(f"Stdout: {result.stdout}")
130
+ print(f"Stderr: {result.stderr}")
131
+
132
+ With environment variables::
133
+
134
+ result = await box.exec('env', env={'FOO': 'bar'})
135
+ print(result.stdout)
136
+ """
137
+
138
+ arg_list = list(args) if args else None
139
+ # Convert env dict to list of tuples if provided
140
+ env_list = list(env.items()) if env else None
141
+
142
+ # Execute via Rust (returns PyExecution)
143
+ execution = await self._box.exec(cmd, arg_list, env_list)
144
+
145
+ # Get streams from Rust execution
146
+ try:
147
+ stdout = execution.stdout()
148
+ except Exception as e:
149
+ logger.error(f"take stdout err: {e}")
150
+ stdout = None
151
+
152
+ try:
153
+ stderr = execution.stderr()
154
+ except Exception as e:
155
+ logger.error(f"take stderr err: {e}")
156
+ stderr = None
157
+
158
+ # Collect stdout and stderr separately
159
+ stdout_lines = []
160
+ stderr_lines = []
161
+
162
+ # Read stdout
163
+ if stdout:
164
+ logger.debug("collecting stdout")
165
+ try:
166
+ async for line in stdout:
167
+ if isinstance(line, bytes):
168
+ stdout_lines.append(line.decode('utf-8', errors='replace'))
169
+ else:
170
+ stdout_lines.append(line)
171
+ except Exception as e:
172
+ logger.error(f"collecting stdout err: {e}")
173
+ pass
174
+
175
+ # Read stderr
176
+ if stderr:
177
+ logger.debug("collecting stderr")
178
+ try:
179
+ async for line in stderr:
180
+ if isinstance(line, bytes):
181
+ stderr_lines.append(line.decode('utf-8', errors='replace'))
182
+ else:
183
+ stderr_lines.append(line)
184
+ except Exception as e:
185
+ logger.error(f"collecting stderr err: {e}")
186
+ pass
187
+
188
+ # Combine lines
189
+ stdout = ''.join(stdout_lines)
190
+ stderr = ''.join(stderr_lines)
191
+
192
+ try:
193
+ exec_result = await execution.wait()
194
+ exit_code = exec_result.exit_code
195
+ except Exception as e:
196
+ logger.error(f"failed to wait execution: {e}")
197
+ exit_code = -1
198
+
199
+ logger.debug(f"exec finish, exit_code: {exit_code}")
200
+
201
+ return ExecResult(exit_code=exit_code, stdout=stdout, stderr=stderr)
202
+
203
+ def shutdown(self):
204
+ """
205
+ Shutdown the box and release resources.
206
+
207
+ Note: Usually not needed as context manager handles cleanup.
208
+ """
209
+ self._box.shutdown()
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: boxlite
3
+ Version: 0.2.1
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,17 @@
1
+ boxlite-0.2.1.dist-info/METADATA,sha256=tIO8FJLGt2BAkAjJmRGZhcZecgBKUVlOLf5yoU0FocI,289
2
+ boxlite-0.2.1.dist-info/WHEEL,sha256=6FIqGwdIERCadatTFtHOIr_ebO64RXZ_R6wcyoBVcQE,105
3
+ boxlite/__init__.py,sha256=2wYQEKJc86jXJIwWmIX1s1OiE3FcRs6k29NzCikcm74,1990
4
+ boxlite/boxlite.cpython-314-darwin.so,sha256=llazx5T25d8uFgc6H63cv6KVjPMUZy8dIuPZtq26syk,12501952
5
+ boxlite/browserbox.py,sha256=TKybwm8UamN8hYB6uosefwcmAQrviYOSw-SShqtHefs,4353
6
+ boxlite/codebox.py,sha256=buppZbGh2_jd5syCXx-xFUMQWvo7DRAMXo5unEAgIZY,3677
7
+ boxlite/computerbox.py,sha256=xEuaUVan9hp_kjVe9rHSazBvZu8X-4BsaCCAjoF7NKw,19481
8
+ boxlite/errors.py,sha256=vuW2uZvkrX9GGvE3SKIogO8r1rBxUplit_3F2CSxglg,957
9
+ boxlite/exec.py,sha256=UWoLBnKUPx93ueHC2E3KVU7evaj06TIb1pHhuShZwbM,507
10
+ boxlite/interactivebox.py,sha256=SOli8NBM2Bo9vQp_c2bM6T4OgetCE5fzfEIEknIvUSI,10414
11
+ boxlite/runtime/boxlite-guest,sha256=IHpRatPJZW8AolX2qnKD2tV78DK0ot18QbbaY7vEgUs,12192664
12
+ boxlite/runtime/boxlite-shim,sha256=HP1NTVb4XDVfDV-JaZWWDeWtmahdo1Y_1v3oIzh1KyU,3254064
13
+ boxlite/runtime/libgvproxy.dylib,sha256=irddG9i5EMEowq0rTIMfdWDdmp_m0qZ2zdHvf5lQH0s,10809824
14
+ boxlite/runtime/libkrun.1.16.0.dylib,sha256=Utvwrj5_AFKDRiBURRDILH-fcELyHvQyHJDtcVY1StI,4272544
15
+ boxlite/runtime/libkrunfw.4.dylib,sha256=CoX3HMtIOawEM9-A6B4pFGw74KnTNl1Eca0X3H22xpU,21979472
16
+ boxlite/simplebox.py,sha256=NHMUclctq8_6-3xWxBL0LH6JtkiOP1SWyYA-tjHr8E8,6337
17
+ boxlite-0.2.1.dist-info/RECORD,,
@@ -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