dh-cli 0.8.3__tar.gz → 0.8.4__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.
- {dh_cli-0.8.3 → dh_cli-0.8.4}/PKG-INFO +1 -1
- {dh_cli-0.8.3 → dh_cli-0.8.4}/pyproject.toml +1 -1
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/aws_batch.py +19 -1
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/submit.py +8 -1
- dh_cli-0.8.4/tests/batch/test_image_override.py +141 -0
- dh_cli-0.8.4/tests/batch/test_submit_image_validation.py +90 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/test_finalize_boltz_tar.py +6 -18
- {dh_cli-0.8.3 → dh_cli-0.8.4}/.gitignore +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/LICENSE +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/README.md +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/_identity.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/boltz.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/cancel.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/clean.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/embed_t5.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/finalize.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/list_jobs.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/local.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/logs.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/orca.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/protmpnn.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/retry.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/status.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/train.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/wait_for.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/fasta_utils.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/h5_utils.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/job_id.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/manifest.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/s3_transport.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/commands.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/cost_report.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/pricing.yaml +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/cloud_commands.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/codeartifact.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/api_client.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/auth.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/engine_commands.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/progress.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/ssh_config.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/studio_commands.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/github_commands.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/deploy.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/local.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/test.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/tf.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/users.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/main.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/utility_commands.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/warehouse.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/test_aws_batch_resources.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/test_submit_cpu_only.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/test_submit_merge.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/conftest.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/A_cache_write.json +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/B_cache_read.json +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/C_plain.json +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/E_service_role.json +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_build_report.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_classify_arn.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cli_exit_codes.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cost_calc.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cost_command.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cur_reconciliation.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_key_command.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_render_formats.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_resolve_base_model.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_s3_walker.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/__init__.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/conftest.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_identity.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_login.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_login_error_paths.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_login_security.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_logout.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_rotate.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_status.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/hz/test_init.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/hz/test_suites.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/hz/test_users.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/test_cloud_gcp.py +0 -0
- {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/test_finalize_protmpnn.py +0 -0
|
@@ -17,6 +17,11 @@ from dh_cli.batch.manifest import (
|
|
|
17
17
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
|
+
_ECR_IMAGE_RE = re.compile(
|
|
21
|
+
r"^\d{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com/[^:@\s]+"
|
|
22
|
+
r"(?::[^\s]+|@sha256:[a-f0-9]{64})?$"
|
|
23
|
+
)
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
class BatchError(Exception):
|
|
22
27
|
"""Error interacting with AWS Batch."""
|
|
@@ -165,14 +170,27 @@ class BatchClient:
|
|
|
165
170
|
create a new revision of the base definition with only the image
|
|
166
171
|
changed, preserving all other containerProperties (roles, volumes, etc.).
|
|
167
172
|
|
|
168
|
-
Returns the ARN of the new revision
|
|
173
|
+
Returns the ARN of the new revision, or the ARN of the latest existing
|
|
174
|
+
revision if its image already matches.
|
|
169
175
|
"""
|
|
176
|
+
if not _ECR_IMAGE_RE.match(image):
|
|
177
|
+
raise BatchError(
|
|
178
|
+
f"Image override must be a fully-qualified ECR URL "
|
|
179
|
+
f"(<account>.dkr.ecr.<region>.amazonaws.com/<repo>[:tag|@digest]); "
|
|
180
|
+
f"got: {image!r}"
|
|
181
|
+
)
|
|
182
|
+
|
|
170
183
|
resp = self.batch.describe_job_definitions(jobDefinitionName=base_definition, status="ACTIVE")
|
|
171
184
|
definitions = resp.get("jobDefinitions", [])
|
|
172
185
|
if not definitions:
|
|
173
186
|
raise BatchError(f"Job definition not found: {base_definition}")
|
|
174
187
|
|
|
175
188
|
latest = sorted(definitions, key=lambda d: d["revision"])[-1]
|
|
189
|
+
if latest["containerProperties"].get("image") == image:
|
|
190
|
+
arn = latest["jobDefinitionArn"]
|
|
191
|
+
logger.info(f"Reusing job definition {arn}; image already matches {image}")
|
|
192
|
+
return arn
|
|
193
|
+
|
|
176
194
|
container_props = latest["containerProperties"].copy()
|
|
177
195
|
container_props["image"] = image
|
|
178
196
|
|
|
@@ -4,7 +4,7 @@ import click
|
|
|
4
4
|
import yaml
|
|
5
5
|
from click.core import ParameterSource
|
|
6
6
|
|
|
7
|
-
from ..aws_batch import BatchClient, BatchError, resolve_dependency
|
|
7
|
+
from ..aws_batch import _ECR_IMAGE_RE, BatchClient, BatchError, resolve_dependency
|
|
8
8
|
from ..job_id import generate_job_id, get_aws_username
|
|
9
9
|
from ..manifest import (
|
|
10
10
|
BATCH_JOBS_BASE,
|
|
@@ -114,6 +114,13 @@ def submit(
|
|
|
114
114
|
job_retry = _pick("retry", retry)
|
|
115
115
|
job_timeout = _pick("timeout", timeout)
|
|
116
116
|
job_image = image or config.get("image")
|
|
117
|
+
if job_image and not _ECR_IMAGE_RE.match(job_image):
|
|
118
|
+
raise click.BadParameter(
|
|
119
|
+
f"--image must be a fully-qualified ECR URL "
|
|
120
|
+
f"(<account>.dkr.ecr.<region>.amazonaws.com/<repo>[:tag|@digest]); "
|
|
121
|
+
f"got: {job_image!r}",
|
|
122
|
+
param_hint="--image",
|
|
123
|
+
)
|
|
117
124
|
|
|
118
125
|
# Parse environment variables
|
|
119
126
|
job_env = dict(config.get("env", {}))
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Tests for BatchClient._register_image_override.
|
|
2
|
+
|
|
3
|
+
Pins two contracts:
|
|
4
|
+
|
|
5
|
+
1. Bare names (anything not a fully-qualified ECR URL) are rejected, so
|
|
6
|
+
typos like ``dh batch submit --image dayhoff-generic`` cannot drift the
|
|
7
|
+
floating ref by registering a revision pointing at Docker Hub.
|
|
8
|
+
2. When the latest active revision already carries the requested image,
|
|
9
|
+
no new revision is registered — the existing ARN is returned. Without
|
|
10
|
+
this, every override invocation bumps the JD revision counter.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from unittest.mock import MagicMock, patch
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_client():
|
|
19
|
+
with patch("dh_cli.batch.aws_batch.boto3") as mock_boto3:
|
|
20
|
+
mock_batch = MagicMock()
|
|
21
|
+
mock_logs = MagicMock()
|
|
22
|
+
mock_boto3.client.side_effect = [mock_batch, mock_logs]
|
|
23
|
+
|
|
24
|
+
from dh_cli.batch.aws_batch import BatchClient
|
|
25
|
+
|
|
26
|
+
return BatchClient(), mock_batch
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_ECR_URL = "123456789012.dkr.ecr.us-east-1.amazonaws.com/dayhoff-generic:abc123"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestImageValidation:
|
|
33
|
+
"""Bare image names must be rejected before any AWS call."""
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize(
|
|
36
|
+
"bad_image",
|
|
37
|
+
[
|
|
38
|
+
"dayhoff-generic",
|
|
39
|
+
"dayhoff-generic:latest",
|
|
40
|
+
"docker.io/library/python:3.12",
|
|
41
|
+
"ghcr.io/foo/bar:1.0",
|
|
42
|
+
"123456789012.dkr.ecr.us-east-1.amazonaws.com/dayhoff-generic with-space",
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
def test_rejects_non_ecr_image(self, bad_image):
|
|
46
|
+
from dh_cli.batch.aws_batch import BatchError
|
|
47
|
+
|
|
48
|
+
client, mock_batch = _make_client()
|
|
49
|
+
with pytest.raises(BatchError, match="fully-qualified ECR URL"):
|
|
50
|
+
client._register_image_override("dayhoff-generic", bad_image)
|
|
51
|
+
|
|
52
|
+
mock_batch.describe_job_definitions.assert_not_called()
|
|
53
|
+
mock_batch.register_job_definition.assert_not_called()
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize(
|
|
56
|
+
"good_image",
|
|
57
|
+
[
|
|
58
|
+
"123456789012.dkr.ecr.us-east-1.amazonaws.com/dayhoff-generic:abc123",
|
|
59
|
+
"123456789012.dkr.ecr.us-west-2.amazonaws.com/foo/bar:latest",
|
|
60
|
+
"123456789012.dkr.ecr.us-east-1.amazonaws.com/x@sha256:"
|
|
61
|
+
+ "a" * 64,
|
|
62
|
+
],
|
|
63
|
+
)
|
|
64
|
+
def test_accepts_ecr_url(self, good_image):
|
|
65
|
+
client, mock_batch = _make_client()
|
|
66
|
+
mock_batch.describe_job_definitions.return_value = {
|
|
67
|
+
"jobDefinitions": [
|
|
68
|
+
{
|
|
69
|
+
"jobDefinitionArn": "arn:aws:batch:...:job-definition/dayhoff-generic:1",
|
|
70
|
+
"revision": 1,
|
|
71
|
+
"type": "container",
|
|
72
|
+
"containerProperties": {"image": "old-image"},
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
mock_batch.register_job_definition.return_value = {
|
|
77
|
+
"jobDefinitionArn": "arn:aws:batch:...:job-definition/dayhoff-generic:2"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
client._register_image_override("dayhoff-generic", good_image)
|
|
81
|
+
mock_batch.register_job_definition.assert_called_once()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TestNoOpReuse:
|
|
85
|
+
"""If latest revision already matches the requested image, reuse it."""
|
|
86
|
+
|
|
87
|
+
def test_reuses_latest_when_image_matches(self):
|
|
88
|
+
client, mock_batch = _make_client()
|
|
89
|
+
mock_batch.describe_job_definitions.return_value = {
|
|
90
|
+
"jobDefinitions": [
|
|
91
|
+
{
|
|
92
|
+
"jobDefinitionArn": "arn:...:job-definition/dayhoff-generic:42",
|
|
93
|
+
"revision": 42,
|
|
94
|
+
"type": "container",
|
|
95
|
+
"containerProperties": {"image": _ECR_URL},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"jobDefinitionArn": "arn:...:job-definition/dayhoff-generic:41",
|
|
99
|
+
"revision": 41,
|
|
100
|
+
"type": "container",
|
|
101
|
+
"containerProperties": {"image": "older"},
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
arn = client._register_image_override("dayhoff-generic", _ECR_URL)
|
|
107
|
+
|
|
108
|
+
assert arn == "arn:...:job-definition/dayhoff-generic:42"
|
|
109
|
+
mock_batch.register_job_definition.assert_not_called()
|
|
110
|
+
|
|
111
|
+
def test_registers_new_revision_when_image_differs(self):
|
|
112
|
+
client, mock_batch = _make_client()
|
|
113
|
+
mock_batch.describe_job_definitions.return_value = {
|
|
114
|
+
"jobDefinitions": [
|
|
115
|
+
{
|
|
116
|
+
"jobDefinitionArn": "arn:...:job-definition/dayhoff-generic:42",
|
|
117
|
+
"revision": 42,
|
|
118
|
+
"type": "container",
|
|
119
|
+
"containerProperties": {"image": "different-image"},
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
mock_batch.register_job_definition.return_value = {
|
|
124
|
+
"jobDefinitionArn": "arn:...:job-definition/dayhoff-generic:43"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
arn = client._register_image_override("dayhoff-generic", _ECR_URL)
|
|
128
|
+
|
|
129
|
+
assert arn == "arn:...:job-definition/dayhoff-generic:43"
|
|
130
|
+
mock_batch.register_job_definition.assert_called_once()
|
|
131
|
+
kwargs = mock_batch.register_job_definition.call_args[1]
|
|
132
|
+
assert kwargs["containerProperties"]["image"] == _ECR_URL
|
|
133
|
+
|
|
134
|
+
def test_raises_when_no_active_definition(self):
|
|
135
|
+
from dh_cli.batch.aws_batch import BatchError
|
|
136
|
+
|
|
137
|
+
client, mock_batch = _make_client()
|
|
138
|
+
mock_batch.describe_job_definitions.return_value = {"jobDefinitions": []}
|
|
139
|
+
|
|
140
|
+
with pytest.raises(BatchError, match="Job definition not found"):
|
|
141
|
+
client._register_image_override("dayhoff-generic", _ECR_URL)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""CLI-layer image validation tests for `dh batch submit`.
|
|
2
|
+
|
|
3
|
+
Pins the contract: a non-ECR --image (or YAML image: ...) is rejected at
|
|
4
|
+
parse time, before any AWS call. Without this, bare names like
|
|
5
|
+
``--image dayhoff-generic`` reach BatchClient and silently mint a JD
|
|
6
|
+
revision pointing at Docker Hub.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import yaml
|
|
13
|
+
from click.testing import CliRunner
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def cli_runner():
|
|
18
|
+
return CliRunner()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _invoke(cli_runner, args, tmp_path):
|
|
22
|
+
base = tmp_path / "jobs"
|
|
23
|
+
with (
|
|
24
|
+
patch("dh_cli.batch.commands.submit.get_aws_username", return_value="jason"),
|
|
25
|
+
patch("dh_cli.batch.commands.submit.BatchClient") as mock_batch_cls,
|
|
26
|
+
patch(
|
|
27
|
+
"dh_cli.batch.commands.submit.generate_job_id",
|
|
28
|
+
return_value="jason-batch-20260519-img00001",
|
|
29
|
+
),
|
|
30
|
+
):
|
|
31
|
+
mock_client = MagicMock()
|
|
32
|
+
mock_client.submit_job.return_value = "aws-uuid-img"
|
|
33
|
+
mock_batch_cls.return_value = mock_client
|
|
34
|
+
|
|
35
|
+
from dh_cli.batch.commands.submit import submit
|
|
36
|
+
|
|
37
|
+
result = cli_runner.invoke(submit, args + ["--base-path", str(base)])
|
|
38
|
+
return result, mock_client, mock_batch_cls
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestCliImageValidation:
|
|
42
|
+
@pytest.mark.parametrize(
|
|
43
|
+
"bad_image",
|
|
44
|
+
["dayhoff-generic", "dayhoff-generic:latest", "ghcr.io/foo/bar:1.0"],
|
|
45
|
+
)
|
|
46
|
+
def test_bare_image_rejected_before_submit(self, cli_runner, tmp_path, bad_image):
|
|
47
|
+
result, _, mock_batch_cls = _invoke(
|
|
48
|
+
cli_runner,
|
|
49
|
+
["--command", "echo hi", "--image", bad_image],
|
|
50
|
+
tmp_path,
|
|
51
|
+
)
|
|
52
|
+
assert result.exit_code != 0
|
|
53
|
+
assert "fully-qualified ECR URL" in result.output
|
|
54
|
+
mock_batch_cls.assert_not_called()
|
|
55
|
+
|
|
56
|
+
def test_yaml_bare_image_rejected_before_submit(self, cli_runner, tmp_path):
|
|
57
|
+
config_path = tmp_path / "job.yaml"
|
|
58
|
+
config_path.write_text(
|
|
59
|
+
yaml.dump(
|
|
60
|
+
{
|
|
61
|
+
"command": "echo hi",
|
|
62
|
+
"image": "dayhoff-generic",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
result, _, mock_batch_cls = _invoke(
|
|
67
|
+
cli_runner, ["-f", str(config_path)], tmp_path
|
|
68
|
+
)
|
|
69
|
+
assert result.exit_code != 0
|
|
70
|
+
assert "fully-qualified ECR URL" in result.output
|
|
71
|
+
mock_batch_cls.assert_not_called()
|
|
72
|
+
|
|
73
|
+
def test_valid_ecr_url_accepted(self, cli_runner, tmp_path):
|
|
74
|
+
good = "123456789012.dkr.ecr.us-east-1.amazonaws.com/dayhoff-generic:abc"
|
|
75
|
+
result, mock_client, _ = _invoke(
|
|
76
|
+
cli_runner,
|
|
77
|
+
["--command", "echo hi", "--image", good],
|
|
78
|
+
tmp_path,
|
|
79
|
+
)
|
|
80
|
+
assert result.exit_code == 0, result.output
|
|
81
|
+
call_kwargs = mock_client.submit_job.call_args[1]
|
|
82
|
+
assert call_kwargs["image_override"] == good
|
|
83
|
+
|
|
84
|
+
def test_no_image_skips_validation(self, cli_runner, tmp_path):
|
|
85
|
+
result, mock_client, _ = _invoke(
|
|
86
|
+
cli_runner, ["--command", "echo hi"], tmp_path
|
|
87
|
+
)
|
|
88
|
+
assert result.exit_code == 0, result.output
|
|
89
|
+
call_kwargs = mock_client.submit_job.call_args[1]
|
|
90
|
+
assert call_kwargs.get("image_override") is None
|
|
@@ -32,9 +32,7 @@ def _build_essential_tar(tar_path: Path, complex_name: str) -> None:
|
|
|
32
32
|
pred_subdir = src_p / f"boltz_results_{complex_name}" / "predictions" / complex_name
|
|
33
33
|
pred_subdir.mkdir(parents=True)
|
|
34
34
|
(pred_subdir / f"{complex_name}_model_0.cif").write_text(f"CIF {complex_name}\n")
|
|
35
|
-
(pred_subdir / f"confidence_{complex_name}_model_0.json").write_text(
|
|
36
|
-
f'{{"cx":"{complex_name}"}}'
|
|
37
|
-
)
|
|
35
|
+
(pred_subdir / f"confidence_{complex_name}_model_0.json").write_text(f'{{"cx":"{complex_name}"}}')
|
|
38
36
|
with tarfile.open(tar_path, mode="w") as tf:
|
|
39
37
|
root = src_p / f"boltz_results_{complex_name}"
|
|
40
38
|
for f in sorted(root.rglob("*")):
|
|
@@ -188,12 +186,8 @@ class TestFinalizeDispatches:
|
|
|
188
186
|
runner = CliRunner()
|
|
189
187
|
|
|
190
188
|
with patch("dh_cli.batch.commands.finalize.load_manifest", return_value=manifest):
|
|
191
|
-
with patch(
|
|
192
|
-
"dh_cli.batch.commands.finalize.
|
|
193
|
-
) as mock_tar_download:
|
|
194
|
-
with patch(
|
|
195
|
-
"dh_cli.batch.commands.finalize._check_completion", return_value=[]
|
|
196
|
-
):
|
|
189
|
+
with patch("dh_cli.batch.commands.finalize._download_boltz_s3_output") as mock_tar_download:
|
|
190
|
+
with patch("dh_cli.batch.commands.finalize._check_completion", return_value=[]):
|
|
197
191
|
with patch("dh_cli.batch.commands.finalize._finalize_boltz") as mock_fb:
|
|
198
192
|
with patch("dh_cli.batch.commands.finalize.save_manifest_s3"):
|
|
199
193
|
result = runner.invoke(
|
|
@@ -230,16 +224,10 @@ class TestFinalizeDispatches:
|
|
|
230
224
|
runner = CliRunner()
|
|
231
225
|
|
|
232
226
|
with patch("dh_cli.batch.commands.finalize.load_manifest", return_value=manifest):
|
|
233
|
-
with patch(
|
|
234
|
-
"dh_cli.batch.commands.finalize._download_boltz_s3_output"
|
|
235
|
-
) as mock_tar_download:
|
|
227
|
+
with patch("dh_cli.batch.commands.finalize._download_boltz_s3_output") as mock_tar_download:
|
|
236
228
|
with patch("dh_cli.batch.s3_transport.download_directory") as mock_dd:
|
|
237
|
-
with patch(
|
|
238
|
-
"dh_cli.batch.commands.finalize.
|
|
239
|
-
):
|
|
240
|
-
with patch(
|
|
241
|
-
"dh_cli.batch.commands.finalize._finalize_embeddings"
|
|
242
|
-
) as mock_fe:
|
|
229
|
+
with patch("dh_cli.batch.commands.finalize._check_completion", return_value=[]):
|
|
230
|
+
with patch("dh_cli.batch.commands.finalize._finalize_embeddings") as mock_fe:
|
|
243
231
|
with patch("dh_cli.batch.commands.finalize.save_manifest_s3"):
|
|
244
232
|
result = runner.invoke(
|
|
245
233
|
finalize_cmd,
|
|
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
|
|
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
|