hyperbrowser 0.92.2__tar.gz → 0.93.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-0.92.2 → hyperbrowser-0.93.0}/PKG-INFO +1 -1
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandbox.py +237 -2
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +70 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +98 -5
- hyperbrowser-0.93.0/hyperbrowser/client/managers/sandboxes/image_build.py +375 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sandboxes/shared.py +19 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +226 -2
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +57 -1
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +98 -5
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/__init__.py +22 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/consts.py +3 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/sandbox.py +167 -17
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/pyproject.toml +1 -1
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/LICENSE +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/README.md +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/async_client.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/sync.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/session.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/transport/sync.py +0 -0
{hyperbrowser-0.92.2 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandbox.py
RENAMED
|
@@ -1,17 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import time
|
|
1
4
|
from typing import Dict, Optional, Union
|
|
2
5
|
|
|
3
6
|
from ....exceptions import HyperbrowserError
|
|
4
7
|
from ....models.sandbox import (
|
|
8
|
+
CompleteSandboxImageBuildParams,
|
|
5
9
|
CreateSandboxParams,
|
|
10
|
+
CreateSandboxImageBuildParams,
|
|
6
11
|
SandboxDetail,
|
|
7
12
|
SandboxExecParams,
|
|
8
13
|
SandboxExposeParams,
|
|
9
14
|
SandboxExposeResult,
|
|
15
|
+
SandboxImageBuild,
|
|
16
|
+
SandboxImageBuildCreateResult,
|
|
17
|
+
SandboxImageListParams,
|
|
10
18
|
SandboxImageListResponse,
|
|
19
|
+
SandboxImageInit,
|
|
11
20
|
SandboxListParams,
|
|
12
21
|
SandboxListResponse,
|
|
13
22
|
SandboxMemorySnapshotParams,
|
|
14
23
|
SandboxMemorySnapshotResult,
|
|
24
|
+
SandboxNetworkPolicy,
|
|
25
|
+
SandboxNetworkUpdateResult,
|
|
15
26
|
SandboxRuntimeSession,
|
|
16
27
|
SandboxSnapshotListParams,
|
|
17
28
|
SandboxSnapshotListResponse,
|
|
@@ -30,6 +41,15 @@ from ..sandboxes.shared import (
|
|
|
30
41
|
_copy_model,
|
|
31
42
|
_expires_within_buffer,
|
|
32
43
|
)
|
|
44
|
+
from ..sandboxes.image_build import (
|
|
45
|
+
IMAGE_BUILD_SOURCE_PLATFORM,
|
|
46
|
+
build_docker_image_from_dockerfile,
|
|
47
|
+
is_terminal_image_build_status,
|
|
48
|
+
make_temp_docker_tag,
|
|
49
|
+
package_docker_image,
|
|
50
|
+
remove_docker_image,
|
|
51
|
+
upload_image_build_artifact,
|
|
52
|
+
)
|
|
33
53
|
from .sandboxes.sandbox_files import (
|
|
34
54
|
DEFAULT_WATCH_TIMEOUT_MS,
|
|
35
55
|
SandboxFileWatchHandle,
|
|
@@ -67,6 +87,12 @@ __all__ = [
|
|
|
67
87
|
]
|
|
68
88
|
|
|
69
89
|
|
|
90
|
+
async def _run_blocking(func, *args, **kwargs):
|
|
91
|
+
loop = asyncio.get_running_loop()
|
|
92
|
+
call = functools.partial(func, *args, **kwargs)
|
|
93
|
+
return await loop.run_in_executor(None, call)
|
|
94
|
+
|
|
95
|
+
|
|
70
96
|
class SandboxHandle:
|
|
71
97
|
def __init__(self, service: "SandboxManager", detail: SandboxDetail):
|
|
72
98
|
self._service = service
|
|
@@ -130,6 +156,10 @@ class SandboxHandle:
|
|
|
130
156
|
def exposed_ports(self):
|
|
131
157
|
return self._detail.exposed_ports
|
|
132
158
|
|
|
159
|
+
@property
|
|
160
|
+
def network(self):
|
|
161
|
+
return self._detail.network
|
|
162
|
+
|
|
133
163
|
def to_dict(self):
|
|
134
164
|
return self._detail.model_dump()
|
|
135
165
|
|
|
@@ -166,6 +196,25 @@ class SandboxHandle:
|
|
|
166
196
|
raise TypeError("params must be a SandboxMemorySnapshotParams instance")
|
|
167
197
|
return await self._service.create_memory_snapshot(self.id, normalized)
|
|
168
198
|
|
|
199
|
+
async def update_network(
|
|
200
|
+
self,
|
|
201
|
+
policy: SandboxNetworkPolicy,
|
|
202
|
+
) -> SandboxNetworkUpdateResult:
|
|
203
|
+
if not isinstance(policy, SandboxNetworkPolicy):
|
|
204
|
+
raise TypeError("policy must be a SandboxNetworkPolicy instance")
|
|
205
|
+
result = await self._service.update_network(self.id, policy)
|
|
206
|
+
self._detail = self._detail.model_copy(update={"network": result.network})
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
async def clear_network(self) -> SandboxNetworkUpdateResult:
|
|
210
|
+
return await self.update_network(
|
|
211
|
+
SandboxNetworkPolicy(
|
|
212
|
+
allow_internet_access=True,
|
|
213
|
+
allow_out=[],
|
|
214
|
+
deny_out=[],
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
169
218
|
async def expose(self, params: SandboxExposeParams) -> SandboxExposeResult:
|
|
170
219
|
if not isinstance(params, SandboxExposeParams):
|
|
171
220
|
raise TypeError("params must be a SandboxExposeParams instance")
|
|
@@ -350,8 +399,16 @@ class SandboxManager:
|
|
|
350
399
|
)
|
|
351
400
|
return SandboxListResponse(**payload)
|
|
352
401
|
|
|
353
|
-
async def list_images(
|
|
354
|
-
|
|
402
|
+
async def list_images(
|
|
403
|
+
self, params: SandboxImageListParams = SandboxImageListParams()
|
|
404
|
+
) -> SandboxImageListResponse:
|
|
405
|
+
if not isinstance(params, SandboxImageListParams):
|
|
406
|
+
raise TypeError("params must be a SandboxImageListParams instance")
|
|
407
|
+
payload = await self._request(
|
|
408
|
+
"GET",
|
|
409
|
+
"/images",
|
|
410
|
+
params=params.model_dump(exclude_none=True, by_alias=True),
|
|
411
|
+
)
|
|
355
412
|
return SandboxImageListResponse(**payload)
|
|
356
413
|
|
|
357
414
|
async def list_snapshots(
|
|
@@ -370,6 +427,170 @@ class SandboxManager:
|
|
|
370
427
|
payload = await self._request("PUT", f"/sandbox/{sandbox_id}/stop")
|
|
371
428
|
return BasicResponse(**payload)
|
|
372
429
|
|
|
430
|
+
async def create_image_build(
|
|
431
|
+
self,
|
|
432
|
+
params: CreateSandboxImageBuildParams,
|
|
433
|
+
) -> SandboxImageBuildCreateResult:
|
|
434
|
+
if not isinstance(params, CreateSandboxImageBuildParams):
|
|
435
|
+
raise TypeError("params must be a CreateSandboxImageBuildParams instance")
|
|
436
|
+
payload = await self._request(
|
|
437
|
+
"POST",
|
|
438
|
+
"/images/builds",
|
|
439
|
+
data=params.model_dump(exclude_none=True, by_alias=True),
|
|
440
|
+
)
|
|
441
|
+
return SandboxImageBuildCreateResult(**payload)
|
|
442
|
+
|
|
443
|
+
async def get_image_build(self, build_id: str) -> SandboxImageBuild:
|
|
444
|
+
payload = await self._request("GET", f"/images/builds/{build_id}")
|
|
445
|
+
return SandboxImageBuild(**payload["build"])
|
|
446
|
+
|
|
447
|
+
async def complete_image_build(
|
|
448
|
+
self,
|
|
449
|
+
build_id: str,
|
|
450
|
+
params: CompleteSandboxImageBuildParams,
|
|
451
|
+
) -> SandboxImageBuild:
|
|
452
|
+
if not isinstance(params, CompleteSandboxImageBuildParams):
|
|
453
|
+
raise TypeError("params must be a CompleteSandboxImageBuildParams instance")
|
|
454
|
+
payload = await self._request(
|
|
455
|
+
"POST",
|
|
456
|
+
f"/images/builds/{build_id}/complete",
|
|
457
|
+
data=params.model_dump(exclude_none=True, by_alias=True),
|
|
458
|
+
)
|
|
459
|
+
return SandboxImageBuild(**payload["build"])
|
|
460
|
+
|
|
461
|
+
async def cancel_image_build(self, build_id: str) -> SandboxImageBuild:
|
|
462
|
+
payload = await self._request("POST", f"/images/builds/{build_id}/cancel")
|
|
463
|
+
return SandboxImageBuild(**payload["build"])
|
|
464
|
+
|
|
465
|
+
async def wait_for_image_build(
|
|
466
|
+
self,
|
|
467
|
+
build_id: str,
|
|
468
|
+
*,
|
|
469
|
+
poll_interval: float = 3.0,
|
|
470
|
+
timeout: Optional[float] = 35 * 60,
|
|
471
|
+
) -> SandboxImageBuild:
|
|
472
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
473
|
+
while True:
|
|
474
|
+
build = await self.get_image_build(build_id)
|
|
475
|
+
if build.status == "completed":
|
|
476
|
+
return build
|
|
477
|
+
if build.status == "failed":
|
|
478
|
+
message = build.error_message or "image build failed"
|
|
479
|
+
if build.error_code:
|
|
480
|
+
message = f"image build failed [{build.error_code}]: {message}"
|
|
481
|
+
raise RuntimeError(message)
|
|
482
|
+
if is_terminal_image_build_status(build.status):
|
|
483
|
+
raise RuntimeError(f"image build {build.status}")
|
|
484
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
485
|
+
raise TimeoutError(f"timed out waiting for image build {build_id}")
|
|
486
|
+
await asyncio.sleep(poll_interval)
|
|
487
|
+
|
|
488
|
+
async def build_image_from_docker_image(
|
|
489
|
+
self,
|
|
490
|
+
*,
|
|
491
|
+
docker_image: str,
|
|
492
|
+
image_name: str,
|
|
493
|
+
platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
|
|
494
|
+
image_init: Optional[SandboxImageInit] = None,
|
|
495
|
+
image_config_user: Optional[str] = None,
|
|
496
|
+
wait: bool = True,
|
|
497
|
+
poll_interval: float = 3.0,
|
|
498
|
+
wait_timeout: Optional[float] = 35 * 60,
|
|
499
|
+
temp_dir: Optional[str] = None,
|
|
500
|
+
upload_timeout: Optional[float] = None,
|
|
501
|
+
) -> SandboxImageBuild:
|
|
502
|
+
artifact = await _run_blocking(
|
|
503
|
+
package_docker_image,
|
|
504
|
+
docker_image,
|
|
505
|
+
platform=platform,
|
|
506
|
+
temp_dir=temp_dir,
|
|
507
|
+
)
|
|
508
|
+
try:
|
|
509
|
+
create_result = await self.create_image_build(
|
|
510
|
+
CreateSandboxImageBuildParams(
|
|
511
|
+
image_name=image_name,
|
|
512
|
+
input_sha256=artifact.sha256_hex,
|
|
513
|
+
input_size_bytes=artifact.size_bytes,
|
|
514
|
+
input_format=artifact.input_format,
|
|
515
|
+
source_platform=artifact.source_platform,
|
|
516
|
+
image_config_user=(
|
|
517
|
+
image_config_user
|
|
518
|
+
if image_config_user is not None
|
|
519
|
+
else artifact.image_config_user
|
|
520
|
+
),
|
|
521
|
+
image_init=image_init
|
|
522
|
+
if image_init is not None
|
|
523
|
+
else artifact.image_init,
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
await _run_blocking(
|
|
527
|
+
upload_image_build_artifact,
|
|
528
|
+
create_result.upload,
|
|
529
|
+
artifact.path,
|
|
530
|
+
timeout=upload_timeout,
|
|
531
|
+
)
|
|
532
|
+
build = await self.complete_image_build(
|
|
533
|
+
create_result.build.id,
|
|
534
|
+
CompleteSandboxImageBuildParams(
|
|
535
|
+
input_sha256=artifact.sha256_hex,
|
|
536
|
+
input_size_bytes=artifact.size_bytes,
|
|
537
|
+
input_format=artifact.input_format,
|
|
538
|
+
),
|
|
539
|
+
)
|
|
540
|
+
if wait:
|
|
541
|
+
return await self.wait_for_image_build(
|
|
542
|
+
build.id,
|
|
543
|
+
poll_interval=poll_interval,
|
|
544
|
+
timeout=wait_timeout,
|
|
545
|
+
)
|
|
546
|
+
return build
|
|
547
|
+
finally:
|
|
548
|
+
artifact.cleanup()
|
|
549
|
+
|
|
550
|
+
async def build_image_from_dockerfile(
|
|
551
|
+
self,
|
|
552
|
+
*,
|
|
553
|
+
context_path,
|
|
554
|
+
image_name: str,
|
|
555
|
+
dockerfile="Dockerfile",
|
|
556
|
+
docker_tag: Optional[str] = None,
|
|
557
|
+
platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
|
|
558
|
+
build_args: Optional[Dict[str, str]] = None,
|
|
559
|
+
image_init: Optional[SandboxImageInit] = None,
|
|
560
|
+
image_config_user: Optional[str] = None,
|
|
561
|
+
wait: bool = True,
|
|
562
|
+
poll_interval: float = 3.0,
|
|
563
|
+
wait_timeout: Optional[float] = 35 * 60,
|
|
564
|
+
temp_dir: Optional[str] = None,
|
|
565
|
+
upload_timeout: Optional[float] = None,
|
|
566
|
+
) -> SandboxImageBuild:
|
|
567
|
+
tag = docker_tag or make_temp_docker_tag()
|
|
568
|
+
remove_tag = docker_tag is None
|
|
569
|
+
try:
|
|
570
|
+
await _run_blocking(
|
|
571
|
+
build_docker_image_from_dockerfile,
|
|
572
|
+
context_path=context_path,
|
|
573
|
+
dockerfile=dockerfile,
|
|
574
|
+
tag=tag,
|
|
575
|
+
platform=platform,
|
|
576
|
+
build_args=build_args,
|
|
577
|
+
)
|
|
578
|
+
return await self.build_image_from_docker_image(
|
|
579
|
+
docker_image=tag,
|
|
580
|
+
image_name=image_name,
|
|
581
|
+
platform=platform,
|
|
582
|
+
image_init=image_init,
|
|
583
|
+
image_config_user=image_config_user,
|
|
584
|
+
wait=wait,
|
|
585
|
+
poll_interval=poll_interval,
|
|
586
|
+
wait_timeout=wait_timeout,
|
|
587
|
+
temp_dir=temp_dir,
|
|
588
|
+
upload_timeout=upload_timeout,
|
|
589
|
+
)
|
|
590
|
+
finally:
|
|
591
|
+
if remove_tag:
|
|
592
|
+
await _run_blocking(remove_docker_image, tag)
|
|
593
|
+
|
|
373
594
|
async def get_runtime_session(self, sandbox_id: str) -> SandboxRuntimeSession:
|
|
374
595
|
detail = await self.get_detail(sandbox_id)
|
|
375
596
|
session = SandboxHandle._to_runtime_session(detail)
|
|
@@ -404,6 +625,20 @@ class SandboxManager:
|
|
|
404
625
|
)
|
|
405
626
|
return SandboxMemorySnapshotResult(**payload)
|
|
406
627
|
|
|
628
|
+
async def update_network(
|
|
629
|
+
self,
|
|
630
|
+
sandbox_id: str,
|
|
631
|
+
policy: SandboxNetworkPolicy,
|
|
632
|
+
) -> SandboxNetworkUpdateResult:
|
|
633
|
+
if not isinstance(policy, SandboxNetworkPolicy):
|
|
634
|
+
raise TypeError("policy must be a SandboxNetworkPolicy instance")
|
|
635
|
+
payload = await self._request(
|
|
636
|
+
"PUT",
|
|
637
|
+
f"/sandbox/{sandbox_id}/network",
|
|
638
|
+
data=policy.model_dump(exclude_none=True, by_alias=True),
|
|
639
|
+
)
|
|
640
|
+
return SandboxNetworkUpdateResult(**payload)
|
|
641
|
+
|
|
407
642
|
async def expose(
|
|
408
643
|
self,
|
|
409
644
|
sandbox_id: str,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
+
import functools
|
|
3
4
|
import inspect
|
|
4
5
|
import io
|
|
5
6
|
import json
|
|
@@ -43,6 +44,42 @@ from ...sandboxes.shared import (
|
|
|
43
44
|
)
|
|
44
45
|
from .sandbox_transport import RuntimeTransport
|
|
45
46
|
|
|
47
|
+
DEFAULT_TRANSFER_CHUNK_SIZE = 64 * 1024
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _run_blocking(func, *args, **kwargs):
|
|
51
|
+
loop = asyncio.get_running_loop()
|
|
52
|
+
call = functools.partial(func, *args, **kwargs)
|
|
53
|
+
return await loop.run_in_executor(None, call)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _aiter_stream_content(stream, chunk_size: int = DEFAULT_TRANSFER_CHUNK_SIZE):
|
|
57
|
+
if isinstance(stream, str):
|
|
58
|
+
yield stream.encode("utf-8")
|
|
59
|
+
return
|
|
60
|
+
if isinstance(stream, (bytes, bytearray)):
|
|
61
|
+
yield bytes(stream)
|
|
62
|
+
return
|
|
63
|
+
if hasattr(stream, "__aiter__"):
|
|
64
|
+
async for chunk in stream:
|
|
65
|
+
if isinstance(chunk, str):
|
|
66
|
+
chunk = chunk.encode("utf-8")
|
|
67
|
+
yield bytes(chunk)
|
|
68
|
+
return
|
|
69
|
+
if hasattr(stream, "read"):
|
|
70
|
+
while True:
|
|
71
|
+
chunk = await _run_blocking(stream.read, chunk_size)
|
|
72
|
+
if not chunk:
|
|
73
|
+
break
|
|
74
|
+
if isinstance(chunk, str):
|
|
75
|
+
chunk = chunk.encode("utf-8")
|
|
76
|
+
yield bytes(chunk)
|
|
77
|
+
return
|
|
78
|
+
for chunk in stream:
|
|
79
|
+
if isinstance(chunk, str):
|
|
80
|
+
chunk = chunk.encode("utf-8")
|
|
81
|
+
yield bytes(chunk)
|
|
82
|
+
|
|
46
83
|
|
|
47
84
|
class SandboxFileWatchHandle:
|
|
48
85
|
def __init__(
|
|
@@ -439,12 +476,45 @@ class SandboxFilesApi:
|
|
|
439
476
|
)
|
|
440
477
|
return SandboxFileTransferResult(**payload)
|
|
441
478
|
|
|
479
|
+
async def upload_stream(
|
|
480
|
+
self,
|
|
481
|
+
path: str,
|
|
482
|
+
stream,
|
|
483
|
+
*,
|
|
484
|
+
content_length: Optional[int] = None,
|
|
485
|
+
chunk_size: int = DEFAULT_TRANSFER_CHUNK_SIZE,
|
|
486
|
+
):
|
|
487
|
+
headers = {}
|
|
488
|
+
if content_length is not None:
|
|
489
|
+
headers["content-length"] = str(content_length)
|
|
490
|
+
payload = await self._transport.request_json(
|
|
491
|
+
"/sandbox/files/upload",
|
|
492
|
+
method="PUT",
|
|
493
|
+
params=self._with_run_as_params({"path": path}),
|
|
494
|
+
content=_aiter_stream_content(stream, chunk_size=chunk_size),
|
|
495
|
+
headers=headers or None,
|
|
496
|
+
)
|
|
497
|
+
return SandboxFileTransferResult(**payload)
|
|
498
|
+
|
|
442
499
|
async def download(self, path: str) -> bytes:
|
|
443
500
|
return await self._transport.request_bytes(
|
|
444
501
|
"/sandbox/files/download",
|
|
445
502
|
params=self._with_run_as_params({"path": path}),
|
|
446
503
|
)
|
|
447
504
|
|
|
505
|
+
async def download_stream(
|
|
506
|
+
self,
|
|
507
|
+
path: str,
|
|
508
|
+
*,
|
|
509
|
+
chunk_size: int = DEFAULT_TRANSFER_CHUNK_SIZE,
|
|
510
|
+
) -> AsyncIterator[bytes]:
|
|
511
|
+
async for chunk in self._transport.stream_bytes(
|
|
512
|
+
"/sandbox/files/download",
|
|
513
|
+
params=self._with_run_as_params({"path": path}),
|
|
514
|
+
chunk_size=chunk_size,
|
|
515
|
+
):
|
|
516
|
+
yield chunk
|
|
517
|
+
|
|
448
518
|
async def make_dir(
|
|
449
519
|
self,
|
|
450
520
|
path: str,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import AsyncIterator, Dict, Optional
|
|
2
|
+
from typing import Any, AsyncIterator, Dict, Optional
|
|
3
3
|
|
|
4
4
|
import httpx
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ from .....sandbox_common import (
|
|
|
11
11
|
parse_json_response,
|
|
12
12
|
resolve_runtime_transport_target,
|
|
13
13
|
)
|
|
14
|
-
from ...sandboxes.shared import _build_query_path
|
|
14
|
+
from ...sandboxes.shared import _build_query_path, _is_replayable_http_content
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class RuntimeTransport:
|
|
@@ -32,7 +32,7 @@ class RuntimeTransport:
|
|
|
32
32
|
method: str = "GET",
|
|
33
33
|
params: Optional[Dict[str, object]] = None,
|
|
34
34
|
json_body: Optional[Dict[str, object]] = None,
|
|
35
|
-
content: Optional[
|
|
35
|
+
content: Optional[Any] = None,
|
|
36
36
|
headers: Optional[Dict[str, str]] = None,
|
|
37
37
|
):
|
|
38
38
|
response = await self._request(
|
|
@@ -58,6 +58,29 @@ class RuntimeTransport:
|
|
|
58
58
|
)
|
|
59
59
|
return response.content
|
|
60
60
|
|
|
61
|
+
async def stream_bytes(
|
|
62
|
+
self,
|
|
63
|
+
path: str,
|
|
64
|
+
*,
|
|
65
|
+
method: str = "GET",
|
|
66
|
+
params: Optional[Dict[str, object]] = None,
|
|
67
|
+
headers: Optional[Dict[str, str]] = None,
|
|
68
|
+
chunk_size: int = 65536,
|
|
69
|
+
) -> AsyncIterator[bytes]:
|
|
70
|
+
client, response = await self._open_binary_stream(
|
|
71
|
+
path,
|
|
72
|
+
method=method,
|
|
73
|
+
params=params,
|
|
74
|
+
headers=headers,
|
|
75
|
+
)
|
|
76
|
+
try:
|
|
77
|
+
async for chunk in response.aiter_bytes(chunk_size=chunk_size):
|
|
78
|
+
if chunk:
|
|
79
|
+
yield chunk
|
|
80
|
+
finally:
|
|
81
|
+
await response.aclose()
|
|
82
|
+
await client.aclose()
|
|
83
|
+
|
|
61
84
|
async def stream_sse(
|
|
62
85
|
self, path: str, params: Optional[Dict[str, object]] = None
|
|
63
86
|
) -> AsyncIterator[Dict[str, object]]:
|
|
@@ -127,7 +150,7 @@ class RuntimeTransport:
|
|
|
127
150
|
method: str = "GET",
|
|
128
151
|
params: Optional[Dict[str, object]] = None,
|
|
129
152
|
json_body: Optional[Dict[str, object]] = None,
|
|
130
|
-
content: Optional[
|
|
153
|
+
content: Optional[Any] = None,
|
|
131
154
|
headers: Optional[Dict[str, str]] = None,
|
|
132
155
|
allow_refresh: bool = True,
|
|
133
156
|
) -> httpx.Response:
|
|
@@ -144,6 +167,12 @@ class RuntimeTransport:
|
|
|
144
167
|
|
|
145
168
|
if response.status_code == 401 and allow_refresh:
|
|
146
169
|
await response.aclose()
|
|
170
|
+
if not _is_replayable_http_content(content):
|
|
171
|
+
return ensure_response_ok(
|
|
172
|
+
response,
|
|
173
|
+
"runtime",
|
|
174
|
+
"Runtime token expired during a streaming request; retry with a fresh stream.",
|
|
175
|
+
)
|
|
147
176
|
refreshed = await self._resolve_connection(True)
|
|
148
177
|
retry = await self._send(
|
|
149
178
|
refreshed,
|
|
@@ -178,6 +207,40 @@ class RuntimeTransport:
|
|
|
178
207
|
ensure_response_ok(response, "runtime")
|
|
179
208
|
return client, response
|
|
180
209
|
|
|
210
|
+
async def _open_binary_stream(
|
|
211
|
+
self,
|
|
212
|
+
path: str,
|
|
213
|
+
*,
|
|
214
|
+
method: str = "GET",
|
|
215
|
+
params: Optional[Dict[str, object]] = None,
|
|
216
|
+
headers: Optional[Dict[str, str]] = None,
|
|
217
|
+
allow_refresh: bool = True,
|
|
218
|
+
):
|
|
219
|
+
connection = await self._resolve_connection(False)
|
|
220
|
+
client, response = await self._send_binary_stream(
|
|
221
|
+
connection,
|
|
222
|
+
path,
|
|
223
|
+
method=method,
|
|
224
|
+
params=params,
|
|
225
|
+
headers=headers,
|
|
226
|
+
)
|
|
227
|
+
if response.status_code == 401 and allow_refresh:
|
|
228
|
+
await response.aclose()
|
|
229
|
+
await client.aclose()
|
|
230
|
+
refreshed = await self._resolve_connection(True)
|
|
231
|
+
client, response = await self._send_binary_stream(
|
|
232
|
+
refreshed,
|
|
233
|
+
path,
|
|
234
|
+
method=method,
|
|
235
|
+
params=params,
|
|
236
|
+
headers=headers,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if not response.is_success:
|
|
240
|
+
await response.aread()
|
|
241
|
+
ensure_response_ok(response, "runtime")
|
|
242
|
+
return client, response
|
|
243
|
+
|
|
181
244
|
async def _send(
|
|
182
245
|
self,
|
|
183
246
|
connection: RuntimeConnection,
|
|
@@ -186,7 +249,7 @@ class RuntimeTransport:
|
|
|
186
249
|
method: str,
|
|
187
250
|
params: Optional[Dict[str, object]],
|
|
188
251
|
json_body: Optional[Dict[str, object]],
|
|
189
|
-
content: Optional[
|
|
252
|
+
content: Optional[Any],
|
|
190
253
|
headers: Optional[Dict[str, str]],
|
|
191
254
|
) -> httpx.Response:
|
|
192
255
|
request_path = _build_query_path(path, params)
|
|
@@ -218,6 +281,36 @@ class RuntimeTransport:
|
|
|
218
281
|
await client.aclose()
|
|
219
282
|
return response
|
|
220
283
|
|
|
284
|
+
async def _send_binary_stream(
|
|
285
|
+
self,
|
|
286
|
+
connection: RuntimeConnection,
|
|
287
|
+
path: str,
|
|
288
|
+
*,
|
|
289
|
+
method: str,
|
|
290
|
+
params: Optional[Dict[str, object]],
|
|
291
|
+
headers: Optional[Dict[str, str]],
|
|
292
|
+
):
|
|
293
|
+
request_path = _build_query_path(path, params)
|
|
294
|
+
target = resolve_runtime_transport_target(
|
|
295
|
+
connection.base_url,
|
|
296
|
+
request_path,
|
|
297
|
+
self._runtime_proxy_override,
|
|
298
|
+
)
|
|
299
|
+
merged_headers = build_headers(connection.token, headers, target.host_header)
|
|
300
|
+
client = httpx.AsyncClient(timeout=self._timeout)
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
request = client.build_request(method, target.url, headers=merged_headers)
|
|
304
|
+
response = await client.send(request, stream=True)
|
|
305
|
+
return client, response
|
|
306
|
+
except BaseException as error:
|
|
307
|
+
await client.aclose()
|
|
308
|
+
raise normalize_network_error(
|
|
309
|
+
error,
|
|
310
|
+
"runtime",
|
|
311
|
+
"Unknown runtime request error",
|
|
312
|
+
)
|
|
313
|
+
|
|
221
314
|
async def _send_stream(
|
|
222
315
|
self,
|
|
223
316
|
connection: RuntimeConnection,
|