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/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__ = ['BoxliteError', 'ExecError', 'TimeoutError', 'ParseError']
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__(f"Command '{command}' failed with exit code {exit_code}: {stderr}")
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
- 'ExecResult',
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 .runtime import Boxlite
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 (like Docker)
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
- 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
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
- 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(
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
- working_dir=kwargs.get('working_dir'),
86
- env=kwargs.get('env', [])
76
+ cpus=cpus,
77
+ runtime=runtime,
78
+ name=name,
79
+ auto_remove=auto_remove,
80
+ **kwargs,
87
81
  )
88
82
 
89
- # Create box directly (no SimpleBox wrapper)
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('env', [])
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
- @property
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
- # Start box directly
119
- await self._box.__aenter__()
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(sys.stdin.fileno(), termios.TCSADRAIN, self._old_tty_settings)
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, '_io_task') and self._io_task is not None:
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 directly
176
- return await self._box.__aexit__(exc_type, exc_val, exc_tb)
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(None, os.read, sys.stdin.fileno(), 1024)
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
- [asyncio.ensure_future(read_task),
209
- asyncio.ensure_future(self._exited.wait())],
210
- return_when=asyncio.FIRST_COMPLETED
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('utf-8', errors='replace'))
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('utf-8', errors='replace'))
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.")
Binary file
Binary file
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
- # Configure logger
13
+ if TYPE_CHECKING:
14
+ from .boxlite import Boxlite
15
+
14
16
  logger = logging.getLogger("boxlite.simplebox")
15
17
 
16
- __all__ = ['SimpleBox']
18
+ __all__ = ["SimpleBox"]
17
19
 
18
20
 
19
21
  class StreamType(IntEnum):
20
- """Stream type for command execution output (deprecated, use execution.py)."""
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
- self,
41
- image: str,
42
- memory_mib: Optional[int] = None,
43
- cpus: Optional[int] = None,
44
- runtime: Optional['Boxlite'] = None,
45
- **kwargs
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
- # 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
+ # 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
- working_dir=kwargs.get('working_dir'),
85
- env=kwargs.get('env', [])
86
+ auto_remove=auto_remove,
87
+ **kwargs,
86
88
  )
87
- self._box = self._runtime.create(box_opts)
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 - delegates to Box.__aenter__."""
91
- self._box.__aenter__()
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
- self,
109
- cmd: str,
110
- *args: str,
111
- env: Optional[dict[str, str]] = None,
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('utf-8', errors='replace'))
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('utf-8', errors='replace'))
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 = ''.join(stdout_lines)
190
- stderr = ''.join(stderr_lines)
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
+ ]
@@ -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))