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,145 @@
1
+ """
2
+ SyncCodeBox - Synchronous wrapper for CodeBox.
3
+
4
+ Provides a synchronous API for Python code execution using greenlet fiber switching.
5
+ API mirrors async CodeBox exactly.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, Optional
9
+
10
+ from ._simplebox import SyncSimpleBox
11
+
12
+ if TYPE_CHECKING:
13
+ from ._boxlite import SyncBoxlite
14
+
15
+ __all__ = ["SyncCodeBox"]
16
+
17
+
18
+ class SyncCodeBox(SyncSimpleBox):
19
+ """
20
+ Synchronous wrapper for CodeBox.
21
+
22
+ Provides synchronous methods for executing Python code in a secure container.
23
+ Built on top of SyncSimpleBox with Python-specific convenience methods.
24
+ API mirrors async CodeBox exactly.
25
+
26
+ Usage (standalone - recommended):
27
+ with SyncCodeBox() as box:
28
+ result = box.run("print('Hello, World!')")
29
+ print(result) # Hello, World!
30
+
31
+ Usage (with explicit runtime):
32
+ with SyncBoxlite.default() as runtime:
33
+ with SyncCodeBox(runtime=runtime) as box:
34
+ result = box.run("print('Hello!')")
35
+ print(result)
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ image: str = "python:slim",
41
+ memory_mib: Optional[int] = None,
42
+ cpus: Optional[int] = None,
43
+ runtime: Optional["SyncBoxlite"] = None,
44
+ name: Optional[str] = None,
45
+ auto_remove: bool = True,
46
+ **kwargs,
47
+ ):
48
+ """
49
+ Create a SyncCodeBox.
50
+
51
+ Args:
52
+ image: Python container image (default: "python:slim")
53
+ memory_mib: Memory limit in MiB (default: system default)
54
+ cpus: Number of CPU cores (default: system default)
55
+ runtime: Optional SyncBoxlite runtime. If None, creates default runtime.
56
+ name: Optional unique name for the box
57
+ auto_remove: Remove box when stopped (default: True)
58
+ **kwargs: Additional BoxOptions parameters
59
+ """
60
+ super().__init__(
61
+ image=image,
62
+ memory_mib=memory_mib,
63
+ cpus=cpus,
64
+ runtime=runtime,
65
+ name=name,
66
+ auto_remove=auto_remove,
67
+ **kwargs,
68
+ )
69
+
70
+ def run(self, code: str, timeout: Optional[int] = None) -> str:
71
+ """
72
+ Execute Python code synchronously.
73
+
74
+ Args:
75
+ code: Python code to execute
76
+ timeout: Execution timeout in seconds (not yet implemented)
77
+
78
+ Returns:
79
+ Combined stdout and stderr output
80
+
81
+ Example:
82
+ with SyncCodeBox() as box:
83
+ result = box.run("print('Hello!')")
84
+ print(result) # Hello!
85
+
86
+ # Multi-line code
87
+ result = box.run('''
88
+ import sys
89
+ print(f"Python {sys.version}")
90
+ ''')
91
+ """
92
+ result = self.exec("/usr/local/bin/python", "-c", code)
93
+ return result.stdout + result.stderr
94
+
95
+ def install_package(self, package: str) -> str:
96
+ """
97
+ Install a Python package using pip.
98
+
99
+ Args:
100
+ package: Package name (e.g., "requests", "numpy==1.24.0")
101
+
102
+ Returns:
103
+ Installation output
104
+
105
+ Example:
106
+ box.install_package("requests")
107
+ result = box.run("import requests; print(requests.__version__)")
108
+ """
109
+ result = self.exec("pip", "install", package)
110
+ return result.stdout + result.stderr
111
+
112
+ def install_packages(self, *packages: str) -> str:
113
+ """
114
+ Install multiple Python packages.
115
+
116
+ Args:
117
+ *packages: Package names to install
118
+
119
+ Returns:
120
+ Installation output
121
+
122
+ Example:
123
+ box.install_packages("requests", "numpy", "pandas")
124
+ """
125
+ result = self.exec("pip", "install", *packages)
126
+ return result.stdout + result.stderr
127
+
128
+ def run_script(self, script_path: str) -> str:
129
+ """
130
+ Execute a Python script file.
131
+
132
+ Reads the script from the host filesystem and executes it in the box.
133
+
134
+ Args:
135
+ script_path: Path to the Python script on the host
136
+
137
+ Returns:
138
+ Script output (stdout + stderr)
139
+
140
+ Example:
141
+ result = box.run_script("./my_script.py")
142
+ """
143
+ with open(script_path, "r") as f:
144
+ code = f.read()
145
+ return self.run(code)
@@ -0,0 +1,203 @@
1
+ """
2
+ SyncExecution - Synchronous wrapper for Execution.
3
+
4
+ Mirrors the native Execution API exactly, but with synchronous methods.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Optional
8
+
9
+ if TYPE_CHECKING:
10
+ from ._boxlite import SyncBoxlite
11
+ from ..boxlite import Execution
12
+
13
+ __all__ = ["SyncExecution", "SyncExecStdout", "SyncExecStderr"]
14
+
15
+
16
+ class SyncExecStdout:
17
+ """
18
+ Synchronous iterator for execution stdout.
19
+
20
+ Mirrors ExecStdout but uses regular iteration instead of async iteration.
21
+
22
+ Usage:
23
+ stdout = execution.stdout()
24
+ for line in stdout:
25
+ print(line)
26
+ """
27
+
28
+ def __init__(self, ctx: "SyncBoxlite", async_stdout) -> None:
29
+ self._ctx = ctx
30
+ self._async_stdout = async_stdout
31
+ self._async_iter = None
32
+
33
+ from ._sync_base import SyncBase
34
+
35
+ self._sync_helper = SyncBase(async_stdout, ctx.loop, ctx.dispatcher_fiber)
36
+
37
+ def _sync(self, coro):
38
+ """Run async operation synchronously."""
39
+ return self._sync_helper._sync(coro)
40
+
41
+ def __iter__(self) -> "SyncExecStdout":
42
+ """Start iteration."""
43
+ self._async_iter = self._async_stdout.__aiter__()
44
+ return self
45
+
46
+ def __next__(self) -> str:
47
+ """Get next line from stdout."""
48
+ if self._async_iter is None:
49
+ self._async_iter = self._async_stdout.__aiter__()
50
+
51
+ try:
52
+ line = self._sync(self._async_iter.__anext__())
53
+ # Decode bytes to string if needed
54
+ if isinstance(line, bytes):
55
+ return line.decode("utf-8", errors="replace")
56
+ return line
57
+ except StopAsyncIteration:
58
+ raise StopIteration
59
+
60
+
61
+ class SyncExecStderr:
62
+ """
63
+ Synchronous iterator for execution stderr.
64
+
65
+ Mirrors ExecStderr but uses regular iteration instead of async iteration.
66
+
67
+ Usage:
68
+ stderr = execution.stderr()
69
+ for line in stderr:
70
+ print(line)
71
+ """
72
+
73
+ def __init__(self, ctx: "SyncBoxlite", async_stderr) -> None:
74
+ self._ctx = ctx
75
+ self._async_stderr = async_stderr
76
+ self._async_iter = None
77
+
78
+ from ._sync_base import SyncBase
79
+
80
+ self._sync_helper = SyncBase(async_stderr, ctx.loop, ctx.dispatcher_fiber)
81
+
82
+ def _sync(self, coro):
83
+ """Run async operation synchronously."""
84
+ return self._sync_helper._sync(coro)
85
+
86
+ def __iter__(self) -> "SyncExecStderr":
87
+ """Start iteration."""
88
+ self._async_iter = self._async_stderr.__aiter__()
89
+ return self
90
+
91
+ def __next__(self) -> str:
92
+ """Get next line from stderr."""
93
+ if self._async_iter is None:
94
+ self._async_iter = self._async_stderr.__aiter__()
95
+
96
+ try:
97
+ line = self._sync(self._async_iter.__anext__())
98
+ # Decode bytes to string if needed
99
+ if isinstance(line, bytes):
100
+ return line.decode("utf-8", errors="replace")
101
+ return line
102
+ except StopAsyncIteration:
103
+ raise StopIteration
104
+
105
+
106
+ class SyncExecution:
107
+ """
108
+ Synchronous wrapper for Execution.
109
+
110
+ Provides the same API as the native Execution class, but with synchronous methods.
111
+ stdout() and stderr() return sync iterables instead of async iterables.
112
+
113
+ Usage:
114
+ execution = box.exec("echo", ["Hello"])
115
+
116
+ # Stream stdout
117
+ for line in execution.stdout():
118
+ print(f"stdout: {line}")
119
+
120
+ # Stream stderr
121
+ for line in execution.stderr():
122
+ print(f"stderr: {line}")
123
+
124
+ # Wait for completion
125
+ result = execution.wait()
126
+ print(f"Exit code: {result.exit_code}")
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ ctx: "SyncBoxlite",
132
+ execution: "Execution",
133
+ ) -> None:
134
+ """
135
+ Create a SyncExecution wrapper.
136
+
137
+ Args:
138
+ ctx: The SyncBoxlite providing event loop and dispatcher
139
+ execution: The native Execution object to wrap
140
+ """
141
+ from ._sync_base import SyncBase
142
+
143
+ self._execution = execution
144
+ self._ctx = ctx
145
+ self._sync_helper = SyncBase(execution, ctx.loop, ctx.dispatcher_fiber)
146
+
147
+ def _sync(self, coro):
148
+ """Run async operation synchronously."""
149
+ return self._sync_helper._sync(coro)
150
+
151
+ @property
152
+ def id(self) -> str:
153
+ """Get the execution ID."""
154
+ return self._execution.id
155
+
156
+ def stdout(self) -> Optional[SyncExecStdout]:
157
+ """
158
+ Get synchronous stdout iterator.
159
+
160
+ Returns:
161
+ SyncExecStdout iterator, or None if stdout is not available.
162
+
163
+ Usage:
164
+ stdout = execution.stdout()
165
+ if stdout:
166
+ for line in stdout:
167
+ print(line)
168
+ """
169
+ async_stdout = self._execution.stdout()
170
+ if async_stdout is None:
171
+ return None
172
+ return SyncExecStdout(self._ctx, async_stdout)
173
+
174
+ def stderr(self) -> Optional[SyncExecStderr]:
175
+ """
176
+ Get synchronous stderr iterator.
177
+
178
+ Returns:
179
+ SyncExecStderr iterator, or None if stderr is not available.
180
+
181
+ Usage:
182
+ stderr = execution.stderr()
183
+ if stderr:
184
+ for line in stderr:
185
+ print(line)
186
+ """
187
+ async_stderr = self._execution.stderr()
188
+ if async_stderr is None:
189
+ return None
190
+ return SyncExecStderr(self._ctx, async_stderr)
191
+
192
+ def wait(self):
193
+ """
194
+ Wait for execution to complete.
195
+
196
+ Returns:
197
+ ExecResult with exit_code and other completion info.
198
+ """
199
+ return self._sync(self._execution.wait())
200
+
201
+ def kill(self) -> None:
202
+ """Kill the running execution."""
203
+ self._sync(self._execution.kill())
@@ -0,0 +1,180 @@
1
+ """
2
+ SyncSimpleBox - Synchronous wrapper for SimpleBox.
3
+
4
+ Provides a synchronous API for box operations.
5
+ API mirrors async SimpleBox exactly.
6
+ """
7
+
8
+ from typing import TYPE_CHECKING, Dict, Optional
9
+
10
+ from ..exec import ExecResult
11
+
12
+ if TYPE_CHECKING:
13
+ from ._boxlite import SyncBoxlite
14
+ from ._box import SyncBox
15
+
16
+ __all__ = ["SyncSimpleBox"]
17
+
18
+
19
+ class SyncSimpleBox:
20
+ """
21
+ Synchronous wrapper for SimpleBox.
22
+
23
+ Provides synchronous methods for executing commands in a BoxLite container.
24
+ Uses SyncBox internally which handles async bridging via greenlet.
25
+ API mirrors async SimpleBox exactly.
26
+
27
+ Usage (standalone - recommended):
28
+ with SyncSimpleBox(image="python:slim") as box:
29
+ result = box.exec("ls", "-la")
30
+ print(result.stdout)
31
+
32
+ Usage (with explicit runtime):
33
+ with SyncBoxlite.default() as runtime:
34
+ with SyncSimpleBox(image="python:slim", runtime=runtime) as box:
35
+ result = box.exec("ls", "-la")
36
+ print(result.stdout)
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ image: str,
42
+ memory_mib: Optional[int] = None,
43
+ cpus: Optional[int] = None,
44
+ runtime: Optional["SyncBoxlite"] = None,
45
+ name: Optional[str] = None,
46
+ auto_remove: bool = True,
47
+ **kwargs,
48
+ ):
49
+ """
50
+ Create a SyncSimpleBox.
51
+
52
+ Args:
53
+ image: Container image to use (e.g., "python:slim", "ubuntu:latest")
54
+ memory_mib: Memory limit in MiB (default: system default)
55
+ cpus: Number of CPU cores (default: system default)
56
+ runtime: Optional SyncBoxlite runtime. If None, creates default runtime.
57
+ name: Optional unique name for the box
58
+ auto_remove: Remove box when stopped (default: True)
59
+ **kwargs: Additional BoxOptions parameters
60
+ """
61
+ from ._boxlite import SyncBoxlite
62
+ from ..boxlite import BoxOptions
63
+
64
+ # Handle optional runtime
65
+ if runtime is None:
66
+ runtime = SyncBoxlite.default()
67
+ self._owns_runtime = True
68
+ else:
69
+ self._owns_runtime = False
70
+
71
+ self._runtime = runtime
72
+
73
+ # Create box options
74
+ self._box_opts = BoxOptions(
75
+ image=image,
76
+ cpus=cpus,
77
+ memory_mib=memory_mib,
78
+ auto_remove=auto_remove,
79
+ **kwargs,
80
+ )
81
+
82
+ # Store for lazy creation in __enter__
83
+ self._name = name
84
+ self._box: Optional["SyncBox"] = None
85
+
86
+ def __enter__(self) -> "SyncSimpleBox":
87
+ """Enter context - starts runtime if owned, then starts the box."""
88
+ # Start runtime if we own it
89
+ if self._owns_runtime:
90
+ self._runtime.start()
91
+
92
+ # Create box via runtime - returns SyncBox!
93
+ self._box = self._runtime.create(self._box_opts, name=self._name)
94
+ return self
95
+
96
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
97
+ """Exit context - stops the box, then stops runtime if owned."""
98
+ # Stop the box (SyncBox.stop() is already sync)
99
+ if self._box is not None:
100
+ self._box.stop()
101
+
102
+ # Stop runtime if we own it
103
+ if self._owns_runtime:
104
+ self._runtime.stop()
105
+
106
+ @property
107
+ def id(self) -> str:
108
+ """Get the box ID."""
109
+ return self._box.id
110
+
111
+ @property
112
+ def name(self) -> Optional[str]:
113
+ """Get the box name (if set)."""
114
+ return self._box.name
115
+
116
+ def info(self):
117
+ """Get box information."""
118
+ return self._box.info()
119
+
120
+ def exec(
121
+ self,
122
+ cmd: str,
123
+ *args: str,
124
+ env: Optional[Dict[str, str]] = None,
125
+ ) -> ExecResult:
126
+ """
127
+ Execute a command in the box synchronously.
128
+
129
+ Args:
130
+ cmd: Command to run (e.g., "ls", "python")
131
+ *args: Command arguments (e.g., "-l", "-a")
132
+ env: Environment variables as dict
133
+
134
+ Returns:
135
+ ExecResult with exit_code, stdout, and stderr
136
+
137
+ Example:
138
+ result = box.exec("ls", "-la")
139
+ print(f"Exit code: {result.exit_code}")
140
+ print(f"Output: {result.stdout}")
141
+ """
142
+ # Convert args to list format expected by SyncBox
143
+ arg_list = list(args) if args else None
144
+ env_list = list(env.items()) if env else None
145
+
146
+ # SyncBox.exec() returns SyncExecution - already sync!
147
+ execution = self._box.exec(cmd, arg_list, env_list)
148
+
149
+ # Collect stdout (sync iteration)
150
+ stdout_lines = []
151
+ for line in execution.stdout():
152
+ if isinstance(line, bytes):
153
+ stdout_lines.append(line.decode("utf-8", errors="replace"))
154
+ else:
155
+ stdout_lines.append(line)
156
+
157
+ # Collect stderr (sync iteration)
158
+ stderr_lines = []
159
+ for line in execution.stderr():
160
+ if isinstance(line, bytes):
161
+ stderr_lines.append(line.decode("utf-8", errors="replace"))
162
+ else:
163
+ stderr_lines.append(line)
164
+
165
+ # Wait for completion (sync)
166
+ result = execution.wait()
167
+
168
+ return ExecResult(
169
+ exit_code=result.exit_code,
170
+ stdout="".join(stdout_lines),
171
+ stderr="".join(stderr_lines),
172
+ )
173
+
174
+ def stop(self) -> None:
175
+ """Stop the box (preserves state for restart)."""
176
+ self._box.stop()
177
+
178
+ def metrics(self):
179
+ """Get box metrics (CPU, memory usage)."""
180
+ return self._box.metrics()
@@ -0,0 +1,137 @@
1
+ """
2
+ Base class for sync wrappers - provides _sync() method.
3
+
4
+ This module contains the core bridging logic that allows sync code to
5
+ execute async operations using greenlet fiber switching.
6
+ """
7
+
8
+ import asyncio
9
+ import inspect
10
+ import traceback
11
+ from typing import Any, Awaitable, Coroutine, TypeVar, Union
12
+
13
+ from greenlet import greenlet
14
+
15
+ __all__ = ["SyncBase", "SyncContextManager"]
16
+
17
+ T = TypeVar("T")
18
+
19
+
20
+ class SyncBase:
21
+ """
22
+ Base class for all sync wrapper objects.
23
+
24
+ Provides the _sync() method that bridges async to sync using greenlet
25
+ fiber switching. This is the core mechanism that allows synchronous
26
+ code to execute asynchronous operations.
27
+
28
+ How it works:
29
+ 1. User calls a sync method (e.g., box.run_command())
30
+ 2. Sync method calls _sync(async_coro)
31
+ 3. _sync() creates an asyncio task and switches to dispatcher fiber
32
+ 4. Dispatcher fiber runs event loop, processes the task
33
+ 5. When task completes, callback switches back to user fiber
34
+ 6. _sync() returns the task result
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ impl_obj: Any,
40
+ loop: asyncio.AbstractEventLoop,
41
+ dispatcher_fiber: greenlet,
42
+ ) -> None:
43
+ """
44
+ Initialize SyncBase.
45
+
46
+ Args:
47
+ impl_obj: The underlying async implementation object
48
+ loop: The asyncio event loop (managed by dispatcher)
49
+ dispatcher_fiber: The greenlet fiber running the event loop
50
+ """
51
+ self._impl = impl_obj
52
+ self._loop = loop
53
+ self._dispatcher_fiber = dispatcher_fiber
54
+
55
+ def _sync(
56
+ self,
57
+ coro: Union[Coroutine[Any, Any, T], Awaitable[T]],
58
+ ) -> T:
59
+ """
60
+ Run async coroutine synchronously using greenlet fiber switching.
61
+
62
+ This is the core bridging method that enables sync-to-async conversion.
63
+
64
+ The method:
65
+ 1. Creates an asyncio task from the coroutine
66
+ 2. Registers a callback to switch back when task completes
67
+ 3. Switches to dispatcher fiber to let event loop run
68
+ 4. Returns the task result (or raises exception)
69
+
70
+ Args:
71
+ coro: The async coroutine to execute
72
+
73
+ Returns:
74
+ The result of the coroutine
75
+
76
+ Raises:
77
+ RuntimeError: If event loop is closed
78
+ Any exception raised by the coroutine
79
+ """
80
+ __tracebackhide__ = True # Hide from pytest tracebacks
81
+
82
+ # Guard: event loop must be open
83
+ if self._loop.is_closed():
84
+ if hasattr(coro, "close"):
85
+ coro.close()
86
+ raise RuntimeError("Event loop is closed! Is BoxLite stopped?")
87
+
88
+ # 1. Get current fiber (user fiber)
89
+ g_self = greenlet.getcurrent()
90
+
91
+ # 2. Create async task from coroutine/future
92
+ # Note: PyO3's async methods return Future objects (not native coroutines),
93
+ # so we use ensure_future() which handles both coroutines and futures.
94
+ task: asyncio.Task = asyncio.ensure_future(coro, loop=self._loop)
95
+
96
+ # 3. Attach debug info for better stack traces
97
+ setattr(task, "__boxlite_stack__", inspect.stack(0))
98
+ setattr(task, "__boxlite_stack_trace__", traceback.extract_stack(limit=10))
99
+
100
+ # 4. When task completes, switch back to us
101
+ task.add_done_callback(lambda _: g_self.switch())
102
+
103
+ # 5. THE CORE LOOP: Keep switching to dispatcher until done
104
+ while not task.done():
105
+ self._dispatcher_fiber.switch()
106
+ # ^^^^^ Control goes to dispatcher fiber
107
+ # Dispatcher runs event loop, processes our task
108
+ # When task completes, callback fires g_self.switch()
109
+ # Control returns HERE
110
+
111
+ # 6. Return result (or raise exception)
112
+ return task.result()
113
+
114
+
115
+ class SyncContextManager(SyncBase):
116
+ """
117
+ SyncBase with context manager support.
118
+
119
+ Provides __enter__ and __exit__ methods for use with 'with' statement.
120
+ Subclasses should override close() for cleanup logic.
121
+ """
122
+
123
+ def __enter__(self) -> "SyncContextManager":
124
+ """Enter context manager."""
125
+ return self
126
+
127
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
128
+ """Exit context manager - calls close()."""
129
+ self.close()
130
+
131
+ def close(self) -> None:
132
+ """
133
+ Close and cleanup resources.
134
+
135
+ Override in subclasses to implement cleanup logic.
136
+ """
137
+ pass