dh-cli 0.8.1__tar.gz → 0.8.2__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 (91) hide show
  1. {dh_cli-0.8.1 → dh_cli-0.8.2}/PKG-INFO +1 -1
  2. {dh_cli-0.8.1 → dh_cli-0.8.2}/pyproject.toml +1 -1
  3. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/bedrock/commands.py +4 -1
  4. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/bedrock/cost_report.py +45 -11
  5. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/batch/test_submit_merge.py +5 -15
  6. {dh_cli-0.8.1 → dh_cli-0.8.2}/.gitignore +0 -0
  7. {dh_cli-0.8.1 → dh_cli-0.8.2}/LICENSE +0 -0
  8. {dh_cli-0.8.1 → dh_cli-0.8.2}/README.md +0 -0
  9. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/__init__.py +0 -0
  10. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/_identity.py +0 -0
  11. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/__init__.py +0 -0
  12. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/aws_batch.py +0 -0
  13. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/__init__.py +0 -0
  14. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/boltz.py +0 -0
  15. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/cancel.py +0 -0
  16. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/clean.py +0 -0
  17. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  18. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/finalize.py +0 -0
  19. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  20. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/local.py +0 -0
  21. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/logs.py +0 -0
  22. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/orca.py +0 -0
  23. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  24. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  25. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/retry.py +0 -0
  26. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/status.py +0 -0
  27. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/submit.py +0 -0
  28. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/train.py +0 -0
  29. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/commands/wait_for.py +0 -0
  30. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/fasta_utils.py +0 -0
  31. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/h5_utils.py +0 -0
  32. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/job_id.py +0 -0
  33. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/manifest.py +0 -0
  34. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/batch/s3_transport.py +0 -0
  35. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/bedrock/__init__.py +0 -0
  36. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/bedrock/pricing.yaml +0 -0
  37. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/cloud_commands.py +0 -0
  38. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/codeartifact.py +0 -0
  39. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/engines_studios/__init__.py +0 -0
  40. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/engines_studios/api_client.py +0 -0
  41. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/engines_studios/auth.py +0 -0
  42. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  43. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/engines_studios/progress.py +0 -0
  44. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  45. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  46. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/github_commands.py +0 -0
  47. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/hz/__init__.py +0 -0
  48. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/hz/deploy.py +0 -0
  49. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/hz/local.py +0 -0
  50. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/hz/test.py +0 -0
  51. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/hz/tf.py +0 -0
  52. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/hz/users.py +0 -0
  53. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/main.py +0 -0
  54. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/utility_commands.py +0 -0
  55. {dh_cli-0.8.1 → dh_cli-0.8.2}/src/dh_cli/warehouse.py +0 -0
  56. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/batch/__init__.py +0 -0
  57. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/batch/test_aws_batch_resources.py +0 -0
  58. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/batch/test_submit_cpu_only.py +0 -0
  59. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/conftest.py +0 -0
  60. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/fixtures/A_cache_write.json +0 -0
  61. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/fixtures/B_cache_read.json +0 -0
  62. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/fixtures/C_plain.json +0 -0
  63. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
  64. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/fixtures/E_service_role.json +0 -0
  65. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
  66. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
  67. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_build_report.py +0 -0
  68. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_classify_arn.py +0 -0
  69. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_cli_exit_codes.py +0 -0
  70. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_cost_calc.py +0 -0
  71. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_cost_command.py +0 -0
  72. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_cur_reconciliation.py +0 -0
  73. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_key_command.py +0 -0
  74. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_render_formats.py +0 -0
  75. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_resolve_base_model.py +0 -0
  76. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/bedrock/test_s3_walker.py +0 -0
  77. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/__init__.py +0 -0
  78. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/conftest.py +0 -0
  79. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
  80. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_identity.py +0 -0
  81. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_login.py +0 -0
  82. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_login_error_paths.py +0 -0
  83. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_login_security.py +0 -0
  84. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_logout.py +0 -0
  85. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_rotate.py +0 -0
  86. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/github/test_status.py +0 -0
  87. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/hz/test_init.py +0 -0
  88. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/hz/test_suites.py +0 -0
  89. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/hz/test_users.py +0 -0
  90. {dh_cli-0.8.1 → dh_cli-0.8.2}/tests/test_cloud_gcp.py +0 -0
  91. {dh_cli-0.8.1 → dh_cli-0.8.2}/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.1
