dh-cli 0.4.1__tar.gz → 0.4.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 (51) hide show
  1. {dh_cli-0.4.1 → dh_cli-0.4.3}/PKG-INFO +1 -1
  2. {dh_cli-0.4.1 → dh_cli-0.4.3}/pyproject.toml +1 -1
  3. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/__init__.py +42 -0
  4. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/users.py +50 -9
  5. {dh_cli-0.4.1 → dh_cli-0.4.3}/tests/hz/test_users.py +48 -1
  6. {dh_cli-0.4.1 → dh_cli-0.4.3}/.gitignore +0 -0
  7. {dh_cli-0.4.1 → dh_cli-0.4.3}/LICENSE +0 -0
  8. {dh_cli-0.4.1 → dh_cli-0.4.3}/README.md +0 -0
  9. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/__init__.py +0 -0
  10. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/__init__.py +0 -0
  11. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/aws_batch.py +0 -0
  12. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/__init__.py +0 -0
  13. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/boltz.py +0 -0
  14. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/cancel.py +0 -0
  15. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/clean.py +0 -0
  16. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/embed_t5.py +0 -0
  17. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/finalize.py +0 -0
  18. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/list_jobs.py +0 -0
  19. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/local.py +0 -0
  20. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/logs.py +0 -0
  21. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/protmpnn.py +0 -0
  22. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
  23. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/retry.py +0 -0
  24. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/status.py +0 -0
  25. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/submit.py +0 -0
  26. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/train.py +0 -0
  27. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/wait_for.py +0 -0
  28. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/fasta_utils.py +0 -0
  29. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/h5_utils.py +0 -0
  30. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/job_id.py +0 -0
  31. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/manifest.py +0 -0
  32. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/s3_transport.py +0 -0
  33. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/cloud_commands.py +0 -0
  34. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/codeartifact.py +0 -0
  35. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/__init__.py +0 -0
  36. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/api_client.py +0 -0
  37. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/auth.py +0 -0
  38. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/engine_commands.py +0 -0
  39. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/progress.py +0 -0
  40. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/ssh_config.py +0 -0
  41. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/studio_commands.py +0 -0
  42. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/github_commands.py +0 -0
  43. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/deploy.py +0 -0
  44. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/local.py +0 -0
  45. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/test.py +0 -0
  46. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/tf.py +0 -0
  47. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/main.py +0 -0
  48. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/utility_commands.py +0 -0
  49. {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/warehouse.py +0 -0
  50. {dh_cli-0.4.1 → dh_cli-0.4.3}/tests/hz/test_init.py +0 -0
  51. {dh_cli-0.4.1 → dh_cli-0.4.3}/tests/hz/test_suites.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dh-cli
3
- Version: 0.4.1
3
+ Version: 0.4.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dh-cli"
7
- version = "0.4.1"
7
+ version = "0.4.3"
8
8
  description = "Dayhoff Labs developer CLI"
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
@@ -10,6 +10,48 @@ hz_app = typer.Typer(
10
10
  context_settings={"help_option_names": ["-h", "--help"]},
11
11
  )
12
12
 
13
+ API_URLS = {
14
+ "prod": "https://api.horizyn.dayhofflabs.com",
15
+ "dev": "https://horizyn-api-dev.dayhofflabs.com",
16
+ }
17
+
18
+
19
+ @hz_app.command()
20
+ def health(
21
+ detailed: bool = typer.Option(False, "-d", "--detailed", help="Include GPU and EFS details."),
22
+ ):
23
+ """Check API health for both prod and dev environments."""
24
+ import json
25
+ from concurrent.futures import ThreadPoolExecutor
26
+ from urllib.request import urlopen
27
+ from urllib.error import URLError
28
+
29
+ path = "/healthz/detailed" if detailed else "/healthz"
30
+
31
+ def _fetch(env: str) -> tuple[str, dict | str]:
32
+ try:
33
+ with urlopen(f"{API_URLS[env]}{path}", timeout=10) as resp:
34
+ return env, json.loads(resp.read())
35
+ except (URLError, OSError) as exc:
36
+ return env, f"unreachable ({exc})"
37
+ except json.JSONDecodeError:
38
+ return env, "invalid response"
39
+
40
+ with ThreadPoolExecutor(max_workers=2) as pool:
41
+ results = dict(pool.map(_fetch, API_URLS))
42
+
43
+ for env in ("prod", "dev"):
44
+ label = f"[{env}] {API_URLS[env]}"
45
+ data = results[env]
46
+ if isinstance(data, str):
47
+ typer.secho(f"{label} ✗ {data}", fg=typer.colors.RED)
48
+ else:
49
+ status = data.get("status", "unknown")
50
+ version = data.get("version", "?")
51
+ color = typer.colors.GREEN if status == "healthy" else typer.colors.YELLOW
52
+ typer.secho(f"{label} v{version} {status}", fg=color)
53
+ typer.echo(json.dumps(data, indent=2))
54
+
13
55
 
14
56
  def _find_workspace_root() -> Path:
15
57
  root = os.environ.get("WORKSPACE_ROOT")
@@ -40,9 +40,26 @@ def _format_csv(emails: set[str]) -> str:
40
40
  return ",".join(sorted(emails))
41
41
 
42
42
 
43
+ def compute_user_tiers(
44
+ allowed_emails: set[str],
45
+ alpha_emails: set[str],
46
+ admin_emails: set[str],
47
+ ) -> tuple[set[str], set[str], set[str]]:
48
+ """Derive beta set and find orphaned emails not on the allowed list.
49
+
50
+ Returns (beta_emails, alpha_only, orphaned_emails) where orphaned
51
+ means present in alpha or admin but missing from allowed_emails.
52
+ """
53
+ orphaned = (alpha_emails | admin_emails) - allowed_emails
54
+ alpha_only = alpha_emails - admin_emails - orphaned
55
+ beta_emails = allowed_emails - alpha_emails - admin_emails
56
+ return beta_emails, alpha_only, orphaned
57
+
58
+
43
59
  @users_app.command("list")
44
60
  def list_users(
45
61
  env: str = typer.Option("prod", "--env", "-e", help="Environment: prod or dev."),
62
+ detailed: bool = typer.Option(False, "-d", "--detailed", help="Show raw SSM parameter lists."),
46
63
  ):
47
64
  """Show all authorized users and their tiers."""
48
65
  ssm = _ssm_client()
@@ -53,7 +70,7 @@ def list_users(
53
70
  allowed_domains = _parse_csv(_get_param(ssm, SSM_PARAMS["allowed_domains"].format(env=env)))
54
71
  allowed_emails = _parse_csv(_get_param(ssm, SSM_PARAMS["allowed_emails"].format(env=env)))
55
72
 
56
- typer.echo(f"\n Horizyn users ({env})\n")
73
+ env_colored = typer.style(env, fg=typer.colors.BRIGHT_GREEN, bold=True)
57
74
 
58
75
  def _print_section(label: str, items: set[str]) -> None:
59
76
  typer.echo(f" {label}:")
@@ -62,13 +79,36 @@ def list_users(
62
79
  else:
63
80
  for item in sorted(items):
64
81
  typer.echo(f" {item}")
82
+ typer.echo()
83
+
84
+ if detailed:
85
+ typer.echo(f"\n Horizyn users ({env_colored}) — detailed")
86
+ typer.echo()
87
+ typer.echo(" Access requires allowed_emails or allowed_domains match.")
88
+ typer.echo(" Allowed users default to beta tier unless listed in alpha or admin.")
89
+ typer.echo()
90
+ _print_section("Admin domains", admin_domains)
91
+ _print_section("Admin emails", admin_emails)
92
+ _print_section("Alpha emails", alpha_emails)
93
+ _print_section("Allowed domains", allowed_domains)
94
+ _print_section("Allowed emails", allowed_emails)
95
+ else:
96
+ beta, alpha_only, orphaned = compute_user_tiers(
97
+ allowed_emails, alpha_emails, admin_emails,
98
+ )
65
99
 
66
- _print_section("Admin domains", admin_domains)
67
- _print_section("Admin emails", admin_emails)
68
- _print_section("Alpha emails", alpha_emails)
69
- _print_section("Allowed domains", allowed_domains)
70
- _print_section("Allowed emails", allowed_emails)
71
- typer.echo()
100
+ typer.echo(f"\n Horizyn users ({env_colored})")
101
+ typer.echo()
102
+ _print_section("Alpha", alpha_only)
103
+ _print_section("Beta", beta)
104
+
105
+ if orphaned:
106
+ for email in sorted(orphaned):
107
+ typer.secho(
108
+ f" ⚠ {email} is in alpha/admin but NOT in allowed_emails",
109
+ fg=typer.colors.RED,
110
+ )
111
+ typer.echo()
72
112
 
73
113
 
74
114
  @users_app.command("add")
@@ -169,11 +209,12 @@ def find_user(
169
209
  if email in values:
170
210
  matches.append(param_key)
171
211
 
212
+ env_colored = typer.style(env, fg=typer.colors.BRIGHT_GREEN, bold=True)
172
213
  if matches:
173
- typer.echo(f" {env}: found")
214
+ typer.echo(f" {env_colored}: found")
174
215
  for m in matches:
175
216
  typer.echo(f" - {m}")
176
217
  else:
177
- typer.echo(f" {env}: not found")
218
+ typer.echo(f" {env_colored}: not found")
178
219
 
179
220
  typer.echo()
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, patch
5
5
  import pytest
6
6
  from click.exceptions import Exit
7
7
 
8
- from dh_cli.hz.users import _format_csv, _parse_csv
8
+ from dh_cli.hz.users import _format_csv, _parse_csv, compute_user_tiers
9
9
 
10
10
 
11
11
  class TestParseCSV:
@@ -206,3 +206,50 @@ class TestPromoteUser:
206
206
  from dh_cli.hz.users import promote_user
207
207
 
208
208
  promote_user("alice@x.com", tier="superadmin", env="prod")
209
+
210
+
211
+ class TestComputeUserTiers:
212
+ def test_beta_is_allowed_minus_alpha_minus_admin(self):
213
+ beta, alpha_only, orphaned = compute_user_tiers(
214
+ allowed_emails={"a@x.com", "b@x.com", "c@x.com"},
215
+ alpha_emails={"b@x.com"},
216
+ admin_emails=set(),
217
+ )
218
+ assert beta == {"a@x.com", "c@x.com"}
219
+ assert alpha_only == {"b@x.com"}
220
+ assert orphaned == set()
221
+
222
+ def test_admin_emails_excluded_from_alpha_and_beta(self):
223
+ beta, alpha_only, orphaned = compute_user_tiers(
224
+ allowed_emails={"a@x.com", "b@x.com", "c@x.com"},
225
+ alpha_emails={"b@x.com", "c@x.com"},
226
+ admin_emails={"c@x.com"},
227
+ )
228
+ assert beta == {"a@x.com"}
229
+ assert alpha_only == {"b@x.com"}
230
+ assert orphaned == set()
231
+
232
+ def test_orphaned_alpha_not_in_allowed(self):
233
+ beta, alpha_only, orphaned = compute_user_tiers(
234
+ allowed_emails={"a@x.com"},
235
+ alpha_emails={"ghost@x.com"},
236
+ admin_emails=set(),
237
+ )
238
+ assert beta == {"a@x.com"}
239
+ assert alpha_only == set()
240
+ assert orphaned == {"ghost@x.com"}
241
+
242
+ def test_orphaned_admin_not_in_allowed(self):
243
+ beta, alpha_only, orphaned = compute_user_tiers(
244
+ allowed_emails={"a@x.com"},
245
+ alpha_emails=set(),
246
+ admin_emails={"ghost@x.com"},
247
+ )
248
+ assert beta == {"a@x.com"}
249
+ assert orphaned == {"ghost@x.com"}
250
+
251
+ def test_all_empty(self):
252
+ beta, alpha_only, orphaned = compute_user_tiers(set(), set(), set())
253
+ assert beta == set()
254
+ assert alpha_only == set()
255
+ assert orphaned == set()
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