dh-cli 0.13.0__tar.gz → 0.13.3__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 (131) hide show
  1. {dh_cli-0.13.0 → dh_cli-0.13.3}/PKG-INFO +1 -2
  2. {dh_cli-0.13.0 → dh_cli-0.13.3}/pyproject.toml +1 -2
  3. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/cost_report.py +8 -3
  4. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/pricing.yaml +6 -0
  5. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/github_commands.py +48 -18
  6. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/main.py +2 -6
  7. dh_cli-0.13.3/tests/bedrock/test_resolve_base_model.py +98 -0
  8. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_login_error_paths.py +30 -0
  9. dh_cli-0.13.0/src/dh_cli/utility_commands.py +0 -194
  10. dh_cli-0.13.0/tests/bedrock/test_resolve_base_model.py +0 -50
  11. {dh_cli-0.13.0 → dh_cli-0.13.3}/.gitignore +0 -0
  12. {dh_cli-0.13.0 → dh_cli-0.13.3}/LICENSE +0 -0
  13. {dh_cli-0.13.0 → dh_cli-0.13.3}/README.md +0 -0
  14. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/__init__.py +0 -0
  15. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/_identity.py +0 -0
  16. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/__init__.py +0 -0
  17. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/aws_batch.py +0 -0
  18. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/__init__.py +0 -0
  19. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/boltz.py +0 -0
  20. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/cancel.py +0 -0
  21. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/clean.py +0 -0
  22. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/cost.py +0 -0
  23. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/disco.py +0 -0
  24. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  25. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/equilibrator.py +0 -0
  26. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/esmfold2.py +0 -0
  27. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/finalize.py +0 -0
  28. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  29. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/local.py +0 -0
  30. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/logs.py +0 -0
  31. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/orca.py +0 -0
  32. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  33. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  34. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/retry.py +0 -0
  35. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/rfd3.py +0 -0
  36. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/status.py +0 -0
  37. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/submit.py +0 -0
  38. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/commands/wait_for.py +0 -0
  39. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/cost_report.py +0 -0
  40. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/fasta_utils.py +0 -0
  41. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/h5_utils.py +0 -0
  42. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/job_id.py +0 -0
  43. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/manifest.py +0 -0
  44. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/batch/s3_transport.py +0 -0
  45. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/__init__.py +0 -0
  46. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/bedrock/commands.py +0 -0
  47. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/cloud_commands.py +0 -0
  48. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/codeartifact.py +0 -0
  49. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/__init__.py +0 -0
  50. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/api_client.py +0 -0
  51. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/auth.py +0 -0
  52. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  53. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/progress.py +0 -0
  54. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/retry.py +0 -0
  55. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  56. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  57. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/engines_studios/studio_resolve.py +0 -0
  58. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/__init__.py +0 -0
  59. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/deploy.py +0 -0
  60. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/local.py +0 -0
  61. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/test.py +0 -0
  62. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/tf.py +0 -0
  63. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/hz/users.py +0 -0
  64. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/sweep.py +0 -0
  65. {dh_cli-0.13.0 → dh_cli-0.13.3}/src/dh_cli/warehouse.py +0 -0
  66. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/__init__.py +0 -0
  67. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/cur_fixtures.py +0 -0
  68. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_aws_batch_resources.py +0 -0
  69. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_command.py +0 -0
  70. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_filters.py +0 -0
  71. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_reader.py +0 -0
  72. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_reconcile.py +0 -0
  73. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_batch_cost_report.py +0 -0
  74. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_image_override.py +0 -0
  75. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_no_train_command.py +0 -0
  76. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_cpu_only.py +0 -0
  77. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_image_validation.py +0 -0
  78. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_job_tags.py +0 -0
  79. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_merge.py +0 -0
  80. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/batch/test_submit_mlflow_env.py +0 -0
  81. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/conftest.py +0 -0
  82. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/A_cache_write.json +0 -0
  83. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/B_cache_read.json +0 -0
  84. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/C_plain.json +0 -0
  85. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
  86. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/E_service_role.json +0 -0
  87. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
  88. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
  89. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_build_report.py +0 -0
  90. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_classify_arn.py +0 -0
  91. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cli_exit_codes.py +0 -0
  92. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cost_calc.py +0 -0
  93. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cost_command.py +0 -0
  94. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_cur_reconciliation.py +0 -0
  95. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_key_command.py +0 -0
  96. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_render_formats.py +0 -0
  97. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_rotate.py +0 -0
  98. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/bedrock/test_s3_walker.py +0 -0
  99. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/__init__.py +0 -0
  100. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_engine_retry_commands.py +0 -0
  101. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_retry.py +0 -0
  102. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_ssh_config.py +0 -0
  103. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_studio_resolution_commands.py +0 -0
  104. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/engines_studios/test_studio_resolve.py +0 -0
  105. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/__init__.py +0 -0
  106. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/conftest.py +0 -0
  107. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
  108. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_identity.py +0 -0
  109. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_login.py +0 -0
  110. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_login_security.py +0 -0
  111. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_logout.py +0 -0
  112. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_rotate.py +0 -0
  113. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/github/test_status.py +0 -0
  114. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/hz/test_init.py +0 -0
  115. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/hz/test_suites.py +0 -0
  116. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/hz/test_users.py +0 -0
  117. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_aws_status.py +0 -0
  118. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_cloud_gcp.py +0 -0
  119. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_disco_command.py +0 -0
  120. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_disco_jobdef.py +0 -0
  121. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_equilibrator_command.py +0 -0
  122. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_esmfold2_cli.py +0 -0
  123. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_boltz_tar.py +0 -0
  124. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_disco.py +0 -0
  125. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_disco_dispatch.py +0 -0
  126. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_equilibrator.py +0 -0
  127. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_esmfold2.py +0 -0
  128. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_protmpnn.py +0 -0
  129. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_finalize_rfd3.py +0 -0
  130. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_rfd3_command.py +0 -0
  131. {dh_cli-0.13.0 → dh_cli-0.13.3}/tests/test_sweep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dh-cli
3
- Version: 0.13.0
3
+ Version: 0.13.3
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
@@ -131,7 +131,6 @@ Requires-Dist: pydantic>=2.0.0
131
131
  Requires-Dist: pyyaml>=6.0
132
132
  Requires-Dist: questionary>=2.0.1
133
133
  Requires-Dist: requests>=2.31.0
134
- Requires-Dist: toml>=0.10
135
134
  Requires-Dist: typer>=0.9.0
136
135
  Provides-Extra: h5
137
136
  Requires-Dist: h5py>=3.0; extra == 'h5'
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dh-cli"
7
- version = "0.13.0"
7
+ version = "0.13.3"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -20,7 +20,6 @@ dependencies = [
20
20
  "pyyaml>=6.0",
21
21
  "questionary>=2.0.1",
22
22
  "requests>=2.31.0",
23
- "toml>=0.10",
24
23
  "typer>=0.9.0",
25
24
  ]
26
25
 
@@ -113,6 +113,7 @@ _BASE_MODELS = (
113
113
  "sonnet-4-5",
114
114
  "haiku-4-5",
115
115
  "fable-5",
116
+ "fable-5-1",
116
117
  )
117
118
 
118
119
 
@@ -122,9 +123,13 @@ def resolve_base_model(model_id: str) -> str:
122
123
  if stripped.startswith(prefix):
123
124
  stripped = stripped[len(prefix) :]
124
125
  break
125
- for base in _BASE_MODELS:
126
- if base in stripped:
127
- return base
126
+ # Longest match wins: one base model's name can be a prefix of another's
127
+ # (`fable-5` inside `claude-fable-5-1`), and a first match would silently
128
+ # price the newer model at the older one's rates. Matching on length rather
129
+ # than position keeps `_BASE_MODELS` order irrelevant.
130
+ matches = [base for base in _BASE_MODELS if base in stripped]
131
+ if matches:
132
+ return max(matches, key=len)
128
133
  raise UnknownModel(f"Unrecognized modelId: {model_id}")
129
134
 
130
135
 
@@ -26,6 +26,12 @@ models:
26
26
  output_per_1m: 50.00
27
27
  cache_write_per_1m: 12.50
28
28
  cache_read_per_1m: 1.00
29
+ fable-5-1:
30
+ input_per_1m: 10.00
31
+ output_per_1m: 50.00
32
+ cache_write_per_1m: 12.50
33
+ # Fable 5.1 cut cache reads 75% vs Fable 5; the only rate that differs.
34
+ cache_read_per_1m: 0.25
29
35
  sonnet-5:
30
36
  input_per_1m: 2.00
31
37
  output_per_1m: 10.00
@@ -41,6 +41,12 @@ NC = "\033[0m"
41
41
  MAX_PAT_AGE_DAYS = 90
42
42
  WARN_PAT_AGE_DAYS = 87
43
43
 
44
+ # Account that holds the github-pat/* secrets (dev). Used only to turn a
45
+ # ResourceNotFound into a "you're in the wrong account" diagnosis; the
46
+ # authoritative grant is the per-secret resource policy in
47
+ # blueprints/terraform/environments/dev/github_pat_secrets.tf.
48
+ PAT_ACCOUNT_ID = "074735440724"
49
+
44
50
  # Path to the gh hosts file (resolved at call time via Path.home()).
45
51
  _HOSTS_YML_RELATIVE = Path(".config") / "gh" / "hosts.yml"
46
52
 
@@ -178,6 +184,46 @@ def _access_denied_diagnosis(session, requested_handle: str) -> None:
178
184
  raise typer.Exit(1)
179
185
 
180
186
 
187
+ def _not_found_diagnosis(session, secret_id: str) -> None:
188
+ """Explain a ResourceNotFound on the PAT secret. Always exits 1.
189
+
190
+ A not-found is far more often a wrong-account session than
191
+ un-provisioned Terraform: `awslogin <admin-profile>` persists
192
+ AWS_PROFILE into ~/.bashrc, so every subsequent shell talks to that
193
+ account, where `github-pat/*` genuinely does not exist. Name the
194
+ account mismatch before sending anyone to an admin.
195
+ """
196
+ try:
197
+ arn = session.client("sts").get_caller_identity()["Arn"]
198
+ # arn:aws:sts::<account>:assumed-role/... — account is field 4.
199
+ # Parsed from the ARN rather than GetCallerIdentity's "Account"
200
+ # because the ARN is the identity fact this module already
201
+ # trusts for handle resolution (see _identity.py).
202
+ account = arn.split(":")[4]
203
+ except Exception: # noqa: BLE001 — diagnosis must not mask the original error
204
+ account = ""
205
+
206
+ if account and account != PAT_ACCOUNT_ID:
207
+ typer.echo(
208
+ RED
209
+ + f"Secret `{secret_id}` not found — your session is in account "
210
+ + f"{account}, but GitHub PATs live in {PAT_ACCOUNT_ID} (dev). "
211
+ + "Run `awslogin dev-devaccess` and retry."
212
+ + NC,
213
+ err=True,
214
+ )
215
+ else:
216
+ typer.echo(
217
+ RED
218
+ + f"GitHub PAT secret `{secret_id}` does not exist. "
219
+ + "Ask an admin to run the Terraform that provisions "
220
+ + "per-developer github_pat_secrets."
221
+ + NC,
222
+ err=True,
223
+ )
224
+ raise typer.Exit(1)
225
+
226
+
181
227
  def _fetch_pat_and_metadata(session, handle: str) -> Tuple[str, dt.datetime]:
182
228
  """Fetch the PAT and its CreatedDate from Secrets Manager.
183
229
 
@@ -192,15 +238,7 @@ def _fetch_pat_and_metadata(session, handle: str) -> Tuple[str, dt.datetime]:
192
238
  try:
193
239
  describe = sm.describe_secret(SecretId=secret_id)
194
240
  except sm.exceptions.ResourceNotFoundException:
195
- typer.echo(
196
- RED
197
- + f"GitHub PAT secret `{secret_id}` does not exist. "
198
- + "Ask an admin to run the Terraform that provisions "
199
- + "per-developer github_pat_secrets."
200
- + NC,
201
- err=True,
202
- )
203
- raise typer.Exit(1)
241
+ _not_found_diagnosis(session, secret_id)
204
242
  except botocore.exceptions.ClientError as exc:
205
243
  code = exc.response.get("Error", {}).get("Code", "")
206
244
  if code == "AccessDeniedException":
@@ -623,15 +661,7 @@ def gh_rotate(
623
661
  try:
624
662
  describe = sm.describe_secret(SecretId=secret_id)
625
663
  except sm.exceptions.ResourceNotFoundException:
626
- typer.echo(
627
- RED
628
- + f"GitHub PAT secret `{secret_id}` does not exist. "
629
- + "Ask an admin to run the Terraform that provisions "
630
- + "per-developer github_pat_secrets."
631
- + NC,
632
- err=True,
633
- )
634
- raise typer.Exit(1)
664
+ _not_found_diagnosis(session, secret_id)
635
665
  except botocore.exceptions.ClientError as exc:
636
666
  code = exc.response.get("Error", {}).get("Code", "")
637
667
  if code == "AccessDeniedException":
@@ -7,7 +7,6 @@ import typer
7
7
  from dh_cli.cloud_commands import aws_app, gcp_app
8
8
  from dh_cli.github_commands import gh_app
9
9
  from dh_cli.sweep import sweep
10
- from dh_cli.utility_commands import build_and_upload_wheel
11
10
  from dh_cli.warehouse import get_from_warehouse_typer
12
11
 
13
12
 
@@ -112,7 +111,7 @@ def _version_option(
112
111
  @app.command("wheel")
113
112
  def wheel_command(
114
113
  target: str = typer.Argument(
115
- help="What to publish: 'cli', 'batch', 'embedders', 'chem', or 'legacy' (PyPI).",
114
+ help="What to publish: 'cli', 'batch', 'embedders', or 'chem'.",
116
115
  ),
117
116
  bump: str = typer.Option(
118
117
  "patch",
@@ -146,7 +145,6 @@ def wheel_command(
146
145
  batch Build and publish dh-batch to CodeArtifact
147
146
  embedders Build and publish dh-embedders to CodeArtifact
148
147
  chem Build and publish dh-chem to CodeArtifact
149
- legacy Build and publish dayhoff-tools to PyPI (default)
150
148
  """
151
149
  from dh_cli.codeartifact import PACKAGE_DIRS
152
150
 
@@ -160,10 +158,8 @@ def wheel_command(
160
158
  dry_run=dry_run,
161
159
  skip_pypi=no_pypi,
162
160
  )
163
- elif target == "legacy":
164
- build_and_upload_wheel(bump_part=bump)
165
161
  else:
166
- typer.echo(f"Unknown target '{target}'. Use 'cli', 'batch', 'embedders', 'chem', or 'legacy'.")
162
+ typer.echo(f"Unknown target '{target}'. Use 'cli', 'batch', 'embedders', or 'chem'.")
167
163
  raise typer.Exit(1)
168
164
 
169
165
 
@@ -0,0 +1,98 @@
1
+ """Model-ID resolution tests.
2
+
3
+ A Bedrock modelId can appear in three shapes for the same underlying
4
+ model:
5
+ - direct: anthropic.claude-opus-4-7
6
+ - US profile: us.anthropic.claude-opus-4-7
7
+ - Global profile: global.anthropic.claude-opus-4-7
8
+
9
+ `resolve_base_model` must collapse all three to the same pricing key.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import pytest
15
+
16
+
17
+ @pytest.mark.parametrize(
18
+ "model_id, expected_base",
19
+ [
20
+ ("anthropic.claude-opus-4-7", "opus-4-7"),
21
+ ("us.anthropic.claude-opus-4-7", "opus-4-7"),
22
+ ("global.anthropic.claude-opus-4-7", "opus-4-7"),
23
+ ("anthropic.claude-opus-4-8", "opus-4-8"),
24
+ ("us.anthropic.claude-opus-4-8", "opus-4-8"),
25
+ ("global.anthropic.claude-opus-4-8", "opus-4-8"),
26
+ ("anthropic.claude-opus-4-6-v1", "opus-4-6"),
27
+ ("us.anthropic.claude-opus-4-6-v1", "opus-4-6"),
28
+ ("global.anthropic.claude-opus-4-6-v1", "opus-4-6"),
29
+ ("anthropic.claude-sonnet-4-6", "sonnet-4-6"),
30
+ ("us.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
31
+ ("global.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
32
+ ("anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
33
+ ("us.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
34
+ ("global.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
35
+ ("anthropic.claude-fable-5", "fable-5"),
36
+ ("us.anthropic.claude-fable-5", "fable-5"),
37
+ ("global.anthropic.claude-fable-5", "fable-5"),
38
+ # A base model whose name is a prefix of a newer one must not swallow
39
+ # it: `fable-5` is a substring of `claude-fable-5-1`, so a first-match
40
+ # or badly-ordered scan silently prices 5.1 as 5. The two differ only
41
+ # in cache reads ($1.00/M vs $0.25/M), so the error is invisible in
42
+ # every other column.
43
+ ("anthropic.claude-fable-5-1", "fable-5-1"),
44
+ ("us.anthropic.claude-fable-5-1", "fable-5-1"),
45
+ ("global.anthropic.claude-fable-5-1", "fable-5-1"),
46
+ ],
47
+ )
48
+ def test_resolve_base_model_known(model_id: str, expected_base: str):
49
+ from dh_cli.bedrock.cost_report import resolve_base_model
50
+
51
+ assert resolve_base_model(model_id) == expected_base
52
+
53
+
54
+ def test_resolve_prefers_the_most_specific_base():
55
+ """Resolution must not depend on `_BASE_MODELS` ordering.
56
+
57
+ Reversing the tuple would flip a first-match implementation's answer.
58
+ """
59
+ from dh_cli.bedrock import cost_report
60
+
61
+ original = cost_report._BASE_MODELS
62
+ try:
63
+ cost_report._BASE_MODELS = tuple(reversed(original))
64
+ assert cost_report.resolve_base_model("global.anthropic.claude-fable-5-1") == "fable-5-1"
65
+ assert cost_report.resolve_base_model("global.anthropic.claude-fable-5") == "fable-5"
66
+ finally:
67
+ cost_report._BASE_MODELS = original
68
+
69
+
70
+ def test_every_base_model_has_pricing():
71
+ """A resolvable base with no pricing row raises only at report time."""
72
+ from dh_cli.bedrock.cost_report import (
73
+ _BASE_MODELS,
74
+ default_pricing_path,
75
+ load_pricing,
76
+ )
77
+
78
+ pricing = load_pricing(default_pricing_path())
79
+ missing = [base for base in _BASE_MODELS if base not in pricing]
80
+ assert not missing, f"base models with no pricing entry: {missing}"
81
+
82
+
83
+ def test_fable_51_cache_reads_are_priced_below_fable_5():
84
+ """The one rate that differs between the two Fable versions."""
85
+ from dh_cli.bedrock.cost_report import default_pricing_path, load_pricing
86
+
87
+ pricing = load_pricing(default_pricing_path())
88
+ assert pricing["fable-5-1"]["cache_read_per_1m"] == 0.25
89
+ assert pricing["fable-5"]["cache_read_per_1m"] == 1.00
90
+ for rate in ("input_per_1m", "output_per_1m", "cache_write_per_1m"):
91
+ assert pricing["fable-5-1"][rate] == pricing["fable-5"][rate]
92
+
93
+
94
+ def test_resolve_base_model_unknown_raises():
95
+ from dh_cli.bedrock.cost_report import UnknownModel, resolve_base_model
96
+
97
+ with pytest.raises(UnknownModel):
98
+ resolve_base_model("global.anthropic.claude-someshinynew-99-v42")
@@ -80,6 +80,36 @@ def test_login_secret_not_found_tells_user_to_ask_admin(fake_subprocess, patch_g
80
80
  assert "--init" not in combined
81
81
 
82
82
 
83
+ def test_login_secret_not_found_in_wrong_account_says_wrong_account(fake_subprocess, patch_gh_path, tmp_hosts_yml):
84
+ """A not-found in a non-dev account is a wrong-profile error.
85
+
86
+ Regression: `awslogin admin-secadmin` persists AWS_PROFILE into
87
+ ~/.bashrc, so every later shell talks to the admin account, where
88
+ github-pat/* genuinely does not exist. The old message blamed
89
+ un-provisioned Terraform and sent the user to an admin.
90
+ """
91
+ session = make_session(arn="arn:aws:sts::293882909581:assumed-role/AWSReservedSSO_SecurityAdministrator_abc123/dma")
92
+ err_response = {
93
+ "Error": {
94
+ "Code": "ResourceNotFoundException",
95
+ "Message": "Secrets Manager can't find the specified secret.",
96
+ }
97
+ }
98
+ session._sm.describe_secret.side_effect = session._sm.exceptions.ResourceNotFoundException(
99
+ err_response, "DescribeSecret"
100
+ )
101
+
102
+ result = _invoke([], session=session, fake_subprocess=fake_subprocess)
103
+ assert result.exit_code != 0
104
+ combined = _all(result)
105
+ # Names both accounts so the user can see the mismatch.
106
+ assert "293882909581" in combined
107
+ assert "074735440724" in combined
108
+ # Points at the fix, not at an admin.
109
+ assert "awslogin dev-devaccess" in combined
110
+ assert "ask an admin" not in combined.lower()
111
+
112
+
83
113
  def test_login_empty_secret_suggests_rotate_init(fake_subprocess, patch_gh_path, tmp_hosts_yml):
84
114
  # Secret exists but no AWSCURRENT version — new-hire state.
85
115
  session = make_session(version_stages={})
@@ -1,194 +0,0 @@
1
- """CLI commands common to all repos."""
2
-
3
- import os
4
- import re
5
- import shutil
6
- import subprocess
7
- from pathlib import Path
8
-
9
- import toml
10
-
11
- # Import cloud helper lazily inside functions to avoid heavy deps at module load
12
-
13
-
14
- def get_current_version_from_toml(file_path="pyproject.toml"):
15
- """Reads the version from a pyproject.toml file."""
16
- try:
17
- with open(file_path, "r") as f:
18
- content = f.read()
19
- version_match = re.search(r'^version\s*=\s*"([^"]+)"', content, re.MULTILINE)
20
- if version_match:
21
- return version_match.group(1)
22
- else:
23
- raise ValueError(f"Could not find version string in {file_path}")
24
- except FileNotFoundError:
25
- raise FileNotFoundError(f"{file_path} not found.")
26
- except Exception as e:
27
- raise e
28
-
29
-
30
- def build_and_upload_wheel(bump_part: str = "patch"):
31
- """Build a Python wheel and upload to PyPI using UV.
32
-
33
- Automatically increments the version number in pyproject.toml before building
34
- based on the bump_part argument ('major', 'minor', 'patch').
35
-
36
- Expects PyPI authentication to be configured via the environment variable:
37
- - UV_PUBLISH_TOKEN
38
-
39
- Args:
40
- bump_part (str): The part of the version to bump. Defaults to 'patch'.
41
- """
42
- if bump_part not in ["major", "minor", "patch"]:
43
- print(f"Error: Invalid bump_part '{bump_part}'. Must be 'major', 'minor', or 'patch'.")
44
- return
45
-
46
- # ANSI color codes
47
- BLUE = "\033[94m"
48
- RESET = "\033[0m"
49
-
50
- # --- Authentication Setup ---
51
- token = os.environ.get("UV_PUBLISH_TOKEN")
52
-
53
- if not token:
54
- print("Error: PyPI authentication not configured.")
55
- print("Please set the UV_PUBLISH_TOKEN environment variable with your PyPI API token.")
56
- return
57
-
58
- # Build the command with token authentication
59
- # IMPORTANT: Mask token for printing
60
- publish_cmd_safe_print = ["uv", "publish", "--token", "*****"]
61
- publish_cmd = ["uv", "publish", "--token", token]
62
- print("Using UV_PUBLISH_TOKEN for authentication.")
63
-
64
- # Use standard pyproject.toml
65
- pyproject_path = "pyproject.toml"
66
- if not Path(pyproject_path).exists():
67
- print("Error: pyproject.toml not found in current directory.")
68
- return
69
- current_version = None # Initialize in case the first try block fails
70
-
71
- try:
72
- # --- Clean dist directory ---
73
- dist_dir = Path("dist")
74
- if dist_dir.exists():
75
- print(f"Removing existing build directory: {dist_dir}")
76
- shutil.rmtree(dist_dir)
77
- # --- End Clean dist directory ---
78
-
79
- # --- Version Bumping Logic ---
80
- current_version = get_current_version_from_toml(pyproject_path)
81
- print(f"Current version: {current_version}")
82
-
83
- try:
84
- major, minor, patch = map(int, current_version.split("."))
85
- except ValueError:
86
- print(f"Error: Could not parse version '{current_version}'. Expected format X.Y.Z")
87
- return
88
-
89
- if bump_part == "major":
90
- major += 1
91
- minor = 0
92
- patch = 0
93
- elif bump_part == "minor":
94
- minor += 1
95
- patch = 0
96
- else: # patch
97
- patch += 1
98
-
99
- new_version = f"{major}.{minor}.{patch}"
100
- print(f"Bumping {bump_part} version to: {new_version}")
101
-
102
- # Read pyproject.toml
103
- with open(pyproject_path, "r") as f:
104
- content = f.read()
105
-
106
- # Replace the version string
107
- pattern = re.compile(f'^version\s*=\s*"{re.escape(current_version)}"', re.MULTILINE)
108
- new_content, num_replacements = pattern.subn(f'version = "{new_version}"', content)
109
-
110
- if num_replacements == 0:
111
- print(f"Error: Could not find 'version = \"{current_version}\"' in {pyproject_path}")
112
- return # Exit before build/publish if version wasn't updated
113
- if num_replacements > 1:
114
- print(f"Warning: Found multiple version lines for '{current_version}'. Only the first was updated.")
115
-
116
- # Write the updated content back
117
- with open(pyproject_path, "w") as f:
118
- f.write(new_content)
119
- print(f"Updated {pyproject_path} with version {new_version}")
120
-
121
- # --- End Version Bumping Logic ---
122
-
123
- # Build wheel and sdist
124
- build_cmd = ["uv", "build"]
125
- print(f"Running command: {BLUE}{' '.join(build_cmd)}{RESET}")
126
- subprocess.run(build_cmd, check=True)
127
-
128
- # Upload to PyPI
129
- print(f"Running command: {BLUE}{' '.join(publish_cmd_safe_print)}{RESET}")
130
- subprocess.run(publish_cmd, check=True)
131
-
132
- print(f"Successfully built and uploaded version {new_version} to PyPI")
133
-
134
- # Re-install DHT in Pixi environment when building from DHT itself
135
- try:
136
- proj_toml = toml.load(pyproject_path)
137
- proj_name = proj_toml.get("project", {}).get("name")
138
- if proj_name == "dayhoff-tools":
139
- print("Re-installing dayhoff-tools into the Pixi environment...")
140
- # Detect if CUDA is available - use cpu environment if not
141
- has_cuda = shutil.which("nvidia-smi") is not None
142
- if has_cuda:
143
- reinstall_cmd = ["pixi", "install"]
144
- else:
145
- reinstall_cmd = ["pixi", "install", "-e", "cpu"]
146
- print(f"Running command: {BLUE}{' '.join(reinstall_cmd)}{RESET}")
147
- subprocess.run(reinstall_cmd, check=True)
148
- print("dayhoff-tools reinstalled in the Pixi environment.")
149
- except subprocess.CalledProcessError as e:
150
- print(f"Warning: Failed to reinstall dayhoff-tools locally: {e}")
151
- except Exception:
152
- pass # Not dayhoff-tools or couldn't read toml
153
-
154
- except FileNotFoundError:
155
- print(f"Error: {pyproject_path} not found.")
156
- # No version change happened, so no rollback needed
157
- except subprocess.CalledProcessError as e:
158
- print(f"Error during build/upload: {e}")
159
- # Attempt to roll back version change only if it was bumped successfully
160
- if current_version and new_version:
161
- try:
162
- print(f"Attempting to revert version in {pyproject_path} back to {current_version}...")
163
- with open(pyproject_path, "r") as f:
164
- content_revert = f.read()
165
- # Use new_version in pattern for reverting
166
- pattern_revert = re.compile(f'^version\s*=\s*"{re.escape(new_version)}"', re.MULTILINE)
167
- reverted_content, num_revert = pattern_revert.subn(f'version = "{current_version}"', content_revert)
168
- if num_revert > 0:
169
- with open(pyproject_path, "w") as f:
170
- f.write(reverted_content)
171
- print(f"Successfully reverted version in {pyproject_path}.")
172
- else:
173
- print(f"Warning: Could not find version {new_version} to revert in {pyproject_path}.")
174
-
175
- except Exception as revert_e:
176
- print(f"Warning: Failed to revert version change: {revert_e}")
177
- except Exception as e:
178
- print(f"An unexpected error occurred: {e}")
179
- # Attempt rollback if version was bumped
180
- if current_version and "new_version" in locals() and new_version:
181
- try:
182
- print(f"Attempting to revert version back to {current_version}...")
183
- with open(pyproject_path, "r") as f:
184
- content_revert = f.read()
185
- pattern_revert = re.compile(f'^version\\s*=\\s*"{re.escape(new_version)}"', re.MULTILINE)
186
- reverted_content, num_revert = pattern_revert.subn(f'version = "{current_version}"', content_revert)
187
- if num_revert > 0:
188
- with open(pyproject_path, "w") as f:
189
- f.write(reverted_content)
190
- print("Successfully reverted version in pyproject.toml.")
191
- else:
192
- print(f"Warning: Could not find version {new_version} to revert.")
193
- except Exception as revert_e:
194
- print(f"Warning: Failed to revert version change: {revert_e}")
@@ -1,50 +0,0 @@
1
- """Model-ID resolution tests.
2
-
3
- A Bedrock modelId can appear in three shapes for the same underlying
4
- model:
5
- - direct: anthropic.claude-opus-4-7
6
- - US profile: us.anthropic.claude-opus-4-7
7
- - Global profile: global.anthropic.claude-opus-4-7
8
-
9
- `resolve_base_model` must collapse all three to the same pricing key.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import pytest
15
-
16
-
17
- @pytest.mark.parametrize(
18
- "model_id, expected_base",
19
- [
20
- ("anthropic.claude-opus-4-7", "opus-4-7"),
21
- ("us.anthropic.claude-opus-4-7", "opus-4-7"),
22
- ("global.anthropic.claude-opus-4-7", "opus-4-7"),
23
- ("anthropic.claude-opus-4-8", "opus-4-8"),
24
- ("us.anthropic.claude-opus-4-8", "opus-4-8"),
25
- ("global.anthropic.claude-opus-4-8", "opus-4-8"),
26
- ("anthropic.claude-opus-4-6-v1", "opus-4-6"),
27
- ("us.anthropic.claude-opus-4-6-v1", "opus-4-6"),
28
- ("global.anthropic.claude-opus-4-6-v1", "opus-4-6"),
29
- ("anthropic.claude-sonnet-4-6", "sonnet-4-6"),
30
- ("us.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
31
- ("global.anthropic.claude-sonnet-4-6", "sonnet-4-6"),
32
- ("anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
33
- ("us.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
34
- ("global.anthropic.claude-haiku-4-5-20251001-v1:0", "haiku-4-5"),
35
- ("anthropic.claude-fable-5", "fable-5"),
36
- ("us.anthropic.claude-fable-5", "fable-5"),
37
- ("global.anthropic.claude-fable-5", "fable-5"),
38
- ],
39
- )
40
- def test_resolve_base_model_known(model_id: str, expected_base: str):
41
- from dh_cli.bedrock.cost_report import resolve_base_model
42
-
43
- assert resolve_base_model(model_id) == expected_base
44
-
45
-
46
- def test_resolve_base_model_unknown_raises():
47
- from dh_cli.bedrock.cost_report import UnknownModel, resolve_base_model
48
-
49
- with pytest.raises(UnknownModel):
50
- resolve_base_model("global.anthropic.claude-someshinynew-99-v42")
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