hyperbrowser 1.7.0__tar.gz → 1.9.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 (123) hide show
  1. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/PKG-INFO +87 -1
  2. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/README.md +86 -0
  3. hyperbrowser-1.9.0/hyperbrowser/build_context.py +8 -0
  4. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandbox.py +178 -0
  5. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/image_build.py +218 -43
  6. hyperbrowser-1.9.0/hyperbrowser/client/managers/sandboxes/image_resolution.py +103 -0
  7. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +175 -0
  8. hyperbrowser-1.9.0/hyperbrowser/image_builds.py +5 -0
  9. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/__init__.py +2 -0
  10. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/sandbox.py +14 -0
  11. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/pyproject.toml +1 -1
  12. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/LICENSE +0 -0
  13. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/__init__.py +0 -0
  14. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/_request.py +0 -0
  15. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/async_client.py +0 -0
  16. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/base.py +0 -0
  17. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
  18. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
  19. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
  20. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
  21. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
  22. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
  23. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
  24. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
  25. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
  26. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
  27. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
  28. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
  29. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
  30. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
  31. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
  32. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
  33. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
  34. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
  35. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
  36. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
  37. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
  38. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
  39. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
  40. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
  41. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
  42. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
  43. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
  44. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
  45. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
  46. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
  47. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/shared.py +0 -0
  48. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
  49. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
  50. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
  51. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
  52. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
  53. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
  54. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
  55. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
  56. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
  57. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
  58. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
  59. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
  60. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
  61. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
  62. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
  63. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
  64. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
  65. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
  66. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
  67. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
  68. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
  69. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
  70. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
  71. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
  72. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
  73. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
  74. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/sync.py +0 -0
  75. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/config.py +0 -0
  76. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/exceptions.py +0 -0
  77. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/_parsers.py +0 -0
  78. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/browser_use.py +0 -0
  79. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
  80. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/cua.py +0 -0
  81. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
  82. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
  83. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
  84. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
  85. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
  86. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/computer_action.py +0 -0
  87. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/consts.py +0 -0
  88. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/crawl.py +0 -0
  89. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/extension.py +0 -0
  90. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/extract.py +0 -0
  91. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/profile.py +0 -0
  92. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/scrape.py +0 -0
  93. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/session.py +0 -0
  94. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/team.py +0 -0
  95. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/volume.py +0 -0
  96. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
  97. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/branding.py +0 -0
  98. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/common.py +0 -0
  99. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/crawl.py +0 -0
  100. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/fetch.py +0 -0
  101. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/search.py +0 -0
  102. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/py.typed +0 -0
  103. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/sandbox_common.py +0 -0
  104. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/__init__.py +0 -0
  105. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/anthropic.py +0 -0
  106. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/openai.py +0 -0
  107. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/schema.py +0 -0
  108. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/async_transport.py +0 -0
  109. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/base.py +0 -0
  110. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/sync.py +0 -0
  111. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/__init__.py +0 -0
  112. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/_json.py +0 -0
  113. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/agents.py +0 -0
  114. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/computer_action.py +0 -0
  115. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/crawl.py +0 -0
  116. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/extension.py +0 -0
  117. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/extract.py +0 -0
  118. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/profile.py +0 -0
  119. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/sandbox.py +0 -0
  120. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/scrape.py +0 -0
  121. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/session.py +0 -0
  122. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/volume.py +0 -0
  123. {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperbrowser
3
- Version: 1.7.0
3
+ Version: 1.9.0
4
4
  Summary: Python SDK for hyperbrowser
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -380,6 +380,92 @@ for event in connection.events():
380
380
  print(event)
381
381
  ```
382
382
 
383
+ ### Cache remote Dockerfile builds
384
+
385
+ Use the public context fingerprint when deriving a cache name. It uses the same
386
+ Dockerfile source selection and `.dockerignore` rules as remote packaging,
387
+ including file contents, modes, paths, and symlinks. It ignores timestamps and
388
+ does not compress or stage the context on disk.
389
+
390
+ ```python
391
+ from hyperbrowser.build_context import docker_build_context_fingerprint
392
+
393
+ fingerprint = docker_build_context_fingerprint("./app")
394
+ # Include build options such as platform and image_init in your cache key too.
395
+ image_name = f"app-{fingerprint[:32]}"
396
+ build = client.sandboxes.build_image_from_dockerfile(
397
+ context_path="./app",
398
+ image_name=image_name,
399
+ expected_context_fingerprint=fingerprint,
400
+ )
401
+ ```
402
+
403
+ If the archived inputs differ from the fingerprint, the SDK raises
404
+ `DockerBuildContextChangedError` before creating or uploading a build. Compute a
405
+ fresh fingerprint and repeat the lookup/build operation. Use the same `dockerfile`
406
+ and `force_full_context` selection when fingerprinting and building (the latter
407
+ is named `remote_full_context` on the build method). The expected fingerprint is
408
+ supported only for remote builds.
409
+
410
+ Fingerprinting streams included files and performs blocking I/O. Async callers
411
+ should use `await asyncio.to_thread(docker_build_context_fingerprint, "./app")`
412
+ (Python 3.9+) or an executor. It does not resolve mutable base-image tags or
413
+ network resources fetched by a Dockerfile; rebuild explicitly when those change.
414
+
415
+ Image listings distinguish `ready` from `uploaded`: a completed team image can
416
+ be ready to launch before its durability backup is uploaded. `ready` is `None`
417
+ when talking to an older server. Keep the returned image ID to pin that revision.
418
+
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
+
383
469
  ## License
384
470
 
385
471
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -352,6 +352,92 @@ for event in connection.events():
352
352
  print(event)
353
353
  ```
354
354
 
355
+ ### Cache remote Dockerfile builds
356
+
357
+ Use the public context fingerprint when deriving a cache name. It uses the same
358
+ Dockerfile source selection and `.dockerignore` rules as remote packaging,
359
+ including file contents, modes, paths, and symlinks. It ignores timestamps and
360
+ does not compress or stage the context on disk.
361
+
362
+ ```python
363
+ from hyperbrowser.build_context import docker_build_context_fingerprint
364
+
365
+ fingerprint = docker_build_context_fingerprint("./app")
366
+ # Include build options such as platform and image_init in your cache key too.
367
+ image_name = f"app-{fingerprint[:32]}"
368
+ build = client.sandboxes.build_image_from_dockerfile(
369
+ context_path="./app",
370
+ image_name=image_name,
371
+ expected_context_fingerprint=fingerprint,
372
+ )
373
+ ```
374
+
375
+ If the archived inputs differ from the fingerprint, the SDK raises
376
+ `DockerBuildContextChangedError` before creating or uploading a build. Compute a
377
+ fresh fingerprint and repeat the lookup/build operation. Use the same `dockerfile`
378
+ and `force_full_context` selection when fingerprinting and building (the latter
379
+ is named `remote_full_context` on the build method). The expected fingerprint is
380
+ supported only for remote builds.
381
+
382
+ Fingerprinting streams included files and performs blocking I/O. Async callers
383
+ should use `await asyncio.to_thread(docker_build_context_fingerprint, "./app")`
384
+ (Python 3.9+) or an executor. It does not resolve mutable base-image tags or
385
+ network resources fetched by a Dockerfile; rebuild explicitly when those change.
386
+
387
+ Image listings distinguish `ready` from `uploaded`: a completed team image can
388
+ be ready to launch before its durability backup is uploaded. `ready` is `None`
389
+ when talking to an older server. Keep the returned image ID to pin that revision.
390
+
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
+
355
441
  ## License
356
442
 
357
443
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,8 @@
1
+ """Public helpers for identifying inputs to remote Dockerfile builds."""
2
+
3
+ from .client.managers.sandboxes.image_build import (
4
+ DockerBuildContextChangedError,
5
+ docker_build_context_fingerprint,
6
+ )
7
+
8
+ __all__ = ["DockerBuildContextChangedError", "docker_build_context_fingerprint"]
@@ -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
@@ -725,6 +897,7 @@ class SandboxManager:
725
897
  dockerfile,
726
898
  platform: str,
727
899
  remote_full_context: bool,
900
+ expected_context_fingerprint: Optional[str],
728
901
  image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]],
