benchloop-client 0.2.23__tar.gz → 0.2.25__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.
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/PKG-INFO +13 -1
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/README.md +12 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/pyproject.toml +1 -1
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/cli.py +2 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/client.py +24 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/_common.py +6 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/metrics.py +14 -3
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/pull.py +8 -3
- benchloop_client-0.2.25/src/aliyun/bmcli/commands/rsi.py +235 -0
- benchloop_client-0.2.25/src/aliyun/bmcli/commands/trajectory.py +680 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_client.py +63 -1
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_metrics.py +32 -2
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_pull.py +80 -0
- benchloop_client-0.2.25/tests/test_cmd_rsi.py +105 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_trajectory.py +487 -42
- benchloop_client-0.2.23/src/aliyun/bmcli/commands/trajectory.py +0 -363
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/.gitignore +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/.python-version +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/RELEASE.md +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/benchloop-cli-design.md +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/bmcli_examples.sh +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/__init__.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/__main__.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/__init__.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/ap_cmd.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/artifacts.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/brm.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/group_data.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/jobs.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/metadata.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/overview.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/report.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/commands/tasks.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/config.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/exit_codes.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/ids.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/output.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/runtime.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/src/aliyun/bmcli/version_check.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/__init__.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/conftest.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cli_options.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_ap_cmd.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_artifacts.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_brm.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_metadata.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_overview.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_report.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_cmd_tasks_jobs.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_config.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_exit_code_behavior.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_exit_codes.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_group_data.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_ids.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_runtime.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_transfer_progress.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/tests/test_version_check.py +0 -0
- {benchloop_client-0.2.23 → benchloop_client-0.2.25}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: benchloop_client
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.25
|
|
4
4
|
Summary: Benchloop CLI — command line tool for benchmark analysis data (metrics, trajectory, reports, artifacts).
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: httpx>=0.25.2
|
|
@@ -84,6 +84,9 @@ bl metadata download --task-id TASK_ID -o ./out
|
|
|
84
84
|
bl report download -j J123 -o ./out
|
|
85
85
|
bl report get -g G123
|
|
86
86
|
bl report download -g G123 -o ./out
|
|
87
|
+
bl rsi get -j J123
|
|
88
|
+
bl rsi trigger -j J123
|
|
89
|
+
bl rsi download -g G123 -o ./out
|
|
87
90
|
bl jobs get -g G1 --all # get supports --all (auto-paginate)
|
|
88
91
|
bl jobs download -g G1 # download is always full
|
|
89
92
|
bl artifacts download -g G1 -o ./out
|
|
@@ -99,6 +102,15 @@ bl brm status --task-id UID
|
|
|
99
102
|
bl brm list -g G1
|
|
100
103
|
```
|
|
101
104
|
|
|
105
|
+
Metrics and Job trajectory requests always explicitly use the Trace source. The
|
|
106
|
+
CLI does not expose a `trace`/`artifacts` source selector.
|
|
107
|
+
|
|
108
|
+
Group trajectory retrieval and downloads use the backend Trace export API. The
|
|
109
|
+
CLI waits for the export URL, downloads the gzip JSON-lines payload, and keeps
|
|
110
|
+
the existing `group_id`/`trajectories` JSON layout. The export wait timeout is
|
|
111
|
+
30 minutes by default; override it in seconds with
|
|
112
|
+
`--download-timeout SECONDS` on trajectory commands or `pull`.
|
|
113
|
+
|
|
102
114
|
Group downloads always auto-paginate and include every Job in the Group. In
|
|
103
115
|
particular, `trajectory`, detailed `metrics`, `report`, and `artifacts` enumerate
|
|
104
116
|
the complete Job list instead of relying on the backend Group endpoints that
|
|
@@ -72,6 +72,9 @@ bl metadata download --task-id TASK_ID -o ./out
|
|
|
72
72
|
bl report download -j J123 -o ./out
|
|
73
73
|
bl report get -g G123
|
|
74
74
|
bl report download -g G123 -o ./out
|
|
75
|
+
bl rsi get -j J123
|
|
76
|
+
bl rsi trigger -j J123
|
|
77
|
+
bl rsi download -g G123 -o ./out
|
|
75
78
|
bl jobs get -g G1 --all # get supports --all (auto-paginate)
|
|
76
79
|
bl jobs download -g G1 # download is always full
|
|
77
80
|
bl artifacts download -g G1 -o ./out
|
|
@@ -87,6 +90,15 @@ bl brm status --task-id UID
|
|
|
87
90
|
bl brm list -g G1
|
|
88
91
|
```
|
|
89
92
|
|
|
93
|
+
Metrics and Job trajectory requests always explicitly use the Trace source. The
|
|
94
|
+
CLI does not expose a `trace`/`artifacts` source selector.
|
|
95
|
+
|
|
96
|
+
Group trajectory retrieval and downloads use the backend Trace export API. The
|
|
97
|
+
CLI waits for the export URL, downloads the gzip JSON-lines payload, and keeps
|
|
98
|
+
the existing `group_id`/`trajectories` JSON layout. The export wait timeout is
|
|
99
|
+
30 minutes by default; override it in seconds with
|
|
100
|
+
`--download-timeout SECONDS` on trajectory commands or `pull`.
|
|
101
|
+
|
|
90
102
|
Group downloads always auto-paginate and include every Job in the Group. In
|
|
91
103
|
particular, `trajectory`, detailed `metrics`, `report`, and `artifacts` enumerate
|
|
92
104
|
the complete Job list instead of relying on the backend Group endpoints that
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "benchloop_client"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.25"
|
|
8
8
|
description = "Benchloop CLI — command line tool for benchmark analysis data (metrics, trajectory, reports, artifacts)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -17,6 +17,7 @@ from aliyun.bmcli.commands import (
|
|
|
17
17
|
overview as overview_cmd,
|
|
18
18
|
pull as pull_cmd,
|
|
19
19
|
report as report_cmd,
|
|
20
|
+
rsi as rsi_cmd,
|
|
20
21
|
tasks as tasks_cmd,
|
|
21
22
|
trajectory as trajectory_cmd,
|
|
22
23
|
)
|
|
@@ -139,6 +140,7 @@ app.add_typer(jobs_cmd.app, name="jobs")
|
|
|
139
140
|
app.add_typer(artifacts_cmd.app, name="artifacts")
|
|
140
141
|
app.add_typer(metadata_cmd.app, name="metadata")
|
|
141
142
|
app.add_typer(brm_cmd.app, name="brm")
|
|
143
|
+
app.add_typer(rsi_cmd.app, name="rsi")
|
|
142
144
|
app.command(
|
|
143
145
|
"ap-cmd",
|
|
144
146
|
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
@@ -38,6 +38,9 @@ class ArtifactMissingError(ClientError):
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
_SSO_LOGIN_HOST = "login.alibaba-inc.com"
|
|
42
|
+
|
|
43
|
+
|
|
41
44
|
class ApiClient:
|
|
42
45
|
"""Thin wrapper over ``httpx.Client`` for the benchmark openapi."""
|
|
43
46
|
|
|
@@ -148,11 +151,16 @@ class ApiClient:
|
|
|
148
151
|
url: str,
|
|
149
152
|
dest_path: str | Path,
|
|
150
153
|
progress: Optional[Any] = None,
|
|
154
|
+
*,
|
|
155
|
+
detect_sso_redirect: bool = False,
|
|
151
156
|
) -> Path:
|
|
152
157
|
"""Stream ``url`` to ``dest_path`` on disk. Returns the path written.
|
|
153
158
|
|
|
154
159
|
``progress``(可选)需提供 ``begin(total)`` / ``advance(nbytes)`` 两个
|
|
155
160
|
方法(如 ``output.TransferStream``);每次重试会重新 ``begin`` 归零。
|
|
161
|
+
|
|
162
|
+
``detect_sso_redirect`` 开启时,被网关 302 到 SSO 登录页抛 ``AuthError``;
|
|
163
|
+
默认关闭,保持"HTML 错误页 = 产物缺失"的既有分类,不影响其他调用方。
|
|
156
164
|
"""
|
|
157
165
|
dest = Path(dest_path)
|
|
158
166
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -161,6 +169,13 @@ class ApiClient:
|
|
|
161
169
|
try:
|
|
162
170
|
with httpx.Client(timeout=self.timeout, follow_redirects=True) as client:
|
|
163
171
|
with client.stream("GET", self._url(url), headers=self._headers()) as resp:
|
|
172
|
+
if detect_sso_redirect and self._is_sso_login_redirect(resp):
|
|
173
|
+
# 网关不认 X-API-Key 时会 302 到 SSO 登录页;这是
|
|
174
|
+
# 认证问题,不能落入下面的“HTML 错误页=文件缺失”误判。
|
|
175
|
+
raise AuthError(
|
|
176
|
+
"endpoint requires SSO login (redirected to "
|
|
177
|
+
f"{_SSO_LOGIN_HOST}); X-API-Key not accepted"
|
|
178
|
+
)
|
|
164
179
|
if resp.status_code == 401:
|
|
165
180
|
raise AuthError("authentication failed (HTTP 401)")
|
|
166
181
|
if resp.status_code == 404:
|
|
@@ -199,6 +214,15 @@ class ApiClient:
|
|
|
199
214
|
raise last_exc
|
|
200
215
|
return dest # pragma: no cover
|
|
201
216
|
|
|
217
|
+
@staticmethod
|
|
218
|
+
def _is_sso_login_redirect(resp: httpx.Response) -> bool:
|
|
219
|
+
targets = [r.headers.get("location", "") for r in resp.history]
|
|
220
|
+
try:
|
|
221
|
+
targets.append(str(resp.url))
|
|
222
|
+
except RuntimeError:
|
|
223
|
+
pass # resp.request 未设置时(如测试里的裸 Response)没有 url
|
|
224
|
+
return any(_SSO_LOGIN_HOST in target for target in targets)
|
|
225
|
+
|
|
202
226
|
@staticmethod
|
|
203
227
|
def _safe_body(resp: httpx.Response) -> Any:
|
|
204
228
|
try:
|
|
@@ -31,6 +31,12 @@ OutputDirOption = typer.Option(
|
|
|
31
31
|
SkipExistingOption = typer.Option(
|
|
32
32
|
False, "--skip-existing", help="Skip files that already exist on disk."
|
|
33
33
|
)
|
|
34
|
+
DownloadTimeoutOption = typer.Option(
|
|
35
|
+
1800,
|
|
36
|
+
"--download-timeout",
|
|
37
|
+
min=1,
|
|
38
|
+
help="Maximum seconds to wait for a Group Trace trajectory export.",
|
|
39
|
+
)
|
|
34
40
|
|
|
35
41
|
|
|
36
42
|
def expand(values: Optional[list[str]]) -> list[str]:
|
|
@@ -25,6 +25,7 @@ from aliyun.bmcli.runtime import get_runtime, report_and_exit, safe_run_concurre
|
|
|
25
25
|
app = typer.Typer(help="Run metrics (Group overall/detail, Job).", no_args_is_help=True)
|
|
26
26
|
|
|
27
27
|
_SCOPES = ("overall", "detail", "all")
|
|
28
|
+
_TRACE_SOURCE = "trace"
|
|
28
29
|
|
|
29
30
|
_GROUP_RUN_METRIC_FIELDS = (
|
|
30
31
|
"job_count",
|
|
@@ -112,7 +113,9 @@ def _validate_scope(scope: str) -> str:
|
|
|
112
113
|
|
|
113
114
|
|
|
114
115
|
def _fetch_job(client, job_id: str):
|
|
115
|
-
data = client.get_json(
|
|
116
|
+
data = client.get_json(
|
|
117
|
+
f"/openapi/metrics/{job_id}", params={"source": _TRACE_SOURCE}
|
|
118
|
+
)
|
|
116
119
|
return _format_job_payload(data, job_id)
|
|
117
120
|
|
|
118
121
|
|
|
@@ -125,7 +128,12 @@ def fetch_group_detail(
|
|
|
125
128
|
nonlocal tokenized_metrics
|
|
126
129
|
data = client.get_json(
|
|
127
130
|
"/openapi/metrics",
|
|
128
|
-
params={
|
|
131
|
+
params={
|
|
132
|
+
"group_id": group_id,
|
|
133
|
+
"skip": skip,
|
|
134
|
+
"limit": limit,
|
|
135
|
+
"source": _TRACE_SOURCE,
|
|
136
|
+
},
|
|
129
137
|
)
|
|
130
138
|
if isinstance(data, dict) and tokenized_metrics is None:
|
|
131
139
|
tokenized_metrics = data.get("tokenized_metrics")
|
|
@@ -154,7 +162,10 @@ def fetch_group_detail(
|
|
|
154
162
|
|
|
155
163
|
|
|
156
164
|
def _fetch_group_overall(client, group_id: str):
|
|
157
|
-
data = client.get_json(
|
|
165
|
+
data = client.get_json(
|
|
166
|
+
f"/openapi/groups/{group_id}/metrics/overall",
|
|
167
|
+
params={"source": _TRACE_SOURCE},
|
|
168
|
+
)
|
|
158
169
|
return _format_group_overall(data, group_id)
|
|
159
170
|
|
|
160
171
|
|
|
@@ -14,6 +14,7 @@ from aliyun.bmcli.commands._common import (
|
|
|
14
14
|
JobOption,
|
|
15
15
|
OutputDirOption,
|
|
16
16
|
SkipExistingOption,
|
|
17
|
+
DownloadTimeoutOption,
|
|
17
18
|
expand,
|
|
18
19
|
resolve_output_dir,
|
|
19
20
|
write_empty_tgz,
|
|
@@ -29,7 +30,10 @@ from aliyun.bmcli.commands.metrics import (
|
|
|
29
30
|
fetch_group_detail,
|
|
30
31
|
)
|
|
31
32
|
from aliyun.bmcli.commands.tasks import _fetch_all as fetch_all_tasks
|
|
32
|
-
from aliyun.bmcli.commands.trajectory import
|
|
33
|
+
from aliyun.bmcli.commands.trajectory import (
|
|
34
|
+
fetch_job_trajectory,
|
|
35
|
+
write_group_trajectories,
|
|
36
|
+
)
|
|
33
37
|
from aliyun.bmcli.client import ArtifactMissingError, AuthError
|
|
34
38
|
from aliyun.bmcli.ids import enforce_mutually_exclusive, require_any
|
|
35
39
|
from aliyun.bmcli.runtime import get_runtime
|
|
@@ -61,6 +65,7 @@ def pull(
|
|
|
61
65
|
),
|
|
62
66
|
output_dir: Optional[str] = OutputDirOption,
|
|
63
67
|
skip_existing: bool = SkipExistingOption,
|
|
68
|
+
download_timeout: int = DownloadTimeoutOption,
|
|
64
69
|
) -> None:
|
|
65
70
|
"""Aggregate downloads for the given groups/jobs into the layout."""
|
|
66
71
|
rt = get_runtime(ctx)
|
|
@@ -107,7 +112,7 @@ def pull(
|
|
|
107
112
|
f"{jid}:trajectory",
|
|
108
113
|
lambda jid=jid: _fetch_and_write_json(
|
|
109
114
|
root / f"{jid}_trajectory.json",
|
|
110
|
-
lambda: client
|
|
115
|
+
lambda: fetch_job_trajectory(client, jid),
|
|
111
116
|
),
|
|
112
117
|
)
|
|
113
118
|
if "report" in selected:
|
|
@@ -160,7 +165,7 @@ def pull(
|
|
|
160
165
|
rt.concurrency,
|
|
161
166
|
trajectory_dest,
|
|
162
167
|
pretty=rt.pretty,
|
|
163
|
-
|
|
168
|
+
download_timeout=download_timeout,
|
|
164
169
|
),
|
|
165
170
|
)
|
|
166
171
|
if "report" in selected:
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""`bmcli rsi` — RSI weakness analysis reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from aliyun.bmcli import output
|
|
10
|
+
from aliyun.bmcli.client import AuthError, NetworkError
|
|
11
|
+
from aliyun.bmcli.commands._common import (
|
|
12
|
+
GroupOption,
|
|
13
|
+
JobOption,
|
|
14
|
+
OutputDirOption,
|
|
15
|
+
SkipExistingOption,
|
|
16
|
+
expand,
|
|
17
|
+
resolve_output_dir,
|
|
18
|
+
)
|
|
19
|
+
from aliyun.bmcli.commands.group_data import (
|
|
20
|
+
fetch_all_jobs,
|
|
21
|
+
fetch_job_resources,
|
|
22
|
+
job_ids,
|
|
23
|
+
raise_for_failures,
|
|
24
|
+
)
|
|
25
|
+
from aliyun.bmcli.ids import enforce_mutually_exclusive, require_any
|
|
26
|
+
from aliyun.bmcli.runtime import get_runtime, report_and_exit, safe_run_concurrent
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(help="RSI weakness analysis reports.", no_args_is_help=True)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _guard(groups: list[str], jobs: list[str]) -> None:
|
|
32
|
+
enforce_mutually_exclusive(groups, jobs)
|
|
33
|
+
require_any(groups, jobs)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _markdown(data: Any, key: str) -> str:
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
return ""
|
|
39
|
+
report = data.get("report")
|
|
40
|
+
if not isinstance(report, dict):
|
|
41
|
+
return ""
|
|
42
|
+
content = report.get(key) or ""
|
|
43
|
+
return content if isinstance(content, str) else ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _fetch_job(client, job_id: str):
|
|
47
|
+
return client.get_json(f"/openapi/jobs/{job_id}/rsi")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _fetch_group(client, group_id: str):
|
|
51
|
+
return client.get_json(f"/openapi/groups/{group_id}/rsi")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _trigger_job(client, job_id: str):
|
|
55
|
+
return client.post_json(f"/openapi/jobs/{job_id}/rsi/trigger")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _trigger_group(client, group_id: str, include_passed: bool):
|
|
59
|
+
return client.post_json(
|
|
60
|
+
f"/openapi/groups/{group_id}/rsi/trigger",
|
|
61
|
+
{"include_passed": include_passed},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _write_markdown(content: str, dest, skip_existing: bool):
|
|
66
|
+
if not content:
|
|
67
|
+
return None
|
|
68
|
+
if skip_existing and dest.exists():
|
|
69
|
+
output.log(f"skip existing {dest}")
|
|
70
|
+
else:
|
|
71
|
+
output.write_text_file(dest, content)
|
|
72
|
+
output.log(f"wrote {dest}")
|
|
73
|
+
return str(dest)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("get")
|
|
77
|
+
def get(
|
|
78
|
+
ctx: typer.Context,
|
|
79
|
+
group: Optional[list[str]] = GroupOption,
|
|
80
|
+
job: Optional[list[str]] = JobOption,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Query Job or Group RSI reports; prints a JSON array."""
|
|
83
|
+
rt = get_runtime(ctx)
|
|
84
|
+
groups = expand(group)
|
|
85
|
+
jobs = expand(job)
|
|
86
|
+
_guard(groups, jobs)
|
|
87
|
+
|
|
88
|
+
if jobs:
|
|
89
|
+
res = safe_run_concurrent(
|
|
90
|
+
jobs, lambda jid: _fetch_job(rt.client, jid), rt.concurrency
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
res = safe_run_concurrent(
|
|
94
|
+
groups, lambda gid: _fetch_group(rt.client, gid), rt.concurrency
|
|
95
|
+
)
|
|
96
|
+
report_and_exit(res, emit=True, pretty=rt.pretty)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command("download")
|
|
100
|
+
def download(
|
|
101
|
+
ctx: typer.Context,
|
|
102
|
+
group: Optional[list[str]] = GroupOption,
|
|
103
|
+
job: Optional[list[str]] = JobOption,
|
|
104
|
+
output_dir: Optional[str] = OutputDirOption,
|
|
105
|
+
skip_existing: bool = SkipExistingOption,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Download RSI report markdown returned inline by the get API."""
|
|
108
|
+
rt = get_runtime(ctx)
|
|
109
|
+
groups = expand(group)
|
|
110
|
+
jobs = expand(job)
|
|
111
|
+
_guard(groups, jobs)
|
|
112
|
+
root = resolve_output_dir(rt.output_dir, output_dir)
|
|
113
|
+
|
|
114
|
+
def job_worker(job_id: str):
|
|
115
|
+
try:
|
|
116
|
+
dest = root / f"{job_id}_rsi.md"
|
|
117
|
+
if skip_existing and dest.exists():
|
|
118
|
+
output.log(f"skip existing {dest}")
|
|
119
|
+
return str(dest)
|
|
120
|
+
return _write_markdown(
|
|
121
|
+
_markdown(_fetch_job(rt.client, job_id), "report_md"),
|
|
122
|
+
dest,
|
|
123
|
+
skip_existing,
|
|
124
|
+
)
|
|
125
|
+
finally:
|
|
126
|
+
job_progress.advance(job_id)
|
|
127
|
+
|
|
128
|
+
def download_group_reports(group_id: str):
|
|
129
|
+
written = []
|
|
130
|
+
failures = []
|
|
131
|
+
try:
|
|
132
|
+
summary_dest = root / f"{group_id}_rsi.md"
|
|
133
|
+
if skip_existing and summary_dest.exists():
|
|
134
|
+
output.log(f"skip existing {summary_dest}")
|
|
135
|
+
summary = str(summary_dest)
|
|
136
|
+
else:
|
|
137
|
+
summary = _write_markdown(
|
|
138
|
+
_markdown(_fetch_group(rt.client, group_id), "summary_md"),
|
|
139
|
+
summary_dest,
|
|
140
|
+
skip_existing,
|
|
141
|
+
)
|
|
142
|
+
if summary:
|
|
143
|
+
written.append(summary)
|
|
144
|
+
except AuthError:
|
|
145
|
+
raise
|
|
146
|
+
except NetworkError as exc:
|
|
147
|
+
failures.append((f"{group_id}:summary", f"network: {exc}"))
|
|
148
|
+
except Exception as exc: # noqa: BLE001 - continue with per-Job reports
|
|
149
|
+
failures.append((f"{group_id}:summary", str(exc)))
|
|
150
|
+
|
|
151
|
+
jobs_data = fetch_all_jobs(rt.client, group_id)
|
|
152
|
+
ids = job_ids(jobs_data)
|
|
153
|
+
report_progress = output.ProgressCounter(
|
|
154
|
+
f"group rsi jobs {group_id}",
|
|
155
|
+
len(ids),
|
|
156
|
+
"jobs",
|
|
157
|
+
action="processed",
|
|
158
|
+
show_eta=True,
|
|
159
|
+
)
|
|
160
|
+
report_progress.start()
|
|
161
|
+
|
|
162
|
+
def download_job_report(api, job_id: str):
|
|
163
|
+
try:
|
|
164
|
+
dest = root / group_id / f"{job_id}_rsi.md"
|
|
165
|
+
if skip_existing and dest.exists():
|
|
166
|
+
output.log(f"skip existing {dest}")
|
|
167
|
+
return str(dest)
|
|
168
|
+
return _write_markdown(
|
|
169
|
+
_markdown(_fetch_job(api, job_id), "report_md"),
|
|
170
|
+
dest,
|
|
171
|
+
skip_existing,
|
|
172
|
+
)
|
|
173
|
+
finally:
|
|
174
|
+
report_progress.advance(job_id)
|
|
175
|
+
|
|
176
|
+
reports = fetch_job_resources(
|
|
177
|
+
rt.client,
|
|
178
|
+
ids,
|
|
179
|
+
download_job_report,
|
|
180
|
+
rt.concurrency,
|
|
181
|
+
)
|
|
182
|
+
written.extend(dest for dest in reports.items if dest)
|
|
183
|
+
failures.extend(reports.failures)
|
|
184
|
+
raise_for_failures("rsi reports", failures)
|
|
185
|
+
return written
|
|
186
|
+
|
|
187
|
+
def group_worker(group_id: str):
|
|
188
|
+
try:
|
|
189
|
+
return download_group_reports(group_id)
|
|
190
|
+
finally:
|
|
191
|
+
group_progress.advance(group_id)
|
|
192
|
+
|
|
193
|
+
if jobs:
|
|
194
|
+
job_progress = output.ProgressCounter(
|
|
195
|
+
"job rsi downloads", len(jobs), "jobs", action="processed"
|
|
196
|
+
)
|
|
197
|
+
job_progress.start()
|
|
198
|
+
res = safe_run_concurrent(jobs, job_worker, rt.concurrency)
|
|
199
|
+
else:
|
|
200
|
+
group_progress = output.ProgressCounter(
|
|
201
|
+
"group rsi downloads", len(groups), "groups", action="processed"
|
|
202
|
+
)
|
|
203
|
+
group_progress.start()
|
|
204
|
+
res = safe_run_concurrent(groups, group_worker, 1)
|
|
205
|
+
report_and_exit(res, emit=False)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command("trigger")
|
|
209
|
+
def trigger(
|
|
210
|
+
ctx: typer.Context,
|
|
211
|
+
group: Optional[list[str]] = GroupOption,
|
|
212
|
+
job: Optional[list[str]] = JobOption,
|
|
213
|
+
include_passed: bool = typer.Option(
|
|
214
|
+
False,
|
|
215
|
+
"--include-passed",
|
|
216
|
+
help="Include passed jobs when triggering a Group RSI report.",
|
|
217
|
+
),
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Trigger Job or Group RSI report generation."""
|
|
220
|
+
rt = get_runtime(ctx)
|
|
221
|
+
groups = expand(group)
|
|
222
|
+
jobs = expand(job)
|
|
223
|
+
_guard(groups, jobs)
|
|
224
|
+
|
|
225
|
+
if jobs:
|
|
226
|
+
res = safe_run_concurrent(
|
|
227
|
+
jobs, lambda jid: _trigger_job(rt.client, jid), rt.concurrency
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
res = safe_run_concurrent(
|
|
231
|
+
groups,
|
|
232
|
+
lambda gid: _trigger_group(rt.client, gid, include_passed),
|
|
233
|
+
rt.concurrency,
|
|
234
|
+
)
|
|
235
|
+
report_and_exit(res, emit=True, pretty=rt.pretty)
|