hyperbrowser 1.8.0__tar.gz → 1.9.1__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 (123) hide show
  1. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/PKG-INFO +51 -1
  2. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/README.md +50 -0
  3. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/sandbox.py +172 -0
  4. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sandboxes/image_build.py +21 -1
  5. hyperbrowser-1.9.1/hyperbrowser/client/managers/sandboxes/image_resolution.py +103 -0
  6. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/sandbox.py +169 -0
  7. hyperbrowser-1.9.1/hyperbrowser/image_builds.py +5 -0
  8. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/__init__.py +2 -0
  9. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/sandbox.py +13 -0
  10. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/pyproject.toml +1 -1
  11. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/LICENSE +0 -0
  12. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/__init__.py +0 -0
  13. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/build_context.py +0 -0
  14. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/_request.py +0 -0
  15. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/async_client.py +0 -0
  16. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/base.py +0 -0
  17. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  18. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  19. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  20. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  21. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  22. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
  23. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  24. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
  25. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
  26. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  27. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  28. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  29. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  30. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  31. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  32. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
  33. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
  34. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  35. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
  36. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  37. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  38. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  39. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
  40. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  41. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  42. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  43. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  44. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
  45. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
  46. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
  47. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sandboxes/shared.py +0 -0
  48. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  49. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  50. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  51. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  52. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  53. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
  54. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  55. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
  56. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
  57. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  58. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  59. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  60. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  61. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  62. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  63. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
  64. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
  65. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  66. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
  67. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  68. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  69. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  70. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
  71. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  72. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  73. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  74. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/client/sync.py +0 -0
  75. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/config.py +0 -0
  76. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/exceptions.py +0 -0
  77. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/_parsers.py +0 -0
  78. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/browser_use.py +0 -0
  79. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  80. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/cua.py +0 -0
  81. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  82. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
  83. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  84. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
  85. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
  86. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/computer_action.py +0 -0
  87. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/consts.py +0 -0
  88. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/crawl.py +0 -0
  89. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/extension.py +0 -0
  90. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/extract.py +0 -0
  91. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/profile.py +0 -0
  92. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/scrape.py +0 -0
  93. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/session.py +0 -0
  94. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/team.py +0 -0
  95. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/volume.py +0 -0
  96. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/web/batch_fetch.py +0 -0
  97. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/web/branding.py +0 -0
  98. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/web/common.py +0 -0
  99. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/web/crawl.py +0 -0
  100. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/web/fetch.py +0 -0
  101. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/models/web/search.py +0 -0
  102. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/py.typed +0 -0
  103. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/sandbox_common.py +0 -0
  104. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/tools/__init__.py +0 -0
  105. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/tools/anthropic.py +0 -0
  106. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/tools/openai.py +0 -0
  107. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/tools/schema.py +0 -0
  108. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/transport/async_transport.py +0 -0
  109. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/transport/base.py +0 -0
  110. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/transport/sync.py +0 -0
  111. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/__init__.py +0 -0
  112. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/_json.py +0 -0
  113. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/agents.py +0 -0
  114. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/computer_action.py +0 -0
  115. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/crawl.py +0 -0
  116. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/extension.py +0 -0
  117. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/extract.py +0 -0
  118. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/profile.py +0 -0
  119. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/sandbox.py +0 -0
  120. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/scrape.py +0 -0
  121. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/session.py +0 -0
  122. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/volume.py +0 -0
  123. {hyperbrowser-1.8.0 → hyperbrowser-1.9.1}/hyperbrowser/types/web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 1.8.0
3
+ Version: 1.9.1
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -416,6 +416,56 @@ Image listings distinguish `ready` from `uploaded`: a completed team image can
416
416
  be ready to launch before its durability backup is uploaded. `ready` is `None`
417
417
  when talking to an older server. Keep the returned image ID to pin that revision.
418
418
 
