benchloop-client 0.1.6__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.
- aliyun/bmcli/__init__.py +3 -0
- aliyun/bmcli/__main__.py +26 -0
- aliyun/bmcli/cli.py +136 -0
- aliyun/bmcli/client.py +145 -0
- aliyun/bmcli/commands/__init__.py +1 -0
- aliyun/bmcli/commands/_common.py +36 -0
- aliyun/bmcli/commands/artifacts.py +138 -0
- aliyun/bmcli/commands/jobs.py +131 -0
- aliyun/bmcli/commands/metrics.py +136 -0
- aliyun/bmcli/commands/overview.py +70 -0
- aliyun/bmcli/commands/pull.py +168 -0
- aliyun/bmcli/commands/report.py +103 -0
- aliyun/bmcli/commands/tasks.py +121 -0
- aliyun/bmcli/commands/trajectory.py +92 -0
- aliyun/bmcli/config.py +120 -0
- aliyun/bmcli/exit_codes.py +15 -0
- aliyun/bmcli/ids.py +87 -0
- aliyun/bmcli/output.py +80 -0
- aliyun/bmcli/runtime.py +147 -0
- benchloop_client-0.1.6.dist-info/METADATA +100 -0
- benchloop_client-0.1.6.dist-info/RECORD +23 -0
- benchloop_client-0.1.6.dist-info/WHEEL +4 -0
- benchloop_client-0.1.6.dist-info/entry_points.txt +2 -0
aliyun/bmcli/config.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Configuration loading with precedence: CLI flag > env > config file > default."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
try: # Python 3.11+
|
|
11
|
+
import tomllib # type: ignore
|
|
12
|
+
except ModuleNotFoundError: # Python 3.10: use the tomli backport
|
|
13
|
+
try:
|
|
14
|
+
import tomli as tomllib # type: ignore
|
|
15
|
+
except ModuleNotFoundError: # pragma: no cover - backport not installed
|
|
16
|
+
tomllib = None # type: ignore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
DEFAULT_HOST = "https://benchmark-analysis.alibaba-inc.com"
|
|
20
|
+
DEFAULT_CONFIG_PATH = Path.home() / ".benchloop" / "config.toml"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Config:
|
|
25
|
+
"""Resolved runtime configuration."""
|
|
26
|
+
|
|
27
|
+
api_key: Optional[str] = None
|
|
28
|
+
host: str = DEFAULT_HOST
|
|
29
|
+
output_dir: str = "."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _read_config_file(path: Path) -> dict[str, Any]:
|
|
33
|
+
"""Parse the [profile] table from a TOML config file.
|
|
34
|
+
|
|
35
|
+
Returns an empty dict if the file is missing, unreadable, or tomllib is
|
|
36
|
+
unavailable (Python 3.10 without a backport).
|
|
37
|
+
"""
|
|
38
|
+
if tomllib is None:
|
|
39
|
+
return {}
|
|
40
|
+
if not path.exists():
|
|
41
|
+
return {}
|
|
42
|
+
try:
|
|
43
|
+
with path.open("rb") as fh:
|
|
44
|
+
data = tomllib.load(fh)
|
|
45
|
+
except (OSError, ValueError):
|
|
46
|
+
return {}
|
|
47
|
+
profile = data.get("profile")
|
|
48
|
+
if isinstance(profile, dict):
|
|
49
|
+
return profile
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def load_config(
|
|
54
|
+
api_key: Optional[str] = None,
|
|
55
|
+
host: Optional[str] = None,
|
|
56
|
+
output_dir: Optional[str] = None,
|
|
57
|
+
config_path: Optional[str] = None,
|
|
58
|
+
) -> Config:
|
|
59
|
+
"""Resolve configuration applying precedence CLI > env > file > default.
|
|
60
|
+
|
|
61
|
+
Env vars: BENCHLOOP_API_KEY, BENCHLOOP_HOST, BENCHLOOP_OUTPUT_DIR.
|
|
62
|
+
BENCHMARK_API_KEY is also accepted as a fallback for the api key.
|
|
63
|
+
"""
|
|
64
|
+
path = Path(config_path).expanduser() if config_path else DEFAULT_CONFIG_PATH
|
|
65
|
+
file_cfg = _read_config_file(path)
|
|
66
|
+
|
|
67
|
+
resolved_key = (
|
|
68
|
+
api_key
|
|
69
|
+
or os.environ.get("BENCHLOOP_API_KEY")
|
|
70
|
+
or os.environ.get("BENCHMARK_API_KEY")
|
|
71
|
+
or file_cfg.get("api_key")
|
|
72
|
+
)
|
|
73
|
+
resolved_host = (
|
|
74
|
+
host
|
|
75
|
+
or os.environ.get("BENCHLOOP_HOST")
|
|
76
|
+
or file_cfg.get("host")
|
|
77
|
+
or DEFAULT_HOST
|
|
78
|
+
)
|
|
79
|
+
resolved_out = (
|
|
80
|
+
output_dir
|
|
81
|
+
or os.environ.get("BENCHLOOP_OUTPUT_DIR")
|
|
82
|
+
or file_cfg.get("output_dir")
|
|
83
|
+
or "."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return Config(
|
|
87
|
+
api_key=resolved_key,
|
|
88
|
+
host=str(resolved_host).rstrip("/"),
|
|
89
|
+
output_dir=str(resolved_out),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def write_config(
|
|
94
|
+
api_key: Optional[str] = None,
|
|
95
|
+
host: Optional[str] = None,
|
|
96
|
+
output_dir: Optional[str] = None,
|
|
97
|
+
config_path: Optional[str] = None,
|
|
98
|
+
) -> Path:
|
|
99
|
+
"""Persist provided values to ~/.benchloop/config.toml (merging existing).
|
|
100
|
+
|
|
101
|
+
Returns the path written. Only non-None values are updated.
|
|
102
|
+
"""
|
|
103
|
+
path = Path(config_path).expanduser() if config_path else DEFAULT_CONFIG_PATH
|
|
104
|
+
existing = _read_config_file(path)
|
|
105
|
+
|
|
106
|
+
if api_key is not None:
|
|
107
|
+
existing["api_key"] = api_key
|
|
108
|
+
if host is not None:
|
|
109
|
+
existing["host"] = host
|
|
110
|
+
if output_dir is not None:
|
|
111
|
+
existing["output_dir"] = output_dir
|
|
112
|
+
|
|
113
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
lines = ["[profile]"]
|
|
115
|
+
for key in ("host", "api_key", "output_dir"):
|
|
116
|
+
if key in existing and existing[key] is not None:
|
|
117
|
+
value = str(existing[key]).replace("\\", "\\\\").replace('"', '\\"')
|
|
118
|
+
lines.append(f'{key} = "{value}"')
|
|
119
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
120
|
+
return path
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Process exit codes used across the CLI.
|
|
2
|
+
|
|
3
|
+
See design doc section 8:
|
|
4
|
+
- 0 all success
|
|
5
|
+
- 1 partial failure (some ids failed)
|
|
6
|
+
- 2 usage / parameter error
|
|
7
|
+
- 3 auth failure (HTTP 401)
|
|
8
|
+
- 4 network / timeout
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
OK = 0
|
|
12
|
+
PARTIAL = 1
|
|
13
|
+
USAGE = 2
|
|
14
|
+
AUTH = 3
|
|
15
|
+
NETWORK = 4
|
aliyun/bmcli/ids.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""ID input expansion and dimension validation helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Callable, Iterable, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _read_stdin() -> list[str]:
|
|
12
|
+
data = sys.stdin.read()
|
|
13
|
+
return data.split()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def expand_ids(
|
|
17
|
+
values: Optional[Iterable[str]],
|
|
18
|
+
stdin_reader: Callable[[], list[str]] = _read_stdin,
|
|
19
|
+
) -> list[str]:
|
|
20
|
+
"""Expand a list of raw flag values into a deduped ordered id list.
|
|
21
|
+
|
|
22
|
+
Supports:
|
|
23
|
+
- repeated flags: ``-g G1 -g G2``
|
|
24
|
+
- comma separation: ``-g G1,G2``
|
|
25
|
+
- ``-`` meaning read from stdin (whitespace / newline separated)
|
|
26
|
+
|
|
27
|
+
Duplicates are removed while preserving first-seen order.
|
|
28
|
+
"""
|
|
29
|
+
out: list[str] = []
|
|
30
|
+
seen: set[str] = set()
|
|
31
|
+
|
|
32
|
+
def _add(token: str) -> None:
|
|
33
|
+
token = token.strip()
|
|
34
|
+
if not token:
|
|
35
|
+
return
|
|
36
|
+
if token not in seen:
|
|
37
|
+
seen.add(token)
|
|
38
|
+
out.append(token)
|
|
39
|
+
|
|
40
|
+
for raw in values or []:
|
|
41
|
+
if raw is None:
|
|
42
|
+
continue
|
|
43
|
+
if raw == "-":
|
|
44
|
+
for tok in stdin_reader():
|
|
45
|
+
_add(tok)
|
|
46
|
+
continue
|
|
47
|
+
for part in raw.split(","):
|
|
48
|
+
_add(part)
|
|
49
|
+
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def enforce_mutually_exclusive(
|
|
54
|
+
groups: list[str], jobs: list[str]
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Raise a usage error if both -g and -j were supplied."""
|
|
57
|
+
if groups and jobs:
|
|
58
|
+
raise typer.BadParameter("-g/--group and -j/--job are mutually exclusive")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def reject_unsupported_dimension(
|
|
62
|
+
resource: str,
|
|
63
|
+
*,
|
|
64
|
+
groups: list[str],
|
|
65
|
+
jobs: list[str],
|
|
66
|
+
allow_group: bool,
|
|
67
|
+
allow_job: bool,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Reject ids for a dimension the resource does not support.
|
|
70
|
+
|
|
71
|
+
Raises ``typer.BadParameter`` (mapped to usage exit code 2) when an
|
|
72
|
+
unsupported dimension is provided (e.g. ``report -g``, ``overview job -g``).
|
|
73
|
+
"""
|
|
74
|
+
if groups and not allow_group:
|
|
75
|
+
raise typer.BadParameter(
|
|
76
|
+
f"{resource} does not support -g/--group (group dimension)"
|
|
77
|
+
)
|
|
78
|
+
if jobs and not allow_job:
|
|
79
|
+
raise typer.BadParameter(
|
|
80
|
+
f"{resource} does not support -j/--job (job dimension)"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def require_any(groups: list[str], jobs: list[str]) -> None:
|
|
85
|
+
"""Raise a usage error if neither -g nor -j was supplied."""
|
|
86
|
+
if not groups and not jobs:
|
|
87
|
+
raise typer.BadParameter("at least one -g/--group or -j/--job is required")
|
aliyun/bmcli/output.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Output helpers: JSON to stdout, files to disk, logs/progress to stderr."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
# Module level verbosity flags, set by the CLI callback.
|
|
11
|
+
_QUIET = False
|
|
12
|
+
_VERBOSE = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_verbosity(quiet: bool = False, verbose: bool = False) -> None:
|
|
16
|
+
global _QUIET, _VERBOSE
|
|
17
|
+
_QUIET = quiet
|
|
18
|
+
_VERBOSE = verbose
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _dumps(obj: Any, pretty: bool) -> str:
|
|
22
|
+
if pretty:
|
|
23
|
+
return json.dumps(obj, ensure_ascii=False, indent=2)
|
|
24
|
+
return json.dumps(obj, ensure_ascii=False, separators=(",", ":"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def emit_json(data: Any, pretty: bool = True) -> None:
|
|
28
|
+
"""Print JSON data to stdout (data channel, pipeable to jq)."""
|
|
29
|
+
sys.stdout.write(_dumps(data, pretty) + "\n")
|
|
30
|
+
sys.stdout.flush()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def write_json_file(path: str | Path, obj: Any, pretty: bool = True) -> Path:
|
|
34
|
+
"""Write a JSON object to ``path``, creating parent dirs. Returns path."""
|
|
35
|
+
p = Path(path)
|
|
36
|
+
ensure_dir(p.parent)
|
|
37
|
+
p.write_text(_dumps(obj, pretty) + "\n", encoding="utf-8")
|
|
38
|
+
return p
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def write_text_file(path: str | Path, text: str) -> Path:
|
|
42
|
+
"""Write raw text (e.g. markdown report) to ``path``. Returns path."""
|
|
43
|
+
p = Path(path)
|
|
44
|
+
ensure_dir(p.parent)
|
|
45
|
+
p.write_text(text, encoding="utf-8")
|
|
46
|
+
return p
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ensure_dir(path: str | Path) -> Path:
|
|
50
|
+
"""Create a directory (and parents) if needed. Returns the path."""
|
|
51
|
+
p = Path(path) if str(path) else Path(".")
|
|
52
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
return p
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def log(message: str) -> None:
|
|
57
|
+
"""Informational log to stderr (suppressed by --quiet)."""
|
|
58
|
+
if not _QUIET:
|
|
59
|
+
sys.stderr.write(message + "\n")
|
|
60
|
+
sys.stderr.flush()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def debug(message: str) -> None:
|
|
64
|
+
"""Debug log to stderr (only with --verbose)."""
|
|
65
|
+
if _VERBOSE and not _QUIET:
|
|
66
|
+
sys.stderr.write(f"[debug] {message}\n")
|
|
67
|
+
sys.stderr.flush()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def warn(message: str) -> None:
|
|
71
|
+
"""Warning to stderr (suppressed by --quiet)."""
|
|
72
|
+
if not _QUIET:
|
|
73
|
+
sys.stderr.write(f"[warn] {message}\n")
|
|
74
|
+
sys.stderr.flush()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def error(message: str) -> None:
|
|
78
|
+
"""Error to stderr (always shown)."""
|
|
79
|
+
sys.stderr.write(f"[error] {message}\n")
|
|
80
|
+
sys.stderr.flush()
|
aliyun/bmcli/runtime.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Shared runtime context and concurrent fetch / partial-failure helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from aliyun.bmcli import output
|
|
12
|
+
from aliyun.bmcli.client import ApiClient, AuthError, NetworkError
|
|
13
|
+
from aliyun.bmcli.config import Config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Runtime:
|
|
18
|
+
"""Resolved configuration plus per-invocation runtime options."""
|
|
19
|
+
|
|
20
|
+
config: Config
|
|
21
|
+
concurrency: int = 5
|
|
22
|
+
timeout: float = 300.0
|
|
23
|
+
retry: int = 2
|
|
24
|
+
cluster: str | None = None
|
|
25
|
+
pretty: bool = True
|
|
26
|
+
output_dir: str = "."
|
|
27
|
+
verbose: bool = False
|
|
28
|
+
quiet: bool = False
|
|
29
|
+
_client: ApiClient | None = field(default=None, repr=False)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def client(self) -> ApiClient:
|
|
33
|
+
if self._client is None:
|
|
34
|
+
self._client = ApiClient(
|
|
35
|
+
host=self.config.host,
|
|
36
|
+
api_key=self.config.api_key,
|
|
37
|
+
timeout=self.timeout,
|
|
38
|
+
retry=self.retry,
|
|
39
|
+
cluster=self.cluster,
|
|
40
|
+
)
|
|
41
|
+
return self._client
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_runtime(ctx: typer.Context) -> Runtime:
|
|
45
|
+
"""Fetch the Runtime stashed on the Typer context by the root callback."""
|
|
46
|
+
rt = ctx.obj
|
|
47
|
+
if not isinstance(rt, Runtime): # pragma: no cover - defensive
|
|
48
|
+
raise typer.BadParameter("CLI not initialized")
|
|
49
|
+
return rt
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class FetchResult:
|
|
54
|
+
"""Outcome of a concurrent multi-id fetch."""
|
|
55
|
+
|
|
56
|
+
items: list[Any]
|
|
57
|
+
failures: list[tuple[str, str]] # (id, error message)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_concurrent(
|
|
61
|
+
ids: list[str],
|
|
62
|
+
worker: Callable[[str], Any],
|
|
63
|
+
concurrency: int,
|
|
64
|
+
) -> FetchResult:
|
|
65
|
+
"""Run ``worker`` over ``ids`` concurrently, preserving input order.
|
|
66
|
+
|
|
67
|
+
A single id failing does not abort the others; its error is collected.
|
|
68
|
+
Auth errors abort early since they affect every request.
|
|
69
|
+
"""
|
|
70
|
+
items: list[Any] = [None] * len(ids)
|
|
71
|
+
failures: list[tuple[str, str]] = []
|
|
72
|
+
|
|
73
|
+
if not ids:
|
|
74
|
+
return FetchResult(items=[], failures=[])
|
|
75
|
+
|
|
76
|
+
max_workers = max(1, min(concurrency, len(ids)))
|
|
77
|
+
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
|
78
|
+
future_map = {pool.submit(worker, _id): idx for idx, _id in enumerate(ids)}
|
|
79
|
+
for future in future_map:
|
|
80
|
+
idx = future_map[future]
|
|
81
|
+
_id = ids[idx]
|
|
82
|
+
try:
|
|
83
|
+
items[idx] = future.result()
|
|
84
|
+
except AuthError:
|
|
85
|
+
# Auth failure is fatal for the whole invocation.
|
|
86
|
+
raise
|
|
87
|
+
except NetworkError as exc:
|
|
88
|
+
failures.append((_id, f"network: {exc}"))
|
|
89
|
+
except Exception as exc: # noqa: BLE001 - collect any per-id failure
|
|
90
|
+
failures.append((_id, str(exc)))
|
|
91
|
+
|
|
92
|
+
ordered = [it for it in items if it is not None]
|
|
93
|
+
return FetchResult(items=ordered, failures=failures)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def report_and_exit(
|
|
97
|
+
result: FetchResult,
|
|
98
|
+
*,
|
|
99
|
+
emit: bool,
|
|
100
|
+
data: Any | None = None,
|
|
101
|
+
pretty: bool = True,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Emit data (optional), print failure summary to stderr, and exit.
|
|
104
|
+
|
|
105
|
+
Exit codes: 0 all success, 1 partial failure. Raises typer.Exit.
|
|
106
|
+
"""
|
|
107
|
+
if emit:
|
|
108
|
+
output.emit_json(data if data is not None else result.items, pretty=pretty)
|
|
109
|
+
|
|
110
|
+
if result.failures:
|
|
111
|
+
output.error(f"{len(result.failures)} id(s) failed:")
|
|
112
|
+
for _id, msg in result.failures:
|
|
113
|
+
output.error(f" - {_id}: {msg}")
|
|
114
|
+
from aliyun.bmcli.exit_codes import NETWORK, PARTIAL
|
|
115
|
+
|
|
116
|
+
# When nothing succeeded and every failure was a network/timeout error,
|
|
117
|
+
# surface the dedicated network exit code (4) rather than partial (1).
|
|
118
|
+
all_network = all(msg.startswith("network:") for _id, msg in result.failures)
|
|
119
|
+
if not result.items and all_network:
|
|
120
|
+
raise typer.Exit(code=NETWORK)
|
|
121
|
+
raise typer.Exit(code=PARTIAL)
|
|
122
|
+
|
|
123
|
+
from aliyun.bmcli.exit_codes import OK
|
|
124
|
+
|
|
125
|
+
raise typer.Exit(code=OK)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def safe_run_concurrent(
|
|
129
|
+
ids: list[str],
|
|
130
|
+
worker: Callable[[str], Any],
|
|
131
|
+
concurrency: int,
|
|
132
|
+
) -> "FetchResult":
|
|
133
|
+
"""Like run_concurrent but maps AuthError→exit 3 and NetworkError→exit 4.
|
|
134
|
+
|
|
135
|
+
Call this instead of run_concurrent in command handlers so that fatal
|
|
136
|
+
errors produce the documented exit codes rather than an unhandled exception.
|
|
137
|
+
"""
|
|
138
|
+
from aliyun.bmcli.exit_codes import AUTH, NETWORK
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
return run_concurrent(ids, worker, concurrency)
|
|
142
|
+
except AuthError as exc:
|
|
143
|
+
output.error(f"authentication error: {exc}")
|
|
144
|
+
raise typer.Exit(code=AUTH) from exc
|
|
145
|
+
except NetworkError as exc:
|
|
146
|
+
output.error(f"network error: {exc}")
|
|
147
|
+
raise typer.Exit(code=NETWORK) from exc
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: benchloop_client
|
|
3
|
+
Version: 0.1.6
|
|
4
|
+
Summary: bmcli — command line tool for benchmark analysis data (metrics, trajectory, reports, artifacts).
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: httpx>=0.25.2
|
|
7
|
+
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
8
|
+
Requires-Dist: typer>=0.9.4
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# benchloop-cli (`bmcli`)
|
|
14
|
+
|
|
15
|
+
Command line tool for fetching and downloading benchmark analysis data
|
|
16
|
+
(run metrics, trajectory, pre-analysis reports, task/job lists, and artifacts)
|
|
17
|
+
by `group_id` / `job_id`.
|
|
18
|
+
|
|
19
|
+
## Install package
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
pip install -i http://yum.tbsite.net/aliyun-pypi/simple/ --trusted-host=yum.tbsite.net bmcli
|
|
23
|
+
|
|
24
|
+
bmcli --api-key $BENCH_LOOP_API_KEY overview get -j $JOB_ID
|
|
25
|
+
bmcli --api-key $BENCH_LOOP_API_KEY metrics get -j $JOB_ID
|
|
26
|
+
|
|
27
|
+
# Option 2: config ~/.benchloop/config.toml by cli, and run bmcli without --api-key option:
|
|
28
|
+
bmcli config set --api-key $BENCHLOOP_API_KEY --host https://benchmark-analysis.alibaba-inc.com
|
|
29
|
+
|
|
30
|
+
bmcli overview get -j $JOB_ID
|
|
31
|
+
bmcli metrics get -j $JOB_ID
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Install and run from source
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# with uv (recommended)
|
|
39
|
+
uv sync
|
|
40
|
+
uv run bmcli --help
|
|
41
|
+
|
|
42
|
+
# or editable install with pip
|
|
43
|
+
python -m venv .venv && source .venv/bin/activate
|
|
44
|
+
pip install -e .
|
|
45
|
+
bmcli --help
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Auth & config
|
|
49
|
+
|
|
50
|
+
Authentication uses a personal api key sent as the `X-API-Key` header.
|
|
51
|
+
|
|
52
|
+
Precedence: CLI flag > env var > `~/.benchloop/config.toml` > default.
|
|
53
|
+
|
|
54
|
+
- Env: `BENCHLOOP_API_KEY` (or `BENCHMARK_API_KEY`), `BENCHLOOP_HOST`, `BENCHLOOP_OUTPUT_DIR`
|
|
55
|
+
- Default host: `https://benchmark-analysis.alibaba-inc.com`
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bmcli config set --api-key $BENCHLOOP_API_KEY --host https://benchmark-analysis.alibaba-inc.com
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
Two-segment `bmcli <resource> <action>` form. `get` prints a JSON array to
|
|
64
|
+
stdout (pipeable to `jq`); `download` writes the standard file layout.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bmcli metrics get -j J123 | jq '.[0]'
|
|
68
|
+
bmcli metrics download -g G1 --scope all -o ./out
|
|
69
|
+
bmcli trajectory download -g G1,G2 -o ./out
|
|
70
|
+
bmcli report download -j J123 --agent all
|
|
71
|
+
bmcli jobs get -g G1 --all # get supports --all (auto-paginate)
|
|
72
|
+
bmcli jobs download -g G1 # download is always full
|
|
73
|
+
bmcli artifacts download -g G1 -o ./out
|
|
74
|
+
bmcli pull -g G1 --with metrics,trajectory,jobs,artifacts -o ./out
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
IDs accept repeated flags, comma-separated values, and `-` to read from stdin.
|
|
78
|
+
`-g` and `-j` are mutually exclusive in a single command.
|
|
79
|
+
|
|
80
|
+
## Exit codes
|
|
81
|
+
|
|
82
|
+
| code | meaning |
|
|
83
|
+
| ---- | ------- |
|
|
84
|
+
| 0 | all success |
|
|
85
|
+
| 1 | partial failure (some ids failed) |
|
|
86
|
+
| 2 | usage / parameter error |
|
|
87
|
+
| 3 | auth failure (HTTP 401) |
|
|
88
|
+
| 4 | network / timeout |
|
|
89
|
+
|
|
90
|
+
stdout carries data only; logs and progress go to stderr. `--quiet` suppresses
|
|
91
|
+
non-error logs, `--verbose` adds debug logs.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Release new package
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
uv build
|
|
98
|
+
twine check dist/bmcli-0.1.6-py3-none-any.whl
|
|
99
|
+
twine upload -r aliyun-pypi dist/bmcli-0.1.6-py3-none-any.whl --verbose
|
|
100
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
aliyun/bmcli/__init__.py,sha256=TkYkNbqespvPiIfcCLuf9m-wQagsVQUM47jjqm0pzqk,99
|
|
2
|
+
aliyun/bmcli/__main__.py,sha256=qQPm9IIfmqXkVmk8zfSG1zkB84dDFFfEcWXWXTImzL4,601
|
|
3
|
+
aliyun/bmcli/cli.py,sha256=WvHmoSaNCNYuO9xleZfjZ8Nn-hB_Qww14MWMklemAZY,4472
|
|
4
|
+
aliyun/bmcli/client.py,sha256=h3pUwmQ0_WG4pih9q3gB5A07cjM0IJ3ErkpNS0rWSx0,5077
|
|
5
|
+
aliyun/bmcli/config.py,sha256=VqRJf0dXINskQTPjubjoWjLZUsffNfOOh-aaPPlzMt4,3545
|
|
6
|
+
aliyun/bmcli/exit_codes.py,sha256=EYGCWM4asW1-rgCt-oO5Gl2a7-VeXmE7gKb0kwLLWoo,257
|
|
7
|
+
aliyun/bmcli/ids.py,sha256=oEj-De0GjPMY9WwaPBNq_p4exJ3B3baOem2IyQCftj8,2369
|
|
8
|
+
aliyun/bmcli/output.py,sha256=xsRIh4rhje6U2gWInglLkCm1SmmmM4viSAYIRVvv4Do,2242
|
|
9
|
+
aliyun/bmcli/runtime.py,sha256=wQK0lOCV5IBndpAQj9B-eks0QucQrBlysiIXrQOKyjI,4688
|
|
10
|
+
aliyun/bmcli/commands/__init__.py,sha256=xK8Hjh-Ic9rab3UI2RQb-lgraIUAEgxOrlGpZsgHlmQ,55
|
|
11
|
+
aliyun/bmcli/commands/_common.py,sha256=a1YcsPLRB-YbmwEn15uUytFugwIonX3cFkYQMjx1kzE,1146
|
|
12
|
+
aliyun/bmcli/commands/artifacts.py,sha256=z_DyEZfHz9pCQSNQwh3fN7_Gb6ca8qoJFpc3N2clJJg,5169
|
|
13
|
+
aliyun/bmcli/commands/jobs.py,sha256=Dl1bZ8MvvoSTYMTul7Ln1cuV759QMwQP_uSxs8T82vA,4751
|
|
14
|
+
aliyun/bmcli/commands/metrics.py,sha256=vSHJcZMg5pWObpM05GHMNcaaQNedu1Aq6cEYzCKzH-Y,4685
|
|
15
|
+
aliyun/bmcli/commands/overview.py,sha256=jjPsLhxIBncYB2xDWRAB6PuQWEFgfpKlYVrLnCr1rS4,2317
|
|
16
|
+
aliyun/bmcli/commands/pull.py,sha256=cdURofZZB4fGHniAfStMkt02rgheTr74DJwFYfr1VBg,6806
|
|
17
|
+
aliyun/bmcli/commands/report.py,sha256=1pdM1oz5o0KSgJ0Z2lFhs7ltY5Sd42rDiznHOVDY2a0,3155
|
|
18
|
+
aliyun/bmcli/commands/tasks.py,sha256=6R831JssRr-NUvv2ftrQQC0DvNm6dt6N0RVMIBYL-dc,4354
|
|
19
|
+
aliyun/bmcli/commands/trajectory.py,sha256=BViov85xYuJ386kLo6xWHsRt_qvJ7nrt2pzMXFiOgqo,2886
|
|
20
|
+
benchloop_client-0.1.6.dist-info/METADATA,sha256=B_QG4kOUeQg7OAjrYN3orHuGVUF51OwzDyu3e4CGu0U,2953
|
|
21
|
+
benchloop_client-0.1.6.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
22
|
+
benchloop_client-0.1.6.dist-info/entry_points.txt,sha256=Nunw6TW3VbmDcuN1pCcDAx7vGmU6G-F7SW87nvI18as,53
|
|
23
|
+
benchloop_client-0.1.6.dist-info/RECORD,,
|