3
+ Version: 0.8.2
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.1"
7
+ version = "0.8.2"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -304,8 +304,11 @@ def bedrock_cost(
304
304
  sys.exit(1)
305
305
 
306
306
  import boto3
307
+ from botocore.config import Config
307
308
 
308
- s3 = boto3.client("s3")
309
+ # Match the thread pool used by walk_logs so urllib3 doesn't block
310
+ # or warn when many parallel GETs are in flight.
311
+ s3 = boto3.client("s3", config=Config(max_pool_connections=32))
309
312
 
310
313
  my_handle: Optional[str] = None
311
314
  if me:
@@ -27,6 +27,7 @@ from __future__ import annotations
27
27
  import datetime as dt
28
28
  import gzip
29
29
  import json
30
+ from concurrent.futures import ThreadPoolExecutor
30
31
  from dataclasses import dataclass, field
31
32
  from pathlib import Path
32
33
  from typing import Any, Iterable, Iterator
@@ -443,14 +444,46 @@ def walk_logs(
443
444
  region: str,
444
445
  start: dt.date,
445
446
  end: dt.date,
447
+ max_workers: int = 32,
446
448
  ) -> Iterator[dict]:
449
+ """Yield every invocation record in `[start, end]` (inclusive, UTC days).
450
+
451
+ Object GETs are parallelised with a thread pool because each day's
452
+ prefix holds hundreds of tiny (~400-byte) gzipped objects and
453
+ per-request latency dominates wall time. Records within a single
454
+ object are yielded in their original NDJSON order; records *across*
455
+ objects may be reordered — downstream aggregation (`build_report`)
456
+ is order-insensitive.
457
+
458
+ `max_workers` caps in-flight S3 GETs per day. The caller's
459
+ `s3_client` should be configured with `max_pool_connections` >=
460
+ `max_workers` (see `botocore.config.Config`) to avoid urllib3
461
+ connection-pool contention.
462
+ """
447
463
  paginator = s3_client.get_paginator("list_objects_v2")
448
464
  seen_keys: set[str] = set()
465
+
466
+ def _fetch_and_parse(key: str) -> list[dict]:
467
+ body = s3_client.get_object(Bucket=bucket, Key=key)["Body"].read()
468
+ decompressed = gzip.decompress(body)
469
+ out: list[dict] = []
470
+ # Each object is one or more JSON records separated by
471
+ # newlines (NDJSON). Older Bedrock traffic produced
472
+ # one-record objects; multi-record objects appeared in
473
+ # our bucket on 2026-04-20. Parse line-by-line so both
474
+ # shapes work, and tolerate a trailing newline.
475
+ for line in decompressed.splitlines():
476
+ if not line.strip():
477
+ continue
478
+ out.append(json.loads(line))
479
+ return out
480
+
449
481
  for day in _iter_days(start, end):
450
482
  prefix = (
451
483
  f"invocation-logs/AWSLogs/{account}/BedrockModelInvocationLogs/"
452
484
  f"{region}/{day.year:04d}/{day.month:02d}/{day.day:02d}/"
453
485
  )
486
+ keys: list[str] = []
454
487
  for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
455
488
  for obj in page.get("Contents", []) or []:
456
489
  key = obj["Key"]
@@ -461,17 +494,18 @@ def walk_logs(
461
494
  if key in seen_keys:
462
495
  continue
463
496
  seen_keys.add(key)
464
- body = s3_client.get_object(Bucket=bucket, Key=key)["Body"].read()
465
- decompressed = gzip.decompress(body)
466
- # Each object is one or more JSON records separated by
467
- # newlines (NDJSON). Older Bedrock traffic produced
468
- # one-record objects; multi-record objects appeared in
469
- # our bucket on 2026-04-20. Parse line-by-line so both
470
- # shapes work, and tolerate a trailing newline.
471
- for line in decompressed.splitlines():
472
- if not line.strip():
473
- continue
474
- yield json.loads(line)
497
+ keys.append(key)
498
+ if not keys:
499
+ continue
500
+ # One pool per day bounds concurrent in-flight GETs and caps
501
+ # peak memory (at most ~max_workers decompressed objects held
502
+ # at once). ex.map preserves submission order, so the day's
503
+ # records stream out in a stable — though not chronological —
504
+ # order.
505
+ with ThreadPoolExecutor(max_workers=max_workers) as ex:
506
+ for records in ex.map(_fetch_and_parse, keys):
507
+ for rec in records:
508
+ yield rec
475
509
 
476
510
 
477
511
  def reconcile_with_cost_explorer(
@@ -91,9 +91,7 @@ class TestCliWinsWhenExplicitlyPassed:
91
91
  "cli_flag,kwarg,cli_default,yaml_alt,_",
92
92
  MERGE_FIELDS,
93
93
  )
94
- def test_cli_at_default_beats_yaml(
95
- self, cli_runner, tmp_path, cli_flag, kwarg, cli_default, yaml_alt, _
96
- ):
94
+ def test_cli_at_default_beats_yaml(self, cli_runner, tmp_path, cli_flag, kwarg, cli_default, yaml_alt, _):
97
95
  """CLI flag set to the Click default still wins over a different YAML value."""
98
96
  yaml_key = cli_flag.lstrip("-")
99
97
  config_path = tmp_path / "job.yaml"
@@ -112,9 +110,7 @@ class TestCliWinsWhenExplicitlyPassed:
112
110
  "cli_flag,kwarg,_,yaml_alt,cli_alt",
113
111
  MERGE_FIELDS,
114
112
  )
115
- def test_cli_at_non_default_beats_yaml(
116
- self, cli_runner, tmp_path, cli_flag, kwarg, _, yaml_alt, cli_alt
117
- ):
113
+ def test_cli_at_non_default_beats_yaml(self, cli_runner, tmp_path, cli_flag, kwarg, _, yaml_alt, cli_alt):
118
114
  """CLI flag at a non-default value also wins over YAML (regression check)."""
119
115
  yaml_key = cli_flag.lstrip("-")
120
116
  config_path = tmp_path / "job.yaml"
@@ -137,9 +133,7 @@ class TestYamlWinsWhenCliOmitted:
137
133
  "cli_flag,kwarg,_,yaml_alt,__",
138
134
  MERGE_FIELDS,
139
135
  )
