hyperbrowser 1.6.0__tar.gz → 1.8.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.8.0}/PKG-INFO +80 -1
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/README.md +79 -0
- hyperbrowser-1.8.0/hyperbrowser/build_context.py +8 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/sandbox.py +8 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +126 -14
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +55 -10
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sandboxes/image_build.py +197 -43
- hyperbrowser-1.8.0/hyperbrowser/client/managers/sandboxes/process_output.py +88 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sandboxes/shared.py +2 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +8 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +140 -14
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +74 -11
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/sandbox.py +3 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/pyproject.toml +1 -1
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/LICENSE +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/async_client.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/client/sync.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/consts.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/transport/sync.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/__init__.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/agents.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/sandbox.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/session.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.0}/hyperbrowser/types/volume.py +0 -0
- {hyperbrowser-1.6.0 → hyperbrowser-1.8.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.8.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
|
|
@@ -337,6 +380,42 @@ for event in connection.events():
|
|
|
337
380
|
print(event)
|
|
338
381
|
```
|
|
339
382
|
|
|
383
|
+
### Cache remote Dockerfile builds
|
|
384
|
+
|
|
385
|
+
Use the public context fingerprint when deriving a cache name. It uses the same
|
|
386
|
+
Dockerfile source selection and `.dockerignore` rules as remote packaging,
|
|
387
|
+
including file contents, modes, paths, and symlinks. It ignores timestamps and
|
|
388
|
+
does not compress or stage the context on disk.
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
from hyperbrowser.build_context import docker_build_context_fingerprint
|
|
392
|
+
|
|
393
|
+
fingerprint = docker_build_context_fingerprint("./app")
|
|
394
|
+
# Include build options such as platform and image_init in your cache key too.
|
|
395
|
+
image_name = f"app-{fingerprint[:32]}"
|
|
396
|
+
build = client.sandboxes.build_image_from_dockerfile(
|
|
397
|
+
context_path="./app",
|
|
398
|
+
image_name=image_name,
|
|
399
|
+
expected_context_fingerprint=fingerprint,
|
|
400
|
+
)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
If the archived inputs differ from the fingerprint, the SDK raises
|
|
404
|
+
`DockerBuildContextChangedError` before creating or uploading a build. Compute a
|
|
405
|
+
fresh fingerprint and repeat the lookup/build operation. Use the same `dockerfile`
|
|
406
|
+
and `force_full_context` selection when fingerprinting and building (the latter
|
|
407
|
+
is named `remote_full_context` on the build method). The expected fingerprint is
|
|
408
|
+
supported only for remote builds.
|
|
409
|
+
|
|
410
|
+
Fingerprinting streams included files and performs blocking I/O. Async callers
|
|
411
|
+
should use `await asyncio.to_thread(docker_build_context_fingerprint, "./app")`
|
|
412
|
+
(Python 3.9+) or an executor. It does not resolve mutable base-image tags or
|
|
413
|
+
network resources fetched by a Dockerfile; rebuild explicitly when those change.
|
|
414
|
+
|
|
415
|
+
Image listings distinguish `ready` from `uploaded`: a completed team image can
|
|
416
|
+
be ready to launch before its durability backup is uploaded. `ready` is `None`
|
|
417
|
+
when talking to an older server. Keep the returned image ID to pin that revision.
|
|
418
|
+
|
|
340
419
|
## License
|
|
341
420
|
|
|
342
421
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -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
|
|
@@ -309,6 +352,42 @@ for event in connection.events():
|
|
|
309
352
|
print(event)
|
|
310
353
|
```
|
|
311
354
|
|
|
355
|
+
### Cache remote Dockerfile builds
|
|
356
|
+
|
|
357
|
+
Use the public context fingerprint when deriving a cache name. It uses the same
|
|
358
|
+
Dockerfile source selection and `.dockerignore` rules as remote packaging,
|
|
359
|
+
including file contents, modes, paths, and symlinks. It ignores timestamps and
|
|
360
|
+
does not compress or stage the context on disk.
|
|
361
|
+
|
|
362
|
+
```python
|
|
363
|
+
from hyperbrowser.build_context import docker_build_context_fingerprint
|
|
364
|
+
|
|
365
|
+
fingerprint = docker_build_context_fingerprint("./app")
|
|
366
|
+
# Include build options such as platform and image_init in your cache key too.
|
|
367
|
+
image_name = f"app-{fingerprint[:32]}"
|
|
368
|
+
build = client.sandboxes.build_image_from_dockerfile(
|
|
369
|
+
context_path="./app",
|
|
370
|
+
image_name=image_name,
|
|
371
|
+
expected_context_fingerprint=fingerprint,
|
|
372
|
+
)
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
If the archived inputs differ from the fingerprint, the SDK raises
|
|
376
|
+
`DockerBuildContextChangedError` before creating or uploading a build. Compute a
|
|
377
|
+
fresh fingerprint and repeat the lookup/build operation. Use the same `dockerfile`
|
|
378
|
+
and `force_full_context` selection when fingerprinting and building (the latter
|
|
379
|
+
is named `remote_full_context` on the build method). The expected fingerprint is
|
|
380
|
+
supported only for remote builds.
|
|
381
|
+
|
|
382
|
+
Fingerprinting streams included files and performs blocking I/O. Async callers
|
|
383
|
+
should use `await asyncio.to_thread(docker_build_context_fingerprint, "./app")`
|
|
384
|
+
(Python 3.9+) or an executor. It does not resolve mutable base-image tags or
|
|
385
|
+
network resources fetched by a Dockerfile; rebuild explicitly when those change.
|
|
386
|
+
|
|
387
|
+
Image listings distinguish `ready` from `uploaded`: a completed team image can
|
|
388
|
+
be ready to launch before its durability backup is uploaded. `ready` is `None`
|
|
389
|
+
when talking to an older server. Keep the returned image ID to pin that revision.
|
|
390
|
+
|
|
312
391
|
## License
|
|
313
392
|
|
|
314
393
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Public helpers for identifying inputs to remote Dockerfile builds."""
|
|
2
|
+
|
|
3
|
+
from .client.managers.sandboxes.image_build import (
|
|
4
|
+
DockerBuildContextChangedError,
|
|
5
|
+
docker_build_context_fingerprint,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = ["DockerBuildContextChangedError", "docker_build_context_fingerprint"]
|
{hyperbrowser-1.6.0 → hyperbrowser-1.8.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:
|
|
@@ -723,6 +725,7 @@ class SandboxManager:
|
|
|
723
725
|
dockerfile,
|
|
724
726
|
platform: str,
|
|
725
727
|
remote_full_context: bool,
|
|
728
|
+
expected_context_fingerprint: Optional[str],
|
|
726
729
|
image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]],
|
|
727
730
|
image_config_user: Optional[str],
|
|
728
731
|
builder_cpus: Optional[int],
|
|
@@ -739,6 +742,7 @@ class SandboxManager:
|
|
|
739
742
|
context_path,
|
|
740
743
|
dockerfile=dockerfile,
|
|
741
744
|
force_full_context=remote_full_context,
|
|
745
|
+
expected_context_fingerprint=expected_context_fingerprint,
|
|
742
746
|
temp_dir=temp_dir,
|
|
743
747
|
)
|
|
744
748
|
build_id = None
|
|
@@ -801,6 +805,7 @@ class SandboxManager:
|
|
|
801
805
|
dockerfile="Dockerfile",
|
|
802
806
|
remote: bool = True,
|
|
803
807
|
remote_full_context: bool = False,
|
|
808
|
+
expected_context_fingerprint: Optional[str] = None,
|
|
804
809
|
docker_tag: Optional[str] = None,
|
|
805
810
|
platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
|
|
806
811
|
build_args: Optional[Dict[str, str]] = None,
|
|
@@ -827,6 +832,7 @@ class SandboxManager:
|
|
|
827
832
|
dockerfile=dockerfile,
|
|
828
833
|
platform=platform,
|
|
829
834
|
remote_full_context=remote_full_context,
|
|
835
|
+
expected_context_fingerprint=expected_context_fingerprint,
|
|
830
836
|
image_init=image_init,
|
|
831
837
|
image_config_user=image_config_user,
|
|
832
838
|
builder_cpus=builder_cpus,
|
|
@@ -838,6 +844,8 @@ class SandboxManager:
|
|
|
838
844
|
temp_dir=temp_dir,
|
|
839
845
|
upload_timeout=upload_timeout,
|
|
840
846
|
)
|
|
847
|
+
if expected_context_fingerprint is not None:
|
|
848
|
+
raise ValueError("expected_context_fingerprint requires remote=True")
|
|
841
849
|
tag = docker_tag or make_temp_docker_tag()
|
|
842
850
|
remove_tag = docker_tag is None
|
|
843
851
|
try:
|
|
@@ -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
|
)
|