dh-cli 0.8.6__tar.gz → 0.8.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {dh_cli-0.8.6 → dh_cli-0.8.7}/PKG-INFO +1 -1
  2. {dh_cli-0.8.6 → dh_cli-0.8.7}/pyproject.toml +1 -1
  3. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/bedrock/commands.py +14 -28
  4. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_key_command.py +47 -20
  5. {dh_cli-0.8.6 → dh_cli-0.8.7}/.gitignore +0 -0
  6. {dh_cli-0.8.6 → dh_cli-0.8.7}/LICENSE +0 -0
  7. {dh_cli-0.8.6 → dh_cli-0.8.7}/README.md +0 -0
  8. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/__init__.py +0 -0
  9. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/_identity.py +0 -0
  10. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/__init__.py +0 -0
  11. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/aws_batch.py +0 -0
  12. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/__init__.py +0 -0
  13. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/boltz.py +0 -0
  14. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/cancel.py +0 -0
  15. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/clean.py +0 -0
  16. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  17. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/finalize.py +0 -0
  18. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  19. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/local.py +0 -0
  20. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/logs.py +0 -0
  21. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/orca.py +0 -0
  22. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  23. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  24. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/retry.py +0 -0
  25. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/status.py +0 -0
  26. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/submit.py +0 -0
  27. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/train.py +0 -0
  28. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/commands/wait_for.py +0 -0
  29. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/fasta_utils.py +0 -0
  30. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/h5_utils.py +0 -0
  31. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/job_id.py +0 -0
  32. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/manifest.py +0 -0
  33. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/batch/s3_transport.py +0 -0
  34. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/bedrock/__init__.py +0 -0
  35. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/bedrock/cost_report.py +0 -0
  36. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/bedrock/pricing.yaml +0 -0
  37. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/cloud_commands.py +0 -0
  38. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/codeartifact.py +0 -0
  39. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/engines_studios/__init__.py +0 -0
  40. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/engines_studios/api_client.py +0 -0
  41. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/engines_studios/auth.py +0 -0
  42. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  43. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/engines_studios/progress.py +0 -0
  44. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  45. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  46. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/github_commands.py +0 -0
  47. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/hz/__init__.py +0 -0
  48. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/hz/deploy.py +0 -0
  49. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/hz/local.py +0 -0
  50. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/hz/test.py +0 -0
  51. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/hz/tf.py +0 -0
  52. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/hz/users.py +0 -0
  53. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/main.py +0 -0
  54. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/utility_commands.py +0 -0
  55. {dh_cli-0.8.6 → dh_cli-0.8.7}/src/dh_cli/warehouse.py +0 -0
  56. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/batch/__init__.py +0 -0
  57. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/batch/test_aws_batch_resources.py +0 -0
  58. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/batch/test_image_override.py +0 -0
  59. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/batch/test_submit_cpu_only.py +0 -0
  60. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/batch/test_submit_image_validation.py +0 -0
  61. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/batch/test_submit_merge.py +0 -0
  62. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/conftest.py +0 -0
  63. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/fixtures/A_cache_write.json +0 -0
  64. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/fixtures/B_cache_read.json +0 -0
  65. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/fixtures/C_plain.json +0 -0
  66. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
  67. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/fixtures/E_service_role.json +0 -0
  68. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
  69. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
  70. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_build_report.py +0 -0
  71. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_classify_arn.py +0 -0
  72. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_cli_exit_codes.py +0 -0
  73. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_cost_calc.py +0 -0
  74. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_cost_command.py +0 -0
  75. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_cur_reconciliation.py +0 -0
  76. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_render_formats.py +0 -0
  77. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_resolve_base_model.py +0 -0
  78. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/bedrock/test_s3_walker.py +0 -0
  79. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/__init__.py +0 -0
  80. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/conftest.py +0 -0
  81. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
  82. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_identity.py +0 -0
  83. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_login.py +0 -0
  84. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_login_error_paths.py +0 -0
  85. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_login_security.py +0 -0
  86. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_logout.py +0 -0
  87. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_rotate.py +0 -0
  88. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/github/test_status.py +0 -0
  89. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/hz/test_init.py +0 -0
  90. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/hz/test_suites.py +0 -0
  91. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/hz/test_users.py +0 -0
  92. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/test_cloud_gcp.py +0 -0
  93. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/test_finalize_boltz_tar.py +0 -0
  94. {dh_cli-0.8.6 → dh_cli-0.8.7}/tests/test_finalize_protmpnn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dh-cli
3
- Version: 0.8.6
3
+ Version: 0.8.7
4
4
  Summary: Dayhoff Labs developer CLI
5
5
  Author-email: Dayhoff Labs <dev@dayhofflabs.com>
6
6
  License: # PolyForm Noncommercial License 1.0.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dh-cli"
7
- version = "0.8.6"
7
+ version = "0.8.7"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -22,6 +22,8 @@ from typing import Optional
22
22
 
23
23
  import click
24
24
 
