hyperbrowser 1.8.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.
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/PKG-INFO +51 -1
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/README.md +50 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandbox.py +172 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/image_build.py +21 -0
- hyperbrowser-1.9.0/hyperbrowser/client/managers/sandboxes/image_resolution.py +103 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +169 -0
- hyperbrowser-1.9.0/hyperbrowser/image_builds.py +5 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/__init__.py +2 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/sandbox.py +13 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/pyproject.toml +1 -1
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/LICENSE +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/build_context.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/async_client.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/shared.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/sync.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/consts.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/session.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/sync.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/__init__.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/agents.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/sandbox.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/session.py +0 -0
- {hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/types/volume.py +0 -0
- {hyperbrowser-1.8.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.
|
|
3
|
+
Version: 1.9.0
|
|
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.
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandbox.py
RENAMED
|
@@ -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
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/image_build.py
RENAMED
|
@@ -443,6 +443,27 @@ def package_docker_build_context_manifest(
|
|
|
443
443
|
raise
|
|
444
444
|
|
|
445
445
|
|
|
446
|
+
def docker_image_digest(
|
|
447
|
+
docker_image: str, *, platform: str = IMAGE_BUILD_SOURCE_PLATFORM
|
|
448
|
+
) -> str:
|
|
449
|
+
"""Inspect platform identity with Docker API 1.49+, without temporary resources."""
|
|
450
|
+
try:
|
|
451
|
+
inspection = _inspect_docker_image(docker_image, platform)
|
|
452
|
+
except RuntimeError as error:
|
|
453
|
+
message = str(error)
|
|
454
|
+
if (
|
|
455
|
+
'"--platform" requires API version' in message
|
|
456
|
+
or "unknown flag: --platform" in message
|
|
457
|
+
):
|
|
458
|
+
raise RuntimeError(
|
|
459
|
+
"Local Docker image imports require a Docker CLI and Engine "
|
|
460
|
+
"supporting API 1.49 or newer (Docker 28.1+). Upgrade Docker "
|
|
461
|
+
"or remove an older DOCKER_API_VERSION override."
|
|
462
|
+
) from error
|
|
463
|
+
raise
|
|
464
|
+
return _normalize_sha256_digest(inspection.get("Id"))
|
|
465
|
+
|
|
466
|
+
|
|
446
467
|
def prepare_docker_image_manifest_source(
|
|
447
468
|
docker_image: str,
|
|
448
469
|
*,
|
|
@@ -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
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandbox.py
RENAMED
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/cua.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/crawl.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extension.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extract.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/profile.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/scrape.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/session.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/team.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/volume.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/crawl.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/process_output.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/crawl.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extension.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extract.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/profile.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/scrape.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/session.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/volume.py
RENAMED
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperbrowser-1.8.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|