ap-client 0.4.0.dev0__tar.gz → 0.4.2__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.
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/PKG-INFO +2 -1
- ap_client-0.4.2/ap_client/akpool_commands.py +179 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/api.py +36 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/cli.py +84 -2
- ap_client-0.4.2/ap_client/exporter.py +728 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/pyproject.toml +2 -1
- ap_client-0.4.0.dev0/ap_client/exporter.py +0 -934
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/__init__.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/config.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/dataset_commands.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/fs_commands.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/instance_commands.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/irepo_sdk.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/managed_ak.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/profile_commands.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/split_publish.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/tbb.py +0 -0
- {ap_client-0.4.0.dev0 → ap_client-0.4.2}/ap_client/waiter.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: Agent Platform API Client & CLI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: irepo-artifact==0.1.2
|
|
6
7
|
Requires-Dist: pyyaml>=6.0
|
|
7
8
|
Requires-Dist: requests>=2.28.0
|
|
8
9
|
Requires-Dist: rich>=13.0.0
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""``ap akpool`` 命令族 —— Account Pool(托管 AK)观测操作。
|
|
2
|
+
|
|
3
|
+
* ``akpool rpm get`` → apiserver ``GET /apis/v1/model-api-keys/rpm-usage``
|
|
4
|
+
(中心侧,走 ``APIClient._get_central``),按 AK + 模型查询当前 RPM 用量与上限。
|
|
5
|
+
|
|
6
|
+
AK 的两种指定方式互斥且必填其一:``--ak-id``(uak-xxx,仅平台托管 AK)或
|
|
7
|
+
``--masked-ak``(掩码后 6 位,未托管 AK 走这条路)。服务端同口径(都缺或都给 →
|
|
8
|
+
400),CLI 侧先拦一道,不发请求。``--time-range`` 是 Go duration 风格
|
|
9
|
+
(``30s``/``5m``/``1h``);显式 ``--from``/``--to``(RFC3339)时覆盖
|
|
10
|
+
``--time-range``。
|
|
11
|
+
|
|
12
|
+
响应是嵌套结构(无信封):``usage.current_rpm``/``usage.sampled_at``、
|
|
13
|
+
``rpm.limit``/``rpm.limit_state``/``rpm.synced_at``、``usage_status``、
|
|
14
|
+
``time_range``、``ak_id``/``masked_api_key``/``model_id``。json/yaml 原样透传;
|
|
15
|
+
plain/table 摊平成单行 key=value 展示。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any, Optional
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
from ap_client import get_client
|
|
25
|
+
|
|
26
|
+
__all__ = ["akpool_app", "register"]
|
|
27
|
+
|
|
28
|
+
akpool_app = typer.Typer(help="Account Pool operations (managed API keys)")
|
|
29
|
+
rpm_app = typer.Typer(help="Account Pool RPM usage operations")
|
|
30
|
+
|
|
31
|
+
akpool_app.add_typer(rpm_app, name="rpm")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def register(app: typer.Typer) -> None:
|
|
35
|
+
"""把 ``akpool`` 挂到 ``ap`` 根命令上。"""
|
|
36
|
+
app.add_typer(akpool_app, name="akpool")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_FORMAT_HELP = "Output format: plain/table/json/yaml (default: AP_FORMAT or command default)"
|
|
40
|
+
_TIME_RANGE_HELP = "Lookback window for the RPM reading (Go duration, e.g. 30s/5m/1h)"
|
|
41
|
+
_ISO_TIME_HELP = "RFC3339 timestamp, e.g. 2026-09-21T12:00:00+08:00"
|
|
42
|
+
|
|
43
|
+
_TIME_RANGE_RE = re.compile(r"^\d+[smh]$")
|
|
44
|
+
|
|
45
|
+
#: plain/table 输出的字段顺序,取值自嵌套响应(见 _flatten_rpm)。
|
|
46
|
+
_RPM_KEYS = (
|
|
47
|
+
"current_rpm",
|
|
48
|
+
"rpm_limit",
|
|
49
|
+
"model",
|
|
50
|
+
"sampled_at",
|
|
51
|
+
"time_range",
|
|
52
|
+
"ak_id",
|
|
53
|
+
"usage_status",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _fmt(output_format: Optional[str]) -> str:
|
|
58
|
+
from .cli import _normalize_output_format
|
|
59
|
+
|
|
60
|
+
return _normalize_output_format(output_format, keep_table=True)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_time_range(value: str) -> str:
|
|
64
|
+
normalized = str(value or "").strip().lower()
|
|
65
|
+
if not _TIME_RANGE_RE.fullmatch(normalized):
|
|
66
|
+
raise typer.BadParameter(f"--time-range must be a duration like 30s/5m/1h, got {value!r}")
|
|
67
|
+
return normalized
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _flatten_rpm(payload: dict) -> dict:
|
|
71
|
+
"""把嵌套的 rpm-usage 响应摊平成 plain/table 展示用的扁平 dict。
|
|
72
|
+
|
|
73
|
+
null 值渲染为 ``-``(如未托管 AK 的 ``ak_id``、未知限额的 ``rpm.limit``)。
|
|
74
|
+
"""
|
|
75
|
+
rpm = payload.get("rpm")
|
|
76
|
+
usage = payload.get("usage")
|
|
77
|
+
flat = {
|
|
78
|
+
"current_rpm": usage.get("current_rpm") if isinstance(usage, dict) else None,
|
|
79
|
+
"rpm_limit": rpm.get("limit") if isinstance(rpm, dict) else None,
|
|
80
|
+
"model": payload.get("model_id"),
|
|
81
|
+
"sampled_at": usage.get("sampled_at") if isinstance(usage, dict) else None,
|
|
82
|
+
"time_range": payload.get("time_range"),
|
|
83
|
+
"ak_id": payload.get("ak_id"),
|
|
84
|
+
"usage_status": payload.get("usage_status"),
|
|
85
|
+
}
|
|
86
|
+
return {key: (value if value is not None else "-") for key, value in flat.items()}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _emit_rpm(payload: Any, output_format: str) -> None:
|
|
90
|
+
from .cli import _print_formatted, _print_rich_table
|
|
91
|
+
|
|
92
|
+
if not isinstance(payload, dict):
|
|
93
|
+
payload = {"value": payload}
|
|
94
|
+
if output_format in ("json", "yaml"):
|
|
95
|
+
_print_formatted(payload, output_format)
|
|
96
|
+
return
|
|
97
|
+
flat = _flatten_rpm(payload)
|
|
98
|
+
keys = [key for key in _RPM_KEYS if key in flat]
|
|
99
|
+
keys += [key for key in flat if key not in keys]
|
|
100
|
+
if output_format == "table":
|
|
101
|
+
_print_rich_table([flat], [(key, key) for key in keys])
|
|
102
|
+
return
|
|
103
|
+
_print_rpm_plain(payload)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _print_rpm_plain(payload: dict) -> None:
|
|
107
|
+
"""plain 输出:标题行(usage_status) + 对齐 key/value,与 _print_job_get_plain 同风格。
|
|
108
|
+
|
|
109
|
+
current_rpm 在限额已知时附利用率百分比;rpm_limit 未知/不限时展示 limit_state。
|
|
110
|
+
ak_id / masked_api_key 为 null(未托管 AK)时整行省略。
|
|
111
|
+
"""
|
|
112
|
+
from .cli import _print_key_values
|
|
113
|
+
|
|
114
|
+
rpm = payload.get("rpm")
|
|
115
|
+
usage = payload.get("usage")
|
|
116
|
+
rpm = rpm if isinstance(rpm, dict) else {}
|
|
117
|
+
usage = usage if isinstance(usage, dict) else {}
|
|
118
|
+
|
|
119
|
+
current = usage.get("current_rpm")
|
|
120
|
+
limit = rpm.get("limit")
|
|
121
|
+
limit_state = rpm.get("limit_state") or "unknown"
|
|
122
|
+
|
|
123
|
+
if isinstance(current, (int, float)) and isinstance(limit, (int, float)) and limit > 0:
|
|
124
|
+
current_text = f"{current} ({current / limit * 100:.1f}% of limit)"
|
|
125
|
+
else:
|
|
126
|
+
current_text = current
|
|
127
|
+
limit_text = f"{limit} ({limit_state})" if limit is not None else limit_state
|
|
128
|
+
|
|
129
|
+
typer.echo(f"AK RPM Usage ({payload.get('usage_status') or 'unknown'})")
|
|
130
|
+
_print_key_values(
|
|
131
|
+
[
|
|
132
|
+
("current_rpm", current_text),
|
|
133
|
+
("rpm_limit", limit_text),
|
|
134
|
+
("model", payload.get("model_id")),
|
|
135
|
+
("ak_id", payload.get("ak_id")),
|
|
136
|
+
("masked_api_key", payload.get("masked_api_key")),
|
|
137
|
+
("sampled_at", usage.get("sampled_at")),
|
|
138
|
+
("time_range", payload.get("time_range")),
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@rpm_app.command("get")
|
|
144
|
+
def rpm_get(
|
|
145
|
+
ak_id: Optional[str] = typer.Option(
|
|
146
|
+
None, "--ak-id", help="Managed AK ID (uak-xxx); mutually exclusive with --masked-ak"
|
|
147
|
+
),
|
|
148
|
+
masked_ak: Optional[str] = typer.Option(
|
|
149
|
+
None, "--masked-ak", help="Last 6 characters of the AK; mutually exclusive with --ak-id"
|
|
150
|
+
),
|
|
151
|
+
model: str = typer.Option(..., "--model", help="Model the RPM reading applies to"),
|
|
152
|
+
time_range: str = typer.Option("5m", "--time-range", help=_TIME_RANGE_HELP),
|
|
153
|
+
from_time: Optional[str] = typer.Option(
|
|
154
|
+
None, "--from", help=f"Window start ({_ISO_TIME_HELP}); overrides --time-range"
|
|
155
|
+
),
|
|
156
|
+
to_time: Optional[str] = typer.Option(
|
|
157
|
+
None, "--to", help=f"Window end ({_ISO_TIME_HELP}); overrides --time-range"
|
|
158
|
+
),
|
|
159
|
+
output_format: str = typer.Option(None, "--format", help=_FORMAT_HELP),
|
|
160
|
+
):
|
|
161
|
+
"""Show the current RPM usage and limit of a managed AK for one model."""
|
|
162
|
+
resolved_ak_id = (ak_id or "").strip()
|
|
163
|
+
resolved_masked_ak = (masked_ak or "").strip()
|
|
164
|
+
if bool(resolved_ak_id) == bool(resolved_masked_ak):
|
|
165
|
+
raise typer.BadParameter("exactly one of --ak-id or --masked-ak is required")
|
|
166
|
+
if not model.strip():
|
|
167
|
+
raise typer.BadParameter("--model must not be empty")
|
|
168
|
+
normalized_range = _check_time_range(time_range)
|
|
169
|
+
output_format = _fmt(output_format)
|
|
170
|
+
|
|
171
|
+
result = get_client().get_akpool_rpm(
|
|
172
|
+
model=model.strip(),
|
|
173
|
+
ak_id=resolved_ak_id or None,
|
|
174
|
+
masked_ak=resolved_masked_ak or None,
|
|
175
|
+
time_range=normalized_range,
|
|
176
|
+
from_time=from_time,
|
|
177
|
+
to_time=to_time,
|
|
178
|
+
)
|
|
179
|
+
_emit_rpm(result, output_format)
|
|
@@ -995,6 +995,42 @@ class APIClient:
|
|
|
995
995
|
}
|
|
996
996
|
}
|
|
997
997
|
|
|
998
|
+
# ==================== Account Pool operations ====================
|
|
999
|
+
|
|
1000
|
+
def get_akpool_rpm(
|
|
1001
|
+
self,
|
|
1002
|
+
*,
|
|
1003
|
+
model: str,
|
|
1004
|
+
ak_id: Optional[str] = None,
|
|
1005
|
+
masked_ak: Optional[str] = None,
|
|
1006
|
+
time_range: Optional[str] = None,
|
|
1007
|
+
from_time: Optional[str] = None,
|
|
1008
|
+
to_time: Optional[str] = None,
|
|
1009
|
+
timeout: TimeoutType = None,
|
|
1010
|
+
) -> dict:
|
|
1011
|
+
"""查询托管 AK 在指定模型上的当前 RPM 用量与上限(GET /apis/v1/model-api-keys/rpm-usage)。
|
|
1012
|
+
|
|
1013
|
+
``ak_id``(uak-xxx) 与 ``masked_ak``(掩码后 6 位) 二选一,都缺或都给
|
|
1014
|
+
服务端返回 400;``from_time``/``to_time``(RFC3339) 显式指定时覆盖
|
|
1015
|
+
``time_range``(Go duration,如 5m/1h,服务端默认 5m)。
|
|
1016
|
+
响应为嵌套结构:``usage.current_rpm``/``usage.sampled_at``、
|
|
1017
|
+
``rpm.limit``/``rpm.limit_state``/``rpm.synced_at``、``usage_status``;
|
|
1018
|
+
查未托管 AK 时 ``ak_id`` 为 null。
|
|
1019
|
+
"""
|
|
1020
|
+
validate_ak_id(ak_id)
|
|
1021
|
+
params: dict = {"model": model}
|
|
1022
|
+
if ak_id is not None:
|
|
1023
|
+
params["ak_id"] = ak_id
|
|
1024
|
+
if masked_ak is not None:
|
|
1025
|
+
params["masked_ak"] = masked_ak
|
|
1026
|
+
if time_range is not None:
|
|
1027
|
+
params["time_range"] = time_range
|
|
1028
|
+
if from_time is not None:
|
|
1029
|
+
params["from"] = from_time
|
|
1030
|
+
if to_time is not None:
|
|
1031
|
+
params["to"] = to_time
|
|
1032
|
+
return self._get_central("/model-api-keys/rpm-usage", params=params, timeout=timeout)
|
|
1033
|
+
|
|
998
1034
|
# ==================== Job operations ====================
|
|
999
1035
|
|
|
1000
1036
|
def create_group(
|
|
@@ -13,6 +13,7 @@ from urllib.parse import quote
|
|
|
13
13
|
|
|
14
14
|
import typer
|
|
15
15
|
from ap_client import __version__, get_client, get_config, tbb
|
|
16
|
+
from ap_client.akpool_commands import register as _register_akpool_commands
|
|
16
17
|
from ap_client.api import (
|
|
17
18
|
_OTHERS_MODEL,
|
|
18
19
|
APIError,
|
|
@@ -25,7 +26,7 @@ from ap_client.api import (
|
|
|
25
26
|
)
|
|
26
27
|
from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
|
|
27
28
|
from ap_client.dataset_commands import register as _register_dataset_commands
|
|
28
|
-
from ap_client.exporter import export_group, export_job
|
|
29
|
+
from ap_client.exporter import export_group, export_job, export_jobs
|
|
29
30
|
from ap_client.fs_commands import register as _register_fs_commands
|
|
30
31
|
from ap_client.instance_commands import register as _register_instance_commands
|
|
31
32
|
from ap_client.managed_ak import (
|
|
@@ -160,6 +161,7 @@ app.add_typer(checkpoint_app, name="checkpoint")
|
|
|
160
161
|
_register_dataset_commands(dataset_app)
|
|
161
162
|
_register_instance_commands(app)
|
|
162
163
|
_register_fs_commands(app)
|
|
164
|
+
_register_akpool_commands(app)
|
|
163
165
|
|
|
164
166
|
_PAI_RUNTIME_ENV_TAGS: tuple[tuple[str, str], ...] = (
|
|
165
167
|
("DLC_JOB_ID", "dlc_job_id"),
|
|
@@ -4665,10 +4667,47 @@ def job_artifacts(
|
|
|
4665
4667
|
_print_formatted(results, output_format)
|
|
4666
4668
|
|
|
4667
4669
|
|
|
4670
|
+
def _validate_export_timeout(value: float) -> float:
|
|
4671
|
+
import math
|
|
4672
|
+
|
|
4673
|
+
if not math.isfinite(value) or value <= 0:
|
|
4674
|
+
raise typer.BadParameter("Timeout must be a finite number greater than zero")
|
|
4675
|
+
return value
|
|
4676
|
+
|
|
4677
|
+
|
|
4678
|
+
def _export_channel_reporter(emit):
|
|
4679
|
+
"""Report completed Jobs, including callbacks from Group worker threads."""
|
|
4680
|
+
from threading import Lock
|
|
4681
|
+
|
|
4682
|
+
from rich.text import Text
|
|
4683
|
+
|
|
4684
|
+
counts = {"oss": 0, "proxy": 0}
|
|
4685
|
+
lock = Lock()
|
|
4686
|
+
|
|
4687
|
+
def report(job_id, status, channel):
|
|
4688
|
+
with lock:
|
|
4689
|
+
suffix = ""
|
|
4690
|
+
if status == "downloaded" and channel in counts:
|
|
4691
|
+
counts[channel] += 1
|
|
4692
|
+
suffix = f" channel={channel}"
|
|
4693
|
+
emit(Text(f"{job_id} {status}{suffix}"))
|
|
4694
|
+
|
|
4695
|
+
return report, counts
|
|
4696
|
+
|
|
4697
|
+
|
|
4668
4698
|
@job_app.command("export")
|
|
4669
4699
|
def job_export(
|
|
4670
|
-
job_id: str = typer.Argument(..., help="Job ID"),
|
|
4700
|
+
job_id: str = typer.Argument(..., help="Job ID or comma-separated Job IDs"),
|
|
4671
4701
|
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
|
|
4702
|
+
workers: int = typer.Option(
|
|
4703
|
+
1, "--workers", "-w", min=1, help="Number of concurrent export workers"
|
|
4704
|
+
),
|
|
4705
|
+
timeout: float = typer.Option(
|
|
4706
|
+
300,
|
|
4707
|
+
"--timeout",
|
|
4708
|
+
callback=_validate_export_timeout,
|
|
4709
|
+
help="SDK request timeout in seconds, not the total export duration",
|
|
4710
|
+
),
|
|
4672
4711
|
logs: bool = typer.Option(False, "--logs", help="Include container logs"),
|
|
4673
4712
|
events: bool = typer.Option(False, "--events", help="Include events"),
|
|
4674
4713
|
no_extract_artifacts: bool = typer.Option(
|
|
@@ -4678,17 +4717,50 @@ def job_export(
|
|
|
4678
4717
|
),
|
|
4679
4718
|
):
|
|
4680
4719
|
"""Export job artifacts to a local directory. Use --logs/--events to include logs and events."""
|
|
4720
|
+
ids = list(dict.fromkeys(value.strip() for value in job_id.split(",")))
|
|
4721
|
+
if not all(ids):
|
|
4722
|
+
raise typer.BadParameter("Job IDs must not be empty", param_hint="job_id")
|
|
4681
4723
|
client = get_client()
|
|
4724
|
+
if len(ids) > 1:
|
|
4725
|
+
report, channels = _export_channel_reporter(print)
|
|
4726
|
+
dest, summary = export_jobs(
|
|
4727
|
+
client,
|
|
4728
|
+
ids,
|
|
4729
|
+
output,
|
|
4730
|
+
workers=workers,
|
|
4731
|
+
result_callback=report,
|
|
4732
|
+
timeout=timeout,
|
|
4733
|
+
include_logs=logs,
|
|
4734
|
+
include_events=events,
|
|
4735
|
+
extract_artifacts=not no_extract_artifacts,
|
|
4736
|
+
)
|
|
4737
|
+
print(
|
|
4738
|
+
f"Jobs exported: downloaded={summary.downloaded} "
|
|
4739
|
+
f"skipped={summary.skipped} failed={len(summary.failures)}"
|
|
4740
|
+
)
|
|
4741
|
+
print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
|
|
4742
|
+
print(f" path: {dest}")
|
|
4743
|
+
for failed_id, error in summary.failures.items():
|
|
4744
|
+
print(f"[red]Failed job {failed_id}:[/] {error}")
|
|
4745
|
+
if summary.failures:
|
|
4746
|
+
raise typer.Exit(code=1)
|
|
4747
|
+
return
|
|
4748
|
+
job_id = ids[0]
|
|
4749
|
+
result = {}
|
|
4682
4750
|
dest = export_job(
|
|
4683
4751
|
client,
|
|
4684
4752
|
job_id,
|
|
4685
4753
|
output,
|
|
4754
|
+
result_callback=lambda jid, status, channel: result.update(status=status, channel=channel),
|
|
4755
|
+
timeout=timeout,
|
|
4686
4756
|
include_logs=logs,
|
|
4687
4757
|
include_events=events,
|
|
4688
4758
|
extract_artifacts=not no_extract_artifacts,
|
|
4689
4759
|
)
|
|
4690
4760
|
print(f"[green]Job exported:[/] {job_id}")
|
|
4691
4761
|
print(f" path: {dest}")
|
|
4762
|
+
if result.get("channel") in {"oss", "proxy"} and result.get("status") == "downloaded":
|
|
4763
|
+
print(f" channel: {result['channel']}")
|
|
4692
4764
|
|
|
4693
4765
|
|
|
4694
4766
|
@job_app.command("wait")
|
|
@@ -6136,6 +6208,12 @@ def group_export(
|
|
|
6136
6208
|
workers: int = typer.Option(
|
|
6137
6209
|
4, "--workers", "-w", min=1, help="Number of concurrent export workers"
|
|
6138
6210
|
),
|
|
6211
|
+
timeout: float = typer.Option(
|
|
6212
|
+
300,
|
|
6213
|
+
"--timeout",
|
|
6214
|
+
callback=_validate_export_timeout,
|
|
6215
|
+
help="SDK request timeout in seconds, not the total export duration",
|
|
6216
|
+
),
|
|
6139
6217
|
logs: bool = typer.Option(False, "--logs", help="Include container logs"),
|
|
6140
6218
|
events: bool = typer.Option(False, "--events", help="Include events"),
|
|
6141
6219
|
no_extract_artifacts: bool = typer.Option(
|
|
@@ -6196,12 +6274,15 @@ def group_export(
|
|
|
6196
6274
|
current_stage=current_stage,
|
|
6197
6275
|
)
|
|
6198
6276
|
|
|
6277
|
+
report, channels = _export_channel_reporter(progress.console.print)
|
|
6199
6278
|
dest, summary = export_group(
|
|
6200
6279
|
client,
|
|
6201
6280
|
group_id,
|
|
6202
6281
|
output,
|
|
6203
6282
|
progress_callback=_on_progress,
|
|
6204
6283
|
workers=workers,
|
|
6284
|
+
result_callback=report,
|
|
6285
|
+
timeout=timeout,
|
|
6205
6286
|
include_logs=logs,
|
|
6206
6287
|
include_events=events,
|
|
6207
6288
|
extract_artifacts=not no_extract_artifacts,
|
|
@@ -6214,6 +6295,7 @@ def group_export(
|
|
|
6214
6295
|
f" [green]{summary.downloaded}[/] jobs downloaded, "
|
|
6215
6296
|
f"[yellow]{summary.skipped}[/] jobs skipped (no artifact)"
|
|
6216
6297
|
)
|
|
6298
|
+
print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
|
|
6217
6299
|
if summary.skipped_job_names:
|
|
6218
6300
|
print(f" Skipped: {', '.join(summary.skipped_job_names)}")
|
|
6219
6301
|
|