dh-cli 0.11.2__tar.gz → 0.12.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.
- {dh_cli-0.11.2 → dh_cli-0.12.0}/PKG-INFO +1 -1
- {dh_cli-0.11.2 → dh_cli-0.12.0}/pyproject.toml +1 -1
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/__init__.py +0 -6
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/submit.py +12 -2
- dh_cli-0.12.0/tests/batch/test_no_train_command.py +34 -0
- dh_cli-0.12.0/tests/batch/test_submit_mlflow_env.py +196 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_disco_jobdef.py +16 -5
- dh_cli-0.11.2/src/dh_cli/batch/commands/train.py +0 -725
- {dh_cli-0.11.2 → dh_cli-0.12.0}/.gitignore +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/LICENSE +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/README.md +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/__init__.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/_identity.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/aws_batch.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/__init__.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/boltz.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/cancel.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/clean.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/disco.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/embed_t5.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/equilibrator.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/esmfold2.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/finalize.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/list_jobs.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/local.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/logs.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/orca.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/protmpnn.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/retry.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/rfd3.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/status.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/wait_for.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/fasta_utils.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/h5_utils.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/job_id.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/manifest.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/s3_transport.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/__init__.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/commands.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/cost_report.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/pricing.yaml +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/cloud_commands.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/codeartifact.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/__init__.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/api_client.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/auth.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/engine_commands.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/progress.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/ssh_config.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/studio_commands.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/github_commands.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/__init__.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/deploy.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/local.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/test.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/tf.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/users.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/main.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/utility_commands.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/warehouse.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/__init__.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_aws_batch_resources.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_image_override.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_submit_cpu_only.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_submit_image_validation.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_submit_merge.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/conftest.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/A_cache_write.json +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/B_cache_read.json +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/C_plain.json +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/E_service_role.json +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_build_report.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_classify_arn.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cli_exit_codes.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cost_calc.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cost_command.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cur_reconciliation.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_key_command.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_render_formats.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_resolve_base_model.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_rotate.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_s3_walker.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/__init__.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/conftest.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_identity.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_login.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_login_error_paths.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_login_security.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_logout.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_rotate.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_status.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/hz/test_init.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/hz/test_suites.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/hz/test_users.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_cloud_gcp.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_disco_command.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_equilibrator_command.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_esmfold2_cli.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_boltz_tar.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_disco.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_disco_dispatch.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_equilibrator.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_esmfold2.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_protmpnn.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_rfd3.py +0 -0
- {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_rfd3_command.py +0 -0
|
@@ -28,7 +28,6 @@ from .commands.retry import retry
|
|
|
28
28
|
from .commands.rfd3 import rfd3
|
|
29
29
|
from .commands.status import status
|
|
30
30
|
from .commands.submit import submit
|
|
31
|
-
from .commands.train import train
|
|
32
31
|
from .commands.wait_for import wait_for
|
|
33
32
|
|
|
34
33
|
|
|
@@ -62,10 +61,6 @@ def batch_cli():
|
|
|
62
61
|
protmpnn Design sequences with ProtMPNN/LigandMPNN
|
|
63
62
|
protmpnn-to-boltz Convert top variants to Boltz validation YAMLs
|
|
64
63
|
|
|
65
|
-
\b
|
|
66
|
-
ML Training:
|
|
67
|
-
train Submit hatchery ML training jobs
|
|
68
|
-
|
|
69
64
|
\b
|
|
70
65
|
Examples:
|
|
71
66
|
# Submit an embedding job
|
|
@@ -111,6 +106,5 @@ batch_cli.add_command(orca)
|
|
|
111
106
|
batch_cli.add_command(protmpnn_to_boltz, name="protmpnn-to-boltz")
|
|
112
107
|
batch_cli.add_command(rfd3)
|
|
113
108
|
batch_cli.add_command(disco)
|
|
114
|
-
batch_cli.add_command(train)
|
|
115
109
|
|
|
116
110
|
__all__ = ["batch_cli"]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Submit command for generic batch jobs."""
|
|
2
2
|
|
|
3
|
+
import os
|
|
4
|
+
|
|
3
5
|
import click
|
|
4
6
|
import yaml
|
|
5
7
|
from click.core import ParameterSource
|
|
@@ -122,13 +124,20 @@ def submit(
|
|
|
122
124
|
param_hint="--image",
|
|
123
125
|
)
|
|
124
126
|
|
|
125
|
-
# Parse environment variables
|
|
127
|
+
# Parse environment variables. The submitter's MLFLOW_TRACKING_URI is
|
|
128
|
+
# forwarded so runtime-clone jobs reach the tracking server without an
|
|
129
|
+
# endpoint constant in any config; an explicit `env:` entry overrides it.
|
|
130
|
+
# Resolved before the plan is rendered so the summary matches what is sent.
|
|
126
131
|
job_env = dict(config.get("env", {}))
|
|
127
132
|
for e in env:
|
|
128
133
|
if "=" in e:
|
|
129
134
|
key, value = e.split("=", 1)
|
|
130
135
|
job_env[key] = value
|
|
131
136
|
|
|
137
|
+
mlflow_tracking_uri = os.environ.get("MLFLOW_TRACKING_URI", "")
|
|
138
|
+
if mlflow_tracking_uri:
|
|
139
|
+
job_env.setdefault("MLFLOW_TRACKING_URI", mlflow_tracking_uri)
|
|
140
|
+
|
|
132
141
|
# Generate job ID
|
|
133
142
|
job_id = generate_job_id("batch")
|
|
134
143
|
|
|
@@ -149,6 +158,8 @@ def submit(
|
|
|
149
158
|
click.echo(f"Image: {job_image}")
|
|
150
159
|
if job_env:
|
|
151
160
|
click.echo(f"Environment: {len(job_env)} variables")
|
|
161
|
+
if job_env.get("MLFLOW_TRACKING_URI"):
|
|
162
|
+
click.echo(f" MLFLOW_TRACKING_URI={job_env['MLFLOW_TRACKING_URI']}")
|
|
152
163
|
|
|
153
164
|
if dry_run:
|
|
154
165
|
click.echo()
|
|
@@ -182,7 +193,6 @@ def submit(
|
|
|
182
193
|
|
|
183
194
|
client = BatchClient()
|
|
184
195
|
|
|
185
|
-
# Prepare environment
|
|
186
196
|
submit_env = {
|
|
187
197
|
"JOB_DIR": str(job_dir),
|
|
188
198
|
"JOB_ID": job_id,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Pins the removal of `dh batch train` (plan D-24).
|
|
2
|
+
|
|
3
|
+
The command was orphaned by the 2026-06-22 runtime-clone conversion: the dojo
|
|
4
|
+
launches training through `dh batch submit` with a wrapper YAML, and
|
|
5
|
+
`dh batch train` submitted the `dayhoff-train` job definition with no `command`,
|
|
6
|
+
so the container ran the image's default `CMD ["python","main.py","--help"]` and
|
|
7
|
+
exited 0 — green submit, green job, no training. These tests keep a future merge
|
|
8
|
+
from silently resurrecting it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from click.testing import CliRunner
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_train_not_registered():
|
|
18
|
+
"""`train` is neither a registered subcommand nor offered in the help."""
|
|
19
|
+
from dh_cli.batch import batch_cli
|
|
20
|
+
|
|
21
|
+
assert "train" not in batch_cli.commands
|
|
22
|
+
|
|
23
|
+
result = CliRunner().invoke(batch_cli, ["--help"])
|
|
24
|
+
assert result.exit_code == 0, result.output
|
|
25
|
+
# Match `train` only as a standalone word, not inside a hyphenated identifier:
|
|
26
|
+
# `\btrain\b` treats `-` as a boundary, so it would false-positive on a
|
|
27
|
+
# legitimate future mention of e.g. the `dayhoff-train` job definition.
|
|
28
|
+
assert re.search(r"(?<![-\w])train(?![-\w])", result.output) is None, result.output
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_train_module_absent():
|
|
32
|
+
"""The module is gone, not merely unregistered."""
|
|
33
|
+
with pytest.raises(ModuleNotFoundError):
|
|
34
|
+
import dh_cli.batch.commands.train # noqa: F401
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Tests for MLFLOW_TRACKING_URI passthrough in `dh batch submit`.
|
|
2
|
+
|
|
3
|
+
Pins the seam that Part 3 (D-23) owns on the command the dojo actually uses.
|
|
4
|
+
`dh batch submit` forwards the submitter's own ``MLFLOW_TRACKING_URI`` into the
|
|
5
|
+
AWS Batch job environment (mirroring the ``WANDB_API_KEY`` passthrough pattern),
|
|
6
|
+
so a runtime-clone dojo job launched from a ``batch-submit-*.yaml`` wrapper
|
|
7
|
+
reaches the tracking server with no endpoint constant in any config.
|
|
8
|
+
|
|
9
|
+
Task 01 implemented the same passthrough in `dh batch train`, which the dojo
|
|
10
|
+
never calls — see plan D-23.
|
|
11
|
+
|
|
12
|
+
When the submitter's environment does not set ``MLFLOW_TRACKING_URI`` (or sets
|
|
13
|
+
it empty), nothing is injected and the dojo config ``tracking_uri`` fallback
|
|
14
|
+
applies inside the job. An explicit ``env:`` entry in the wrapper YAML is an
|
|
15
|
+
operator override and must win over the passthrough.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from unittest.mock import MagicMock, patch
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
import yaml
|
|
22
|
+
from click.testing import CliRunner
|
|
23
|
+
|
|
24
|
+
WRAPPER_ENV = {
|
|
25
|
+
"GH_TOKEN": "ghp_fake_for_test",
|
|
26
|
+
"DOJO_GIT_REF": "mlflow3",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def cli_runner():
|
|
32
|
+
return CliRunner()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _invoke(cli_runner, tmp_path, env, config=None):
|
|
36
|
+
"""Invoke `dh batch submit` with BatchClient and the submitter env mocked."""
|
|
37
|
+
base = tmp_path / "jobs"
|
|
38
|
+
if config is None:
|
|
39
|
+
args = ["--command", "echo hi"]
|
|
40
|
+
else:
|
|
41
|
+
config_path = tmp_path / "job.yaml"
|
|
42
|
+
config_path.write_text(yaml.dump(config))
|
|
43
|
+
args = ["-f", str(config_path)]
|
|
44
|
+
|
|
45
|
+
with (
|
|
46
|
+
patch("dh_cli.batch.commands.submit.get_aws_username", return_value="jason"),
|
|
47
|
+
patch("dh_cli.batch.commands.submit.BatchClient") as mock_batch_cls,
|
|
48
|
+
patch(
|
|
49
|
+
"dh_cli.batch.commands.submit.generate_job_id",
|
|
50
|
+
return_value="jason-batch-20260725-mlf00001",
|
|
51
|
+
),
|
|
52
|
+
patch.dict("dh_cli.batch.commands.submit.os.environ", env, clear=True),
|
|
53
|
+
):
|
|
54
|
+
mock_client = MagicMock()
|
|
55
|
+
mock_client.submit_job.return_value = "aws-uuid-mlflow"
|
|
56
|
+
mock_batch_cls.return_value = mock_client
|
|
57
|
+
|
|
58
|
+
from dh_cli.batch.commands.submit import submit
|
|
59
|
+
|
|
60
|
+
result = cli_runner.invoke(submit, args + ["--base-path", str(base)])
|
|
61
|
+
return result, mock_client
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _submitted_env(mock_client):
|
|
65
|
+
return mock_client.submit_job.call_args[1]["environment"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestTrackingUriPassthrough:
|
|
69
|
+
def test_env_var_forwarded_when_set(self, cli_runner, tmp_path):
|
|
70
|
+
"""When the submitter has MLFLOW_TRACKING_URI set, the job inherits it."""
|
|
71
|
+
uri = "http://internal-mlflow-dev-1099724111.us-east-1.elb.amazonaws.com:5000"
|
|
72
|
+
result, mock_client = _invoke(cli_runner, tmp_path, {"MLFLOW_TRACKING_URI": uri})
|
|
73
|
+
assert result.exit_code == 0, result.output
|
|
74
|
+
assert _submitted_env(mock_client).get("MLFLOW_TRACKING_URI") == uri
|
|
75
|
+
|
|
76
|
+
def test_forwards_arbitrary_value(self, cli_runner, tmp_path):
|
|
77
|
+
"""The value is passed through verbatim, not matched against a literal."""
|
|
78
|
+
uri = "http://some-other-endpoint.internal:5000"
|
|
79
|
+
result, mock_client = _invoke(cli_runner, tmp_path, {"MLFLOW_TRACKING_URI": uri})
|
|
80
|
+
assert result.exit_code == 0, result.output
|
|
81
|
+
assert _submitted_env(mock_client).get("MLFLOW_TRACKING_URI") == uri
|
|
82
|
+
|
|
83
|
+
def test_absent_when_submitter_unset(self, cli_runner, tmp_path):
|
|
84
|
+
"""With no MLFLOW_TRACKING_URI in the submitter env, none is injected."""
|
|
85
|
+
result, mock_client = _invoke(cli_runner, tmp_path, {})
|
|
86
|
+
assert result.exit_code == 0, result.output
|
|
87
|
+
assert "MLFLOW_TRACKING_URI" not in _submitted_env(mock_client)
|
|
88
|
+
|
|
89
|
+
def test_empty_value_not_injected(self, cli_runner, tmp_path):
|
|
90
|
+
"""An empty MLFLOW_TRACKING_URI is treated as unset (no blank injection)."""
|
|
91
|
+
result, mock_client = _invoke(cli_runner, tmp_path, {"MLFLOW_TRACKING_URI": ""})
|
|
92
|
+
assert result.exit_code == 0, result.output
|
|
93
|
+
assert "MLFLOW_TRACKING_URI" not in _submitted_env(mock_client)
|
|
94
|
+
|
|
95
|
+
def test_wrapper_env_wins_over_passthrough(self, cli_runner, tmp_path):
|
|
96
|
+
"""An explicit wrapper `env:` entry is an operator override and survives."""
|
|
97
|
+
wrapper_uri = "http://operator-chosen-endpoint.internal:5000"
|
|
98
|
+
result, mock_client = _invoke(
|
|
99
|
+
cli_runner,
|
|
100
|
+
tmp_path,
|
|
101
|
+
{"MLFLOW_TRACKING_URI": "http://submitter-shell-endpoint.internal:5000"},
|
|
102
|
+
config={
|
|
103
|
+
"command": "echo hi",
|
|
104
|
+
"env": {**WRAPPER_ENV, "MLFLOW_TRACKING_URI": wrapper_uri},
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
assert result.exit_code == 0, result.output
|
|
108
|
+
assert _submitted_env(mock_client)["MLFLOW_TRACKING_URI"] == wrapper_uri
|
|
109
|
+
|
|
110
|
+
def test_passthrough_does_not_disturb_other_env(self, cli_runner, tmp_path):
|
|
111
|
+
"""Injection is additive: JOB_DIR/JOB_ID and wrapper env are untouched."""
|
|
112
|
+
uri = "http://internal-mlflow-dev-1099724111.us-east-1.elb.amazonaws.com:5000"
|
|
113
|
+
result, mock_client = _invoke(
|
|
114
|
+
cli_runner,
|
|
115
|
+
tmp_path,
|
|
116
|
+
{"MLFLOW_TRACKING_URI": uri},
|
|
117
|
+
config={"command": "echo hi", "env": dict(WRAPPER_ENV)},
|
|
118
|
+
)
|
|
119
|
+
assert result.exit_code == 0, result.output
|
|
120
|
+
env = _submitted_env(mock_client)
|
|
121
|
+
assert env["MLFLOW_TRACKING_URI"] == uri
|
|
122
|
+
assert env["JOB_ID"] == "jason-batch-20260725-mlf00001"
|
|
123
|
+
assert env["JOB_DIR"] == str(tmp_path / "jobs" / "jason-batch-20260725-mlf00001")
|
|
124
|
+
for key, value in WRAPPER_ENV.items():
|
|
125
|
+
assert env[key] == value
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestSubmitSummaryReportsInjection:
|
|
129
|
+
"""The printed plan must not contradict what is actually submitted.
|
|
130
|
+
|
|
131
|
+
The release gate saw ``Environment: 2 variables`` on a job whose AWS
|
|
132
|
+
environment held three, because the summary counted only the wrapper's
|
|
133
|
+
``env:`` before the passthrough was merged in. Harmless to the job, but it
|
|
134
|
+
reads as if the injection did not happen — the exact signal an operator
|
|
135
|
+
checks the line for.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def test_summary_counts_the_injected_variable(self, cli_runner, tmp_path):
|
|
139
|
+
uri = "http://internal-mlflow-dev-1099724111.us-east-1.elb.amazonaws.com:5000"
|
|
140
|
+
result, mock_client = _invoke(
|
|
141
|
+
cli_runner,
|
|
142
|
+
tmp_path,
|
|
143
|
+
{"MLFLOW_TRACKING_URI": uri},
|
|
144
|
+
config={"command": "echo hi", "env": dict(WRAPPER_ENV)},
|
|
145
|
+
)
|
|
146
|
+
assert result.exit_code == 0, result.output
|
|
147
|
+
|
|
148
|
+
submitted = _submitted_env(mock_client)
|
|
149
|
+
operator_visible = {k: v for k, v in submitted.items() if k not in ("JOB_DIR", "JOB_ID")}
|
|
150
|
+
assert len(operator_visible) == 3, operator_visible
|
|
151
|
+
assert f"Environment: {len(operator_visible)} variables" in result.output
|
|
152
|
+
|
|
153
|
+
def test_summary_names_the_tracking_uri(self, cli_runner, tmp_path):
|
|
154
|
+
"""The URI is echoed so the operator can confirm the endpoint, not just the count."""
|
|
155
|
+
uri = "http://internal-mlflow-dev-1099724111.us-east-1.elb.amazonaws.com:5000"
|
|
156
|
+
result, _mock_client = _invoke(cli_runner, tmp_path, {"MLFLOW_TRACKING_URI": uri})
|
|
157
|
+
assert result.exit_code == 0, result.output
|
|
158
|
+
assert f"MLFLOW_TRACKING_URI={uri}" in result.output
|
|
159
|
+
|
|
160
|
+
def test_summary_unchanged_when_no_uri_to_inject(self, cli_runner, tmp_path):
|
|
161
|
+
"""With nothing to forward, the count is just the wrapper's own env."""
|
|
162
|
+
result, _mock_client = _invoke(
|
|
163
|
+
cli_runner, tmp_path, {}, config={"command": "echo hi", "env": dict(WRAPPER_ENV)}
|
|
164
|
+
)
|
|
165
|
+
assert result.exit_code == 0, result.output
|
|
166
|
+
assert "Environment: 2 variables" in result.output
|
|
167
|
+
assert "MLFLOW_TRACKING_URI" not in result.output
|
|
168
|
+
|
|
169
|
+
def test_dry_run_reports_the_injection(self, cli_runner, tmp_path):
|
|
170
|
+
"""``--dry-run`` returns before submission, so its summary is the only
|
|
171
|
+
evidence the operator gets; it must still show the passthrough."""
|
|
172
|
+
uri = "http://internal-mlflow-dev-1099724111.us-east-1.elb.amazonaws.com:5000"
|
|
173
|
+
base = tmp_path / "jobs"
|
|
174
|
+
config_path = tmp_path / "job.yaml"
|
|
175
|
+
config_path.write_text(yaml.dump({"command": "echo hi", "env": dict(WRAPPER_ENV)}))
|
|
176
|
+
|
|
177
|
+
with (
|
|
178
|
+
patch("dh_cli.batch.commands.submit.get_aws_username", return_value="jason"),
|
|
179
|
+
patch("dh_cli.batch.commands.submit.BatchClient"),
|
|
180
|
+
patch(
|
|
181
|
+
"dh_cli.batch.commands.submit.generate_job_id",
|
|
182
|
+
return_value="jason-batch-20260725-mlf00001",
|
|
183
|
+
),
|
|
184
|
+
patch.dict(
|
|
185
|
+
"dh_cli.batch.commands.submit.os.environ",
|
|
186
|
+
{"MLFLOW_TRACKING_URI": uri},
|
|
187
|
+
clear=True,
|
|
188
|
+
),
|
|
189
|
+
):
|
|
190
|
+
from dh_cli.batch.commands.submit import submit
|
|
191
|
+
|
|
192
|
+
result = cli_runner.invoke(submit, ["-f", str(config_path), "--base-path", str(base), "--dry-run"])
|
|
193
|
+
|
|
194
|
+
assert result.exit_code == 0, result.output
|
|
195
|
+
assert "Environment: 3 variables" in result.output
|
|
196
|
+
assert f"MLFLOW_TRACKING_URI={uri}" in result.output
|
|
@@ -28,18 +28,29 @@ SPIKE_VCPU = "8"
|
|
|
28
28
|
SPIKE_MEMORY = "30000"
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _find_dma_root() -> Path:
|
|
31
|
+
def _find_dma_root() -> Path | None:
|
|
32
32
|
"""Walk up from this test file to the workspace root that holds both the
|
|
33
|
-
`dayhoff-tools` and `blueprints` sibling repos.
|
|
33
|
+
`dayhoff-tools` and `blueprints` sibling repos.
|
|
34
|
+
|
|
35
|
+
Returns ``None`` when the sibling `blueprints` repo is not checked out
|
|
36
|
+
alongside `dayhoff-tools` (e.g. standalone CI of this repo), so the module
|
|
37
|
+
can skip rather than crash at collection time.
|
|
38
|
+
"""
|
|
34
39
|
for parent in Path(__file__).resolve().parents:
|
|
35
40
|
if (parent / "blueprints").is_dir() and (parent / "dayhoff-tools").is_dir():
|
|
36
41
|
return parent
|
|
37
|
-
|
|
42
|
+
return None
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
DMA_ROOT = _find_dma_root()
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
|
|
47
|
+
pytestmark = pytest.mark.skipif(
|
|
48
|
+
DMA_ROOT is None,
|
|
49
|
+
reason="blueprints sibling repo not checked out; cross-repo job-def test needs the dma monorepo layout",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
TF_DIR = DMA_ROOT / "blueprints" / "terraform" / "environments" / "dev" if DMA_ROOT else None
|
|
53
|
+
TF_FILE = TF_DIR / "batch_job_definitions.tf" if TF_DIR else None
|
|
43
54
|
|
|
44
55
|
|
|
45
56
|
def _extract_block(text: str, header: str) -> str:
|