25
+ from dh_cli._identity import HandleResolutionError, resolve_handle_from_session
26
+
25
27
  from . import cost_report as cr
26
28
 
27
29
  # Hard-coded for the dev account. Kept here (not as user-facing flags)
@@ -58,37 +60,21 @@ def bedrock():
58
60
  # --------------------------------------------------------------------------
59
61
 
60
62
 
61
- def _resolve_handle_from_sts() -> str:
62
- """Pull the developer handle from the current SSO session.
63
-
64
- A DeveloperAccess SSO caller identity looks like::
63
+ def _resolve_handle() -> str:
64
+ """Resolve the developer handle from the current SSO session.
65
65
 
66
- arn:aws:sts::074735440724:assumed-role/AWSReservedSSO_DeveloperAccess_<id>/<handle>
67
-
68
- We return `<handle>`. Raises ClickException if STS fails or the ARN
69
- shape isn't recognisable.
66
+ Thin wrapper around `_identity.resolve_handle_from_session` that
67
+ builds a boto3 Session and turns `HandleResolutionError` into a
68
+ `ClickException` so Click prints it cleanly. Shared with `dh gh`
69
+ via `_identity.py` so both commands behave identically when the
70
+ caller's session can't be classified.
70
71
  """
71
72
  import boto3
72
73
 
73
74
  try:
74
- sts = boto3.client("sts")
75
- arn = sts.get_caller_identity()["Arn"]
76
- except Exception as exc:
77
- raise click.ClickException(
78
- f"Could not call sts:GetCallerIdentity to resolve your handle: {exc}\n"
79
- "Run `awslogin dev-devaccess` (or pass --handle explicitly)."
80
- )
81
-
82
- principal = cr.classify_arn(arn)
83
- # Accept any AWSReservedSSO_* session: DeveloperAccess,
84
- # DevAdminAccess, SecurityAdministrator, etc. all carry the
85
- # IdC username as the role-session-name, which is what we want
86
- # as the developer handle.
87
- if principal.principal_type in ("claude-code", "claude-code-other", "cursor"):
88
- return principal.principal_name
89
- raise click.ClickException(
90
- f"Couldn't infer a developer handle from your identity ({arn}). Pass --handle explicitly."
91
- )
75
+ return resolve_handle_from_session(boto3.Session())
76
+ except HandleResolutionError as exc:
77
+ raise click.ClickException(str(exc))
92
78
 
93
79
 
94
80
  # --------------------------------------------------------------------------
@@ -135,7 +121,7 @@ def bedrock_key(handle: Optional[str], region: str, mode: str):
135
121
  """
136
122
  import boto3
137
123
 
138
- resolved = handle or _resolve_handle_from_sts()
124
+ resolved = handle or _resolve_handle()
139
125
  secret_id = f"{_SECRET_PREFIX}{resolved}"
140
126
 
141
127
  try:
