endpoints-submission-cli 0.1.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.
Files changed (36) hide show
  1. endpoints_submission_cli/__init__.py +6 -0
  2. endpoints_submission_cli/_http.py +125 -0
  3. endpoints_submission_cli/commands/__init__.py +3 -0
  4. endpoints_submission_cli/commands/common.py +88 -0
  5. endpoints_submission_cli/commands/runs/__init__.py +27 -0
  6. endpoints_submission_cli/commands/runs/create.py +117 -0
  7. endpoints_submission_cli/commands/runs/delete.py +53 -0
  8. endpoints_submission_cli/commands/runs/get.py +35 -0
  9. endpoints_submission_cli/commands/runs/list.py +39 -0
  10. endpoints_submission_cli/commands/runs/pin.py +34 -0
  11. endpoints_submission_cli/commands/runs/unpin.py +34 -0
  12. endpoints_submission_cli/commands/submissions/__init__.py +29 -0
  13. endpoints_submission_cli/commands/submissions/add_run.py +166 -0
  14. endpoints_submission_cli/commands/submissions/create.py +241 -0
  15. endpoints_submission_cli/commands/submissions/get.py +40 -0
  16. endpoints_submission_cli/commands/submissions/list.py +39 -0
  17. endpoints_submission_cli/commands/submissions/remove_run.py +175 -0
  18. endpoints_submission_cli/commands/submissions/update.py +238 -0
  19. endpoints_submission_cli/commands/submissions/withdraw.py +59 -0
  20. endpoints_submission_cli/exceptions.py +41 -0
  21. endpoints_submission_cli/main.py +25 -0
  22. endpoints_submission_cli/runs/__init__.py +3 -0
  23. endpoints_submission_cli/runs/api.py +119 -0
  24. endpoints_submission_cli/runs/formatters.py +83 -0
  25. endpoints_submission_cli/runs/parser.py +121 -0
  26. endpoints_submission_cli/submissions/__init__.py +3 -0
  27. endpoints_submission_cli/submissions/api.py +140 -0
  28. endpoints_submission_cli/submissions/builder.py +385 -0
  29. endpoints_submission_cli/submissions/formatters.py +93 -0
  30. endpoints_submission_cli/submissions/github.py +332 -0
  31. endpoints_submission_cli-0.1.0.dist-info/METADATA +194 -0
  32. endpoints_submission_cli-0.1.0.dist-info/RECORD +36 -0
  33. endpoints_submission_cli-0.1.0.dist-info/WHEEL +5 -0
  34. endpoints_submission_cli-0.1.0.dist-info/entry_points.txt +2 -0
  35. endpoints_submission_cli-0.1.0.dist-info/licenses/LICENSE.md +177 -0
  36. endpoints_submission_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,6 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """endpoints-submission-cli: CLI for managing MLPerf rolling submission runs and submissions."""
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["__version__"]
@@ -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,3 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2024 MLCommons
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ """CLI command groups for endpoints-submission-cli."""
@@ -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)