hyperbrowser 1.6.0__tar.gz → 1.7.0__tar.gz
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.
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/PKG-INFO +44 -1
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/README.md +43 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/sandbox.py +2 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +126 -14
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +55 -10
- hyperbrowser-1.7.0/hyperbrowser/client/managers/sandboxes/process_output.py +88 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sandboxes/shared.py +2 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +2 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +140 -14
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +74 -11
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/sandbox.py +2 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/pyproject.toml +1 -1
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/LICENSE +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/async_client.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sandboxes/image_build.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/sync.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/consts.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/transport/sync.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/agents.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/sandbox.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/types/web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperbrowser
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Python SDK for hyperbrowser
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -323,6 +323,49 @@ sandbox.files.write(
|
|
|
323
323
|
)
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
+
### Run commands and collect output
|
|
327
|
+
|
|
328
|
+
`sandbox.exec()` and `sandbox.processes.start()` stream command output from the
|
|
329
|
+
receiver as soon as execution starts. The SDK collects stdout and stderr in
|
|
330
|
+
memory, so a completed result is not limited to the receiver's replay buffer.
|
|
331
|
+
This requires a receiver supporting streaming `POST /sandbox/processes`; roll
|
|
332
|
+
out the receiver before upgrading the SDK.
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
result = sandbox.exec("make test", max_output_bytes=128 * 1024 * 1024)
|
|
336
|
+
print(result.stdout, result.stderr, result.exit_code)
|
|
337
|
+
|
|
338
|
+
process = sandbox.processes.start("make test")
|
|
339
|
+
try:
|
|
340
|
+
for event in process.stream():
|
|
341
|
+
if event.type == "stdout":
|
|
342
|
+
print(event.data, end="")
|
|
343
|
+
result = process.wait()
|
|
344
|
+
finally:
|
|
345
|
+
process.disconnect()
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The combined output limit defaults to 64 MiB per command and can be adjusted
|
|
349
|
+
with `max_output_bytes`. Exceeding it raises `output_limit_exceeded`. A broken
|
|
350
|
+
stream, missing output, or receiver truncation raises `incomplete_output`.
|
|
351
|
+
These errors include the process ID and do not automatically rerun the command.
|
|
352
|
+
|
|
353
|
+
Process streams use a separate 60-second read-idle timeout once response headers
|
|
354
|
+
arrive. Output and the receiver's 15-second heartbeats reset this timeout, so quiet
|
|
355
|
+
commands can run longer than the client's ordinary HTTP timeout. That ordinary
|
|
356
|
+
timeout still applies to connection setup and waiting for response headers.
|
|
357
|
+
|
|
358
|
+
`start()` returns after the process starts and collects in the background.
|
|
359
|
+
`wait(timeout_sec=...)` limits the local wait; collection continues after a wait
|
|
360
|
+
timeout. The timeout passed to `start()` or `exec()` limits command execution.
|
|
361
|
+
A local wait timeout raises `TimeoutError` (`asyncio.TimeoutError` in the async API).
|
|
362
|
+
`disconnect()` stops collection and leaves the command running. Use `kill()` to
|
|
363
|
+
stop it. Reattaching with `get()` can retrieve only retained receiver output;
|
|
364
|
+
`wait()` raises if that output has been truncated.
|
|
365
|
+
|
|
366
|
+
The async API has the same behavior: await `exec()`, `start()`, `wait()`, and
|
|
367
|
+
`disconnect()`, and use `async for` with `stream()`.
|
|
368
|
+
|
|
326
369
|
### Resume terminal output after reconnect
|
|
327
370
|
|
|
328
371
|
```python
|
|
@@ -295,6 +295,49 @@ sandbox.files.write(
|
|
|
295
295
|
)
|
|
296
296
|
```
|
|
297
297
|
|
|
298
|
+
### Run commands and collect output
|
|
299
|
+
|
|
300
|
+
`sandbox.exec()` and `sandbox.processes.start()` stream command output from the
|
|
301
|
+
receiver as soon as execution starts. The SDK collects stdout and stderr in
|
|
302
|
+
memory, so a completed result is not limited to the receiver's replay buffer.
|
|
303
|
+
This requires a receiver supporting streaming `POST /sandbox/processes`; roll
|
|
304
|
+
out the receiver before upgrading the SDK.
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
result = sandbox.exec("make test", max_output_bytes=128 * 1024 * 1024)
|
|
308
|
+
print(result.stdout, result.stderr, result.exit_code)
|
|
309
|
+
|
|
310
|
+
process = sandbox.processes.start("make test")
|
|
311
|
+
try:
|
|
312
|
+
for event in process.stream():
|
|
313
|
+
if event.type == "stdout":
|
|
314
|
+
print(event.data, end="")
|
|
315
|
+
result = process.wait()
|
|
316
|
+
finally:
|
|
317
|
+
process.disconnect()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The combined output limit defaults to 64 MiB per command and can be adjusted
|
|
321
|
+
with `max_output_bytes`. Exceeding it raises `output_limit_exceeded`. A broken
|
|
322
|
+
stream, missing output, or receiver truncation raises `incomplete_output`.
|
|
323
|
+
These errors include the process ID and do not automatically rerun the command.
|
|
324
|
+
|
|
325
|
+
Process streams use a separate 60-second read-idle timeout once response headers
|
|
326
|
+
arrive. Output and the receiver's 15-second heartbeats reset this timeout, so quiet
|
|
327
|
+
commands can run longer than the client's ordinary HTTP timeout. That ordinary
|
|
328
|
+
timeout still applies to connection setup and waiting for response headers.
|
|
329
|
+
|
|
330
|
+
`start()` returns after the process starts and collects in the background.
|
|
331
|
+
`wait(timeout_sec=...)` limits the local wait; collection continues after a wait
|
|
332
|
+
timeout. The timeout passed to `start()` or `exec()` limits command execution.
|
|
333
|
+
A local wait timeout raises `TimeoutError` (`asyncio.TimeoutError` in the async API).
|
|
334
|
+
`disconnect()` stops collection and leaves the command running. Use `kill()` to
|
|
335
|
+
stop it. Reattaching with `get()` can retrieve only retained receiver output;
|
|
336
|
+
`wait()` raises if that output has been truncated.
|
|
337
|
+
|
|
338
|
+
The async API has the same behavior: await `exec()`, `start()`, `wait()`, and
|
|
339
|
+
`disconnect()`, and use `async for` with `stream()`.
|
|
340
|
+
|
|
298
341
|
### Resume terminal output after reconnect
|
|
299
342
|
|
|
300
343
|
```python
|
{hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/async_manager/sandbox.py
RENAMED
|
@@ -307,6 +307,7 @@ class SandboxHandle:
|
|
|
307
307
|
timeout_ms: Optional[int] = None,
|
|
308
308
|
timeout_sec: Optional[int] = None,
|
|
309
309
|
run_as: Optional[str] = None,
|
|
310
|
+
max_output_bytes: int = 64 * 1024 * 1024,
|
|
310
311
|
):
|
|
311
312
|
return await self.processes.exec(
|
|
312
313
|
input,
|
|
@@ -315,6 +316,7 @@ class SandboxHandle:
|
|
|
315
316
|
timeout_ms=timeout_ms,
|
|
316
317
|
timeout_sec=timeout_sec,
|
|
317
318
|
run_as=run_as,
|
|
319
|
+
max_output_bytes=max_output_bytes,
|
|
318
320
|
)
|
|
319
321
|
|
|
320
322
|
async def get_process(self, process_id: str) -> SandboxProcessHandle:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import base64
|
|
2
3
|
from typing import AsyncIterator, Dict, Optional, Union
|
|
3
4
|
|
|
@@ -16,6 +17,11 @@ from .....types import (
|
|
|
16
17
|
SandboxProcessStdinParams as SandboxProcessStdinParamsDict,
|
|
17
18
|
)
|
|
18
19
|
from ...sandboxes.shared import _normalize_exec_params
|
|
20
|
+
from ...sandboxes.process_output import (
|
|
21
|
+
DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
|
|
22
|
+
ProcessOutput,
|
|
23
|
+
validate_output_limit,
|
|
24
|
+
)
|
|
19
25
|
from .sandbox_transport import RuntimeTransport
|
|
20
26
|
|
|
21
27
|
DEFAULT_PROCESS_KILL_WAIT_SECONDS = 5.0
|
|
@@ -25,6 +31,10 @@ class SandboxProcessHandle:
|
|
|
25
31
|
def __init__(self, transport: RuntimeTransport, summary: SandboxProcessSummary):
|
|
26
32
|
self._transport = transport
|
|
27
33
|
self._summary = summary
|
|
34
|
+
self._output = None
|
|
35
|
+
self._collector = None
|
|
36
|
+
self._events = None
|
|
37
|
+
self._changed = asyncio.Event()
|
|
28
38
|
|
|
29
39
|
@property
|
|
30
40
|
def id(self) -> str:
|
|
@@ -50,6 +60,15 @@ class SandboxProcessHandle:
|
|
|
50
60
|
timeout_ms: Optional[int] = None,
|
|
51
61
|
timeout_sec: Optional[int] = None,
|
|
52
62
|
) -> SandboxProcessResult:
|
|
63
|
+
if self._collector is not None:
|
|
64
|
+
timeout = None
|
|
65
|
+
if timeout_sec is not None and timeout_sec > 0:
|
|
66
|
+
timeout = timeout_sec
|
|
67
|
+
elif timeout_ms is not None and timeout_ms > 0:
|
|
68
|
+
timeout = timeout_ms / 1000
|
|
69
|
+
if not self._collector.done():
|
|
70
|
+
await asyncio.wait_for(asyncio.shield(self._collector), timeout)
|
|
71
|
+
return self._collected_result()
|
|
53
72
|
payload = await self._transport.request_json(
|
|
54
73
|
f"/sandbox/processes/{self.id}/wait",
|
|
55
74
|
method="POST",
|
|
@@ -60,6 +79,10 @@ class SandboxProcessHandle:
|
|
|
60
79
|
headers={"content-type": "application/json"},
|
|
61
80
|
)
|
|
62
81
|
result = SandboxProcessResult(**payload["result"])
|
|
82
|
+
if result.output_truncated:
|
|
83
|
+
raise ProcessOutput(self.id, 0).failure(
|
|
84
|
+
"Retained process output is incomplete; collect output from process start"
|
|
85
|
+
)
|
|
63
86
|
self._summary = SandboxProcessSummary(
|
|
64
87
|
id=result.id,
|
|
65
88
|
status=result.status,
|
|
@@ -133,6 +156,21 @@ class SandboxProcessHandle:
|
|
|
133
156
|
)
|
|
134
157
|
|
|
135
158
|
async def stream(self, from_seq: Optional[int] = None) -> AsyncIterator[object]:
|
|
159
|
+
if self._output is not None:
|
|
160
|
+
index = 0
|
|
161
|
+
while True:
|
|
162
|
+
self._changed.clear()
|
|
163
|
+
while index < len(self._output.events):
|
|
164
|
+
event = self._output.events[index]
|
|
165
|
+
index += 1
|
|
166
|
+
if from_seq is None or event.seq >= from_seq:
|
|
167
|
+
yield event
|
|
168
|
+
if self._collector.done():
|
|
169
|
+
yield SandboxProcessExitEvent(
|
|
170
|
+
type="exit", result=self._collected_result()
|
|
171
|
+
)
|
|
172
|
+
return
|
|
173
|
+
await self._changed.wait()
|
|
136
174
|
params = {"from_seq": from_seq} if from_seq and from_seq > 0 else None
|
|
137
175
|
async for event in self._transport.stream_sse(
|
|
138
176
|
f"/sandbox/processes/{self.id}/stream",
|
|
@@ -153,6 +191,70 @@ class SandboxProcessHandle:
|
|
|
153
191
|
result=SandboxProcessResult(**data),
|
|
154
192
|
)
|
|
155
193
|
|
|
194
|
+
def _collected_result(self) -> SandboxProcessResult:
|
|
195
|
+
if self._output.error is not None:
|
|
196
|
+
raise self._output.error
|
|
197
|
+
if self._output.result is None:
|
|
198
|
+
raise self._output.failure(
|
|
199
|
+
"Command stream ended before its completion event"
|
|
200
|
+
)
|
|
201
|
+
result = self._output.result
|
|
202
|
+
self._summary = self._summary.model_copy(
|
|
203
|
+
update={
|
|
204
|
+
"status": result.status,
|
|
205
|
+
"exit_code": result.exit_code,
|
|
206
|
+
"completed_at": result.completed_at,
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
async def _collect(self) -> None:
|
|
212
|
+
try:
|
|
213
|
+
async for event in self._events:
|
|
214
|
+
self._output.consume(event)
|
|
215
|
+
self._changed.set()
|
|
216
|
+
if self._output.result is not None:
|
|
217
|
+
return
|
|
218
|
+
self._output.error = self._output.failure(
|
|
219
|
+
"Command stream ended before its completion event"
|
|
220
|
+
)
|
|
221
|
+
except asyncio.CancelledError:
|
|
222
|
+
if self._output.result is None:
|
|
223
|
+
self._output.error = self._output.failure(
|
|
224
|
+
"Command output collection disconnected"
|
|
225
|
+
)
|
|
226
|
+
except Exception as error:
|
|
227
|
+
self._output.error = (
|
|
228
|
+
self._output.failure(str(error))
|
|
229
|
+
if not hasattr(error, "code")
|
|
230
|
+
else error
|
|
231
|
+
)
|
|
232
|
+
finally:
|
|
233
|
+
try:
|
|
234
|
+
await self._events.aclose()
|
|
235
|
+
except Exception as error:
|
|
236
|
+
if self._output.result is None and self._output.error is None:
|
|
237
|
+
self._output.error = self._output.failure(str(error))
|
|
238
|
+
finally:
|
|
239
|
+
self._changed.set()
|
|
240
|
+
|
|
241
|
+
async def disconnect(self) -> None:
|
|
242
|
+
"""Stop collecting output; the detached command continues running."""
|
|
243
|
+
if self._collector is not None and not self._collector.done():
|
|
244
|
+
if self._output.result is None and self._output.error is None:
|
|
245
|
+
self._output.error = self._output.failure(
|
|
246
|
+
"Command output collection disconnected"
|
|
247
|
+
)
|
|
248
|
+
self._collector.cancel()
|
|
249
|
+
try:
|
|
250
|
+
await self._collector
|
|
251
|
+
except asyncio.CancelledError:
|
|
252
|
+
pass
|
|
253
|
+
finally:
|
|
254
|
+
# Cancellation may happen before the collector gets its first turn.
|
|
255
|
+
await self._events.aclose()
|
|
256
|
+
self._changed.set()
|
|
257
|
+
|
|
156
258
|
async def result(self) -> SandboxProcessResult:
|
|
157
259
|
return await self.wait()
|
|
158
260
|
|
|
@@ -170,22 +272,21 @@ class SandboxProcessesApi:
|
|
|
170
272
|
timeout_ms: Optional[int] = None,
|
|
171
273
|
timeout_sec: Optional[int] = None,
|
|
172
274
|
run_as: Optional[str] = None,
|
|
275
|
+
max_output_bytes: int = DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
|
|
173
276
|
) -> SandboxProcessResult:
|
|
174
|
-
|
|
277
|
+
handle = await self.start(
|
|
175
278
|
input,
|
|
176
279
|
cwd=cwd,
|
|
177
280
|
env=env,
|
|
178
281
|
timeout_ms=timeout_ms,
|
|
179
282
|
timeout_sec=timeout_sec,
|
|
180
283
|
run_as=run_as,
|
|
284
|
+
max_output_bytes=max_output_bytes,
|
|
181
285
|
)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
headers={"content-type": "application/json"},
|
|
187
|
-
)
|
|
188
|
-
return SandboxProcessResult(**payload["result"])
|
|
286
|
+
try:
|
|
287
|
+
return await handle.wait()
|
|
288
|
+
finally:
|
|
289
|
+
await handle.disconnect()
|
|
189
290
|
|
|
190
291
|
async def start(
|
|
191
292
|
self,
|
|
@@ -196,7 +297,9 @@ class SandboxProcessesApi:
|
|
|
196
297
|
timeout_ms: Optional[int] = None,
|
|
197
298
|
timeout_sec: Optional[int] = None,
|
|
198
299
|
run_as: Optional[str] = None,
|
|
300
|
+
max_output_bytes: int = DEFAULT_MAX_PROCESS_OUTPUT_BYTES,
|
|
199
301
|
) -> SandboxProcessHandle:
|
|
302
|
+
validate_output_limit(max_output_bytes)
|
|
200
303
|
params = _normalize_exec_params(
|
|
201
304
|
input,
|
|
202
305
|
cwd=cwd,
|
|
@@ -205,16 +308,25 @@ class SandboxProcessesApi:
|
|
|
205
308
|
timeout_sec=timeout_sec,
|
|
206
309
|
run_as=run_as,
|
|
207
310
|
)
|
|
208
|
-
|
|
311
|
+
events = self._transport.stream_sse(
|
|
209
312
|
"/sandbox/processes",
|
|
210
313
|
method="POST",
|
|
211
314
|
json_body=dump_request(params, SandboxExecParams),
|
|
212
|
-
headers={"content-type": "application/json"},
|
|
213
|
-
)
|
|
214
|
-
return SandboxProcessHandle(
|
|
215
|
-
self._transport,
|
|
216
|
-
SandboxProcessSummary(**payload["process"]),
|
|
217
315
|
)
|
|
316
|
+
try:
|
|
317
|
+
started = await events.__anext__()
|
|
318
|
+
if started["event"] != "started":
|
|
319
|
+
raise RuntimeError("Expected process start event")
|
|
320
|
+
handle = SandboxProcessHandle(
|
|
321
|
+
self._transport, SandboxProcessSummary(**started["data"])
|
|
322
|
+
)
|
|
323
|
+
except BaseException:
|
|
324
|
+
await events.aclose()
|
|
325
|
+
raise
|
|
326
|
+
handle._events = events
|
|
327
|
+
handle._output = ProcessOutput(handle.id, max_output_bytes)
|
|
328
|
+
handle._collector = asyncio.create_task(handle._collect())
|
|
329
|
+
return handle
|
|
218
330
|
|
|
219
331
|
async def get(self, process_id: str) -> SandboxProcessHandle:
|
|
220
332
|
payload = await self._transport.request_json(f"/sandbox/processes/{process_id}")
|
|
@@ -3,6 +3,7 @@ from typing import Any, AsyncIterator, Dict, Optional
|
|
|
3
3
|
|
|
4
4
|
import httpx
|
|
5
5
|
|
|
6
|
+
from .....exceptions import HyperbrowserError
|
|
6
7
|
from .....sandbox_common import (
|
|
7
8
|
RuntimeConnection,
|
|
8
9
|
build_headers,
|
|
@@ -12,7 +13,11 @@ from .....sandbox_common import (
|
|
|
12
13
|
request_context,
|
|
13
14
|
resolve_runtime_transport_target,
|
|
14
15
|
)
|
|
15
|
-
from ...sandboxes.shared import
|
|
16
|
+
from ...sandboxes.shared import (
|
|
17
|
+
PROCESS_STREAM_IDLE_TIMEOUT_SECONDS,
|
|
18
|
+
_build_query_path,
|
|
19
|
+
_is_replayable_http_content,
|
|
20
|
+
)
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
class RuntimeTransport:
|
|
@@ -83,9 +88,16 @@ class RuntimeTransport:
|
|
|
83
88
|
await client.aclose()
|
|
84
89
|
|
|
85
90
|
async def stream_sse(
|
|
86
|
-
self,
|
|
91
|
+
self,
|
|
92
|
+
path: str,
|
|
93
|
+
params: Optional[Dict[str, object]] = None,
|
|
94
|
+
*,
|
|
95
|
+
method: str = "GET",
|
|
96
|
+
json_body: Optional[Dict[str, object]] = None,
|
|
87
97
|
) -> AsyncIterator[Dict[str, object]]:
|
|
88
|
-
client, response = await self._open_stream(
|
|
98
|
+
client, response = await self._open_stream(
|
|
99
|
+
path, params=params, method=method, json_body=json_body
|
|
100
|
+
)
|
|
89
101
|
event_name = "message"
|
|
90
102
|
event_id = None
|
|
91
103
|
data_lines = []
|
|
@@ -115,6 +127,8 @@ class RuntimeTransport:
|
|
|
115
127
|
|
|
116
128
|
try:
|
|
117
129
|
async for line in response.aiter_lines():
|
|
130
|
+
# HTTPX < 0.24 includes line terminators in aiter_lines().
|
|
131
|
+
line = line.rstrip("\r\n")
|
|
118
132
|
if line == "":
|
|
119
133
|
event = flush_event()
|
|
120
134
|
if event is not None:
|
|
@@ -194,18 +208,45 @@ class RuntimeTransport:
|
|
|
194
208
|
*,
|
|
195
209
|
params: Optional[Dict[str, object]] = None,
|
|
196
210
|
allow_refresh: bool = True,
|
|
211
|
+
method: str = "GET",
|
|
212
|
+
json_body: Optional[Dict[str, object]] = None,
|
|
197
213
|
):
|
|
198
214
|
connection = await self._resolve_connection(False)
|
|
199
|
-
client, response = await self._send_stream(
|
|
215
|
+
client, response = await self._send_stream(
|
|
216
|
+
connection, path, params=params, method=method, json_body=json_body
|
|
217
|
+
)
|
|
200
218
|
if response.status_code == 401 and allow_refresh:
|
|
201
219
|
await response.aclose()
|
|
202
220
|
await client.aclose()
|
|
203
221
|
refreshed = await self._resolve_connection(True)
|
|
204
|
-
client, response = await self._send_stream(
|
|
222
|
+
client, response = await self._send_stream(
|
|
223
|
+
refreshed, path, params=params, method=method, json_body=json_body
|
|
224
|
+
)
|
|
205
225
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
try:
|
|
227
|
+
if not response.is_success:
|
|
228
|
+
await response.aread()
|
|
229
|
+
ensure_response_ok(response, "runtime")
|
|
230
|
+
if method == "POST" and "text/event-stream" not in response.headers.get(
|
|
231
|
+
"content-type", ""
|
|
232
|
+
):
|
|
233
|
+
raise HyperbrowserError(
|
|
234
|
+
"Receiver does not support streaming command start; update the receiver. The command may have started; do not retry it automatically.",
|
|
235
|
+
code="streaming_not_supported",
|
|
236
|
+
service="runtime",
|
|
237
|
+
)
|
|
238
|
+
if "text/event-stream" in response.headers.get("content-type", ""):
|
|
239
|
+
# HTTPX passes this timeout extension through to body reads.
|
|
240
|
+
# Change it before reading the body, after response headers have
|
|
241
|
+
# arrived under the ordinary request timeout. Heartbeats reset
|
|
242
|
+
# this idle timeout independently of the command's deadline.
|
|
243
|
+
response.request.extensions["timeout"]["read"] = (
|
|
244
|
+
PROCESS_STREAM_IDLE_TIMEOUT_SECONDS
|
|
245
|
+
)
|
|
246
|
+
except BaseException:
|
|
247
|
+
await response.aclose()
|
|
248
|
+
await client.aclose()
|
|
249
|
+
raise
|
|
209
250
|
return client, response
|
|
210
251
|
|
|
211
252
|
async def _open_binary_stream(
|
|
@@ -320,6 +361,8 @@ class RuntimeTransport:
|
|
|
320
361
|
path: str,
|
|
321
362
|
*,
|
|
322
363
|
params: Optional[Dict[str, object]],
|
|
364
|
+
method: str = "GET",
|
|
365
|
+
json_body: Optional[Dict[str, object]] = None,
|
|
323
366
|
):
|
|
324
367
|
request_path = _build_query_path(path, params)
|
|
325
368
|
target = resolve_runtime_transport_target(
|
|
@@ -335,7 +378,9 @@ class RuntimeTransport:
|
|
|
335
378
|
client = httpx.AsyncClient(timeout=self._timeout)
|
|
336
379
|
|
|
337
380
|
try:
|
|
338
|
-
request = client.build_request(
|
|
381
|
+
request = client.build_request(
|
|
382
|
+
method, target.url, headers=headers, json=json_body
|
|
383
|
+
)
|
|
339
384
|
response = await client.send(request, stream=True)
|
|
340
385
|
return client, response
|
|
341
386
|
except BaseException as error:
|
|
@@ -344,5 +389,5 @@ class RuntimeTransport:
|
|
|
344
389
|
error,
|
|
345
390
|
"runtime",
|
|
346
391
|
"Unknown runtime request error",
|
|
347
|
-
request_context(
|
|
392
|
+
request_context(method, path),
|
|
348
393
|
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Collection and validation of the receiver's command event stream."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import codecs
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from ....exceptions import HyperbrowserError
|
|
8
|
+
from ....models.sandbox import SandboxProcessOutputEvent, SandboxProcessResult
|
|
9
|
+
|
|
10
|
+
DEFAULT_MAX_PROCESS_OUTPUT_BYTES = 64 * 1024 * 1024
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcessOutput:
|
|
14
|
+
def __init__(self, process_id: str, max_bytes: int):
|
|
15
|
+
self.process_id = process_id
|
|
16
|
+
self.max_bytes = max_bytes
|
|
17
|
+
self.size = 0
|
|
18
|
+
self.seq = 0
|
|
19
|
+
self.events: List[SandboxProcessOutputEvent] = []
|
|
20
|
+
self.result: Optional[SandboxProcessResult] = None
|
|
21
|
+
self.error: Optional[BaseException] = None
|
|
22
|
+
self._chunks: Dict[str, List[str]] = {"stdout": [], "stderr": []}
|
|
23
|
+
self._decoders = {
|
|
24
|
+
stream: codecs.getincrementaldecoder("utf-8")(errors="replace")
|
|
25
|
+
for stream in ("stdout", "stderr", "system")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def failure(
|
|
29
|
+
self, message: str, code: str = "incomplete_output"
|
|
30
|
+
) -> HyperbrowserError:
|
|
31
|
+
return HyperbrowserError(
|
|
32
|
+
message,
|
|
33
|
+
code=code,
|
|
34
|
+
service="runtime",
|
|
35
|
+
details={"process_id": self.process_id, "last_seq": self.seq},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def consume(self, event) -> None:
|
|
39
|
+
kind, data = event["event"], event["data"]
|
|
40
|
+
if kind == "output":
|
|
41
|
+
if data["seq"] != self.seq + 1:
|
|
42
|
+
raise self.failure("Command output contains a sequence gap")
|
|
43
|
+
stream = data["stream"]
|
|
44
|
+
if stream not in self._decoders:
|
|
45
|
+
raise self.failure("Unknown command output stream")
|
|
46
|
+
raw = (
|
|
47
|
+
base64.b64decode(data["data"], validate=True)
|
|
48
|
+
if data.get("encoding") == "base64"
|
|
49
|
+
else data["data"].encode("utf-8")
|
|
50
|
+
)
|
|
51
|
+
self.size += len(raw)
|
|
52
|
+
if self.size > self.max_bytes:
|
|
53
|
+
raise self.failure(
|
|
54
|
+
"Command output exceeds max_output_bytes; increase the collection limit or disconnect a detached process",
|
|
55
|
+
"output_limit_exceeded",
|
|
56
|
+
)
|
|
57
|
+
self.seq = data["seq"]
|
|
58
|
+
text = self._decoders[stream].decode(raw)
|
|
59
|
+
self._chunks["stdout" if stream == "stdout" else "stderr"].append(text)
|
|
60
|
+
self.events.append(
|
|
61
|
+
SandboxProcessOutputEvent(
|
|
62
|
+
type=stream,
|
|
63
|
+
seq=self.seq,
|
|
64
|
+
data=text,
|
|
65
|
+
timestamp=data["timestamp"],
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
elif kind == "done":
|
|
69
|
+
if data.get("last_seq") != self.seq or data.get("output_truncated", False):
|
|
70
|
+
raise self.failure("Receiver reported incomplete command output")
|
|
71
|
+
for stream, decoder in self._decoders.items():
|
|
72
|
+
self._chunks["stdout" if stream == "stdout" else "stderr"].append(
|
|
73
|
+
decoder.decode(b"", final=True)
|
|
74
|
+
)
|
|
75
|
+
result = dict(data)
|
|
76
|
+
result["stdout"] = "".join(self._chunks["stdout"])
|
|
77
|
+
result["stderr"] = "".join(self._chunks["stderr"])
|
|
78
|
+
self.result = SandboxProcessResult(**result)
|
|
79
|
+
elif kind == "error":
|
|
80
|
+
raise self.failure(
|
|
81
|
+
str(data.get("error", "Command stream failed")),
|
|
82
|
+
data.get("code", "incomplete_output"),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def validate_output_limit(value: int) -> None:
|
|
87
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
88
|
+
raise ValueError("max_output_bytes must be a positive integer")
|
{hyperbrowser-1.6.0 → hyperbrowser-1.7.0}/hyperbrowser/client/managers/sync_manager/sandbox.py
RENAMED
|
@@ -299,6 +299,7 @@ class SandboxHandle:
|
|
|
299
299
|
timeout_ms: Optional[int] = None,
|
|
300
300
|
timeout_sec: Optional[int] = None,
|
|
301
301
|
run_as: Optional[str] = None,
|
|
302
|
+
max_output_bytes: int = 64 * 1024 * 1024,
|
|
302
303
|
):
|
|
303
304
|
return self.processes.exec(
|
|
304
305
|
input,
|
|
@@ -307,6 +308,7 @@ class SandboxHandle:
|
|
|
307
308
|
timeout_ms=timeout_ms,
|
|
308
309
|
timeout_sec=timeout_sec,
|
|
309
310
|
run_as=run_as,
|
|
311
|
+
max_output_bytes=max_output_bytes,
|
|
310
312
|
)
|
|
311
313
|
|
|
312
314
|
def get_process(self, process_id: str) -> SandboxProcessHandle:
|