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/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()
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ bmcli = aliyun.bmcli.__main__:main