endpoints-submission-cli 0.0.0__py3-none-any.whl
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.
- endpoints_submission_cli/__init__.py +6 -0
- endpoints_submission_cli/_http.py +125 -0
- endpoints_submission_cli/commands/__init__.py +3 -0
- endpoints_submission_cli/commands/common.py +88 -0
- endpoints_submission_cli/commands/runs/__init__.py +27 -0
- endpoints_submission_cli/commands/runs/create.py +117 -0
- endpoints_submission_cli/commands/runs/delete.py +53 -0
- endpoints_submission_cli/commands/runs/get.py +35 -0
- endpoints_submission_cli/commands/runs/list.py +39 -0
- endpoints_submission_cli/commands/runs/pin.py +34 -0
- endpoints_submission_cli/commands/runs/unpin.py +34 -0
- endpoints_submission_cli/commands/submissions/__init__.py +29 -0
- endpoints_submission_cli/commands/submissions/add_run.py +166 -0
- endpoints_submission_cli/commands/submissions/create.py +241 -0
- endpoints_submission_cli/commands/submissions/get.py +40 -0
- endpoints_submission_cli/commands/submissions/list.py +39 -0
- endpoints_submission_cli/commands/submissions/remove_run.py +175 -0
- endpoints_submission_cli/commands/submissions/update.py +238 -0
- endpoints_submission_cli/commands/submissions/withdraw.py +59 -0
- endpoints_submission_cli/exceptions.py +41 -0
- endpoints_submission_cli/main.py +25 -0
- endpoints_submission_cli/runs/__init__.py +3 -0
- endpoints_submission_cli/runs/api.py +119 -0
- endpoints_submission_cli/runs/formatters.py +83 -0
- endpoints_submission_cli/runs/parser.py +121 -0
- endpoints_submission_cli/submissions/__init__.py +3 -0
- endpoints_submission_cli/submissions/api.py +140 -0
- endpoints_submission_cli/submissions/builder.py +385 -0
- endpoints_submission_cli/submissions/formatters.py +93 -0
- endpoints_submission_cli/submissions/github.py +332 -0
- endpoints_submission_cli-0.0.0.dist-info/METADATA +194 -0
- endpoints_submission_cli-0.0.0.dist-info/RECORD +52 -0
- endpoints_submission_cli-0.0.0.dist-info/WHEEL +5 -0
- endpoints_submission_cli-0.0.0.dist-info/entry_points.txt +2 -0
- endpoints_submission_cli-0.0.0.dist-info/licenses/LICENSE.md +177 -0
- endpoints_submission_cli-0.0.0.dist-info/top_level.txt +2 -0
- submission_checker/__init__.py +7 -0
- submission_checker/checker.py +309 -0
- submission_checker/cli.py +123 -0
- submission_checker/models/__init__.py +45 -0
- submission_checker/models/aggregate/__init__.py +6 -0
- submission_checker/models/aggregate/context.py +168 -0
- submission_checker/models/aggregate/point_result.py +259 -0
- submission_checker/models/file/__init__.py +17 -0
- submission_checker/models/file/accuracy.py +57 -0
- submission_checker/models/file/point_config.py +193 -0
- submission_checker/models/file/point_summary.py +90 -0
- submission_checker/models/file/system.py +133 -0
- submission_checker/models/loader.py +138 -0
- submission_checker/models/regions.py +126 -0
- submission_checker/models/results.py +94 -0
- submission_checker/models/structure.py +147 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""HTTP transport layer for the MLPerf Submission API (httpx-based).
|
|
4
|
+
|
|
5
|
+
All HTTP plumbing lives here: timeouts, base-URL resolution, auth header,
|
|
6
|
+
error translators, and the four verb helpers used by the domain API modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Any, NoReturn
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from .exceptions import APIError, AuthError
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"get_token",
|
|
20
|
+
"_base_url",
|
|
21
|
+
"_headers",
|
|
22
|
+
"_raise_status",
|
|
23
|
+
"_raise_request",
|
|
24
|
+
"_get",
|
|
25
|
+
"_post",
|
|
26
|
+
"_patch",
|
|
27
|
+
"_delete",
|
|
28
|
+
"_UPLOAD_TIMEOUT",
|
|
29
|
+
"_DOWNLOAD_TIMEOUT",
|
|
30
|
+
"_API_TIMEOUT",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
_DEFAULT_BASE_URL = "http://localhost:8080"
|
|
34
|
+
|
|
35
|
+
# All values are per-operation idle timeouts (time without a byte transferred), not wall-clock totals.
|
|
36
|
+
# A slow-but-steady transfer never times out; only a stalled connection does.
|
|
37
|
+
_UPLOAD_TIMEOUT = httpx.Timeout(connect=10.0, write=300.0, read=120.0, pool=5.0) # write: each chunk send; read: wait for server ack after full upload
|
|
38
|
+
_DOWNLOAD_TIMEOUT = httpx.Timeout(connect=10.0, read=300.0, write=30.0, pool=5.0) # read: gap between received bytes; accommodates large archives on slow links
|
|
39
|
+
_API_TIMEOUT = httpx.Timeout(connect=10.0, read=30.0, write=30.0, pool=5.0) # standard JSON calls; no large payloads expected
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _base_url() -> str:
|
|
43
|
+
return os.environ.get("MLPERF_API_BASE_URL", _DEFAULT_BASE_URL)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_token(explicit_token: str | None) -> str:
|
|
47
|
+
"""Resolve the API token from the explicit flag or PRISM_USER_API_TOKEN env var."""
|
|
48
|
+
token = explicit_token or os.environ.get("PRISM_USER_API_TOKEN")
|
|
49
|
+
if not token:
|
|
50
|
+
raise AuthError(
|
|
51
|
+
"No API token provided. Pass --token or set PRISM_USER_API_TOKEN."
|
|
52
|
+
)
|
|
53
|
+
return token
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _headers(token: str) -> dict[str, str]:
|
|
57
|
+
return {"X-API-Key": token}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Translates an httpx HTTP error (4xx/5xx response) into our own APIError/AuthError.
|
|
61
|
+
def _raise_status(exc: httpx.HTTPStatusError) -> NoReturn:
|
|
62
|
+
if exc.response.status_code in (401, 403):
|
|
63
|
+
raise AuthError(f"Authentication failed (HTTP {exc.response.status_code})") from exc
|
|
64
|
+
body = exc.response.text[:500] # truncate to avoid dumping full HTML/JSON error pages into logs
|
|
65
|
+
raise APIError(f"API error {exc.response.status_code}: {body}") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# Translates an httpx network/transport error (timeout, connection refused, DNS failure, etc.)
|
|
69
|
+
def _raise_request(exc: httpx.RequestError) -> NoReturn:
|
|
70
|
+
raise APIError(f"Request failed: {type(exc).__name__}: {exc}") from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get(path: str, token: str, **kwargs: Any) -> Any:
|
|
74
|
+
try:
|
|
75
|
+
r = httpx.get(
|
|
76
|
+
f"{_base_url()}{path}", headers=_headers(token), timeout=_API_TIMEOUT, **kwargs
|
|
77
|
+
)
|
|
78
|
+
r.raise_for_status()
|
|
79
|
+
return r.json()
|
|
80
|
+
except httpx.HTTPStatusError as exc:
|
|
81
|
+
_raise_status(exc)
|
|
82
|
+
except httpx.RequestError as exc:
|
|
83
|
+
_raise_request(exc)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _post(path: str, token: str, **kwargs: Any) -> Any:
|
|
87
|
+
try:
|
|
88
|
+
r = httpx.post(
|
|
89
|
+
f"{_base_url()}{path}", headers=_headers(token), timeout=_API_TIMEOUT, **kwargs
|
|
90
|
+
)
|
|
91
|
+
r.raise_for_status()
|
|
92
|
+
return r.json()
|
|
93
|
+
except httpx.HTTPStatusError as exc:
|
|
94
|
+
_raise_status(exc)
|
|
95
|
+
except httpx.RequestError as exc:
|
|
96
|
+
_raise_request(exc)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _patch(path: str, token: str, **kwargs: Any) -> Any:
|
|
100
|
+
try:
|
|
101
|
+
r = httpx.patch(
|
|
102
|
+
f"{_base_url()}{path}", headers=_headers(token), timeout=_API_TIMEOUT, **kwargs
|
|
103
|
+
)
|
|
104
|
+
r.raise_for_status()
|
|
105
|
+
return r.json()
|
|
106
|
+
except httpx.HTTPStatusError as exc:
|
|
107
|
+
_raise_status(exc)
|
|
108
|
+
except httpx.RequestError as exc:
|
|
109
|
+
_raise_request(exc)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _delete(path: str, token: str, **kwargs: Any) -> Any:
|
|
113
|
+
try:
|
|
114
|
+
r = httpx.delete(
|
|
115
|
+
f"{_base_url()}{path}", headers=_headers(token), timeout=_API_TIMEOUT, **kwargs
|
|
116
|
+
)
|
|
117
|
+
r.raise_for_status()
|
|
118
|
+
# 204 No Content returns empty body
|
|
119
|
+
if r.status_code == 204:
|
|
120
|
+
return None
|
|
121
|
+
return r.json()
|
|
122
|
+
except httpx.HTTPStatusError as exc:
|
|
123
|
+
_raise_status(exc)
|
|
124
|
+
except httpx.RequestError as exc:
|
|
125
|
+
_raise_request(exc)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Shared helpers for all CLI commands."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.json import JSON
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
from .. import _http
|
|
18
|
+
from ..exceptions import AuthError, SubmissionCheckError
|
|
19
|
+
|
|
20
|
+
__all__ = ["_console", "_get_token", "_SEVERITY_STYLE", "_run_submission_checker", "output_json"]
|
|
21
|
+
|
|
22
|
+
_console = Console(stderr=True)
|
|
23
|
+
_stdout_console = Console()
|
|
24
|
+
|
|
25
|
+
_SEVERITY_STYLE = {
|
|
26
|
+
"error": "bold red",
|
|
27
|
+
"warning": "yellow",
|
|
28
|
+
"info": "dim",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def output_json(data: Any) -> None:
|
|
33
|
+
"""Print *data* as syntax-highlighted JSON (plain when piped)."""
|
|
34
|
+
_stdout_console.print(JSON(json.dumps(data, default=str)))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _get_token(token: str | None) -> str:
|
|
38
|
+
try:
|
|
39
|
+
return _http.get_token(token)
|
|
40
|
+
except AuthError as exc:
|
|
41
|
+
_console.print(f"[bold red]Auth error:[/bold red] {exc}")
|
|
42
|
+
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run_submission_checker(submission_dir: Path) -> None:
|
|
46
|
+
"""Run the SubmissionChecker on *submission_dir*; print results table; raise on errors."""
|
|
47
|
+
from submission_checker.checker import SubmissionChecker
|
|
48
|
+
|
|
49
|
+
report = SubmissionChecker(submission_dir).run()
|
|
50
|
+
|
|
51
|
+
# --- write Rich table to log file ---
|
|
52
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
53
|
+
log_path = Path.cwd() / f"submission_checker_{timestamp}.log"
|
|
54
|
+
|
|
55
|
+
table = Table(show_lines=True, expand=False)
|
|
56
|
+
table.add_column("Rule", style="cyan", no_wrap=True)
|
|
57
|
+
table.add_column("§ Ref", no_wrap=True)
|
|
58
|
+
table.add_column("Severity", no_wrap=True)
|
|
59
|
+
table.add_column("Message")
|
|
60
|
+
table.add_column("Path")
|
|
61
|
+
|
|
62
|
+
for r in report.results:
|
|
63
|
+
sev = r.severity.value
|
|
64
|
+
style = _SEVERITY_STYLE.get(sev, "")
|
|
65
|
+
table.add_row(
|
|
66
|
+
r.rule,
|
|
67
|
+
r.spec_ref,
|
|
68
|
+
f"[{style}]{sev}[/{style}]" if style else sev,
|
|
69
|
+
r.message,
|
|
70
|
+
str(r.path) if r.path else "",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with open(log_path, "w", encoding="utf-8") as fh:
|
|
74
|
+
file_console = Console(file=fh, no_color=True, width=220)
|
|
75
|
+
file_console.print(
|
|
76
|
+
f"Submission Checker Report — {timestamp}\n"
|
|
77
|
+
f"Directory : {submission_dir}\n"
|
|
78
|
+
f"Results : {len(report.results)} checks"
|
|
79
|
+
f" ({len(report.errors)} error(s), {len(report.warnings)} warning(s))\n"
|
|
80
|
+
)
|
|
81
|
+
file_console.print(table)
|
|
82
|
+
|
|
83
|
+
_console.print(f"[dim]Checker report written to {log_path}[/dim]")
|
|
84
|
+
|
|
85
|
+
errors = report.errors
|
|
86
|
+
if errors:
|
|
87
|
+
msgs = "\n".join(f" [{e.rule}] {e.message}" for e in errors)
|
|
88
|
+
raise SubmissionCheckError(f"Submission checker found {len(errors)} error(s):\n{msgs}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""runs command group."""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .create import runs_create
|
|
8
|
+
from .delete import runs_delete
|
|
9
|
+
from .get import runs_get
|
|
10
|
+
from .list import runs_list
|
|
11
|
+
from .pin import runs_pin
|
|
12
|
+
from .unpin import runs_unpin
|
|
13
|
+
|
|
14
|
+
__all__ = ["runs"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group(name="runs")
|
|
18
|
+
def runs() -> None:
|
|
19
|
+
"""Manage benchmark runs."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
runs.add_command(runs_list)
|
|
23
|
+
runs.add_command(runs_create)
|
|
24
|
+
runs.add_command(runs_get)
|
|
25
|
+
runs.add_command(runs_delete)
|
|
26
|
+
runs.add_command(runs_pin)
|
|
27
|
+
runs.add_command(runs_unpin)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""runs create command."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from ...exceptions import APIError, ArchiveError, RunFolderError
|
|
16
|
+
from ...runs import api as runs_api
|
|
17
|
+
from ...runs.parser import build_archive, parse_run_folder
|
|
18
|
+
from ..common import _console, _get_token
|
|
19
|
+
|
|
20
|
+
__all__ = ["runs_create"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.command("create")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--path",
|
|
26
|
+
"path",
|
|
27
|
+
required=True,
|
|
28
|
+
type=click.Path(path_type=Path),
|
|
29
|
+
help="Path to the local run folder.",
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--token",
|
|
33
|
+
envvar="PRISM_USER_API_TOKEN",
|
|
34
|
+
default=None,
|
|
35
|
+
help="PRISM API key (mlc_...).",
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"--expires-at",
|
|
39
|
+
default=None,
|
|
40
|
+
help="Expiry datetime in ISO 8601 format (e.g. 2026-01-01T00:00:00). Defaults to server policy.",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--pinned",
|
|
44
|
+
is_flag=True,
|
|
45
|
+
default=False,
|
|
46
|
+
help="Pin the run immediately to prevent automatic expiry.",
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--dry-run",
|
|
50
|
+
is_flag=True,
|
|
51
|
+
default=False,
|
|
52
|
+
help="Print the parsed payload as JSON and exit without calling the API.",
|
|
53
|
+
)
|
|
54
|
+
def runs_create(path: Path, token: str | None, expires_at: str | None, pinned: bool, dry_run: bool) -> None:
|
|
55
|
+
"""Create a run from a local benchmark result folder.
|
|
56
|
+
|
|
57
|
+
Parses system_info.json, config.yaml, and result_summary.json from PATH,
|
|
58
|
+
registers the run with the Submission API, and uploads the run folder as
|
|
59
|
+
an archive. If the archive upload fails the run record is deleted
|
|
60
|
+
(rollback to clean state).
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
payload = parse_run_folder(path)
|
|
64
|
+
except RunFolderError as exc:
|
|
65
|
+
_console.print(f"[bold red]Run folder error:[/bold red] {exc}")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
if expires_at is not None:
|
|
69
|
+
payload["expires_at"] = expires_at
|
|
70
|
+
if pinned:
|
|
71
|
+
payload["pinned"] = True
|
|
72
|
+
|
|
73
|
+
if dry_run:
|
|
74
|
+
print(json.dumps(payload, indent=2, default=str))
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
resolved_token = _get_token(token)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
run_out = runs_api.create_run(resolved_token, payload)
|
|
81
|
+
except APIError as exc:
|
|
82
|
+
_console.print(f"[bold red]API error creating run:[/bold red] {exc}")
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
run_id: str = run_out["id"]
|
|
86
|
+
|
|
87
|
+
# At this point the run record exists in the DB but has no archive yet.
|
|
88
|
+
# If the upload fails we must roll back by deleting the run record so we
|
|
89
|
+
# don't leave an orphaned entry with no associated data.
|
|
90
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
91
|
+
archive_path = build_archive(path, Path(tmp) / f"{path.name}.tar.gz")
|
|
92
|
+
try:
|
|
93
|
+
upload_result = runs_api.upload_run_archive(resolved_token, run_id, archive_path)
|
|
94
|
+
except (APIError, ArchiveError, OSError) as exc:
|
|
95
|
+
_console.print(
|
|
96
|
+
f"[bold red]Archive upload failed:[/bold red] {exc}\n"
|
|
97
|
+
f"[yellow]Rolling back run {run_id}…[/yellow]"
|
|
98
|
+
)
|
|
99
|
+
# Step 1: attempt to delete the partial archive from storage (best-effort,
|
|
100
|
+
# ignore failure — storage may not have received anything).
|
|
101
|
+
with contextlib.suppress(APIError):
|
|
102
|
+
runs_api.delete_run_archive(resolved_token, run_id)
|
|
103
|
+
# Step 2: delete the run DB record to restore clean state.
|
|
104
|
+
try:
|
|
105
|
+
runs_api.delete_run(resolved_token, run_id)
|
|
106
|
+
_console.print("[green]Rollback successful — run deleted.[/green]")
|
|
107
|
+
except APIError as rb_exc:
|
|
108
|
+
_console.print(
|
|
109
|
+
f"[bold red]Rollback also failed:[/bold red] {rb_exc}\n"
|
|
110
|
+
f"Orphaned run ID: {run_id}"
|
|
111
|
+
)
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
archive_uri = upload_result.get("archive_uri") if upload_result else None
|
|
115
|
+
_console.print(f"[bold green]Run created:[/bold green] {run_id}")
|
|
116
|
+
if archive_uri:
|
|
117
|
+
_console.print(f"[dim]Archive:[/dim] {archive_uri}")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""runs delete command."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ...exceptions import APIError
|
|
12
|
+
from ...runs import api as runs_api
|
|
13
|
+
from ..common import _console, _get_token
|
|
14
|
+
|
|
15
|
+
__all__ = ["runs_delete"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command("delete")
|
|
19
|
+
@click.option("--run-id", required=True, help="Run UUID.")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--token",
|
|
22
|
+
envvar="PRISM_USER_API_TOKEN",
|
|
23
|
+
default=None,
|
|
24
|
+
help="PRISM API key (mlc_...).",
|
|
25
|
+
)
|
|
26
|
+
def runs_delete(run_id: str, token: str | None) -> None:
|
|
27
|
+
"""Delete a run and its stored archive.
|
|
28
|
+
|
|
29
|
+
If the run is part of an active submission the API will reject the request.
|
|
30
|
+
Withdraw the submission first in that case.
|
|
31
|
+
|
|
32
|
+
The archive is removed from GCS first so that a DB-delete failure leaves
|
|
33
|
+
nothing orphaned in storage. A 404 on archive deletion means the run has
|
|
34
|
+
no archive and is silently skipped.
|
|
35
|
+
"""
|
|
36
|
+
resolved_token = _get_token(token)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
runs_api.delete_run_archive(resolved_token, run_id)
|
|
40
|
+
except APIError as exc:
|
|
41
|
+
if "404" not in str(exc):
|
|
42
|
+
_console.print(
|
|
43
|
+
f"[yellow]Warning:[/yellow] Archive deletion failed: {exc}\n"
|
|
44
|
+
"Continuing to delete the run record."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
runs_api.delete_run(resolved_token, run_id)
|
|
49
|
+
except APIError as exc:
|
|
50
|
+
_console.print(f"[bold red]Error deleting run:[/bold red] {exc}")
|
|
51
|
+
sys.exit(1)
|
|
52
|
+
|
|
53
|
+
_console.print(f"[bold green]Run deleted:[/bold green] {run_id}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""runs get command."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ...exceptions import APIError
|
|
12
|
+
from ...runs import api as runs_api
|
|
13
|
+
from ..common import _console, _get_token, output_json
|
|
14
|
+
|
|
15
|
+
__all__ = ["runs_get"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command("get")
|
|
19
|
+
@click.option("--run-id", required=True, help="Run UUID.")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--token",
|
|
22
|
+
envvar="PRISM_USER_API_TOKEN",
|
|
23
|
+
default=None,
|
|
24
|
+
help="PRISM API key (mlc_...).",
|
|
25
|
+
)
|
|
26
|
+
def runs_get(run_id: str, token: str | None) -> None:
|
|
27
|
+
"""Get full details of a specific run."""
|
|
28
|
+
resolved_token = _get_token(token)
|
|
29
|
+
try:
|
|
30
|
+
run = runs_api.get_run(resolved_token, run_id)
|
|
31
|
+
except APIError as exc:
|
|
32
|
+
_console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
|
|
35
|
+
output_json(run)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""runs list command."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ...exceptions import APIError
|
|
12
|
+
from ...runs import api as runs_api
|
|
13
|
+
from ...runs.formatters import print_runs_table
|
|
14
|
+
from ..common import _console, _get_token, output_json
|
|
15
|
+
|
|
16
|
+
__all__ = ["runs_list"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command("list")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--token",
|
|
22
|
+
envvar="PRISM_USER_API_TOKEN",
|
|
23
|
+
default=None,
|
|
24
|
+
help="PRISM API key (mlc_...).",
|
|
25
|
+
)
|
|
26
|
+
@click.option("-j", "--json", "as_json", is_flag=True, default=False, help="Output raw JSON.")
|
|
27
|
+
def runs_list(token: str | None, as_json: bool) -> None:
|
|
28
|
+
"""List all runs for the authenticated user."""
|
|
29
|
+
resolved_token = _get_token(token)
|
|
30
|
+
try:
|
|
31
|
+
run_list = runs_api.list_runs(resolved_token)
|
|
32
|
+
except APIError as exc:
|
|
33
|
+
_console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
if as_json:
|
|
37
|
+
output_json(run_list)
|
|
38
|
+
else:
|
|
39
|
+
print_runs_table(run_list)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""runs pin command."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ...exceptions import APIError
|
|
12
|
+
from ...runs import api as runs_api
|
|
13
|
+
from ..common import _console, _get_token
|
|
14
|
+
|
|
15
|
+
__all__ = ["runs_pin"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command("pin")
|
|
19
|
+
@click.option("--run-id", required=True, help="Run UUID.")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--token",
|
|
22
|
+
envvar="PRISM_USER_API_TOKEN",
|
|
23
|
+
default=None,
|
|
24
|
+
help="PRISM API key (mlc_...).",
|
|
25
|
+
)
|
|
26
|
+
def runs_pin(run_id: str, token: str | None) -> None:
|
|
27
|
+
"""Pin a run to prevent expiry (sets expires_at = null)."""
|
|
28
|
+
resolved_token = _get_token(token)
|
|
29
|
+
try:
|
|
30
|
+
runs_api.pin_run(resolved_token, run_id)
|
|
31
|
+
except APIError as exc:
|
|
32
|
+
_console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
_console.print(f"[bold green]Run pinned:[/bold green] {run_id}")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""runs unpin command."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ...exceptions import APIError
|
|
12
|
+
from ...runs import api as runs_api
|
|
13
|
+
from ..common import _console, _get_token
|
|
14
|
+
|
|
15
|
+
__all__ = ["runs_unpin"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.command("unpin")
|
|
19
|
+
@click.option("--run-id", required=True, help="Run UUID.")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--token",
|
|
22
|
+
envvar="PRISM_USER_API_TOKEN",
|
|
23
|
+
default=None,
|
|
24
|
+
help="PRISM API key (mlc_...).",
|
|
25
|
+
)
|
|
26
|
+
def runs_unpin(run_id: str, token: str | None) -> None:
|
|
27
|
+
"""Unpin a run to restore normal expiry behaviour."""
|
|
28
|
+
resolved_token = _get_token(token)
|
|
29
|
+
try:
|
|
30
|
+
runs_api.unpin_run(resolved_token, run_id)
|
|
31
|
+
except APIError as exc:
|
|
32
|
+
_console.print(f"[bold red]Error:[/bold red] {exc}")
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
_console.print(f"[bold green]Run unpinned:[/bold green] {run_id}")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""submissions command group."""
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .add_run import submissions_add_run
|
|
8
|
+
from .create import submissions_create
|
|
9
|
+
from .get import submissions_get
|
|
10
|
+
from .list import submissions_list
|
|
11
|
+
from .remove_run import submissions_remove_run
|
|
12
|
+
from .update import submissions_update
|
|
13
|
+
from .withdraw import submissions_withdraw
|
|
14
|
+
|
|
15
|
+
__all__ = ["submissions"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group(name="submissions")
|
|
19
|
+
def submissions() -> None:
|
|
20
|
+
"""Manage MLPerf submissions."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
submissions.add_command(submissions_list)
|
|
24
|
+
submissions.add_command(submissions_create)
|
|
25
|
+
submissions.add_command(submissions_get)
|
|
26
|
+
submissions.add_command(submissions_update)
|
|
27
|
+
submissions.add_command(submissions_withdraw)
|
|
28
|
+
submissions.add_command(submissions_add_run)
|
|
29
|
+
submissions.add_command(submissions_remove_run)
|