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.
Files changed (111) hide show
  1. {dh_cli-0.11.2 → dh_cli-0.12.0}/PKG-INFO +1 -1
  2. {dh_cli-0.11.2 → dh_cli-0.12.0}/pyproject.toml +1 -1
  3. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/__init__.py +0 -6
  4. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/submit.py +12 -2
  5. dh_cli-0.12.0/tests/batch/test_no_train_command.py +34 -0
  6. dh_cli-0.12.0/tests/batch/test_submit_mlflow_env.py +196 -0
  7. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_disco_jobdef.py +16 -5
  8. dh_cli-0.11.2/src/dh_cli/batch/commands/train.py +0 -725
  9. {dh_cli-0.11.2 → dh_cli-0.12.0}/.gitignore +0 -0
  10. {dh_cli-0.11.2 → dh_cli-0.12.0}/LICENSE +0 -0
  11. {dh_cli-0.11.2 → dh_cli-0.12.0}/README.md +0 -0
  12. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/__init__.py +0 -0
  13. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/_identity.py +0 -0
  14. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/aws_batch.py +0 -0
  15. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/__init__.py +0 -0
  16. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/boltz.py +0 -0
  17. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/cancel.py +0 -0
  18. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/clean.py +0 -0
  19. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/disco.py +0 -0
  20. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  21. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/equilibrator.py +0 -0
  22. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/esmfold2.py +0 -0
  23. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/finalize.py +0 -0
  24. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  25. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/local.py +0 -0
  26. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/logs.py +0 -0
  27. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/orca.py +0 -0
  28. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  29. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  30. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/retry.py +0 -0
  31. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/rfd3.py +0 -0
  32. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/status.py +0 -0
  33. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/commands/wait_for.py +0 -0
  34. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/fasta_utils.py +0 -0
  35. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/h5_utils.py +0 -0
  36. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/job_id.py +0 -0
  37. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/manifest.py +0 -0
  38. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/batch/s3_transport.py +0 -0
  39. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/__init__.py +0 -0
  40. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/commands.py +0 -0
  41. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/cost_report.py +0 -0
  42. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/bedrock/pricing.yaml +0 -0
  43. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/cloud_commands.py +0 -0
  44. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/codeartifact.py +0 -0
  45. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/__init__.py +0 -0
  46. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/api_client.py +0 -0
  47. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/auth.py +0 -0
  48. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  49. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/progress.py +0 -0
  50. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  51. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  52. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/github_commands.py +0 -0
  53. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/__init__.py +0 -0
  54. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/deploy.py +0 -0
  55. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/local.py +0 -0
  56. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/test.py +0 -0
  57. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/tf.py +0 -0
  58. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/hz/users.py +0 -0
  59. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/main.py +0 -0
  60. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/utility_commands.py +0 -0
  61. {dh_cli-0.11.2 → dh_cli-0.12.0}/src/dh_cli/warehouse.py +0 -0
  62. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/__init__.py +0 -0
  63. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_aws_batch_resources.py +0 -0
  64. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_image_override.py +0 -0
  65. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_submit_cpu_only.py +0 -0
  66. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_submit_image_validation.py +0 -0
  67. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/batch/test_submit_merge.py +0 -0
  68. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/conftest.py +0 -0
  69. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/A_cache_write.json +0 -0
  70. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/B_cache_read.json +0 -0
  71. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/C_plain.json +0 -0
  72. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
  73. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/E_service_role.json +0 -0
  74. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
  75. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
  76. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_build_report.py +0 -0
  77. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_classify_arn.py +0 -0
  78. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cli_exit_codes.py +0 -0
  79. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cost_calc.py +0 -0
  80. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cost_command.py +0 -0
  81. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_cur_reconciliation.py +0 -0
  82. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_key_command.py +0 -0
  83. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_render_formats.py +0 -0
  84. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_resolve_base_model.py +0 -0
  85. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_rotate.py +0 -0
  86. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/bedrock/test_s3_walker.py +0 -0
  87. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/__init__.py +0 -0
  88. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/conftest.py +0 -0
  89. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
  90. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_identity.py +0 -0
  91. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_login.py +0 -0
  92. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_login_error_paths.py +0 -0
  93. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_login_security.py +0 -0
  94. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_logout.py +0 -0
  95. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_rotate.py +0 -0
  96. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/github/test_status.py +0 -0
  97. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/hz/test_init.py +0 -0
  98. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/hz/test_suites.py +0 -0
  99. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/hz/test_users.py +0 -0
  100. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_cloud_gcp.py +0 -0
  101. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_disco_command.py +0 -0
  102. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_equilibrator_command.py +0 -0
  103. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_esmfold2_cli.py +0 -0
  104. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_boltz_tar.py +0 -0
  105. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_disco.py +0 -0
  106. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_disco_dispatch.py +0 -0
  107. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_equilibrator.py +0 -0
  108. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_esmfold2.py +0 -0
  109. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_protmpnn.py +0 -0
  110. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_finalize_rfd3.py +0 -0
  111. {dh_cli-0.11.2 → dh_cli-0.12.0}/tests/test_rfd3_command.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dh-cli
3
- Version: 0.11.2
3
+ Version: 0.12.0
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.11.2"
7
+ version = "0.12.0"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -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
- raise RuntimeError("could not locate the dma workspace root")
42
+ return None
38
43
 
39
44
 
40
45
  DMA_ROOT = _find_dma_root()
41
- TF_DIR = DMA_ROOT / "blueprints" / "terraform" / "environments" / "dev"
42
- TF_FILE = TF_DIR / "batch_job_definitions.tf"
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: