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.
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/PKG-INFO +87 -1
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/README.md +86 -0
- hyperbrowser-1.9.0/hyperbrowser/build_context.py +8 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandbox.py +178 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/image_build.py +218 -43
- hyperbrowser-1.9.0/hyperbrowser/client/managers/sandboxes/image_resolution.py +103 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +175 -0
- hyperbrowser-1.9.0/hyperbrowser/image_builds.py +5 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/__init__.py +2 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/sandbox.py +14 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/pyproject.toml +1 -1
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/LICENSE +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/async_client.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/volume.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/dockerignore.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/process_output.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sandboxes/shared.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/volume.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/client/sync.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/cua.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/jev_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/agents/meta_computer_use.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/consts.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/session.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/volume.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/transport/sync.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/__init__.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/agents.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/sandbox.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/session.py +0 -0
- {hyperbrowser-1.7.0 → hyperbrowser-1.9.0}/hyperbrowser/types/volume.py +0 -0
- {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.
|
|
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"]
|
{hyperbrowser-1.7.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
|
|
@@ -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:
|