hyperbrowser 1.2.2__tar.gz → 1.3.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.2.2 → hyperbrowser-1.3.0}/PKG-INFO +2 -1
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/README.md +1 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/sandbox.py +205 -22
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/volume.py +8 -0
- hyperbrowser-1.3.0/hyperbrowser/client/managers/sandboxes/dockerfile_analysis.py +293 -0
- hyperbrowser-1.3.0/hyperbrowser/client/managers/sandboxes/dockerignore.py +254 -0
- hyperbrowser-1.3.0/hyperbrowser/client/managers/sandboxes/image_build.py +1484 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/sandbox.py +198 -21
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/volume.py +8 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/__init__.py +24 -1
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/agents/cua.py +15 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/sandbox.py +81 -2
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/volume.py +6 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/__init__.py +14 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/agents.py +8 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/sandbox.py +64 -1
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/pyproject.toml +1 -1
- hyperbrowser-1.2.2/hyperbrowser/client/managers/sandboxes/image_build.py +0 -375
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/LICENSE +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/_request.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/async_client.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/base.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/agents/cua.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/computer_action.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/crawl.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/extension.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/extract.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/profile.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/scrape.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/session.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/team.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/web/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/web/crawl.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sandboxes/shared.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/agents/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/agents/browser_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/agents/cua.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/computer_action.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/crawl.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/extension.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/extract.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/profile.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/sandboxes/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_processes.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_terminal.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_transport.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/scrape.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/session.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/team.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/web/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/web/batch_fetch.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/sync_manager/web/crawl.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/sync.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/config.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/exceptions.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/_parsers.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/agents/browser_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/agents/claude_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/agents/gemini_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/agents/grok_computer_use.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/agents/hyper_agent.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/computer_action.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/consts.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/crawl.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/extension.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/extract.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/profile.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/scrape.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/session.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/team.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/web/batch_fetch.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/web/branding.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/web/common.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/web/crawl.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/web/fetch.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/models/web/search.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/py.typed +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/sandbox_common.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/tools/__init__.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/tools/anthropic.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/tools/openai.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/tools/schema.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/transport/async_transport.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/transport/base.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/transport/sync.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/_json.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/computer_action.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/crawl.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/extension.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/extract.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/profile.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/scrape.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/session.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/types/volume.py +0 -0
- {hyperbrowser-1.2.2 → hyperbrowser-1.3.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.3.0
|
|
4
4
|
Summary: Python SDK for hyperbrowser
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -250,6 +250,7 @@ sandbox = client.sandboxes.create(
|
|
|
250
250
|
)
|
|
251
251
|
|
|
252
252
|
sandbox.stop()
|
|
253
|
+
client.volumes.delete(same_volume.id)
|
|
253
254
|
client.close()
|
|
254
255
|
```
|
|
255
256
|
|
{hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/sandbox.py
RENAMED
|
@@ -9,12 +9,14 @@ from ....models.sandbox import (
|
|
|
9
9
|
CompleteSandboxImageBuildParams,
|
|
10
10
|
CreateSandboxParams,
|
|
11
11
|
CreateSandboxImageBuildParams,
|
|
12
|
+
ReuseSandboxDockerImageParams,
|
|
12
13
|
SandboxDetail,
|
|
13
14
|
SandboxExecParams,
|
|
14
15
|
SandboxExposeParams,
|
|
15
16
|
SandboxExposeResult,
|
|
16
17
|
SandboxImageBuild,
|
|
17
18
|
SandboxImageBuildCreateResult,
|
|
19
|
+
SandboxDockerImageReuseResult,
|
|
18
20
|
SandboxImageBuildListParams,
|
|
19
21
|
SandboxImageBuildListResponse,
|
|
20
22
|
SandboxImageListParams,
|
|
@@ -39,6 +41,7 @@ from ....models.session import BasicResponse
|
|
|
39
41
|
from ....types import (
|
|
40
42
|
CompleteSandboxImageBuildParams as CompleteSandboxImageBuildParamsDict,
|
|
41
43
|
CreateSandboxImageBuildParams as CreateSandboxImageBuildParamsDict,
|
|
44
|
+
ReuseSandboxDockerImageParams as ReuseSandboxDockerImageParamsDict,
|
|
42
45
|
CreateSandboxParams as CreateSandboxParamsDict,
|
|
43
46
|
SandboxExecParams as SandboxExecParamsDict,
|
|
44
47
|
SandboxExposeParams as SandboxExposeParamsDict,
|
|
@@ -67,9 +70,12 @@ from ..sandboxes.image_build import (
|
|
|
67
70
|
build_docker_image_from_dockerfile,
|
|
68
71
|
is_terminal_image_build_status,
|
|
69
72
|
make_temp_docker_tag,
|
|
70
|
-
|
|
73
|
+
merge_image_init,
|
|
74
|
+
package_docker_build_context_manifest,
|
|
75
|
+
package_docker_image_manifest,
|
|
76
|
+
prepare_docker_image_manifest_source,
|
|
71
77
|
remove_docker_image,
|
|
72
|
-
|
|
78
|
+
upload_missing_image_build_artifacts,
|
|
73
79
|
)
|
|
74
80
|
from .sandboxes.sandbox_files import (
|
|
75
81
|
DEFAULT_WATCH_TIMEOUT_MS,
|
|
@@ -494,6 +500,20 @@ class SandboxManager:
|
|
|
494
500
|
payload = await self._request("GET", f"/images/builds/{build_id}")
|
|
495
501
|
return SandboxImageBuild(**payload["build"])
|
|
496
502
|
|
|
503
|
+
async def reuse_docker_image(
|
|
504
|
+
self,
|
|
505
|
+
params: Union[
|
|
506
|
+
ReuseSandboxDockerImageParamsDict,
|
|
507
|
+
ReuseSandboxDockerImageParams,
|
|
508
|
+
],
|
|
509
|
+
) -> SandboxDockerImageReuseResult:
|
|
510
|
+
payload = await self._request(
|
|
511
|
+
"POST",
|
|
512
|
+
"/images/builds/reuse",
|
|
513
|
+
data=dump_request(params, ReuseSandboxDockerImageParams),
|
|
514
|
+
)
|
|
515
|
+
return SandboxDockerImageReuseResult(**payload)
|
|
516
|
+
|
|
497
517
|
async def complete_image_build(
|
|
498
518
|
self,
|
|
499
519
|
build_id: str,
|
|
@@ -566,18 +586,158 @@ class SandboxManager:
|
|
|
566
586
|
temp_dir: Optional[str] = None,
|
|
567
587
|
upload_timeout: Optional[float] = None,
|
|
568
588
|
) -> SandboxImageBuild:
|
|
569
|
-
|
|
570
|
-
|
|
589
|
+
source = await _run_blocking(
|
|
590
|
+
prepare_docker_image_manifest_source,
|
|
571
591
|
docker_image,
|
|
572
592
|
platform=platform,
|
|
593
|
+
)
|
|
594
|
+
try:
|
|
595
|
+
explicit_image_init = (
|
|
596
|
+
coerce_request(image_init, SandboxImageInit, name="image_init")
|
|
597
|
+
if image_init is not None
|
|
598
|
+
else None
|
|
599
|
+
)
|
|
600
|
+
normalized_image_init = merge_image_init(
|
|
601
|
+
source.image_init,
|
|
602
|
+
explicit_image_init,
|
|
603
|
+
)
|
|
604
|
+
normalized_image_config_user = (
|
|
605
|
+
image_config_user
|
|
606
|
+
if image_config_user is not None
|
|
607
|
+
else source.image_config_user
|
|
608
|
+
)
|
|
609
|
+
try:
|
|
610
|
+
reused = await self.reuse_docker_image(
|
|
611
|
+
ReuseSandboxDockerImageParams(
|
|
612
|
+
image_name=image_name,
|
|
613
|
+
source_image_digest=source.image_digest,
|
|
614
|
+
source_platform=platform,
|
|
615
|
+
image_config_user=normalized_image_config_user,
|
|
616
|
+
image_init=normalized_image_init,
|
|
617
|
+
)
|
|
618
|
+
)
|
|
619
|
+
except HyperbrowserError as error:
|
|
620
|
+
if error.status_code != 404:
|
|
621
|
+
raise
|
|
622
|
+
reused = None
|
|
623
|
+
if reused is not None and reused.hit:
|
|
624
|
+
if reused.build is None:
|
|
625
|
+
raise RuntimeError(
|
|
626
|
+
"exact image cache response is missing its completed build"
|
|
627
|
+
)
|
|
628
|
+
return reused.build
|
|
629
|
+
|
|
630
|
+
packaged = await _run_blocking(
|
|
631
|
+
package_docker_image_manifest,
|
|
632
|
+
docker_image,
|
|
633
|
+
source.image_digest,
|
|
634
|
+
source.config,
|
|
635
|
+
platform=platform,
|
|
636
|
+
temp_dir=temp_dir,
|
|
637
|
+
)
|
|
638
|
+
build_id = None
|
|
639
|
+
build_started = False
|
|
640
|
+
try:
|
|
641
|
+
artifact = packaged.artifact
|
|
642
|
+
create_result = await self.create_image_build(
|
|
643
|
+
CreateSandboxImageBuildParams(
|
|
644
|
+
image_name=image_name,
|
|
645
|
+
input_sha256=artifact.sha256_hex,
|
|
646
|
+
input_size_bytes=artifact.size_bytes,
|
|
647
|
+
input_format=artifact.input_format,
|
|
648
|
+
source_platform=artifact.source_platform,
|
|
649
|
+
image_config_user=normalized_image_config_user,
|
|
650
|
+
image_init=normalized_image_init,
|
|
651
|
+
docker_image_manifest=packaged.manifest,
|
|
652
|
+
)
|
|
653
|
+
)
|
|
654
|
+
build_id = create_result.build.id
|
|
655
|
+
await _run_blocking(
|
|
656
|
+
upload_missing_image_build_artifacts,
|
|
657
|
+
create_result.uploads,
|
|
658
|
+
packaged.layers,
|
|
659
|
+
label="Docker image layer",
|
|
660
|
+
timeout=upload_timeout,
|
|
661
|
+
)
|
|
662
|
+
build = await self._complete_image_build_resilient(
|
|
663
|
+
build_id,
|
|
664
|
+
artifact,
|
|
665
|
+
)
|
|
666
|
+
build_started = True
|
|
667
|
+
if wait:
|
|
668
|
+
return await self.wait_for_image_build(
|
|
669
|
+
build.id,
|
|
670
|
+
poll_interval=poll_interval,
|
|
671
|
+
timeout=wait_timeout,
|
|
672
|
+
)
|
|
673
|
+
return build
|
|
674
|
+
except Exception:
|
|
675
|
+
if build_id is not None and not build_started:
|
|
676
|
+
try:
|
|
677
|
+
await self.cancel_image_build(build_id)
|
|
678
|
+
except Exception:
|
|
679
|
+
pass
|
|
680
|
+
raise
|
|
681
|
+
finally:
|
|
682
|
+
await _run_blocking(packaged.cleanup)
|
|
683
|
+
finally:
|
|
684
|
+
await _run_blocking(source.cleanup)
|
|
685
|
+
|
|
686
|
+
async def _complete_image_build_resilient(
|
|
687
|
+
self,
|
|
688
|
+
build_id: str,
|
|
689
|
+
artifact,
|
|
690
|
+
) -> SandboxImageBuild:
|
|
691
|
+
params = CompleteSandboxImageBuildParams(
|
|
692
|
+
input_sha256=artifact.sha256_hex,
|
|
693
|
+
input_size_bytes=artifact.size_bytes,
|
|
694
|
+
input_format=artifact.input_format,
|
|
695
|
+
)
|
|
696
|
+
try:
|
|
697
|
+
return await self.complete_image_build(build_id, params)
|
|
698
|
+
except HyperbrowserError as exc:
|
|
699
|
+
if exc.status_code != 409:
|
|
700
|
+
raise
|
|
701
|
+
if "already in progress" in str(exc).lower():
|
|
702
|
+
return await self.get_image_build(build_id)
|
|
703
|
+
current = await self.get_image_build(build_id)
|
|
704
|
+
if current.status not in ("awaiting_upload", "upload_verified"):
|
|
705
|
+
raise
|
|
706
|
+
await asyncio.sleep(2)
|
|
707
|
+
return await self.complete_image_build(build_id, params)
|
|
708
|
+
|
|
709
|
+
async def _build_image_from_remote_dockerfile(
|
|
710
|
+
self,
|
|
711
|
+
*,
|
|
712
|
+
context_path,
|
|
713
|
+
image_name: str,
|
|
714
|
+
dockerfile,
|
|
715
|
+
platform: str,
|
|
716
|
+
remote_full_context: bool,
|
|
717
|
+
image_init: Optional[Union[SandboxImageInitDict, SandboxImageInit]],
|
|
718
|
+
image_config_user: Optional[str],
|
|
719
|
+
wait: bool,
|
|
720
|
+
poll_interval: float,
|
|
721
|
+
wait_timeout: Optional[float],
|
|
722
|
+
temp_dir: Optional[str],
|
|
723
|
+
upload_timeout: Optional[float],
|
|
724
|
+
) -> SandboxImageBuild:
|
|
725
|
+
packaged = await _run_blocking(
|
|
726
|
+
package_docker_build_context_manifest,
|
|
727
|
+
context_path,
|
|
728
|
+
dockerfile=dockerfile,
|
|
729
|
+
force_full_context=remote_full_context,
|
|
573
730
|
temp_dir=temp_dir,
|
|
574
731
|
)
|
|
732
|
+
build_id = None
|
|
733
|
+
build_started = False
|
|
575
734
|
try:
|
|
576
735
|
normalized_image_init = (
|
|
577
736
|
coerce_request(image_init, SandboxImageInit, name="image_init")
|
|
578
737
|
if image_init is not None
|
|
579
|
-
else
|
|
738
|
+
else None
|
|
580
739
|
)
|
|
740
|
+
artifact = packaged.artifact
|
|
581
741
|
create_result = await self.create_image_build(
|
|
582
742
|
CreateSandboxImageBuildParams(
|
|
583
743
|
image_name=image_name,
|
|
@@ -585,28 +745,22 @@ class SandboxManager:
|
|
|
585
745
|
input_size_bytes=artifact.size_bytes,
|
|
586
746
|
input_format=artifact.input_format,
|
|
587
747
|
source_platform=artifact.source_platform,
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if image_config_user is not None
|
|
591
|
-
else artifact.image_config_user
|
|
592
|
-
),
|
|
748
|
+
dockerfile_path=packaged.manifest.dockerfile_path,
|
|
749
|
+
image_config_user=image_config_user,
|
|
593
750
|
image_init=normalized_image_init,
|
|
751
|
+
context_manifest=packaged.manifest,
|
|
594
752
|
)
|
|
595
753
|
)
|
|
754
|
+
build_id = create_result.build.id
|
|
596
755
|
await _run_blocking(
|
|
597
|
-
|
|
598
|
-
create_result.
|
|
599
|
-
|
|
756
|
+
upload_missing_image_build_artifacts,
|
|
757
|
+
create_result.uploads,
|
|
758
|
+
packaged.bundles,
|
|
759
|
+
label="build context bundle",
|
|
600
760
|
timeout=upload_timeout,
|
|
601
761
|
)
|
|
602
|
-
build = await self.
|
|
603
|
-
|
|
604
|
-
CompleteSandboxImageBuildParams(
|
|
605
|
-
input_sha256=artifact.sha256_hex,
|
|
606
|
-
input_size_bytes=artifact.size_bytes,
|
|
607
|
-
input_format=artifact.input_format,
|
|
608
|
-
),
|
|
609
|
-
)
|
|
762
|
+
build = await self._complete_image_build_resilient(build_id, artifact)
|
|
763
|
+
build_started = True
|
|
610
764
|
if wait:
|
|
611
765
|
return await self.wait_for_image_build(
|
|
612
766
|
build.id,
|
|
@@ -614,8 +768,15 @@ class SandboxManager:
|
|
|
614
768
|
timeout=wait_timeout,
|
|
615
769
|
)
|
|
616
770
|
return build
|
|
771
|
+
except Exception:
|
|
772
|
+
if build_id is not None and not build_started:
|
|
773
|
+
try:
|
|
774
|
+
await self.cancel_image_build(build_id)
|
|
775
|
+
except Exception:
|
|
776
|
+
pass
|
|
777
|
+
raise
|
|
617
778
|
finally:
|
|
618
|
-
|
|
779
|
+
await _run_blocking(packaged.cleanup)
|
|
619
780
|
|
|
620
781
|
async def build_image_from_dockerfile(
|
|
621
782
|
self,
|
|
@@ -623,6 +784,8 @@ class SandboxManager:
|
|
|
623
784
|
context_path,
|
|
624
785
|
image_name: str,
|
|
625
786
|
dockerfile="Dockerfile",
|
|
787
|
+
remote: bool = True,
|
|
788
|
+
remote_full_context: bool = False,
|
|
626
789
|
docker_tag: Optional[str] = None,
|
|
627
790
|
platform: str = IMAGE_BUILD_SOURCE_PLATFORM,
|
|
628
791
|
build_args: Optional[Dict[str, str]] = None,
|
|
@@ -634,6 +797,26 @@ class SandboxManager:
|
|
|
634
797
|
temp_dir: Optional[str] = None,
|
|
635
798
|
upload_timeout: Optional[float] = None,
|
|
636
799
|
) -> SandboxImageBuild:
|
|
800
|
+
if remote:
|
|
801
|
+
if docker_tag is not None or build_args:
|
|
802
|
+
raise ValueError(
|
|
803
|
+
"docker_tag and build_args require remote=False; remote "
|
|
804
|
+
"Dockerfile builds send the build context to Hyperbrowser"
|
|
805
|
+
)
|
|
806
|
+
return await self._build_image_from_remote_dockerfile(
|
|
807
|
+
context_path=context_path,
|
|
808
|
+
image_name=image_name,
|
|
809
|
+
dockerfile=dockerfile,
|
|
810
|
+
platform=platform,
|
|
811
|
+
remote_full_context=remote_full_context,
|
|
812
|
+
image_init=image_init,
|
|
813
|
+
image_config_user=image_config_user,
|
|
814
|
+
wait=wait,
|
|
815
|
+
poll_interval=poll_interval,
|
|
816
|
+
wait_timeout=wait_timeout,
|
|
817
|
+
temp_dir=temp_dir,
|
|
818
|
+
upload_timeout=upload_timeout,
|
|
819
|
+
)
|
|
637
820
|
tag = docker_tag or make_temp_docker_tag()
|
|
638
821
|
remove_tag = docker_tag is None
|
|
639
822
|
try:
|
{hyperbrowser-1.2.2 → hyperbrowser-1.3.0}/hyperbrowser/client/managers/async_manager/volume.py
RENAMED
|
@@ -4,6 +4,7 @@ from hyperbrowser.client._request import dump_request
|
|
|
4
4
|
from hyperbrowser.models.volume import (
|
|
5
5
|
CreateVolumeParams,
|
|
6
6
|
Volume,
|
|
7
|
+
VolumeDeleteResult,
|
|
7
8
|
VolumeListParams,
|
|
8
9
|
VolumeListResponse,
|
|
9
10
|
)
|
|
@@ -43,3 +44,10 @@ class VolumeManager:
|
|
|
43
44
|
self._client._build_url(f"/volume/{volume_id}")
|
|
44
45
|
)
|
|
45
46
|
return Volume(**response.data)
|
|
47
|
+
|
|
48
|
+
async def delete(self, volume_id: str) -> VolumeDeleteResult:
|
|
49
|
+
"""Delete a volume by id or name. Ambiguous names and active mounts return 409."""
|
|
50
|
+
response = await self._client.transport.delete(
|
|
51
|
+
self._client._build_url(f"/volume/{volume_id}")
|
|
52
|
+
)
|
|
53
|
+
return VolumeDeleteResult(**response.data)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Conservative Dockerfile analysis for remote build context selection.
|
|
2
|
+
|
|
3
|
+
This module intentionally does not try to execute or fully reproduce the
|
|
4
|
+
BuildKit Dockerfile frontend. It recognizes the context-consuming syntax the
|
|
5
|
+
SDK can analyze safely and requests a full context for anything ambiguous.
|
|
6
|
+
That keeps sparse uploads an optimization rather than a correctness
|
|
7
|
+
requirement.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import shlex
|
|
13
|
+
from typing import Dict, List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_KNOWN_INSTRUCTIONS = frozenset(
|
|
17
|
+
{
|
|
18
|
+
"ADD",
|
|
19
|
+
"ARG",
|
|
20
|
+
"CMD",
|
|
21
|
+
"COPY",
|
|
22
|
+
"ENTRYPOINT",
|
|
23
|
+
"ENV",
|
|
24
|
+
"EXPOSE",
|
|
25
|
+
"FROM",
|
|
26
|
+
"HEALTHCHECK",
|
|
27
|
+
"LABEL",
|
|
28
|
+
"MAINTAINER",
|
|
29
|
+
"ONBUILD",
|
|
30
|
+
"RUN",
|
|
31
|
+
"SHELL",
|
|
32
|
+
"STOPSIGNAL",
|
|
33
|
+
"USER",
|
|
34
|
+
"VOLUME",
|
|
35
|
+
"WORKDIR",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
_KNOWN_RUN_FLAGS = frozenset({"device", "mount", "network", "security"})
|
|
39
|
+
_INSTRUCTION_PATTERN = re.compile(r"^([A-Za-z]+)(?:[ \t]+(.*))?$")
|
|
40
|
+
_DIRECTIVE_PATTERN = re.compile(
|
|
41
|
+
r"(?im)^\s*#\s*(?P<name>escape|syntax)\s*=\s*(?P<value>\S+)"
|
|
42
|
+
)
|
|
43
|
+
_FLAG_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
44
|
+
_SCP_GIT_SOURCE_PATTERN = re.compile(r"^git@[^:/\s]+:.+")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _AnalysisFallback(Exception):
|
|
48
|
+
def __init__(self, reason: str):
|
|
49
|
+
super().__init__(reason)
|
|
50
|
+
self.reason = reason
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class _DockerfileSourceAnalyzer:
|
|
54
|
+
def __init__(self, data: bytes):
|
|
55
|
+
self._data = data
|
|
56
|
+
|
|
57
|
+
def analyze(self) -> Tuple[List[List[str]], str]:
|
|
58
|
+
try:
|
|
59
|
+
text = self._data.decode("utf-8")
|
|
60
|
+
except UnicodeDecodeError:
|
|
61
|
+
return [], "dockerfile_parse_failed"
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
self._validate_parser_directives(text)
|
|
65
|
+
# Heredocs are valid BuildKit syntax, but treating the entire
|
|
66
|
+
# Dockerfile as full context is safer than partially parsing them.
|
|
67
|
+
if "<<" in text:
|
|
68
|
+
raise _AnalysisFallback("dockerfile_instruction_parse_failed")
|
|
69
|
+
logical_lines = self._logical_lines(text)
|
|
70
|
+
return self._analyze_lines(logical_lines), ""
|
|
71
|
+
except _AnalysisFallback as exc:
|
|
72
|
+
return [], exc.reason
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _validate_parser_directives(text: str) -> None:
|
|
76
|
+
for match in _DIRECTIVE_PATTERN.finditer(text):
|
|
77
|
+
name = match.group("name").lower()
|
|
78
|
+
value = match.group("value")
|
|
79
|
+
if name == "escape" and value != "\\":
|
|
80
|
+
raise _AnalysisFallback("dockerfile_parse_failed")
|
|
81
|
+
if name == "syntax" and not _is_official_dockerfile_frontend(value):
|
|
82
|
+
raise _AnalysisFallback("custom_dockerfile_frontend")
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def _logical_lines(text: str) -> List[str]:
|
|
86
|
+
logical_lines = []
|
|
87
|
+
parts: List[str] = []
|
|
88
|
+
for raw_line in text.splitlines():
|
|
89
|
+
stripped = raw_line.strip()
|
|
90
|
+
if not parts and (not stripped or stripped.startswith("#")):
|
|
91
|
+
continue
|
|
92
|
+
if parts and (not stripped or stripped.startswith("#")):
|
|
93
|
+
# BuildKit has nuanced rules for comments and empty lines in a
|
|
94
|
+
# continuation. Full context is the safe answer here.
|
|
95
|
+
raise _AnalysisFallback("dockerfile_parse_failed")
|
|
96
|
+
if stripped.endswith("\\"):
|
|
97
|
+
parts.append(stripped[:-1].rstrip())
|
|
98
|
+
continue
|
|
99
|
+
parts.append(stripped)
|
|
100
|
+
logical_lines.append(" ".join(parts))
|
|
101
|
+
parts = []
|
|
102
|
+
if parts:
|
|
103
|
+
raise _AnalysisFallback("dockerfile_parse_failed")
|
|
104
|
+
return logical_lines
|
|
105
|
+
|
|
106
|
+
def _analyze_lines(self, logical_lines: List[str]) -> List[List[str]]:
|
|
107
|
+
groups = []
|
|
108
|
+
for line in logical_lines:
|
|
109
|
+
instruction, body = _parse_instruction(line)
|
|
110
|
+
if instruction not in _KNOWN_INSTRUCTIONS:
|
|
111
|
+
raise _AnalysisFallback("dockerfile_instruction_parse_failed")
|
|
112
|
+
if instruction in ("COPY", "ADD"):
|
|
113
|
+
group = self._analyze_copy_or_add(instruction, body)
|
|
114
|
+
if group:
|
|
115
|
+
groups.append(group)
|
|
116
|
+
elif instruction == "RUN":
|
|
117
|
+
groups.extend(self._analyze_run(body))
|
|
118
|
+
return groups
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def _analyze_copy_or_add(instruction: str, body: str) -> List[str]:
|
|
122
|
+
try:
|
|
123
|
+
flags, values = _parse_copy_add_values(body)
|
|
124
|
+
except (ValueError, json.JSONDecodeError):
|
|
125
|
+
raise _AnalysisFallback("dockerfile_instruction_parse_failed")
|
|
126
|
+
|
|
127
|
+
if instruction == "COPY" and "from" in flags:
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
local_sources = []
|
|
131
|
+
for source in values[:-1]:
|
|
132
|
+
if "$" in source or "\x00" in source:
|
|
133
|
+
reason = (
|
|
134
|
+
"copy_source_requires_expansion"
|
|
135
|
+
if instruction == "COPY"
|
|
136
|
+
else "add_source_requires_expansion"
|
|
137
|
+
)
|
|
138
|
+
raise _AnalysisFallback(reason)
|
|
139
|
+
if "\\" in source:
|
|
140
|
+
# A backslash is a path character on Unix but a separator on
|
|
141
|
+
# Windows. Full context keeps selection platform-independent.
|
|
142
|
+
raise _AnalysisFallback("dockerfile_source_path")
|
|
143
|
+
if instruction == "ADD" and _is_remote_add_source(source):
|
|
144
|
+
continue
|
|
145
|
+
if _source_has_pattern(source):
|
|
146
|
+
# Python glob semantics differ from Go filepath.Match for
|
|
147
|
+
# dotfiles, character classes, and recursive patterns.
|
|
148
|
+
raise _AnalysisFallback("dockerfile_source_pattern")
|
|
149
|
+
local_sources.append(source)
|
|
150
|
+
return local_sources
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def _analyze_run(body: str) -> List[List[str]]:
|
|
154
|
+
try:
|
|
155
|
+
flags, _ = _split_leading_flags(body)
|
|
156
|
+
except ValueError:
|
|
157
|
+
raise _AnalysisFallback("run_mount_parse_failed")
|
|
158
|
+
|
|
159
|
+
groups = []
|
|
160
|
+
for name, value in flags:
|
|
161
|
+
if name not in _KNOWN_RUN_FLAGS:
|
|
162
|
+
raise _AnalysisFallback("dockerfile_instruction_parse_failed")
|
|
163
|
+
if name != "mount":
|
|
164
|
+
continue
|
|
165
|
+
if not value or any(character in value for character in ('"', "'")):
|
|
166
|
+
raise _AnalysisFallback("run_mount_parse_failed")
|
|
167
|
+
options = _parse_mount_options(value)
|
|
168
|
+
if options.get("type", "bind") != "bind" or options.get("from"):
|
|
169
|
+
continue
|
|
170
|
+
source = options.get("source") or options.get("src") or "."
|
|
171
|
+
if "$" in source or "\x00" in source:
|
|
172
|
+
raise _AnalysisFallback("run_bind_source_requires_expansion")
|
|
173
|
+
groups.append([source])
|
|
174
|
+
return groups
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def analyze_dockerfile_sources(data: bytes) -> Tuple[List[List[str]], str]:
|
|
178
|
+
"""Return local source groups or a reason to upload the full context."""
|
|
179
|
+
|
|
180
|
+
return _DockerfileSourceAnalyzer(data).analyze()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _parse_instruction(line: str) -> Tuple[str, str]:
|
|
184
|
+
match = _INSTRUCTION_PATTERN.fullmatch(line)
|
|
185
|
+
if match is None:
|
|
186
|
+
raise _AnalysisFallback("dockerfile_parse_failed")
|
|
187
|
+
return match.group(1).upper(), match.group(2) or ""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _parse_copy_add_values(body: str) -> Tuple[Dict[str, str], List[str]]:
|
|
191
|
+
parsed_flags, remaining = _split_leading_flags(body)
|
|
192
|
+
flags = dict(parsed_flags)
|
|
193
|
+
if remaining.startswith("["):
|
|
194
|
+
values = json.loads(remaining)
|
|
195
|
+
if not isinstance(values, list) or not all(
|
|
196
|
+
isinstance(value, str) for value in values
|
|
197
|
+
):
|
|
198
|
+
raise ValueError("invalid JSON COPY/ADD")
|
|
199
|
+
else:
|
|
200
|
+
if "\\" in remaining:
|
|
201
|
+
# BuildKit's shell-form COPY/ADD word splitting is not POSIX shlex.
|
|
202
|
+
# Backslash-containing forms use the full context rather than risk
|
|
203
|
+
# selecting a different set of sources.
|
|
204
|
+
raise ValueError("escaped shell COPY/ADD requires full context")
|
|
205
|
+
values = shlex.split(remaining, posix=True)
|
|
206
|
+
if len(values) < 2:
|
|
207
|
+
raise ValueError("COPY/ADD is missing source or destination")
|
|
208
|
+
return flags, values
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _split_leading_flags(body: str) -> Tuple[List[Tuple[str, str]], str]:
|
|
212
|
+
flags = []
|
|
213
|
+
remaining = body.lstrip()
|
|
214
|
+
while remaining.startswith("--"):
|
|
215
|
+
raw_token, remaining = _take_word(remaining)
|
|
216
|
+
decoded = shlex.split(raw_token, posix=True)
|
|
217
|
+
if len(decoded) != 1 or not decoded[0].startswith("--"):
|
|
218
|
+
raise ValueError("invalid Dockerfile instruction flag")
|
|
219
|
+
key, has_value, value = decoded[0][2:].partition("=")
|
|
220
|
+
key = key.lower()
|
|
221
|
+
if not _FLAG_NAME_PATTERN.fullmatch(key):
|
|
222
|
+
raise ValueError("invalid Dockerfile instruction flag")
|
|
223
|
+
if key == "mount" and any(character in raw_token for character in ('"', "'")):
|
|
224
|
+
# BuildKit parses mount values as CSV after Dockerfile word
|
|
225
|
+
# processing. Quoted CSV values are deliberately outside the
|
|
226
|
+
# subset implemented here.
|
|
227
|
+
raise ValueError("quoted RUN mount requires full context")
|
|
228
|
+
flags.append((key, value if has_value else ""))
|
|
229
|
+
remaining = remaining.lstrip()
|
|
230
|
+
return flags, remaining
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _take_word(value: str) -> Tuple[str, str]:
|
|
234
|
+
quote: Optional[str] = None
|
|
235
|
+
escaped = False
|
|
236
|
+
for index, character in enumerate(value):
|
|
237
|
+
if escaped:
|
|
238
|
+
escaped = False
|
|
239
|
+
continue
|
|
240
|
+
if character == "\\" and quote != "'":
|
|
241
|
+
escaped = True
|
|
242
|
+
continue
|
|
243
|
+
if quote is not None:
|
|
244
|
+
if character == quote:
|
|
245
|
+
quote = None
|
|
246
|
+
continue
|
|
247
|
+
if character in ('"', "'"):
|
|
248
|
+
quote = character
|
|
249
|
+
elif character.isspace():
|
|
250
|
+
return value[:index], value[index:]
|
|
251
|
+
if quote is not None or escaped:
|
|
252
|
+
raise ValueError("unterminated Dockerfile instruction word")
|
|
253
|
+
return value, ""
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _parse_mount_options(value: str) -> Dict[str, str]:
|
|
257
|
+
options = {}
|
|
258
|
+
for field in value.split(","):
|
|
259
|
+
key, has_value, option_value = field.partition("=")
|
|
260
|
+
key = key.strip().lower()
|
|
261
|
+
if not key:
|
|
262
|
+
raise _AnalysisFallback("run_mount_parse_failed")
|
|
263
|
+
options[key] = option_value.strip() if has_value else ""
|
|
264
|
+
return options
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _source_has_pattern(source: str) -> bool:
|
|
268
|
+
return any(character in source for character in ("*", "?", "["))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _is_official_dockerfile_frontend(reference: str) -> bool:
|
|
272
|
+
normalized = reference.strip()
|
|
273
|
+
if normalized.startswith("docker-image://"):
|
|
274
|
+
normalized = normalized[len("docker-image://") :]
|
|
275
|
+
normalized = normalized.split("@", 1)[0]
|
|
276
|
+
last_slash = normalized.rfind("/")
|
|
277
|
+
last_colon = normalized.rfind(":")
|
|
278
|
+
if last_colon > last_slash:
|
|
279
|
+
normalized = normalized[:last_colon]
|
|
280
|
+
return normalized in {
|
|
281
|
+
"docker/dockerfile",
|
|
282
|
+
"docker.io/docker/dockerfile",
|
|
283
|
+
"index.docker.io/docker/dockerfile",
|
|
284
|
+
"docker/dockerfile-upstream",
|
|
285
|
+
"docker.io/docker/dockerfile-upstream",
|
|
286
|
+
"index.docker.io/docker/dockerfile-upstream",
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _is_remote_add_source(source: str) -> bool:
|
|
291
|
+
if _SCP_GIT_SOURCE_PATTERN.match(source):
|
|
292
|
+
return True
|
|
293
|
+
return source.startswith(("http://", "https://", "git://", "ssh://"))
|