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.
Files changed (94) hide show
  1. {dh_cli-0.8.3 → dh_cli-0.8.4}/PKG-INFO +1 -1
  2. {dh_cli-0.8.3 → dh_cli-0.8.4}/pyproject.toml +1 -1
  3. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/aws_batch.py +19 -1
  4. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/submit.py +8 -1
  5. dh_cli-0.8.4/tests/batch/test_image_override.py +141 -0
  6. dh_cli-0.8.4/tests/batch/test_submit_image_validation.py +90 -0
  7. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/test_finalize_boltz_tar.py +6 -18
  8. {dh_cli-0.8.3 → dh_cli-0.8.4}/.gitignore +0 -0
  9. {dh_cli-0.8.3 → dh_cli-0.8.4}/LICENSE +0 -0
  10. {dh_cli-0.8.3 → dh_cli-0.8.4}/README.md +0 -0
  11. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/__init__.py +0 -0
  12. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/_identity.py +0 -0
  13. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/__init__.py +0 -0
  14. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/__init__.py +0 -0
  15. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/boltz.py +0 -0
  16. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/cancel.py +0 -0
  17. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/clean.py +0 -0
  18. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  19. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/finalize.py +0 -0
  20. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  21. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/local.py +0 -0
  22. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/logs.py +0 -0
  23. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/orca.py +0 -0
  24. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  25. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  26. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/retry.py +0 -0
  27. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/status.py +0 -0
  28. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/train.py +0 -0
  29. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/commands/wait_for.py +0 -0
  30. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/fasta_utils.py +0 -0
  31. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/h5_utils.py +0 -0
  32. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/job_id.py +0 -0
  33. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/manifest.py +0 -0
  34. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/batch/s3_transport.py +0 -0
  35. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/__init__.py +0 -0
  36. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/commands.py +0 -0
  37. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/cost_report.py +0 -0
  38. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/bedrock/pricing.yaml +0 -0
  39. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/cloud_commands.py +0 -0
  40. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/codeartifact.py +0 -0
  41. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/__init__.py +0 -0
  42. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/api_client.py +0 -0
  43. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/auth.py +0 -0
  44. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  45. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/progress.py +0 -0
  46. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  47. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  48. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/github_commands.py +0 -0
  49. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/__init__.py +0 -0
  50. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/deploy.py +0 -0
  51. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/local.py +0 -0
  52. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/test.py +0 -0
  53. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/tf.py +0 -0
  54. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/hz/users.py +0 -0
  55. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/main.py +0 -0
  56. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/utility_commands.py +0 -0
  57. {dh_cli-0.8.3 → dh_cli-0.8.4}/src/dh_cli/warehouse.py +0 -0
  58. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/__init__.py +0 -0
  59. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/test_aws_batch_resources.py +0 -0
  60. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/test_submit_cpu_only.py +0 -0
  61. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/batch/test_submit_merge.py +0 -0
  62. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/conftest.py +0 -0
  63. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/A_cache_write.json +0 -0
  64. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/B_cache_read.json +0 -0
  65. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/C_plain.json +0 -0
  66. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
  67. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/E_service_role.json +0 -0
  68. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
  69. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
  70. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_build_report.py +0 -0
  71. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_classify_arn.py +0 -0
  72. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cli_exit_codes.py +0 -0
  73. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cost_calc.py +0 -0
  74. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cost_command.py +0 -0
  75. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_cur_reconciliation.py +0 -0
  76. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_key_command.py +0 -0
  77. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_render_formats.py +0 -0
  78. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_resolve_base_model.py +0 -0
  79. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/bedrock/test_s3_walker.py +0 -0
  80. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/__init__.py +0 -0
  81. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/conftest.py +0 -0
  82. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
  83. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_identity.py +0 -0
  84. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_login.py +0 -0
  85. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_login_error_paths.py +0 -0
  86. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_login_security.py +0 -0
  87. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_logout.py +0 -0
  88. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_rotate.py +0 -0
  89. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/github/test_status.py +0 -0
  90. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/hz/test_init.py +0 -0
  91. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/hz/test_suites.py +0 -0
  92. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/hz/test_users.py +0 -0
  93. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/test_cloud_gcp.py +0 -0
  94. {dh_cli-0.8.3 → dh_cli-0.8.4}/tests/test_finalize_protmpnn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dh-cli
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: Dayhoff Labs developer CLI
5
5
  Author-email: Dayhoff Labs <dev@dayhofflabs.com>
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dh-cli"
7
- version = "0.8.3"
7
+ version = "0.8.4"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -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._download_boltz_s3_output"
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._check_completion", return_value=[]
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