419
+ ### Reuse an image or join a build
420
+
421
+ `get_or_build_image` provides the same operation on sync and async clients. Give
422
+ it either a remote Dockerfile context or a local Docker image. It derives a name
423
+ from the input identity and image initialization options, reuses a ready team
424
+ image, or submits a build and joins a compatible concurrent build automatically.
425
+ The optional prefix is a namespace, not a fixed image alias: different inputs
426
+ produce different names under the same prefix.
427
+
428
+ ```python
429
+ resolved = client.sandboxes.get_or_build_image(
430
+ context_path="./app", # alternatively: docker_image="local/app:latest"
431
+ image_name_prefix="my-app",
432
+ wait_timeout=3600,
433
+ )
434
+ print(resolved.outcome) # "reused", "joined", or "created"
435
+ sandbox = client.sandboxes.create({
436
+ "image_name": resolved.image_name,
437
+ "image_id": resolved.image_id,
438
+ })
439
+ ```
440
+
441
+ With `wait=False`, a submitted/joined build is returned as `resolved.build`;
442
+ `image_id` is populated only when ready. `find_ready_image(name)` exposes the
443
+ exact-name lookup separately. Older servers fall back to uploaded-image reuse.
444
+ The public `hyperbrowser.image_builds.image_build_name` helper lets integrations
445
+ derive the same name from an existing context fingerprint or Docker image digest.
446
+ Passing `expected_context_fingerprint` or `expected_image_digest` avoids repeating
447
+ identity discovery; supply a fresh identity for each resolution request. Changes
448
+ between identity discovery and packaging are rejected instead of published under
449
+ the wrong name. Local Docker images must already be available in the daemon.
450
+
451
+ Automatic local-image identity discovery requires a Docker CLI and Engine
452
+ supporting **API 1.49 or newer (Docker 28.1+)** for platform-specific inspection.
453
+ Upgrade Docker and check for an older `DOCKER_API_VERSION` override if the helper
454
+ reports this requirement. Remote Dockerfile builds do not require local Docker.
455
+ The existing explicit-name import method retains its inspection fallback.
456
+
457
+ `force_build=True` skips ready-image lookup but still joins matching active builds
458
+ and permits existing layer/artifact caches. Use it to refresh mutable base tags or
459
+ external Dockerfile downloads. Joining does not change an existing builder's
460
+ resources. Lookup and creation use separate API calls; if another build completes
461
+ between them, an additional revision can be submitted.
462
+
463
+ Each caller owns its polling timeout. Canceling that wait does not cancel an
464
+ accepted backend build. Uploads have a separate inactivity allowance
465
+ (`upload_timeout=600` by default), not a total upload-duration limit. The existing
466
+ `build_image_from_dockerfile` and `build_image_from_docker_image` methods retain
467
+ their explicit-name behavior and continue to report build conflicts directly.
468
+
419
469
  ## License
420
470
 
421
471
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -388,6 +388,56 @@ Image listings distinguish `ready` from `uploaded`: a completed team image can
388
388
  be ready to launch before its durability backup is uploaded. `ready` is `None`
389
389
  when talking to an older server. Keep the returned image ID to pin that revision.
390
390
 
391
+ ### Reuse an image or join a build
392
+
393
+ `get_or_build_image` provides the same operation on sync and async clients. Give
394
+ it either a remote Dockerfile context or a local Docker image. It derives a name
395
+ from the input identity and image initialization options, reuses a ready team
396
+ image, or submits a build and joins a compatible concurrent build automatically.
397
+ The optional prefix is a namespace, not a fixed image alias: different inputs
398
+ produce different names under the same prefix.
399
+
400
+ ```python
401
+ resolved = client.sandboxes.get_or_build_image(
402
+ context_path="./app", # alternatively: docker_image="local/app:latest"
403
+ image_name_prefix="my-app",
404
+ wait_timeout=3600,
405
+ )
406
+ print(resolved.outcome) # "reused", "joined", or "created"
407
+ sandbox = client.sandboxes.create({
408
+ "image_name": resolved.image_name,
409
+ "image_id": resolved.image_id,
410
+ })
411
+ ```
412
+
413
+ With `wait=False`, a submitted/joined build is returned as `resolved.build`;
414
+ `image_id` is populated only when ready. `find_ready_image(name)` exposes the
415
+ exact-name lookup separately. Older servers fall back to uploaded-image reuse.
416
+ The public `hyperbrowser.image_builds.image_build_name` helper lets integrations
417
+ derive the same name from an existing context fingerprint or Docker image digest.
418
+ Passing `expected_context_fingerprint` or `expected_image_digest` avoids repeating
419
+ identity discovery; supply a fresh identity for each resolution request. Changes
420
+ between identity discovery and packaging are rejected instead of published under
421
+ the wrong name. Local Docker images must already be available in the daemon.
422
+
423
+ Automatic local-image identity discovery requires a Docker CLI and Engine
424
+ supporting **API 1.49 or newer (Docker 28.1+)** for platform-specific inspection.
425
+ Upgrade Docker and check for an older `DOCKER_API_VERSION` override if the helper
426
+ reports this requirement. Remote Dockerfile builds do not require local Docker.
427
+ The existing explicit-name import method retains its inspection fallback.
428
+
429
+ `force_build=True` skips ready-image lookup but still joins matching active builds
430
+ and permits existing layer/artifact caches. Use it to refresh mutable base tags or
431
+ external Dockerfile downloads. Joining does not change an existing builder's
432
+ resources. Lookup and creation use separate API calls; if another build completes
433
+ between them, an additional revision can be submitted.
434
+
435
+ Each caller owns its polling timeout. Canceling that wait does not cancel an
436
+ accepted backend build. Uploads have a separate inactivity allowance
437
+ (`upload_timeout=600` by default), not a total upload-duration limit. The existing
438
+ `build_image_from_dockerfile` and `build_image_from_docker_image` methods retain
439
+ their explicit-name behavior and continue to report build conflicts directly.
440
+
391
441
  ## License
392
442
 
393
443
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import functools
3
3
  import time
4
+ from pathlib import Path
4
5
  from typing import Dict, Optional, Union
5
6
 
6
7
  from ..._request import coerce_request, dump_request
@@ -15,6 +16,8 @@ from ....models.sandbox import (
15
16
  SandboxExposeParams,
16
17
  SandboxExposeResult,
17
18
  SandboxImageBuild,
19
+ SandboxImageBuildResolution,
20
+ SandboxImageSummary,
18
21
  SandboxImageBuildCreateResult,
19
22
  SandboxDockerImageReuseResult,
20
23
  SandboxImageBuildListParams,
@@ -63,6 +66,11 @@ from ....sandbox_common import (
63
66
  parse_json_response,
64
67
  should_retry_get,
65
68
  )
69
+ from ..sandboxes.image_resolution import (
70
+ image_build_name,
71
+ matching_image_build,
72
+ completed_image_id,
73
+ )
66
74
  from ..sandboxes.shared import (
67
75
  _build_sandbox_exposed_url,
68
76
  _copy_model,
@@ -70,6 +78,8 @@ from ..sandboxes.shared import (
70
78
  )
71
79
  from ..sandboxes.image_build import (
72
80
  IMAGE_BUILD_SOURCE_PLATFORM,
81
+ docker_build_context_fingerprint,
82
+ docker_image_digest,
73
83
  build_docker_image_from_dockerfile,
74
84
  is_terminal_image_build_status,
75
85
  make_temp_docker_tag,
@@ -459,6 +469,159 @@ class SandboxManager:
459
469
  )
460
470
  return SandboxImageListResponse(**payload)
461
471
 
472
+ async def find_ready_image(self, image_name: str) -> Optional[SandboxImageSummary]:
473
+ """Find an exact ready team image, including revisions awaiting backup."""
474
+ page = 1
475
+ while True:
476
+ response = await self.list_images(
477
+ SandboxImageListParams(
478
+ search=image_name, sources=["team"], page=page, limit=100
479
+ )
480
+ )
481
+ for image in response.images:
482
+ if image.image_name == image_name and (
483
+ image.uploaded or getattr(image, "ready", False)
484
+ ):
485
+ return image
486
+ if len(response.images) < 100:
487
+ return None
488
+ if response.total_count is not None and page * 100 >= response.total_count:
489
+ return None
490
+ page += 1
491
+
492
+ async def get_or_build_image(
493
+ self,
494
+ *,
495
+ context_path: Optional[Union[str, Path]] = None,
496
+ docker_image: Optional[str] = None,
497
+ image_name_prefix: str = "hb",
498
+ dockerfile: str = "Dockerfile",
499
+ platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
500
+ remote_full_context: bool = False,
501
+ expected_context_fingerprint: Optional[str] = None,
502
+ expected_image_digest: Optional[str] = None,
503
+ image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]] = None,
504
+ image_config_user: Optional[str] = None,
505
+ builder_cpus: Optional[int] = None,
506
+ builder_memory_mib: Optional[int] = None,
507
+ builder_scratch_mib: Optional[int] = None,
508
+ force_build: bool = False,
509
+ wait: bool = True,
510
+ poll_interval: float = 3.0,
511
+ wait_timeout: Optional[float] = 35 * 60,
512
+ upload_timeout: Optional[float] = 600,
513
+ temp_dir: Optional[str] = None,
514
+ ) -> SandboxImageBuildResolution:
515
+ """Reuse, join, or build content-derived remote Dockerfile/image inputs.
516
+
517
+ Supply exactly one of context_path or docker_image. Names include source
518
+ contents, platform and image initialization overrides. force_build skips
519
+ ready-image lookup, but joins matching active builds and retains builder
520
+ layer/artifact caches. Canceling polling never cancels the backend build.
521
+ wait_timeout applies to this caller's polling, independently of uploads.
522
+ This composes existing APIs; lookup plus creation is not server-atomic.
523
+ """
524
+ platform = platform.strip().lower()
525
+ if platform != "linux/amd64":
526
+ raise ValueError("Image builds require platform='linux/amd64'")
527
+ if (context_path is None) == (docker_image is None):
528
+ raise ValueError("Supply exactly one of context_path or docker_image")
529
+ if context_path is not None:
530
+ if expected_image_digest is not None:
531
+ raise ValueError("expected_image_digest requires docker_image")
532
+ fingerprint = expected_context_fingerprint
533
+ if fingerprint is None:
534
+ fingerprint = await _run_blocking(
535
+ docker_build_context_fingerprint,
536
+ context_path,
537
+ dockerfile=dockerfile,
538
+ force_full_context=remote_full_context,
539
+ )
540
+ source = "dockerfile"
541
+ input_format = "dockerfile_context_manifest_v1"
542
+ else:
543
+ if (
544
+ expected_context_fingerprint is not None
545
+ or remote_full_context
546
+ or dockerfile != "Dockerfile"
547
+ ):
548
+ raise ValueError("Dockerfile context options require context_path")
549
+ fingerprint = expected_image_digest
550
+ if fingerprint is None:
551
+ fingerprint = await _run_blocking(
552
+ docker_image_digest, docker_image, platform=platform
553
+ )
554
+ source = "prebuilt"
555
+ input_format = "docker_image_manifest_v1"
556
+ image_name = image_build_name(
557
+ source=source,
558
+ fingerprint=fingerprint,
559
+ name_prefix=image_name_prefix,
560
+ platform=platform,
561
+ image_init=image_init,
562
+ image_config_user=image_config_user,
563
+ )
564
+ if not force_build:
565
+ image = await self.find_ready_image(image_name)
566
+ if image is not None:
567
+ return SandboxImageBuildResolution(
568
+ outcome="reused",
569
+ image_name=image_name,
570
+ image_id=image.id,
571
+ )
572
+ common = dict(
573
+ image_name=image_name,
574
+ platform=platform,
575
+ image_init=image_init,
576
+ image_config_user=image_config_user,
577
+ builder_cpus=builder_cpus,
578
+ builder_memory_mib=builder_memory_mib,
579
+ builder_scratch_mib=builder_scratch_mib,
580
+ wait=False,
581
+ upload_timeout=upload_timeout,
582
+ temp_dir=temp_dir,
583
+ )
584
+ common = {
585
+ key: value
586
+ for key, value in common.items()
587
+ if not (key.startswith("builder_") and value is None)
588
+ }
589
+ outcome = "created"
590
+ try:
591
+ if context_path is not None:
592
+ build = await self.build_image_from_dockerfile(
593
+ context_path=context_path,
594
+ dockerfile=dockerfile,
595
+ remote=True,
596
+ remote_full_context=remote_full_context,
597
+ expected_context_fingerprint=fingerprint,
598
+ **common,
599
+ )
600
+ else:
601
+ build = await self.build_image_from_docker_image(
602
+ docker_image=docker_image,
603
+ expected_image_digest=fingerprint,
604
+ **common,
605
+ )
606
+ except HyperbrowserError as error:
607
+ existing = matching_image_build(error, image_name, input_format)
608
+ if existing is None:
609
+ raise
610
+ build = existing
611
+ outcome = "joined"
612
+ if wait and build.status != "completed":
613
+ build = await self.wait_for_image_build(
614
+ build.id,
615
+ poll_interval=poll_interval,
616
+ timeout=wait_timeout,
617
+ )
618
+ return SandboxImageBuildResolution(
619
+ outcome=outcome,
620
+ image_name=image_name,
621
+ image_id=completed_image_id(build),
622
+ build=build,
623
+ )
624
+
462
625
  async def list_snapshots(
463
626
  self,
464
627
  params: Optional[
@@ -582,6 +745,7 @@ class SandboxManager:
582
745
  *,
583
746
  docker_image: str,
584
747
  image_name: str,
748
+ expected_image_digest: Optional[str] = None,
585
749
  platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
586
750
  image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]] = None,
587
751
  image_config_user: Optional[str] = None,
@@ -600,6 +764,14 @@ class SandboxManager:
600
764
  platform=platform,
601
765
  )
602
766
  try:
767
+ if (
768
+ expected_image_digest is not None
769
+ and source.image_digest != expected_image_digest.lower()
770
+ ):
771
+ raise RuntimeError(
772
+ "Docker image changed after its cache identity was computed. "
773
+ "Retry with a fresh image digest."
774
+ )
603
775
  explicit_image_init = (
604
776
  coerce_request(image_init, SandboxImageInit, name="image_init")
605
777
  if image_init is not None
@@ -65,7 +65,6 @@ _RESERVED_IMAGE_INIT_ENV_KEYS = {
65
65
  "USER",
66
66
  "LOGNAME",
67
67
  "SHELL",
68
- "PATH",
69
68
  "PWD",
70
69
  "DISPLAY",
71
70
  }
@@ -443,6 +442,27 @@ def package_docker_build_context_manifest(
443
442
  raise
444
443
 
445
444
 
445
+ def docker_image_digest(
446
+ docker_image: str, *, platform: str = IMAGE_BUILD_SOURCE_PLATFORM
447
+ ) -> str:
448
+ """Inspect platform identity with Docker API 1.49+, without temporary resources."""
449
+ try:
450
+ inspection = _inspect_docker_image(docker_image, platform)
451
+ except RuntimeError as error:
452
+ message = str(error)
453
+ if (
454
+ '"--platform" requires API version' in message
455
+ or "unknown flag: --platform" in message
456
+ ):
457
+ raise RuntimeError(
458
+ "Local Docker image imports require a Docker CLI and Engine "
459
+ "supporting API 1.49 or newer (Docker 28.1+). Upgrade Docker "
460
+ "or remove an older DOCKER_API_VERSION override."
461
+ ) from error
462
+ raise
463
+ return _normalize_sha256_digest(inspection.get("Id"))
464
+
465
+
446
466
  def prepare_docker_image_manifest_source(
447
467
  docker_image: str,
448
468
  *,
@@ -0,0 +1,103 @@
1
+ """Shared identity and validation for opt-in image resolution."""
2
+
3
+ import hashlib
4
+ import json
5
+ import re
6
+ from collections.abc import Mapping
7
+ from typing import Literal, Optional, Union
8
+
9
+ from ....exceptions import HyperbrowserError
10
+ from ....models.sandbox import SandboxImageBuild, SandboxImageInit
11
+ from ....types import SandboxImageInit as SandboxImageInitDict
12
+ from ..._request import coerce_request
13
+
14
+
15
+ def image_build_name(
16
+ *,
17
+ source: Literal["dockerfile", "prebuilt"],
18
+ fingerprint: str,
19
+ name_prefix: str = "hb",
20
+ platform: str = "linux/amd64",
21
+ image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]] = None,
22
+ image_config_user: Optional[str] = None,
23
+ ) -> str:
24
+ """Name an immutable input identity without packaging or uploading it.
25
+
26
+ Fingerprints identify effective Dockerfile contexts or platform-specific
27
+ Docker image digests. Builder resources and wait policies do not change the
28
+ image contents and are excluded. Mutable external inputs (base tags, network
29
+ downloads) require an explicit force_build to request another build.
30
+ """
31
+ platform = platform.strip().lower()
32
+ if not re.fullmatch(r"[a-z0-9]+/[a-z0-9]+(?:/[a-z0-9]+)?", platform):
33
+ raise ValueError("platform must be an OCI platform such as 'linux/amd64'")
34
+ if not re.fullmatch(r"[A-Za-z0-9_-]{1,21}", name_prefix):
35
+ raise ValueError("image_name_prefix must be 1-21 letters, digits, '_' or '-'")
36
+ if source == "prebuilt":
37
+ fingerprint = fingerprint.lower()
38
+ if not re.fullmatch(r"sha256:[0-9a-f]{64}", fingerprint):
39
+ raise ValueError("expected_image_digest must be a sha256: Docker digest")
40
+ payload = "docker_image\0{}\0platform\0{}".format(fingerprint, platform)
41
+ elif source == "dockerfile":
42
+ if not re.fullmatch(r"[0-9a-f]{64}", fingerprint):
43
+ raise ValueError(
44
+ "expected_context_fingerprint must be a SHA-256 hex digest"
45
+ )
46
+ payload = "dockerfile-context-v3\0{}\0platform\0{}".format(
47
+ fingerprint, platform
48
+ )
49
+ else:
50
+ raise ValueError("source must be 'dockerfile' or 'prebuilt'")
51
+ options = {}
52
+ if image_init is not None:
53
+ normalized = coerce_request(image_init, SandboxImageInit, name="image_init")
54
+ initialization = normalized.model_dump(by_alias=True, exclude_none=True)
55
+ if initialization:
56
+ options["imageInit"] = initialization
57
+ if image_config_user is not None:
58
+ options["imageConfigUser"] = image_config_user.strip()
59
+ if options:
60
+ payload += "\0options\0" + json.dumps(
61
+ options, sort_keys=True, separators=(",", ":")
62
+ )
63
+ digest = hashlib.blake2b(payload.encode(), digest_size=8).hexdigest()
64
+ name = "{}__{}__{}__{}".format(
65
+ name_prefix, source, digest, platform.replace("/", "-")
66
+ )
67
+ if len(name) > 64:
68
+ raise ValueError(
69
+ "image_name_prefix and platform produce a name longer than 64 characters"
70
+ )
71
+ return name
72
+
73
+
74
+ def matching_image_build(
75
+ error: HyperbrowserError, image_name: str, input_format: str
76
+ ) -> Optional[SandboxImageBuild]:
77
+ if error.status_code != 409 or error.code != "image_build_in_progress":
78
+ return None
79
+ if not isinstance(error.details, Mapping):
80
+ return None
81
+ data = error.details.get("build")
82
+ if not isinstance(data, Mapping):
83
+ return None
84
+ metadata = data.get("metadata")
85
+ if not isinstance(metadata, Mapping) or metadata.get("inputFormat") != input_format:
86
+ return None
87
+ if metadata.get("sourcePlatform", "linux/amd64") != "linux/amd64":
88
+ return None
89
+ try:
90
+ build = SandboxImageBuild(**dict(data))
91
+ except (TypeError, ValueError):
92
+ return None
93
+ if not build.id or build.image_name != image_name:
94
+ return None
95
+ return build
96
+
97
+
98
+ def completed_image_id(build: SandboxImageBuild) -> Optional[str]:
99
+ if build.status != "completed":
100
+ return None
101
+ if not build.image_id:
102
+ raise RuntimeError("Completed image build did not return an image ID")
103
+ return build.image_id
@@ -1,4 +1,5 @@
1
1
  import time
2
+ from pathlib import Path
2
3
  from typing import Dict, Optional, Union
3
4
 
4
5
  from ..._request import coerce_request, dump_request
@@ -13,6 +14,8 @@ from ....models.sandbox import (
13
14
  SandboxExposeParams,
14
15
  SandboxExposeResult,
15
16
  SandboxImageBuild,
17
+ SandboxImageBuildResolution,
18
+ SandboxImageSummary,
16
19
  SandboxImageBuildCreateResult,
17
20
  SandboxDockerImageReuseResult,
18
21
  SandboxImageBuildListParams,
@@ -61,6 +64,11 @@ from ....sandbox_common import (
61
64
  parse_json_response,
62
65
  should_retry_get,
63
66
  )
67
+ from ..sandboxes.image_resolution import (
68
+ image_build_name,
69
+ matching_image_build,
70
+ completed_image_id,
71
+ )
64
72
  from ..sandboxes.shared import (
65
73
  _build_sandbox_exposed_url,
66
74
  _copy_model,
@@ -68,6 +76,8 @@ from ..sandboxes.shared import (
68
76
  )
69
77
  from ..sandboxes.image_build import (
70
78
  IMAGE_BUILD_SOURCE_PLATFORM,
79
+ docker_build_context_fingerprint,
80
+ docker_image_digest,
71
81
  build_docker_image_from_dockerfile,
72
82
  is_terminal_image_build_status,
73
83
  make_temp_docker_tag,
@@ -451,6 +461,156 @@ class SandboxManager:
451
461
  )
452
462
  return SandboxImageListResponse(**payload)
453
463
 
464
+ def find_ready_image(self, image_name: str) -> Optional[SandboxImageSummary]:
465
+ """Find an exact ready team image, including revisions awaiting backup."""
466
+ page = 1
467
+ while True:
468
+ response = self.list_images(
469
+ SandboxImageListParams(
470
+ search=image_name, sources=["team"], page=page, limit=100
471
+ )
472
+ )
473
+ for image in response.images:
474
+ if image.image_name == image_name and (
475
+ image.uploaded or getattr(image, "ready", False)
476
+ ):
477
+ return image
478
+ if len(response.images) < 100:
479
+ return None
480
+ if response.total_count is not None and page * 100 >= response.total_count:
481
+ return None
482
+ page += 1
483
+
484
+ def get_or_build_image(
485
+ self,
486
+ *,
487
+ context_path: Optional[Union[str, Path]] = None,
488
+ docker_image: Optional[str] = None,
489
+ image_name_prefix: str = "hb",
490
+ dockerfile: str = "Dockerfile",
491
+ platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
492
+ remote_full_context: bool = False,
493
+ expected_context_fingerprint: Optional[str] = None,
494
+ expected_image_digest: Optional[str] = None,
495
+ image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]] = None,
496
+ image_config_user: Optional[str] = None,
497
+ builder_cpus: Optional[int] = None,
498
+ builder_memory_mib: Optional[int] = None,
499
+ builder_scratch_mib: Optional[int] = None,
500
+ force_build: bool = False,
501
+ wait: bool = True,
502
+ poll_interval: float = 3.0,
503
+ wait_timeout: Optional[float] = 35 * 60,
504
+ upload_timeout: Optional[float] = 600,
505
+ temp_dir: Optional[str] = None,
506
+ ) -> SandboxImageBuildResolution:
507
+ """Reuse, join, or build content-derived remote Dockerfile/image inputs.
508
+
509
+ Supply exactly one of context_path or docker_image. Names include source
510
+ contents, platform and image initialization overrides. force_build skips
511
+ ready-image lookup, but joins matching active builds and retains builder
512
+ layer/artifact caches. Canceling polling never cancels the backend build.
513
+ wait_timeout applies to this caller's polling, independently of uploads.
514
+ This composes existing APIs; lookup plus creation is not server-atomic.
515
+ """
516
+ platform = platform.strip().lower()
517
+ if platform != "linux/amd64":
518
+ raise ValueError("Image builds require platform='linux/amd64'")
519
+ if (context_path is None) == (docker_image is None):
520
+ raise ValueError("Supply exactly one of context_path or docker_image")
521
+ if context_path is not None:
522
+ if expected_image_digest is not None:
523
+ raise ValueError("expected_image_digest requires docker_image")
524
+ fingerprint = expected_context_fingerprint
525
+ if fingerprint is None:
526
+ fingerprint = docker_build_context_fingerprint(
527
+ context_path,
528
+ dockerfile=dockerfile,
529
+ force_full_context=remote_full_context,
530
+ )
531
+ source = "dockerfile"
532
+ input_format = "dockerfile_context_manifest_v1"
533
+ else:
534
+ if (
535
+ expected_context_fingerprint is not None
536
+ or remote_full_context
537
+ or dockerfile != "Dockerfile"
538
+ ):
539
+ raise ValueError("Dockerfile context options require context_path")
540
+ fingerprint = expected_image_digest
541
+ if fingerprint is None:
542
+ fingerprint = docker_image_digest(docker_image, platform=platform)
543
+ source = "prebuilt"
544
+ input_format = "docker_image_manifest_v1"
545
+ image_name = image_build_name(
546
+ source=source,
547
+ fingerprint=fingerprint,
548
+ name_prefix=image_name_prefix,
549
+ platform=platform,
550
+ image_init=image_init,
551
+ image_config_user=image_config_user,
552
+ )
553
+ if not force_build:
554
+ image = self.find_ready_image(image_name)
555
+ if image is not None:
556
+ return SandboxImageBuildResolution(
557
+ outcome="reused",
558
+ image_name=image_name,
559
+ image_id=image.id,
560
+ )
561
+ common = dict(
562
+ image_name=image_name,
563
+ platform=platform,
564
+ image_init=image_init,
565
+ image_config_user=image_config_user,
566
+ builder_cpus=builder_cpus,
567
+ builder_memory_mib=builder_memory_mib,
568
+ builder_scratch_mib=builder_scratch_mib,
569
+ wait=False,
570
+ upload_timeout=upload_timeout,
571
+ temp_dir=temp_dir,
572
+ )
573
+ common = {
574
+ key: value
575
+ for key, value in common.items()
576
+ if not (key.startswith("builder_") and value is None)
577
+ }
578
+ outcome = "created"
579
+ try:
580
+ if context_path is not None:
581
+ build = self.build_image_from_dockerfile(
582
+ context_path=context_path,
583
+ dockerfile=dockerfile,
584
+ remote=True,
585
+ remote_full_context=remote_full_context,
586
+ expected_context_fingerprint=fingerprint,
587
+ **common,
588
+ )
589
+ else:
590
+ build = self.build_image_from_docker_image(
591
+ docker_image=docker_image,
592
+ expected_image_digest=fingerprint,
593
+ **common,
594
+ )
595
+ except HyperbrowserError as error:
596
+ existing = matching_image_build(error, image_name, input_format)
597
+ if existing is None:
598
+ raise
599
+ build = existing
600
+ outcome = "joined"
601
+ if wait and build.status != "completed":
602
+ build = self.wait_for_image_build(
603
+ build.id,
604
+ poll_interval=poll_interval,
605
+ timeout=wait_timeout,
606
+ )
607
+ return SandboxImageBuildResolution(
608
+ outcome=outcome,
609
+ image_name=image_name,
610
+ image_id=completed_image_id(build),
611
+ build=build,
612
+ )
613
+
454
614
  def list_snapshots(
455
615
  self,
456
616
  params: Optional[
@@ -574,6 +734,7 @@ class SandboxManager:
574
734
  *,
575
735
  docker_image: str,
576
736
  image_name: str,
737
+ expected_image_digest: Optional[str] = None,
577
738
  platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
578
739
  image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]] = None,
579
740
  image_config_user: Optional[str] = None,
@@ -591,6 +752,14 @@ class SandboxManager:
591
752
  platform=platform,
592
753
  )
593
754
  try:
755
+ if (
756
+ expected_image_digest is not None
757
+ and source.image_digest != expected_image_digest.lower()
758
+ ):
759
+ raise RuntimeError(
760
+ "Docker image changed after its cache identity was computed. "
761
+ "Retry with a fresh image digest."
762
+ )
594
763
  explicit_image_init = (
595
764
  coerce_request(image_init, SandboxImageInit, name="image_init")
596
765
  if image_init is not None
@@ -0,0 +1,5 @@
1
+ """Content-derived image names for build-or-wait workflows."""
2
+
3
+ from .client.managers.sandboxes.image_resolution import image_build_name
4
+
5
+ __all__ = ["image_build_name"]
@@ -364,6 +364,7 @@ from .sandbox import (
364
364
  CompleteSandboxImageBuildParams,
365
365
  SandboxImageBuildUpload,
366
366
  SandboxImageBuild,
367
+ SandboxImageBuildResolution,
367
368
  SandboxImageBuildCreateResult,
368
369
  SandboxDockerImageReuseResult,
369
370
  SandboxImageBuildListParams,
@@ -695,6 +696,7 @@ __all__ = [
695
696
  "CompleteSandboxImageBuildParams",
696
697
  "SandboxImageBuildUpload",
697
698
  "SandboxImageBuild",
699
+ "SandboxImageBuildResolution",
698
700
  "SandboxImageBuildCreateResult",
699
701
  "SandboxDockerImageReuseResult",
700
702
  "SandboxImageBuildListParams",
@@ -523,6 +523,19 @@ class SandboxImageBuild(SandboxBaseModel):
523
523
  return _parse_optional_datetime(value)
524
524
 
525
525
 
526
+ class SandboxImageBuildResolution(SandboxBaseModel):
527
+ """The result of resolving content-derived image inputs.
528
+
529
+ image_id is populated only for a ready image. With wait=False, build
530
+ identifies the submitted or joined build, which the caller can poll later.
531
+ """
532
+
533
+ outcome: Literal["reused", "joined", "created"]
534
+ image_name: str
535
+ image_id: Optional[str] = None
536
+ build: Optional[SandboxImageBuild] = None
537
+
538
+
526
539
  class SandboxImageBuildCreateResult(SandboxBaseModel):
527
540
  build: SandboxImageBuild
528
541
  upload: Optional[SandboxImageBuildUpload] = None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hyperbrowser"
3
- version = "1.8.0"
3
+ version = "1.9.1"
4
4
  description = "Python SDK for hyperbrowser"
5
5
  authors = ["Nikhil Shahi <nshahi1998@gmail.com>"]
6
6
  license = "MIT"
File without changes