dh-cli 0.7.0__tar.gz → 0.8.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 (79) hide show
  1. {dh_cli-0.7.0 → dh_cli-0.8.0}/PKG-INFO +1 -1
  2. {dh_cli-0.7.0 → dh_cli-0.8.0}/pyproject.toml +1 -1
  3. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/aws_batch.py +1 -1
  4. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/submit.py +3 -2
  5. dh_cli-0.8.0/tests/batch/__init__.py +0 -0
  6. dh_cli-0.8.0/tests/batch/test_aws_batch_resources.py +92 -0
  7. dh_cli-0.8.0/tests/batch/test_submit_cpu_only.py +162 -0
  8. {dh_cli-0.7.0 → dh_cli-0.8.0}/.gitignore +0 -0
  9. {dh_cli-0.7.0 → dh_cli-0.8.0}/LICENSE +0 -0
  10. {dh_cli-0.7.0 → dh_cli-0.8.0}/README.md +0 -0
  11. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/__init__.py +0 -0
  12. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/__init__.py +0 -0
  13. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/__init__.py +0 -0
  14. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/boltz.py +0 -0
  15. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/cancel.py +0 -0
  16. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/clean.py +0 -0
  17. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  18. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/finalize.py +0 -0
  19. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  20. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/local.py +0 -0
  21. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/logs.py +0 -0
  22. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/orca.py +0 -0
  23. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  24. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  25. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/retry.py +0 -0
  26. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/status.py +0 -0
  27. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/train.py +0 -0
  28. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/commands/wait_for.py +0 -0
  29. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/fasta_utils.py +0 -0
  30. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/h5_utils.py +0 -0
  31. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/job_id.py +0 -0
  32. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/manifest.py +0 -0
  33. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/batch/s3_transport.py +0 -0
  34. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/bedrock/__init__.py +0 -0
  35. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/bedrock/commands.py +0 -0
  36. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/bedrock/cost_report.py +0 -0
  37. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/bedrock/pricing.yaml +0 -0
  38. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/cloud_commands.py +0 -0
  39. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/codeartifact.py +0 -0
  40. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/engines_studios/__init__.py +0 -0
  41. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/engines_studios/api_client.py +0 -0
  42. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/engines_studios/auth.py +0 -0
  43. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  44. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/engines_studios/progress.py +0 -0
  45. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  46. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  47. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/github_commands.py +0 -0
  48. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/hz/__init__.py +0 -0
  49. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/hz/deploy.py +0 -0
  50. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/hz/local.py +0 -0
  51. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/hz/test.py +0 -0
  52. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/hz/tf.py +0 -0
  53. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/hz/users.py +0 -0
  54. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/main.py +0 -0
  55. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/utility_commands.py +0 -0
  56. {dh_cli-0.7.0 → dh_cli-0.8.0}/src/dh_cli/warehouse.py +0 -0
  57. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/conftest.py +0 -0
  58. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/fixtures/A_cache_write.json +0 -0
  59. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/fixtures/B_cache_read.json +0 -0
  60. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/fixtures/C_plain.json +0 -0
  61. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
  62. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/fixtures/E_service_role.json +0 -0
  63. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
  64. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
  65. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_build_report.py +0 -0
  66. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_classify_arn.py +0 -0
  67. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_cli_exit_codes.py +0 -0
  68. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_cost_calc.py +0 -0
  69. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_cost_command.py +0 -0
  70. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_cur_reconciliation.py +0 -0
  71. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_key_command.py +0 -0
  72. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_render_formats.py +0 -0
  73. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_resolve_base_model.py +0 -0
  74. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/bedrock/test_s3_walker.py +0 -0
  75. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/hz/test_init.py +0 -0
  76. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/hz/test_suites.py +0 -0
  77. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/hz/test_users.py +0 -0
  78. {dh_cli-0.7.0 → dh_cli-0.8.0}/tests/test_cloud_gcp.py +0 -0
  79. {dh_cli-0.7.0 → dh_cli-0.8.0}/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.7.0
3
+ Version: 0.8.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.7.0"
7
+ version = "0.8.0"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -124,7 +124,7 @@ class BatchClient:
124
124
  resource_requirements.append({"type": "VCPU", "value": str(vcpus)})
125
125
  if memory_mb is not None:
126
126
  resource_requirements.append({"type": "MEMORY", "value": str(memory_mb)})
127
- if gpus is not None:
127
+ if gpus not in (None, 0):
128
128
  resource_requirements.append({"type": "GPU", "value": str(gpus)})
129
129
 
130
130
  container_overrides: dict[str, Any] = {}
@@ -71,7 +71,7 @@ def submit(
71
71
  queue: t4-1x-spot
72
72
  memory: 30G
73
73
  vcpus: 8
74
- gpus: 1
74
+ gpus: 1 # omit, set to 0, or null for CPU-only
75
75
  array: 10
76
76
  retry: 3
77
77
  timeout: 6h
@@ -113,11 +113,12 @@ def submit(
113
113
  timeout_seconds = _parse_timeout(job_timeout)
114
114
 
115
115
  # Show plan
116
+ gpus_display = "no GPUs" if job_gpus in (None, 0) else f"{job_gpus} GPUs"
116
117
  click.echo()
117
118
  click.echo(f"Job ID: {job_id}")
118
119
  click.echo(f"Command: {job_command}")
119
120
  click.echo(f"Queue: {job_queue}")
120
- click.echo(f"Resources: {job_vcpus} vCPUs, {job_memory} memory, {job_gpus} GPUs")
121
+ click.echo(f"Resources: {job_vcpus} vCPUs, {job_memory} memory, {gpus_display}")
121
122
  click.echo(f"Array Size: {job_array}")
122
123
  click.echo(f"Retry: {job_retry}")
123
124
  click.echo(f"Timeout: {job_timeout} ({timeout_seconds}s)")
File without changes
@@ -0,0 +1,92 @@
1
+ """Tests for BatchClient resource-requirement construction.
2
+
3
+ These tests mock only boto3.client("batch") and assert on the
4
+ submit_args passed to submit_job. They pin the GPU-handling contract:
5
+ gpus=None and gpus=0 both mean "no GPU requirement"; positive values
6
+ are forwarded verbatim.
7
+ """
8
+
9
+ from unittest.mock import MagicMock, patch
10
+
11
+
12
+ def _make_client():
13
+ """Construct a BatchClient with mocked boto3 batch/logs clients."""
14
+ with patch("dh_cli.batch.aws_batch.boto3") as mock_boto3:
15
+ mock_batch = MagicMock()
16
+ mock_logs = MagicMock()
17
+ mock_boto3.client.side_effect = [mock_batch, mock_logs]
18
+ mock_batch.submit_job.return_value = {"jobId": "aws-uuid"}
19
+
20
+ from dh_cli.batch.aws_batch import BatchClient
21
+
22
+ client = BatchClient()
23
+ return client, mock_batch
24
+
25
+
26
+ def _get_gpu_resource(submit_args):
27
+ """Extract the GPU resourceRequirement entry from submit_args, or None."""
28
+ overrides = submit_args.get("containerOverrides", {})
29
+ for req in overrides.get("resourceRequirements", []):
30
+ if req.get("type") == "GPU":
31
+ return req
32
+ return None
33
+
34
+
35
+ class TestGpuResourceRequirement:
36
+ """gpus=None and gpus=0 must both omit the GPU resource requirement."""
37
+
38
+ def test_gpus_none_omits_gpu_resource(self):
39
+ client, mock_batch = _make_client()
40
+ client.submit_job(
41
+ job_name="j",
42
+ job_definition="dayhoff-generic",
43
+ job_queue="cpu-spot",
44
+ vcpus=4,
45
+ memory_mb=8192,
46
+ gpus=None,
47
+ command=["sh", "-c", "echo hi"],
48
+ )
49
+ submit_args = mock_batch.submit_job.call_args[1]
50
+ assert _get_gpu_resource(submit_args) is None
51
+
52
+ def test_gpus_zero_omits_gpu_resource(self):
53
+ client, mock_batch = _make_client()
54
+ client.submit_job(
55
+ job_name="j",
56
+ job_definition="dayhoff-generic",
57
+ job_queue="cpu-spot",
58
+ vcpus=4,
59
+ memory_mb=8192,
60
+ gpus=0,
61
+ command=["sh", "-c", "echo hi"],
62
+ )
63
+ submit_args = mock_batch.submit_job.call_args[1]
64
+ assert _get_gpu_resource(submit_args) is None
65
+
66
+ def test_gpus_one_includes_gpu_resource(self):
67
+ client, mock_batch = _make_client()
68
+ client.submit_job(
69
+ job_name="j",
70
+ job_definition="dayhoff-generic",
71
+ job_queue="t4-1x-spot",
72
+ vcpus=4,
73
+ memory_mb=8192,
74
+ gpus=1,
75
+ command=["sh", "-c", "echo hi"],
76
+ )
77
+ submit_args = mock_batch.submit_job.call_args[1]
78
+ assert _get_gpu_resource(submit_args) == {"type": "GPU", "value": "1"}
79
+
80
+ def test_gpus_two_includes_gpu_resource(self):
81
+ client, mock_batch = _make_client()
82
+ client.submit_job(
83
+ job_name="j",
84
+ job_definition="dayhoff-generic",
85
+ job_queue="t4-1x-spot",
86
+ vcpus=4,
87
+ memory_mb=8192,
88
+ gpus=2,
89
+ command=["sh", "-c", "echo hi"],
90
+ )
91
+ submit_args = mock_batch.submit_job.call_args[1]
92
+ assert _get_gpu_resource(submit_args) == {"type": "GPU", "value": "2"}
@@ -0,0 +1,162 @@
1
+ """Tests for CPU-only submission paths through the CLI.
2
+
3
+ Uses Click's CliRunner with BatchClient.submit_job mocked. Pins the
4
+ contract: users can express "no GPU" via --gpus 0, YAML gpus: 0, or
5
+ YAML gpus: null; the implicit --gpus 1 default still applies when
6
+ nothing is specified; and the dry-run echo reads "no GPUs" rather
7
+ than "None GPUs" or "0 GPUs".
8
+ """
9
+
10
+ from unittest.mock import MagicMock, patch
11
+
12
+ import pytest
13
+ import yaml
14
+ from click.testing import CliRunner
15
+
16
+
17
+ @pytest.fixture
18
+ def cli_runner():
19
+ return CliRunner()
20
+
21
+
22
+ def _invoke(cli_runner, args, tmp_path):
23
+ """Invoke submit with BatchClient mocked and base-path tucked under tmp_path."""
24
+ base = tmp_path / "jobs"
25
+ with (
26
+ patch("dh_cli.batch.commands.submit.get_aws_username", return_value="jason"),
27
+ patch("dh_cli.batch.commands.submit.BatchClient") as mock_batch_cls,
28
+ patch(
29
+ "dh_cli.batch.commands.submit.generate_job_id",
30
+ return_value="jason-batch-20260511-cpu00001",
31
+ ),
32
+ ):
33
+ mock_client = MagicMock()
34
+ mock_client.submit_job.return_value = "aws-uuid-cpu"
35
+ mock_batch_cls.return_value = mock_client
36
+
37
+ from dh_cli.batch.commands.submit import submit
38
+
39
+ result = cli_runner.invoke(submit, args + ["--base-path", str(base)])
40
+ return result, mock_client
41
+
42
+
43
+ class TestCpuOnlySubmissionPaths:
44
+ """--gpus 0, YAML gpus: 0, and YAML gpus: null must all submit with no GPU."""
45
+
46
+ def test_cli_gpus_zero_sends_no_gpu_requirement(self, cli_runner, tmp_path):
47
+ result, mock_client = _invoke(
48
+ cli_runner,
49
+ ["--command", "echo hi", "--queue", "cpu-spot", "--gpus", "0"],
50
+ tmp_path,
51
+ )
52
+ assert result.exit_code == 0, result.output
53
+ call_kwargs = mock_client.submit_job.call_args[1]
54
+ assert call_kwargs["gpus"] == 0
55
+
56
+ def test_yaml_gpus_zero_sends_no_gpu_requirement(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
+ "queue": "cpu-spot",
63
+ "gpus": 0,
64
+ }
65
+ )
66
+ )
67
+ result, mock_client = _invoke(cli_runner, ["-f", str(config_path)], tmp_path)
68
+ assert result.exit_code == 0, result.output
69
+ call_kwargs = mock_client.submit_job.call_args[1]
70
+ assert call_kwargs["gpus"] == 0
71
+
72
+ def test_yaml_gpus_null_sends_no_gpu_requirement(self, cli_runner, tmp_path):
73
+ config_path = tmp_path / "job.yaml"
74
+ config_path.write_text(
75
+ yaml.dump(
76
+ {
77
+ "command": "echo hi",
78
+ "queue": "cpu-spot",
79
+ "gpus": None,
80
+ }
81
+ )
82
+ )
83
+ result, mock_client = _invoke(cli_runner, ["-f", str(config_path)], tmp_path)
84
+ assert result.exit_code == 0, result.output
85
+ call_kwargs = mock_client.submit_job.call_args[1]
86
+ assert call_kwargs["gpus"] is None
87
+
88
+
89
+ class TestGpuDefaultPreserved:
90
+ """Omitting --gpus and YAML gpus still gives the implicit GPU=1 default."""
91
+
92
+ def test_cli_gpus_omitted_gets_implicit_one(self, cli_runner, tmp_path):
93
+ result, mock_client = _invoke(cli_runner, ["--command", "echo hi"], tmp_path)
94
+ assert result.exit_code == 0, result.output
95
+ call_kwargs = mock_client.submit_job.call_args[1]
96
+ assert call_kwargs["gpus"] == 1
97
+
98
+ def test_cli_gpus_one_still_works(self, cli_runner, tmp_path):
99
+ result, mock_client = _invoke(cli_runner, ["--command", "echo hi", "--gpus", "1"], tmp_path)
100
+ assert result.exit_code == 0, result.output
101
+ call_kwargs = mock_client.submit_job.call_args[1]
102
+ assert call_kwargs["gpus"] == 1
103
+
104
+ def test_yaml_gpus_one_still_works(self, cli_runner, tmp_path):
105
+ config_path = tmp_path / "job.yaml"
106
+ config_path.write_text(yaml.dump({"command": "echo hi", "gpus": 1}))
107
+ result, mock_client = _invoke(cli_runner, ["-f", str(config_path)], tmp_path)
108
+ assert result.exit_code == 0, result.output
109
+ call_kwargs = mock_client.submit_job.call_args[1]
110
+ assert call_kwargs["gpus"] == 1
111
+
112
+
113
+ class TestCliBeatsYamlOnZero:
114
+ """--gpus 0 on the CLI must beat YAML gpus: 1."""
115
+
116
+ def test_cli_gpus_zero_overrides_yaml_one(self, cli_runner, tmp_path):
117
+ config_path = tmp_path / "job.yaml"
118
+ config_path.write_text(yaml.dump({"command": "echo hi", "gpus": 1}))
119
+ result, mock_client = _invoke(cli_runner, ["-f", str(config_path), "--gpus", "0"], tmp_path)
120
+ assert result.exit_code == 0, result.output
121
+ call_kwargs = mock_client.submit_job.call_args[1]
122
+ assert call_kwargs["gpus"] == 0
123
+
124
+
125
+ class TestDryRunEcho:
126
+ """The dry-run plan line must read 'no GPUs' or 'N GPUs', never 'None'/'0 GPUs'."""
127
+
128
+ def test_dry_run_echo_reads_no_gpus_when_gpus_zero(self, cli_runner, tmp_path):
129
+ result, _ = _invoke(
130
+ cli_runner,
131
+ [
132
+ "--command",
133
+ "echo hi",
134
+ "--queue",
135
+ "cpu-spot",
136
+ "--gpus",
137
+ "0",
138
+ "--dry-run",
139
+ ],
140
+ tmp_path,
141
+ )
142
+ assert result.exit_code == 0, result.output
143
+ assert "no GPUs" in result.output
144
+ assert "None GPUs" not in result.output
145
+ assert "0 GPUs" not in result.output
146
+
147
+ def test_dry_run_echo_reads_no_gpus_when_yaml_gpus_null(self, cli_runner, tmp_path):
148
+ config_path = tmp_path / "job.yaml"
149
+ config_path.write_text(yaml.dump({"command": "echo hi", "queue": "cpu-spot", "gpus": None}))
150
+ result, _ = _invoke(cli_runner, ["-f", str(config_path), "--dry-run"], tmp_path)
151
+ assert result.exit_code == 0, result.output
152
+ assert "no GPUs" in result.output
153
+ assert "None GPUs" not in result.output
154
+
155
+ def test_dry_run_echo_reads_n_gpus_when_gpus_positive(self, cli_runner, tmp_path):
156
+ result, _ = _invoke(
157
+ cli_runner,
158
+ ["--command", "echo hi", "--gpus", "2", "--dry-run"],
159
+ tmp_path,
160
+ )
161
+ assert result.exit_code == 0, result.output
162
+ assert "2 GPUs" in result.output
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