@@ -316,7 +302,7 @@ def bedrock_cost(
316
302
 
317
303
  my_handle: Optional[str] = None
318
304
  if me:
319
- my_handle = _resolve_handle_from_sts()
305
+ my_handle = _resolve_handle()
320
306
 
321
307
  try:
322
308
  records = cr.walk_logs(
@@ -32,15 +32,32 @@ def _build_sts_stub(arn: str = _DEV_ARN) -> MagicMock:
32
32
  return sts
33
33
 
34
34
 
35
+ def _build_session_stub(sts: MagicMock) -> MagicMock:
36
+ """boto3.Session() stub whose .client('sts') returns `sts`.
37
+
38
+ Handle resolution flows through `_identity.resolve_handle_from_session`,
39
+ which calls `session.client('sts')`. The Secrets Manager fetch in
40
+ `bedrock_key` still goes through `boto3.client('secretsmanager', ...)`
41
+ directly, which is patched separately.
42
+ """
43
+ session = MagicMock()
44
+ session.client.return_value = sts
45
+ return session
46
+
47
+
35
48
  def _patch_boto3(*, sts=None, sm=None):
49
+ sts_stub = sts or _build_sts_stub()
50
+ session_stub = _build_session_stub(sts_stub)
51
+
36
52
  def _client(name, **_kwargs):
37
- if name == "sts":
38
- return sts or _build_sts_stub()
39
53
  if name == "secretsmanager":
40
54
  return sm or _build_sm_stub()
41
- raise AssertionError(f"unexpected boto3 client: {name}")
55
+ raise AssertionError(f"unexpected boto3.client call: {name}")
42
56
 
43
- return patch("boto3.client", side_effect=_client)
57
+ return (
58
+ patch("boto3.Session", return_value=session_stub),
59
+ patch("boto3.client", side_effect=_client),
60
+ )
44
61
 
45
62
 
46
63
  def _invoke(args):
@@ -52,7 +69,8 @@ def _invoke(args):
52
69
 
53
70
  def test_key_default_prints_json_and_resolves_handle_from_sts():
54
71
  sm = _build_sm_stub()
55
- with _patch_boto3(sts=_build_sts_stub(), sm=sm):
72
+ p_sess, p_cli = _patch_boto3(sts=_build_sts_stub(), sm=sm)
73
+ with p_sess, p_cli:
56
74
  result = _invoke([])
57
75
  assert result.exit_code == 0, result.output
58
76
  sm.get_secret_value.assert_called_once_with(SecretId="cursor-bedrock/dma")
@@ -66,26 +84,28 @@ def test_key_handle_override_skips_sts():
66
84
  can still fetch (for e.g. break-glass debugging)."""
67
85
  sm = _build_sm_stub()
68
86
 
69
- called: dict[str, int] = {"sts": 0}
87
+ called: dict[str, int] = {"session": 0}
88
+
89
+ def _session_factory(*_args, **_kwargs):
90
+ called["session"] += 1
91
+ return _build_session_stub(_build_sts_stub("arn:aws:iam::074735440724:root"))
70
92
 
71
93
  def _client(name, **_kwargs):
72
- if name == "sts":
73
- called["sts"] += 1
74
- return _build_sts_stub("arn:aws:iam::074735440724:root")
75
94
  if name == "secretsmanager":
76
95
  return sm
77
96
  raise AssertionError(name)
78
97
 
79
- with patch("boto3.client", side_effect=_client):
98
+ with patch("boto3.Session", side_effect=_session_factory), patch("boto3.client", side_effect=_client):
80
99
  result = _invoke(["--handle", "jason"])
81
100
  assert result.exit_code == 0, result.output
82
- assert called["sts"] == 0
101
+ assert called["session"] == 0
83
102
  sm.get_secret_value.assert_called_once_with(SecretId="cursor-bedrock/jason")
84
103
 
85
104
 
86
105
  def test_key_export_mode_prints_shell_exports():
87
106
  sm = _build_sm_stub()
88
- with _patch_boto3(sm=sm):
107
+ p_sess, p_cli = _patch_boto3(sm=sm)
108
+ with p_sess, p_cli:
89
109
  result = _invoke(["--export"])
90
110
  assert result.exit_code == 0, result.output
91
111
  out = result.output
@@ -96,7 +116,8 @@ def test_key_export_mode_prints_shell_exports():
96
116
 
97
117
  def test_key_secret_fetch_failure_exits_non_zero():
98
118
  sm = _build_sm_stub(RuntimeError("AccessDeniedException: secretsmanager:GetSecretValue"))
99
- with _patch_boto3(sm=sm):
119
+ p_sess, p_cli = _patch_boto3(sm=sm)
120
+ with p_sess, p_cli:
100
121
  result = _invoke(["--handle", "nobody"])
101
122
  assert result.exit_code != 0
102
123
  assert "Could not read secret" in result.output
@@ -104,24 +125,30 @@ def test_key_secret_fetch_failure_exits_non_zero():
104
125
 
105
126
  def test_key_unrecognised_caller_arn_exits_with_hint():
106
127
  """If STS returns something we can't classify, the error message must
107
- tell the user to pass --handle explicitly instead of silently guessing."""
128
+ tell the user to pass --handle explicitly instead of silently guessing.
129
+
130
+ Message comes from `_identity.HandleResolutionError` (shared with
131
+ `dh gh login`), so both commands surface the same hint."""
108
132
  sts = _build_sts_stub("arn:aws:iam::074735440724:federated-user/weird")
109
- with _patch_boto3(sts=sts):
133
+ p_sess, p_cli = _patch_boto3(sts=sts)
134
+ with p_sess, p_cli:
110
135
  result = _invoke([])
111
136
  assert result.exit_code != 0
112
137
  assert "--handle" in result.output
138
+ assert "awslogin" in result.output
113
139
 
114
140
 
115
141
  def test_key_resolves_handle_from_dev_admin_access_session():
116
142
  """DevAdminAccess (and any other AWSReservedSSO_* role) must resolve
117
143
  to the session-name as the developer handle. Regression: earlier the
118
144
  gate only accepted DeveloperAccess sessions, which broke `dh bedrock
119
- key` for anyone running under an admin profile."""
120
- sts = _build_sts_stub(
121
- "arn:aws:sts::074735440724:assumed-role/AWSReservedSSO_DevAdminAccess_2506d659c73583cc/dma"
122
- )
145
+ key` for anyone running under an admin profile. The shared
146
+ `_identity` helper uses a regex that matches any `AWSReservedSSO_*`
147
+ role by construction."""
148
+ sts = _build_sts_stub("arn:aws:sts::074735440724:assumed-role/AWSReservedSSO_DevAdminAccess_2506d659c73583cc/dma")
123
149
  sm = _build_sm_stub()
124
- with _patch_boto3(sts=sts, sm=sm):
150
+ p_sess, p_cli = _patch_boto3(sts=sts, sm=sm)
151
+ with p_sess, p_cli:
125
152
  result = _invoke([])
126
153
  assert result.exit_code == 0, result.output
127
154
  sm.get_secret_value.assert_called_once_with(SecretId="cursor-bedrock/dma")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes