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.
- {dh_cli-0.4.1 → dh_cli-0.4.3}/PKG-INFO +1 -1
- {dh_cli-0.4.1 → dh_cli-0.4.3}/pyproject.toml +1 -1
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/__init__.py +42 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/users.py +50 -9
- {dh_cli-0.4.1 → dh_cli-0.4.3}/tests/hz/test_users.py +48 -1
- {dh_cli-0.4.1 → dh_cli-0.4.3}/.gitignore +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/LICENSE +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/README.md +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/__init__.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/__init__.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/aws_batch.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/__init__.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/boltz.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/cancel.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/clean.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/embed_t5.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/finalize.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/list_jobs.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/local.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/logs.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/protmpnn.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/retry.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/status.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/submit.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/train.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/commands/wait_for.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/fasta_utils.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/h5_utils.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/job_id.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/manifest.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/batch/s3_transport.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/cloud_commands.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/codeartifact.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/__init__.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/api_client.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/auth.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/engine_commands.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/progress.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/ssh_config.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/engines_studios/studio_commands.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/github_commands.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/deploy.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/local.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/test.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/hz/tf.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/main.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/utility_commands.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/src/dh_cli/warehouse.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/tests/hz/test_init.py +0 -0
- {dh_cli-0.4.1 → dh_cli-0.4.3}/tests/hz/test_suites.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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" {
|
|
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" {
|
|
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
|
|
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
|
|
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
|