hyperbrowser 0.92.3__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.
Files changed (93) hide show
  1. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/PKG-INFO +1 -1
  2. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandbox.py +237 -2
  3. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +70 -0
  4. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +98 -5
  5. hyperbrowser-0.93.0/hyperbrowser/client/managers/sandboxes/image_build.py +375 -0
  6. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sandboxes/shared.py +19 -0
  7. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +226 -2
  8. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +57 -1
  9. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +98 -5
  10. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/__init__.py +22 -0
  11. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/sandbox.py +167 -17
  12. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/pyproject.toml +1 -1
  13. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/LICENSE +0 -0
  14. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/README.md +0 -0
  15. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/__init__.py +0 -0
  16. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/async_client.py +0 -0
  17. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/base.py +0 -0
  18. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  19. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  20. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  21. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  22. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  23. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  24. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  25. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  26. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  27. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  28. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  29. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  30. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
  31. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  32. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  33. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  34. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  35. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
  36. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  37. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  38. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  39. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  40. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  41. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  42. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  43. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  44. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  45. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  46. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  47. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  48. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  49. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  50. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  51. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  52. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
  53. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  54. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  55. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  56. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  57. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
  58. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  59. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  60. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  61. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/client/sync.py +0 -0
  62. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/config.py +0 -0
  63. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/exceptions.py +0 -0
  64. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/_parsers.py +0 -0
  65. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/browser_use.py +0 -0
  66. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  67. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/cua.py +0 -0
  68. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  69. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  70. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/computer_action.py +0 -0
  71. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/consts.py +0 -0
  72. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/crawl.py +0 -0
  73. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/extension.py +0 -0
  74. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/extract.py +0 -0
  75. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/profile.py +0 -0
  76. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/scrape.py +0 -0
  77. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/session.py +0 -0
  78. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/team.py +0 -0
  79. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/volume.py +0 -0
  80. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
  81. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/web/branding.py +0 -0
  82. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/web/common.py +0 -0
  83. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/web/crawl.py +0 -0
  84. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/web/fetch.py +0 -0
  85. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/models/web/search.py +0 -0
  86. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/sandbox_common.py +0 -0
  87. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/tools/__init__.py +0 -0
  88. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/tools/anthropic.py +0 -0
  89. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/tools/openai.py +0 -0
  90. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/tools/schema.py +0 -0
  91. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/transport/async_transport.py +0 -0
  92. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/transport/base.py +0 -0
  93. {hyperbrowser-0.92.3 → hyperbrowser-0.93.0}/hyperbrowser/transport/sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 0.92.3
3
+ Version: 0.93.0
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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(self) -> SandboxImageListResponse:
354
- payload = await self._request("GET", "/images")
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, Union
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[Union[str, bytes]] = None,
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[Union[str, bytes]] = None,
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[Union[str, bytes]],
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,