140
- def test_yaml_wins_when_cli_not_passed(
141
- self, cli_runner, tmp_path, cli_flag, kwarg, _, yaml_alt, __
142
- ):
136
+ def test_yaml_wins_when_cli_not_passed(self, cli_runner, tmp_path, cli_flag, kwarg, _, yaml_alt, __):
143
137
  yaml_key = cli_flag.lstrip("-")
144
138
  config_path = tmp_path / "job.yaml"
145
139
  config_path.write_text(yaml.dump({"command": "echo hi", yaml_key: yaml_alt}))
@@ -157,9 +151,7 @@ class TestDefaultWhenNeitherSet:
157
151
  "_,kwarg,cli_default,__,___",
158
152
  MERGE_FIELDS,
159
153
  )
160
- def test_click_default_applies(
161
- self, cli_runner, tmp_path, _, kwarg, cli_default, __, ___
162
- ):
154
+ def test_click_default_applies(self, cli_runner, tmp_path, _, kwarg, cli_default, __, ___):
163
155
  result, mock_client = _invoke(cli_runner, ["--command", "echo hi"], tmp_path)
164
156
  assert result.exit_code == 0, result.output
165
157
  call_kwargs = mock_client.submit_job.call_args[1]
@@ -218,9 +210,7 @@ class TestGpuRegressionFromReport:
218
210
 
219
211
  def test_explicit_gpus_one_beats_yaml_gpus_zero(self, cli_runner, tmp_path):
220
212
  config_path = tmp_path / "job.yaml"
221
- config_path.write_text(
222
- yaml.dump({"command": "echo hi", "queue": "t4-1x-spot", "gpus": 0})
223
- )
213
+ config_path.write_text(yaml.dump({"command": "echo hi", "queue": "t4-1x-spot", "gpus": 0}))
224
214
  result, mock_client = _invoke(
225
215
  cli_runner,
226
216
  ["-f", str(config_path), "--gpus", "1"],
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