729
902
  image_config_user: Optional[str],
730
903
  builder_cpus: Optional[int],
@@ -741,6 +914,7 @@ class SandboxManager:
741
914
  context_path,
742
915
  dockerfile=dockerfile,
743
916
  force_full_context=remote_full_context,
917
+ expected_context_fingerprint=expected_context_fingerprint,
744
918
  temp_dir=temp_dir,
745
919
  )
746
920
  build_id = None
@@ -803,6 +977,7 @@ class SandboxManager:
803
977
  dockerfile="Dockerfile",
804
978
  remote: bool = True,
805
979
  remote_full_context: bool = False,
980
+ expected_context_fingerprint: Optional[str] = None,
806
981
  docker_tag: Optional[str] = None,
807
982
  platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
808
983
  build_args: Optional[Dict[str, str]] = None,
@@ -829,6 +1004,7 @@ class SandboxManager:
829
1004
  dockerfile=dockerfile,
830
1005
  platform=platform,
831
1006
  remote_full_context=remote_full_context,
1007
+ expected_context_fingerprint=expected_context_fingerprint,
832
1008
  image_init=image_init,
833
1009
  image_config_user=image_config_user,
834
1010
  builder_cpus=builder_cpus,
@@ -840,6 +1016,8 @@ class SandboxManager:
840
1016
  temp_dir=temp_dir,
841
1017
  upload_timeout=upload_timeout,
842
1018
  )
1019
+ if expected_context_fingerprint is not None:
1020
+ raise ValueError("expected_context_fingerprint requires remote=True")
843
1021
  tag = docker_tag or make_temp_docker_tag()
844
1022
  remove_tag = docker_tag is None
845
1023
  try: