ap-client 0.3.1.dev2__tar.gz → 0.3.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.3.1.dev2 → ap_client-0.3.2}/PKG-INFO +3 -3
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/cli.py +0 -2
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/dataset_commands.py +126 -160
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/fs_commands.py +1 -12
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/instance_commands.py +80 -38
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/irepo_sdk.py +128 -43
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/split_publish.py +1 -6
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/pyproject.toml +3 -3
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/__init__.py +0 -0
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/api.py +0 -0
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/config.py +0 -0
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/exporter.py +0 -0
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/managed_ak.py +0 -0
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/profile_commands.py +0 -0
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/tbb.py +0 -0
- {ap_client-0.3.1.dev2 → ap_client-0.3.2}/ap_client/waiter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Agent Platform API Client & CLI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -9,9 +9,9 @@ Requires-Dist: rich>=13.0.0
|
|
|
9
9
|
Requires-Dist: typer>=0.9.0
|
|
10
10
|
Requires-Dist: websockets>=13.0
|
|
11
11
|
Provides-Extra: all
|
|
12
|
-
Requires-Dist: instance-repo[oss]
|
|
12
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.2; extra == 'all'
|
|
13
13
|
Provides-Extra: dataset
|
|
14
|
-
Requires-Dist: instance-repo[oss]
|
|
14
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.2; extra == 'dataset'
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
A lightweight Python SDK and command line interface for Agent Platform. It provides helpers for configuring API access and managing templates, datasets, jobs, and groups.
|
|
@@ -844,8 +844,6 @@ def _print_detail(data: object) -> None:
|
|
|
844
844
|
rows.append((key, _format_nested_detail(value)))
|
|
845
845
|
elif key == "image" and isinstance(value, str):
|
|
846
846
|
rows.append((key, _shorten_image(value)))
|
|
847
|
-
elif value is None:
|
|
848
|
-
continue
|
|
849
847
|
else:
|
|
850
848
|
rows.append((key, _format_plain_value(value, max_len=0)))
|
|
851
849
|
width = max((len(label) for label, _text in rows), default=0)
|
|
@@ -120,6 +120,7 @@ _NO_BENCHMARK = "none"
|
|
|
120
120
|
_FORMAT_HELP = "Output format: plain/table/json/yaml (default: AP_FORMAT or command default)"
|
|
121
121
|
|
|
122
122
|
_METADATA_MODELS = ("version_first", "split_first")
|
|
123
|
+
_VERSION_SPLIT_METADATA_MODEL = "version_first"
|
|
123
124
|
|
|
124
125
|
#: CLI **不替 SDK 决定元数据模型**:不下发 ``metadata_model``,由 SDK 按 profile 归一
|
|
125
126
|
#: (当前为 ``split_first``)。
|
|
@@ -155,10 +156,10 @@ _NO_VERSION_NOTE = "未指定 --version 表示无版本空间(split_first),不
|
|
|
155
156
|
# ---------------------------------------------------------------------------
|
|
156
157
|
|
|
157
158
|
|
|
158
|
-
def _fmt(output_format: Optional[str]) -> str:
|
|
159
|
+
def _fmt(output_format: Optional[str], *, default: str = "plain") -> str:
|
|
159
160
|
from .cli import _normalize_output_format
|
|
160
161
|
|
|
161
|
-
return _normalize_output_format(output_format, keep_table=True)
|
|
162
|
+
return _normalize_output_format(output_format, default=default, keep_table=True)
|
|
162
163
|
|
|
163
164
|
|
|
164
165
|
def _fail(message: str, hint: str, exit_code: int = 1) -> "typer.Exit":
|
|
@@ -168,11 +169,6 @@ def _fail(message: str, hint: str, exit_code: int = 1) -> "typer.Exit":
|
|
|
168
169
|
return typer.Exit(exit_code)
|
|
169
170
|
|
|
170
171
|
|
|
171
|
-
def _note(message: str) -> None:
|
|
172
|
-
"""退出码 0 的路径只用 ``note:``(设计 §5.1);一律 stderr。"""
|
|
173
|
-
typer.echo(f"note: {message}", err=True)
|
|
174
|
-
|
|
175
|
-
|
|
176
172
|
def _describe(exc: BaseException) -> str:
|
|
177
173
|
message = getattr(exc, "message", None)
|
|
178
174
|
text = str(message or "").strip() or str(exc).strip()
|
|
@@ -412,6 +408,21 @@ def _api_biz_code(exc: BaseException) -> Optional[int]:
|
|
|
412
408
|
return None
|
|
413
409
|
|
|
414
410
|
|
|
411
|
+
def _business_code(exc: BaseException) -> Optional[int]:
|
|
412
|
+
code = getattr(exc, "biz_code", None)
|
|
413
|
+
if isinstance(code, int) and not isinstance(code, bool):
|
|
414
|
+
return code
|
|
415
|
+
return _api_biz_code(exc)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _last_admin_hint(dataset: str) -> str:
|
|
419
|
+
return (
|
|
420
|
+
"不能撤销或降级 dataset 的最后一个 admin;先授予另一个用户 admin:"
|
|
421
|
+
f"ap dataset access grant {dataset} --user <另一工号> --role admin,"
|
|
422
|
+
"确认成功后再重试当前命令"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
415
426
|
#: apiserver dataset 控制面业务码 → 固定 hint(设计 §5.1)。
|
|
416
427
|
_DATASET_CODE_HINTS: dict[int, str] = {
|
|
417
428
|
92008: "该环境未开启 dataset 能力,请联系管理员",
|
|
@@ -524,7 +535,12 @@ def _updated_range(from_value: Optional[str], to_value: Optional[str]) -> tuple[
|
|
|
524
535
|
|
|
525
536
|
|
|
526
537
|
def _fetch_version(
|
|
527
|
-
dataset: str,
|
|
538
|
+
dataset: str,
|
|
539
|
+
version: str,
|
|
540
|
+
*,
|
|
541
|
+
stage: str,
|
|
542
|
+
environment: Optional[str] = None,
|
|
543
|
+
metadata_model: str = "",
|
|
528
544
|
) -> dict:
|
|
529
545
|
"""读 version 详情(含内嵌 ``splits[]``);空 version 的报错必须点明"无版本空间"语义。
|
|
530
546
|
|
|
@@ -538,7 +554,12 @@ def _fetch_version(
|
|
|
538
554
|
env = _environment(environment)
|
|
539
555
|
repo = _dataset_repo()
|
|
540
556
|
try:
|
|
541
|
-
detail = repo.versions.get(
|
|
557
|
+
detail = repo.versions.get(
|
|
558
|
+
dataset,
|
|
559
|
+
version,
|
|
560
|
+
**_env_kwargs(env),
|
|
561
|
+
**({"metadata_model": metadata_model} if metadata_model else {}),
|
|
562
|
+
)
|
|
542
563
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
543
564
|
if version:
|
|
544
565
|
hint = (
|
|
@@ -650,47 +671,6 @@ def _dataset_repo() -> "Repo":
|
|
|
650
671
|
raise typer.Exit(2) from exc
|
|
651
672
|
|
|
652
673
|
|
|
653
|
-
def _dataset_available_envs(repo: "Repo", dataset: str) -> list[str]:
|
|
654
|
-
"""dataset 元数据分布的 environment 候选;详情读不到时退回固定枚举。"""
|
|
655
|
-
envs: list[str] = []
|
|
656
|
-
try:
|
|
657
|
-
detail = repo.datasets.get_detail(dataset)
|
|
658
|
-
except Exception: # noqa: BLE001 - 引导信息尽力而为,不打断主流程
|
|
659
|
-
detail = None
|
|
660
|
-
raw = detail.get("available_envs") if isinstance(detail, dict) else None
|
|
661
|
-
if isinstance(raw, list):
|
|
662
|
-
envs = [str(env).strip() for env in raw if str(env).strip()]
|
|
663
|
-
from .irepo_sdk import DATASET_ENVIRONMENTS
|
|
664
|
-
|
|
665
|
-
for fallback in DATASET_ENVIRONMENTS:
|
|
666
|
-
if fallback not in envs:
|
|
667
|
-
envs.append(fallback)
|
|
668
|
-
return envs
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
def _note_environment_distribution(repo: "Repo", dataset: str, probe, *, what: str) -> None:
|
|
672
|
-
"""空结果时逐 environment 探测数据分布,引导用户换 ``--environment`` 重试。
|
|
673
|
-
|
|
674
|
-
``probe(env)`` 返回该 environment 下的条数;读失败按 -1 计。全部为空/读失败
|
|
675
|
-
(dataset 真没数据)时不提示,避免噪声。
|
|
676
|
-
"""
|
|
677
|
-
counts: list[tuple[str, int]] = []
|
|
678
|
-
for env in _dataset_available_envs(repo, dataset):
|
|
679
|
-
try:
|
|
680
|
-
counts.append((env, int(probe(env))))
|
|
681
|
-
except Exception: # noqa: BLE001 - 旧 SDK 不接受 environment 等情况
|
|
682
|
-
counts.append((env, -1))
|
|
683
|
-
if not any(count > 0 for _env, count in counts):
|
|
684
|
-
return
|
|
685
|
-
shown = ", ".join(f"{env}={count if count >= 0 else '读失败'}" for env, count in counts)
|
|
686
|
-
with_data = [env for env, count in counts if count > 0]
|
|
687
|
-
suggest = with_data[0] if len(with_data) == 1 else f"<{'/'.join(with_data)}>"
|
|
688
|
-
_note(
|
|
689
|
-
f"{what}在本次查询的 environment 下没有数据;各 environment 探测:{shown}。"
|
|
690
|
-
f"数据可能在另一个元数据 environment,加 --environment {suggest} 重试"
|
|
691
|
-
)
|
|
692
|
-
|
|
693
|
-
|
|
694
674
|
def _patch_version(
|
|
695
675
|
dataset: str,
|
|
696
676
|
version: str,
|
|
@@ -699,6 +679,7 @@ def _patch_version(
|
|
|
699
679
|
stage: str,
|
|
700
680
|
fallback_hint: str = "",
|
|
701
681
|
environment: Optional[str] = None,
|
|
682
|
+
metadata_model: str = "",
|
|
702
683
|
) -> None:
|
|
703
684
|
"""split 写操作统一走 SDK;body 为 splits/split_run_types 单键。
|
|
704
685
|
|
|
@@ -710,7 +691,14 @@ def _patch_version(
|
|
|
710
691
|
|
|
711
692
|
repo = _dataset_repo()
|
|
712
693
|
try:
|
|
713
|
-
update_version_detail(
|
|
694
|
+
update_version_detail(
|
|
695
|
+
repo,
|
|
696
|
+
dataset,
|
|
697
|
+
version,
|
|
698
|
+
body,
|
|
699
|
+
environment=_environment(environment),
|
|
700
|
+
metadata_model=metadata_model,
|
|
701
|
+
)
|
|
714
702
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
715
703
|
code = getattr(exc, "biz_code", None)
|
|
716
704
|
if code == 94018:
|
|
@@ -846,14 +834,16 @@ def _bindings(repo, dataset: str) -> list[dict]:
|
|
|
846
834
|
|
|
847
835
|
|
|
848
836
|
def _bindings_of(repo, dataset: str, principal_id: str, principal_type: str) -> list[dict]:
|
|
849
|
-
"""某 principal 在此 dataset
|
|
837
|
+
"""某 principal 在此 dataset 上的现存绑定;读失败时停止变更。"""
|
|
850
838
|
from .irepo_sdk import _reason
|
|
851
839
|
|
|
852
840
|
try:
|
|
853
841
|
rows = _bindings(repo, dataset)
|
|
854
|
-
except Exception as exc: # noqa: BLE001
|
|
855
|
-
|
|
856
|
-
|
|
842
|
+
except Exception as exc: # noqa: BLE001
|
|
843
|
+
raise _fail(
|
|
844
|
+
f"dataset {dataset} 现有授权查询失败:{_reason(exc)}",
|
|
845
|
+
"确认 dataset-admin 权限和当前集群后重试",
|
|
846
|
+
) from exc
|
|
857
847
|
return [
|
|
858
848
|
row
|
|
859
849
|
for row in rows
|
|
@@ -934,7 +924,7 @@ def dataset_list(
|
|
|
934
924
|
"""List datasets from the registry. Defaults to JSON; use --format table for a table."""
|
|
935
925
|
from .cli import _print_formatted, get_client
|
|
936
926
|
|
|
937
|
-
output_format = _fmt(output_format)
|
|
927
|
+
output_format = _fmt(output_format, default="json")
|
|
938
928
|
resolved_query = str(query or "").strip()
|
|
939
929
|
legacy_search = str(search or "").strip()
|
|
940
930
|
if legacy_search:
|
|
@@ -947,10 +937,6 @@ def dataset_list(
|
|
|
947
937
|
f"或 ap dataset list --query {query}",
|
|
948
938
|
exit_code=2,
|
|
949
939
|
)
|
|
950
|
-
if not resolved_query:
|
|
951
|
-
_note(
|
|
952
|
-
f"位置参数搜索已兼容为 --query 的等价形式:ap dataset list --query {legacy_search}"
|
|
953
|
-
)
|
|
954
940
|
resolved_query = legacy_search
|
|
955
941
|
|
|
956
942
|
if legacy:
|
|
@@ -981,7 +967,6 @@ def dataset_list(
|
|
|
981
967
|
exit_code=2,
|
|
982
968
|
)
|
|
983
969
|
result = get_client().list_all_datasets(resolved_query or None)
|
|
984
|
-
_note("--legacy 使用旧数据源(与 ap job create --dataset 读取的一致),已自动翻页拉全量")
|
|
985
970
|
if output_format == "table":
|
|
986
971
|
from .cli import _print_records_table
|
|
987
972
|
|
|
@@ -1047,14 +1032,6 @@ def dataset_list(
|
|
|
1047
1032
|
records, result = fetch(resolved_page)
|
|
1048
1033
|
pagination = _pagination_envelope(result)
|
|
1049
1034
|
|
|
1050
|
-
total = _pagination_total(pagination)
|
|
1051
|
-
if total is not None and total > len(records):
|
|
1052
|
-
_note(
|
|
1053
|
-
f"共 {total} 个 dataset,本次返回第 {resolved_page} 页的 {len(records)} 条"
|
|
1054
|
-
f"(每页 {resolved_page_size});继续翻页用 --page/--page-size,"
|
|
1055
|
-
f"拉全量用 --legacy"
|
|
1056
|
-
)
|
|
1057
|
-
|
|
1058
1035
|
payload: dict[str, Any] = {}
|
|
1059
1036
|
if mine:
|
|
1060
1037
|
payload["owned_by_me"] = True
|
|
@@ -1063,11 +1040,7 @@ def dataset_list(
|
|
|
1063
1040
|
payload["datasets"] = [_legacy_dataset_record(record) for record in records]
|
|
1064
1041
|
payload["total"] = _pagination_total(pagination, fallback=len(records))
|
|
1065
1042
|
payload["pagination"] = pagination
|
|
1066
|
-
|
|
1067
|
-
_print_table(rows, _DATASET_COLUMNS, output_format)
|
|
1068
|
-
else:
|
|
1069
|
-
# Legacy plain output is JSON, as for dataset versions / instances.
|
|
1070
|
-
_print_formatted(payload, output_format)
|
|
1043
|
+
_emit(payload, rows, _DATASET_COLUMNS, output_format)
|
|
1071
1044
|
|
|
1072
1045
|
|
|
1073
1046
|
#: ``dataset get`` 尾部 environment 摘要表的列。
|
|
@@ -1115,6 +1088,13 @@ def dataset_get(
|
|
|
1115
1088
|
"""Show dataset detail, including version / split summary stats."""
|
|
1116
1089
|
from .irepo_sdk import _reason
|
|
1117
1090
|
|
|
1091
|
+
dataset = dataset.strip()
|
|
1092
|
+
if not dataset:
|
|
1093
|
+
raise _fail(
|
|
1094
|
+
"dataset 查询失败:名称不能为空",
|
|
1095
|
+
"传入非空 dataset 名称,例如 ap dataset get alibaba/a",
|
|
1096
|
+
exit_code=2,
|
|
1097
|
+
)
|
|
1118
1098
|
output_format = _fmt(output_format)
|
|
1119
1099
|
env = _environment(environment)
|
|
1120
1100
|
repo = _dataset_repo()
|
|
@@ -1263,7 +1243,6 @@ def dataset_claim(
|
|
|
1263
1243
|
if not detail.get("claimable", False):
|
|
1264
1244
|
# 幂等:已认领就不是错误(设计 §4.1),stdout 仍按 --format 输出现状。
|
|
1265
1245
|
owner = str(detail.get("owner") or "").strip() or "<unknown>"
|
|
1266
|
-
_note(f"dataset {dataset} 已被认领(owner={owner}),无需重复认领")
|
|
1267
1246
|
_emit_detail(
|
|
1268
1247
|
{
|
|
1269
1248
|
"dataset": dataset,
|
|
@@ -1279,21 +1258,30 @@ def dataset_claim(
|
|
|
1279
1258
|
|
|
1280
1259
|
if not (workspace or "").strip():
|
|
1281
1260
|
candidates: list[Any] = []
|
|
1261
|
+
candidate_error = ""
|
|
1282
1262
|
try:
|
|
1283
1263
|
candidates = repo.datasets.list_claim_workspaces() or []
|
|
1284
1264
|
except Exception as exc: # noqa: BLE001 - 列不出来不影响报用法错
|
|
1285
|
-
|
|
1265
|
+
candidate_error = _reason(exc)
|
|
1266
|
+
choices: list[str] = []
|
|
1286
1267
|
for item in candidates:
|
|
1287
1268
|
queue_id = str(getattr(item, "queue_id", "") or getattr(item, "id", "") or "").strip()
|
|
1288
1269
|
name = str(getattr(item, "name", "") or "").strip()
|
|
1289
1270
|
if not queue_id and isinstance(item, dict):
|
|
1290
1271
|
queue_id = str(item.get("queue_id") or item.get("id") or "")
|
|
1291
1272
|
name = str(item.get("name") or "")
|
|
1292
|
-
|
|
1273
|
+
if queue_id:
|
|
1274
|
+
choices.append(f"{queue_id} ({name})" if name else queue_id)
|
|
1275
|
+
if choices:
|
|
1276
|
+
hint = f"可选 workspace:{', '.join(choices)};"
|
|
1277
|
+
elif candidate_error:
|
|
1278
|
+
hint = f"可选 workspace 列举失败:{candidate_error};"
|
|
1279
|
+
else:
|
|
1280
|
+
hint = ""
|
|
1281
|
+
hint += f"执行 ap dataset claim {dataset} --workspace <queue-id> --benchmark <benchmark>"
|
|
1293
1282
|
raise _fail(
|
|
1294
1283
|
f"dataset {dataset} 认领失败:服务端要求 workspace_id,但没给 --workspace",
|
|
1295
|
-
|
|
1296
|
-
f"{dataset} --workspace <queue-id> --benchmark <benchmark>",
|
|
1284
|
+
hint,
|
|
1297
1285
|
exit_code=2,
|
|
1298
1286
|
)
|
|
1299
1287
|
|
|
@@ -1311,9 +1299,7 @@ def dataset_claim(
|
|
|
1311
1299
|
require_benchmark=bool(benchmark_id),
|
|
1312
1300
|
)
|
|
1313
1301
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
1314
|
-
code =
|
|
1315
|
-
if code is None:
|
|
1316
|
-
code = _api_biz_code(exc)
|
|
1302
|
+
code = _business_code(exc)
|
|
1317
1303
|
if code == 92010:
|
|
1318
1304
|
# 已被认领:以服务端 92010 为准(不本地推断),幂等 exit 0。
|
|
1319
1305
|
owner = str(detail.get("owner") or "").strip()
|
|
@@ -1324,7 +1310,6 @@ def dataset_claim(
|
|
|
1324
1310
|
except Exception: # noqa: BLE001 - 取不到 owner 不影响幂等退出
|
|
1325
1311
|
owner = ""
|
|
1326
1312
|
owner = owner or "<unknown>"
|
|
1327
|
-
_note(f"dataset {dataset} 已被认领(owner={owner}),无需重复认领")
|
|
1328
1313
|
_emit_detail(
|
|
1329
1314
|
{
|
|
1330
1315
|
"dataset": dataset,
|
|
@@ -1422,7 +1407,13 @@ def dataset_access_grant(
|
|
|
1422
1407
|
try:
|
|
1423
1408
|
repo.datasets.revoke(dataset, principal_id, stale, principal_type=resolved_type)
|
|
1424
1409
|
except Exception as exc: # noqa: BLE001
|
|
1425
|
-
|
|
1410
|
+
code = _business_code(exc)
|
|
1411
|
+
if code == 92011:
|
|
1412
|
+
raise _fail(
|
|
1413
|
+
f"dataset {dataset} 旧角色 {stale} 撤销失败:{_reason(exc)}",
|
|
1414
|
+
_last_admin_hint(dataset),
|
|
1415
|
+
) from exc
|
|
1416
|
+
if code != 24401:
|
|
1426
1417
|
raise _fail(
|
|
1427
1418
|
f"dataset {dataset} 旧角色 {stale} 撤销失败:{_reason(exc)}",
|
|
1428
1419
|
f"先手动撤销:ap dataset access revoke {dataset} "
|
|
@@ -1430,19 +1421,14 @@ def dataset_access_grant(
|
|
|
1430
1421
|
) from exc
|
|
1431
1422
|
else:
|
|
1432
1423
|
replaced.append(stale)
|
|
1433
|
-
_note(f"已撤销 {principal_id} 在 {dataset} 上的旧角色 {stale}")
|
|
1434
1424
|
|
|
1435
1425
|
already = False
|
|
1436
1426
|
try:
|
|
1437
1427
|
repo.datasets.grant(dataset, principal_id, resolved_role, principal_type=resolved_type)
|
|
1438
1428
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
1439
|
-
code =
|
|
1429
|
+
code = _business_code(exc)
|
|
1440
1430
|
if code == 24404:
|
|
1441
1431
|
already = True
|
|
1442
|
-
_note(
|
|
1443
|
-
f"dataset {dataset} 上 {principal_id} 的 {resolved_role} 绑定已存在"
|
|
1444
|
-
f"({_reason(exc)})"
|
|
1445
|
-
)
|
|
1446
1432
|
elif code == 24402:
|
|
1447
1433
|
raise _fail(
|
|
1448
1434
|
f"dataset {dataset} 授权失败:入参非法({_reason(exc)})",
|
|
@@ -1516,7 +1502,6 @@ def dataset_access_revoke(
|
|
|
1516
1502
|
if row["role"] in _ROLE_IDS
|
|
1517
1503
|
]
|
|
1518
1504
|
if not targets:
|
|
1519
|
-
_note(f"dataset {dataset} 上 {principal_id} 已无绑定,无需撤销")
|
|
1520
1505
|
_emit_detail(
|
|
1521
1506
|
{
|
|
1522
1507
|
"dataset": dataset,
|
|
@@ -1536,14 +1521,9 @@ def dataset_access_revoke(
|
|
|
1536
1521
|
try:
|
|
1537
1522
|
repo.datasets.revoke(dataset, principal_id, target, principal_type=resolved_type)
|
|
1538
1523
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
1539
|
-
code =
|
|
1524
|
+
code = _business_code(exc)
|
|
1540
1525
|
if code == 24401:
|
|
1541
|
-
# 幂等:目标态已达成(设计 §5.1),不用 error: 前缀。
|
|
1542
1526
|
missing.append(target)
|
|
1543
|
-
_note(
|
|
1544
|
-
f"dataset {dataset} 上 {principal_id} 的 {target} 绑定不存在,"
|
|
1545
|
-
f"已处于目标状态({_reason(exc)})"
|
|
1546
|
-
)
|
|
1547
1527
|
continue
|
|
1548
1528
|
if code == 24402:
|
|
1549
1529
|
raise _fail(
|
|
@@ -1551,6 +1531,11 @@ def dataset_access_revoke(
|
|
|
1551
1531
|
"检查工号/用户组 id 与 --principal-type 是否匹配",
|
|
1552
1532
|
exit_code=2,
|
|
1553
1533
|
) from exc
|
|
1534
|
+
if code == 92011:
|
|
1535
|
+
raise _fail(
|
|
1536
|
+
f"dataset {dataset} 撤销 {target} 失败:{_reason(exc)}",
|
|
1537
|
+
_last_admin_hint(dataset),
|
|
1538
|
+
) from exc
|
|
1554
1539
|
raise _fail(
|
|
1555
1540
|
f"dataset {dataset} 撤销 {target} 失败:{_reason(exc)}",
|
|
1556
1541
|
"撤销需要 dataset-admin;确认该环境已开启 dataset RBAC",
|
|
@@ -1615,37 +1600,6 @@ def dataset_version_list(
|
|
|
1615
1600
|
) from exc
|
|
1616
1601
|
|
|
1617
1602
|
records = [_model_dict(record) for record in (getattr(result, "items", None) or [])]
|
|
1618
|
-
if not records:
|
|
1619
|
-
# 空结果常见根因是元数据 environment 不匹配(version 落在另一个 env),
|
|
1620
|
-
# 探测一遍并引导切换,而不是只回一张空表。
|
|
1621
|
-
_note_environment_distribution(
|
|
1622
|
-
repo,
|
|
1623
|
-
dataset,
|
|
1624
|
-
lambda env: (
|
|
1625
|
-
_pagination_total(
|
|
1626
|
-
_pagination_envelope(
|
|
1627
|
-
repo.versions.list_paged(
|
|
1628
|
-
dataset,
|
|
1629
|
-
status=resolved_status or None,
|
|
1630
|
-
page=1,
|
|
1631
|
-
page_size=1,
|
|
1632
|
-
environment=env,
|
|
1633
|
-
)
|
|
1634
|
-
),
|
|
1635
|
-
fallback=0,
|
|
1636
|
-
)
|
|
1637
|
-
or 0
|
|
1638
|
-
),
|
|
1639
|
-
what="version",
|
|
1640
|
-
)
|
|
1641
|
-
if records and all(
|
|
1642
|
-
"split_count" not in record and "instance_count" not in record for record in records
|
|
1643
|
-
):
|
|
1644
|
-
# 旧 SDK(1.0.9.dev0 及以前)的版本列表投影丢服务端统计字段:如实说明,不静默空列。
|
|
1645
|
-
_note(
|
|
1646
|
-
"已安装的 instance-repo 未返回版本统计(split_count/instance_count),"
|
|
1647
|
-
"SPLITS/INSTANCES 两列为空;升级到 1.1.0 及以上可恢复"
|
|
1648
|
-
)
|
|
1649
1603
|
rows = [_version_row(record) for record in records]
|
|
1650
1604
|
_emit(
|
|
1651
1605
|
{"dataset": dataset, "versions": records, "pagination": _pagination_envelope(result)},
|
|
@@ -1673,10 +1627,15 @@ def dataset_version_get(
|
|
|
1673
1627
|
def dataset_version_create(
|
|
1674
1628
|
version: str = typer.Argument(..., help="Dataset version to create"),
|
|
1675
1629
|
dataset: str = typer.Option(..., "--dataset", help="Dataset name"),
|
|
1630
|
+
storage_path: Optional[str] = typer.Option(
|
|
1631
|
+
None,
|
|
1632
|
+
"--storage-path",
|
|
1633
|
+
help="Explicit oss:// storage path (otherwise auto-computed from data-plane addressing)",
|
|
1634
|
+
),
|
|
1676
1635
|
output_format: str = typer.Option(None, "--format", help=_FORMAT_HELP),
|
|
1677
1636
|
):
|
|
1678
1637
|
"""Create a draft version. storage_path is auto-computed from data-plane addressing."""
|
|
1679
|
-
from .irepo_sdk import SdkMissing, load_repo, sdk_error_exit
|
|
1638
|
+
from .irepo_sdk import SdkMissing, _is_addressing_failure, _reason, load_repo, sdk_error_exit
|
|
1680
1639
|
|
|
1681
1640
|
output_format = _fmt(output_format)
|
|
1682
1641
|
resolved = _version_value(version)
|
|
@@ -1694,9 +1653,19 @@ def dataset_version_create(
|
|
|
1694
1653
|
except SdkMissing as exc:
|
|
1695
1654
|
typer.echo(str(exc), err=True)
|
|
1696
1655
|
raise typer.Exit(2) from exc
|
|
1656
|
+
create_kwargs = {}
|
|
1657
|
+
explicit_storage_path = str(storage_path or "").strip()
|
|
1658
|
+
if explicit_storage_path:
|
|
1659
|
+
create_kwargs["storage_path"] = explicit_storage_path
|
|
1697
1660
|
try:
|
|
1698
|
-
record = repo.versions.create(dataset, resolved)
|
|
1661
|
+
record = repo.versions.create(dataset, resolved, **create_kwargs)
|
|
1699
1662
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
1663
|
+
if _is_addressing_failure(exc):
|
|
1664
|
+
raise _fail(
|
|
1665
|
+
f"dataset {dataset} 版本 {resolved} 创建失败:{_reason(exc)}",
|
|
1666
|
+
"设置 AP_CLUSTER,或在命令前使用 ap --cluster <cluster>;"
|
|
1667
|
+
"也可传 --storage-path oss://<bucket>/<prefix>/",
|
|
1668
|
+
) from exc
|
|
1700
1669
|
sdk_error_exit(exc, what=f"dataset {dataset} 版本 {resolved}", stage="创建")
|
|
1701
1670
|
# 与 ``version get`` 同一个视图:同一个对象在两个命令里字段集、字段顺序、空值处理
|
|
1702
1671
|
# 必须一致(``to_dict()`` 原样输出会把 SDK 补的 False/[]/{} 也打出来)。
|
|
@@ -1746,7 +1715,11 @@ def dataset_split_list(
|
|
|
1746
1715
|
|
|
1747
1716
|
if resolved_version:
|
|
1748
1717
|
detail = _fetch_version(
|
|
1749
|
-
dataset,
|
|
1718
|
+
dataset,
|
|
1719
|
+
resolved_version,
|
|
1720
|
+
stage="split 查询",
|
|
1721
|
+
environment=environment,
|
|
1722
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1750
1723
|
)
|
|
1751
1724
|
records = _split_records(detail)
|
|
1752
1725
|
rows = [_split_row(record, detail) for record in records]
|
|
@@ -1755,27 +1728,6 @@ def dataset_split_list(
|
|
|
1755
1728
|
rows = [_split_row(record, {}) for record in records]
|
|
1756
1729
|
if resolved_run_type:
|
|
1757
1730
|
rows = [row for row in rows if str(row["run_type"]).lower() == resolved_run_type]
|
|
1758
|
-
if not rows and not resolved_run_type:
|
|
1759
|
-
# 空结果常见根因是元数据 environment 不匹配,探测一遍并引导切换。
|
|
1760
|
-
probe_repo = _dataset_repo()
|
|
1761
|
-
if resolved_version:
|
|
1762
|
-
|
|
1763
|
-
def _probe(env: str) -> int:
|
|
1764
|
-
try:
|
|
1765
|
-
detail = probe_repo.versions.get(dataset, resolved_version, environment=env)
|
|
1766
|
-
except Exception: # noqa: BLE001 - 该 env 没有这个版本
|
|
1767
|
-
return -1
|
|
1768
|
-
return len(_split_records(_model_dict(detail)))
|
|
1769
|
-
|
|
1770
|
-
else:
|
|
1771
|
-
|
|
1772
|
-
def _probe(env: str) -> int:
|
|
1773
|
-
page = probe_repo.splits.list(
|
|
1774
|
-
dataset, version_scope="unversioned", page_size=1, environment=env
|
|
1775
|
-
)
|
|
1776
|
-
return _pagination_total(_pagination_envelope(page), fallback=0) or 0
|
|
1777
|
-
|
|
1778
|
-
_note_environment_distribution(probe_repo, dataset, _probe, what="split")
|
|
1779
1731
|
_emit(
|
|
1780
1732
|
{"dataset": dataset, "version": resolved_version, "splits": rows},
|
|
1781
1733
|
rows,
|
|
@@ -1798,7 +1750,11 @@ def dataset_split_get(
|
|
|
1798
1750
|
|
|
1799
1751
|
if resolved_version:
|
|
1800
1752
|
detail = _fetch_version(
|
|
1801
|
-
dataset,
|
|
1753
|
+
dataset,
|
|
1754
|
+
resolved_version,
|
|
1755
|
+
stage="split 查询",
|
|
1756
|
+
environment=environment,
|
|
1757
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1802
1758
|
)
|
|
1803
1759
|
for record in _split_records(detail):
|
|
1804
1760
|
if str(record.get("name") or "") == split:
|
|
@@ -1863,20 +1819,22 @@ def dataset_split_create(
|
|
|
1863
1819
|
)
|
|
1864
1820
|
|
|
1865
1821
|
detail = _fetch_version(
|
|
1866
|
-
dataset,
|
|
1822
|
+
dataset,
|
|
1823
|
+
resolved_version,
|
|
1824
|
+
stage="split 创建前查询",
|
|
1825
|
+
environment=environment,
|
|
1826
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1867
1827
|
)
|
|
1868
1828
|
|
|
1869
1829
|
existing = _split_names(detail)
|
|
1870
1830
|
if split in existing:
|
|
1871
1831
|
# 幂等:已存在同名 split(设计 §4.3)。
|
|
1872
|
-
_note(f"dataset {dataset} 版本 {_version_label(resolved_version)} 已有 split {split}")
|
|
1873
|
-
if resolved_run_type:
|
|
1874
|
-
_note(f"要改 run_type 请用:ap dataset split update {split} --dataset {dataset}")
|
|
1875
1832
|
for record in _split_records(detail):
|
|
1876
1833
|
if str(record.get("name") or "") == split:
|
|
1877
1834
|
payload = dict(record)
|
|
1878
1835
|
payload.setdefault("dataset", dataset)
|
|
1879
1836
|
payload.setdefault("version", resolved_version)
|
|
1837
|
+
payload["created"] = False
|
|
1880
1838
|
_emit_detail(payload, output_format)
|
|
1881
1839
|
return
|
|
1882
1840
|
_emit_detail(
|
|
@@ -1893,6 +1851,7 @@ def dataset_split_create(
|
|
|
1893
1851
|
{"splits": names},
|
|
1894
1852
|
stage="split 创建",
|
|
1895
1853
|
environment=environment,
|
|
1854
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1896
1855
|
)
|
|
1897
1856
|
|
|
1898
1857
|
# ② run_type 只能第二次 PATCH:服务端 splits 与 split_run_types 互斥。
|
|
@@ -1910,6 +1869,7 @@ def dataset_split_create(
|
|
|
1910
1869
|
# 第一段 PATCH 已建出 split,失败后的恢复动作是补打 run_type,不是重跑 create。
|
|
1911
1870
|
fallback_hint=f"split {split} 已创建但 run_type 未设置,补打:{retry}",
|
|
1912
1871
|
environment=environment,
|
|
1872
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1913
1873
|
)
|
|
1914
1874
|
|
|
1915
1875
|
_emit_detail(
|
|
@@ -1949,8 +1909,9 @@ def dataset_split_update(
|
|
|
1949
1909
|
resolved_run_type = _check_choice(run_type, _RUN_TYPES, "--run-type")
|
|
1950
1910
|
|
|
1951
1911
|
if not resolved_version:
|
|
1952
|
-
# 无版本空间改 run_type 走 recordsvc 的 split 记录(PATCH /datasets/splits/detail)
|
|
1953
|
-
#
|
|
1912
|
+
# 无版本空间改 run_type 走 recordsvc 的 split 记录(PATCH /datasets/splits/detail):
|
|
1913
|
+
# 写端点仅限 online——SDK 1.1.2 起派生/显式 pre 都会前置 SchemaError 拒绝,
|
|
1914
|
+
# 不再出现"存在性检查读 pre、写入默默落 online"的读写环境撕裂。
|
|
1954
1915
|
env = _environment(environment)
|
|
1955
1916
|
if env == "pre":
|
|
1956
1917
|
raise _fail(
|
|
@@ -1997,7 +1958,11 @@ def dataset_split_update(
|
|
|
1997
1958
|
return
|
|
1998
1959
|
|
|
1999
1960
|
detail = _fetch_version(
|
|
2000
|
-
dataset,
|
|
1961
|
+
dataset,
|
|
1962
|
+
resolved_version,
|
|
1963
|
+
stage="split 更新前查询",
|
|
1964
|
+
environment=environment,
|
|
1965
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
2001
1966
|
)
|
|
2002
1967
|
|
|
2003
1968
|
if split not in _split_names(detail):
|
|
@@ -2015,6 +1980,7 @@ def dataset_split_update(
|
|
|
2015
1980
|
{"split_run_types": [{"name": split, "run_type": resolved_run_type}]},
|
|
2016
1981
|
stage="split 更新",
|
|
2017
1982
|
environment=environment,
|
|
1983
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
2018
1984
|
)
|
|
2019
1985
|
_emit_detail(
|
|
2020
1986
|
{
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
1. ``--user`` 是**空间归主的工号**,不是被授权人;``fs access grant/revoke`` 的
|
|
15
15
|
被授权主体用 ``--principal``。``--principal`` 优先;只给了 ``--user`` 时把它
|
|
16
|
-
|
|
16
|
+
当被授权主体、空间归主取当前用户,结果中的 owner/principal 字段反映最终解释。
|
|
17
17
|
2. 数据面寻址(含 ACL 资源 id 里的 storage_env)对客屏蔽:用户只需配 AP 基础环境
|
|
18
18
|
变量(``AP_CLUSTER`` / ``AP_API_KEY``),storage_env 由 SDK 依据 cluster 自动发现
|
|
19
19
|
(``repo.profile.storage_env``)。CLI 只把它用于**回显** ``{storage_env}/{uid}``,
|
|
@@ -99,10 +99,6 @@ def _fail(message: str, hint: str, exit_code: int = 1) -> "typer.Exit":
|
|
|
99
99
|
return typer.Exit(exit_code)
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
def _note(message: str) -> None:
|
|
103
|
-
typer.echo(f"note: {message}", err=True)
|
|
104
|
-
|
|
105
|
-
|
|
106
102
|
def _describe(exc: BaseException) -> str:
|
|
107
103
|
message = getattr(exc, "message", None)
|
|
108
104
|
text = str(message or "").strip() or str(exc).strip()
|
|
@@ -462,10 +458,6 @@ def _resolve_principal(repo, *, user: Optional[str], principal: Optional[str]) -
|
|
|
462
458
|
exit_code=2,
|
|
463
459
|
)
|
|
464
460
|
owner = _resolve_uid(repo, None)
|
|
465
|
-
_note(
|
|
466
|
-
f"--principal 未给:把 --user={fallback} 当作被授权主体,"
|
|
467
|
-
f"空间归主取当前工号 {owner};要授权别人的空间请显式写 --user <归主> --principal <主体>"
|
|
468
|
-
)
|
|
469
461
|
return owner, fallback
|
|
470
462
|
|
|
471
463
|
|
|
@@ -508,7 +500,6 @@ def fs_access_grant(
|
|
|
508
500
|
if not is_already_satisfied(exc):
|
|
509
501
|
sdk_error_exit(exc, what=f"用户存储空间 {owner} 的授权", stage="授予")
|
|
510
502
|
already = True
|
|
511
|
-
_note(f"用户存储空间 {owner} 上 {grantee} 的 {resolved_role} 绑定已存在:{_describe(exc)}")
|
|
512
503
|
|
|
513
504
|
_emit_detail(
|
|
514
505
|
{
|
|
@@ -583,7 +574,6 @@ def fs_access_revoke(
|
|
|
583
574
|
if per_role and not targets:
|
|
584
575
|
targets = _roles_of(repo, owner, grantee, resolved_type, subpath)
|
|
585
576
|
if not targets:
|
|
586
|
-
_note(f"用户存储空间 {owner} 上 {grantee} 没有任何绑定,无需撤销")
|
|
587
577
|
_emit_detail(
|
|
588
578
|
_revoke_payload(owner, storage_env, resolved_type, grantee, subpath, [], []),
|
|
589
579
|
output_format,
|
|
@@ -610,7 +600,6 @@ def fs_access_revoke(
|
|
|
610
600
|
if not is_already_satisfied(exc):
|
|
611
601
|
sdk_error_exit(exc, what=f"用户存储空间 {owner} 的授权", stage="撤销")
|
|
612
602
|
missing.append(target or "*")
|
|
613
|
-
_note(f"用户存储空间 {owner} 上 {grantee} 的绑定本就不存在:{_describe(exc)}")
|
|
614
603
|
else:
|
|
615
604
|
revoked.append(target or "*")
|
|
616
605
|
|
|
@@ -24,6 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
|
|
25
25
|
import inspect
|
|
26
26
|
import shutil
|
|
27
|
+
import tempfile
|
|
27
28
|
from pathlib import Path
|
|
28
29
|
from typing import Any, List, Optional
|
|
29
30
|
|
|
@@ -120,11 +121,6 @@ def _fail(message: str, hint: str, exit_code: int = 1) -> "typer.Exit":
|
|
|
120
121
|
return typer.Exit(exit_code)
|
|
121
122
|
|
|
122
123
|
|
|
123
|
-
def _note(message: str) -> None:
|
|
124
|
-
"""退出码 0 的路径只用 ``note:``(设计 §5.1);一律 stderr。"""
|
|
125
|
-
typer.echo(f"note: {message}", err=True)
|
|
126
|
-
|
|
127
|
-
|
|
128
124
|
def _load_repo_or_exit():
|
|
129
125
|
"""懒加载 SDK;缺 extra → 安装提示 + 退出码 2(设计 §2)。"""
|
|
130
126
|
from .irepo_sdk import SdkMissing, load_repo
|
|
@@ -136,6 +132,25 @@ def _load_repo_or_exit():
|
|
|
136
132
|
raise typer.Exit(2) from exc
|
|
137
133
|
|
|
138
134
|
|
|
135
|
+
def _probe_pull_private_endpoint(repo: Any, dataset: str) -> bool | None:
|
|
136
|
+
content = getattr(repo, "_content", None)
|
|
137
|
+
instances = getattr(repo, "instances", None)
|
|
138
|
+
credentials = getattr(content, "_credentials", None)
|
|
139
|
+
probe = getattr(content, "_probe_private_endpoint", None)
|
|
140
|
+
layout_factory = getattr(instances, "_layout", None)
|
|
141
|
+
if not all(callable(value) for value in (credentials, probe, layout_factory)):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
from instance_repo._routing import _plan_oss_route
|
|
145
|
+
|
|
146
|
+
layout = layout_factory()
|
|
147
|
+
sts = credentials(layout.sts_prefix(dataset), dataset)
|
|
148
|
+
route = _plan_oss_route(sts.get("endpoint", ""))
|
|
149
|
+
if not route.standard:
|
|
150
|
+
return None
|
|
151
|
+
return bool(probe(route.private_endpoint))
|
|
152
|
+
|
|
153
|
+
|
|
139
154
|
#: SDK ``instance_repo.validate.detect_format`` 的取值 → 对应校验函数名。分派必须与 SDK
|
|
140
155
|
#: ``InstancesClient.validate`` 一致;认不出的格式回退 ``check_layout``(与 SDK 同样的兜底)。
|
|
141
156
|
_VALIDATOR_BY_FORMAT: dict[str, str] = {
|
|
@@ -217,12 +232,7 @@ def _describe(exc: BaseException) -> str:
|
|
|
217
232
|
|
|
218
233
|
|
|
219
234
|
def _sdk_read_kwargs(sdk_obj: Any, method: str, *, environment: str, metadata_model: str) -> dict:
|
|
220
|
-
"""按 SDK
|
|
221
|
-
|
|
222
|
-
``environment`` / ``metadata_model`` 的按调用覆盖是 ``feat/apcli-dataset`` 起才有的
|
|
223
|
-
(1.1.0 前只有分支构建带);用已发布的旧 SDK 时这里不下发,并在 stderr 如实说明读的是
|
|
224
|
-
profile 派生的集合——而不是让它抛 TypeError,也不是静默读错集合。
|
|
225
|
-
"""
|
|
235
|
+
"""按 SDK 能力下发 environment / metadata_model,不允许静默忽略显式值。"""
|
|
226
236
|
fn = getattr(sdk_obj, method, None)
|
|
227
237
|
accepted: set[str] = set()
|
|
228
238
|
if fn is not None:
|
|
@@ -245,10 +255,10 @@ def _sdk_read_kwargs(sdk_obj: Any, method: str, *, environment: str, metadata_mo
|
|
|
245
255
|
elif value:
|
|
246
256
|
dropped.append(key)
|
|
247
257
|
if dropped:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
"
|
|
251
|
-
|
|
258
|
+
raise _fail(
|
|
259
|
+
f"已安装的 instance-repo 不支持按调用指定 {'/'.join(dropped)}",
|
|
260
|
+
"升级到 instance-repo 1.1.2 及以上后重试",
|
|
261
|
+
exit_code=2,
|
|
252
262
|
)
|
|
253
263
|
return kwargs
|
|
254
264
|
|
|
@@ -394,6 +404,12 @@ def instance_list(
|
|
|
394
404
|
output_format = _fmt(output_format)
|
|
395
405
|
resolved_version = _version_value(version)
|
|
396
406
|
repo = _load_repo_or_exit()
|
|
407
|
+
read_kwargs = _sdk_read_kwargs(
|
|
408
|
+
repo.instances,
|
|
409
|
+
"list_paged",
|
|
410
|
+
environment=_environment(environment),
|
|
411
|
+
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
412
|
+
)
|
|
397
413
|
try:
|
|
398
414
|
result = repo.instances.list_paged(
|
|
399
415
|
dataset,
|
|
@@ -401,12 +417,7 @@ def instance_list(
|
|
|
401
417
|
split=split,
|
|
402
418
|
page=page,
|
|
403
419
|
page_size=page_size,
|
|
404
|
-
**
|
|
405
|
-
repo.instances,
|
|
406
|
-
"list_paged",
|
|
407
|
-
environment=_environment(environment),
|
|
408
|
-
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
409
|
-
),
|
|
420
|
+
**read_kwargs,
|
|
410
421
|
)
|
|
411
422
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
412
423
|
raise _fail(
|
|
@@ -445,18 +456,19 @@ def instance_get(
|
|
|
445
456
|
output_format = _fmt(output_format)
|
|
446
457
|
resolved_version = _version_value(version)
|
|
447
458
|
repo = _load_repo_or_exit()
|
|
459
|
+
read_kwargs = _sdk_read_kwargs(
|
|
460
|
+
repo.instances,
|
|
461
|
+
"get",
|
|
462
|
+
environment=_environment(environment),
|
|
463
|
+
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
464
|
+
)
|
|
448
465
|
try:
|
|
449
466
|
record = repo.instances.get(
|
|
450
467
|
dataset,
|
|
451
468
|
resolved_version,
|
|
452
469
|
instance,
|
|
453
470
|
split=split,
|
|
454
|
-
**
|
|
455
|
-
repo.instances,
|
|
456
|
-
"get",
|
|
457
|
-
environment=_environment(environment),
|
|
458
|
-
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
459
|
-
),
|
|
471
|
+
**read_kwargs,
|
|
460
472
|
)
|
|
461
473
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
462
474
|
raise _fail(
|
|
@@ -499,7 +511,6 @@ def _confirm_version_autocreate(repo, dataset: str, version: str, split: str, *,
|
|
|
499
511
|
|
|
500
512
|
label = _version_label(version)
|
|
501
513
|
if yes:
|
|
502
|
-
_note(f"版本 {label} 不存在,上传时将由服务端自动创建(已用 --yes 跳过确认)")
|
|
503
514
|
return
|
|
504
515
|
if not sys.stdin.isatty():
|
|
505
516
|
raise _fail(
|
|
@@ -559,6 +570,7 @@ def instance_push(
|
|
|
559
570
|
pushed: list[dict] = []
|
|
560
571
|
failed: list[dict] = []
|
|
561
572
|
unconfirmed: list[str] = []
|
|
573
|
+
result_count_mismatch = False
|
|
562
574
|
if len(targets) == 1:
|
|
563
575
|
try:
|
|
564
576
|
result = repo.instances.push(
|
|
@@ -609,15 +621,18 @@ def instance_push(
|
|
|
609
621
|
)
|
|
610
622
|
else:
|
|
611
623
|
results_list = list(results_many or [])
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
f"用 ap instance list --dataset {dataset} --split {split} 核对实际内容"
|
|
616
|
-
)
|
|
624
|
+
result_count_mismatch = len(results_list) != len(targets)
|
|
625
|
+
if len(results_list) < len(targets):
|
|
626
|
+
unconfirmed.extend(targets[len(results_list) :])
|
|
617
627
|
for path, result in zip(targets, results_list):
|
|
618
628
|
pushed.append(_push_row(path, result))
|
|
619
629
|
|
|
620
630
|
_print_push_summary(dataset, resolved, split, pushed, failed, unconfirmed, output_format)
|
|
631
|
+
if result_count_mismatch:
|
|
632
|
+
raise _fail(
|
|
633
|
+
f"SDK 返回了 {len(results_list)}/{len(targets)} 个上传结果,无法确认完整写入状态",
|
|
634
|
+
f"用 ap instance list --dataset {dataset} --split {split} 核对实际内容",
|
|
635
|
+
)
|
|
621
636
|
|
|
622
637
|
|
|
623
638
|
def _push_row(path: str, result: Any) -> dict:
|
|
@@ -691,19 +706,46 @@ def instance_pull(
|
|
|
691
706
|
|
|
692
707
|
repo = _load_repo_or_exit()
|
|
693
708
|
try:
|
|
694
|
-
|
|
695
|
-
written = repo.instances.pull(dataset, resolved, instance, root, split)
|
|
709
|
+
private_endpoint_reachable = _probe_pull_private_endpoint(repo, dataset)
|
|
696
710
|
except Exception as exc: # noqa: BLE001
|
|
697
|
-
sdk_error_exit(exc, what=f"instance {instance}", stage="
|
|
711
|
+
sdk_error_exit(exc, what=f"instance {instance}", stage="拉取前私网探测")
|
|
712
|
+
if private_endpoint_reachable is False:
|
|
713
|
+
raise _fail(
|
|
714
|
+
f"instance {instance} 拉取失败:当前网络无法访问私网 OSS endpoint",
|
|
715
|
+
"instance pull 不支持公网下载;请在 AP dev cluster / ROCK VPC 内执行",
|
|
716
|
+
)
|
|
698
717
|
|
|
699
|
-
|
|
718
|
+
# SDK 的 swe/custom 下载会先往输出目录写 <id>.json 与 .probe 中间文件,再由
|
|
719
|
+
# _place_under_target 搬进 <id>/——把用户目录直接交给它,未加 --overwrite 也会
|
|
720
|
+
# 覆盖目标同级的同名文件。先落到独立临时目录,归一后再整体搬入最终目标。
|
|
721
|
+
staging = Path(tempfile.mkdtemp(prefix=f"ap-pull-{instance}-"))
|
|
722
|
+
try:
|
|
723
|
+
try:
|
|
724
|
+
written = repo.instances.pull(dataset, resolved, instance, staging, split)
|
|
725
|
+
except Exception as exc: # noqa: BLE001
|
|
726
|
+
sdk_error_exit(exc, what=f"instance {instance}", stage="拉取")
|
|
727
|
+
staged = _place_under_target(Path(str(written)), staging / instance)
|
|
728
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
729
|
+
if target.exists():
|
|
730
|
+
if not overwrite:
|
|
731
|
+
raise _fail(
|
|
732
|
+
f"instance {instance} 拉取目标 {target} 已存在",
|
|
733
|
+
"换一个 --output/-o 目录,或加 --overwrite 允许覆盖",
|
|
734
|
+
)
|
|
735
|
+
if target.is_dir():
|
|
736
|
+
shutil.rmtree(target)
|
|
737
|
+
else:
|
|
738
|
+
target.unlink()
|
|
739
|
+
shutil.move(str(staging / instance), str(target))
|
|
740
|
+
finally:
|
|
741
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
700
742
|
payload = {
|
|
701
743
|
"instance": instance,
|
|
702
744
|
"dataset": dataset,
|
|
703
745
|
"version": resolved,
|
|
704
746
|
"split": split,
|
|
705
747
|
"output": str(target),
|
|
706
|
-
"content": str(
|
|
748
|
+
"content": str(target / staged.name),
|
|
707
749
|
}
|
|
708
750
|
_emit_detail(payload, output_format)
|
|
709
751
|
|
|
@@ -24,19 +24,20 @@
|
|
|
24
24
|
cluster 概念。
|
|
25
25
|
|
|
26
26
|
因此 :func:`load_repo` 用 ``inspect.signature`` 做一次能力探测:签名支持的参数
|
|
27
|
-
显式传入,不支持的回退为进程内环境变量注入(不覆盖用户已设的值)
|
|
28
|
-
提示升级。
|
|
27
|
+
显式传入,不支持的回退为进程内环境变量注入(不覆盖用户已设的值)。
|
|
29
28
|
"""
|
|
30
29
|
|
|
31
30
|
from __future__ import annotations
|
|
32
31
|
|
|
33
32
|
import inspect
|
|
33
|
+
import json
|
|
34
34
|
import os
|
|
35
|
+
import time
|
|
35
36
|
from typing import TYPE_CHECKING, Any, NoReturn
|
|
36
37
|
|
|
37
38
|
import typer
|
|
38
39
|
|
|
39
|
-
from .config import
|
|
40
|
+
from .config import _parse_bool, get_config
|
|
40
41
|
|
|
41
42
|
if TYPE_CHECKING: # pragma: no cover - 仅供类型检查,运行时不 import SDK
|
|
42
43
|
from instance_repo import Repo
|
|
@@ -59,14 +60,6 @@ _INSTALL_HINT = (
|
|
|
59
60
|
"Install it with: pip install 'ap-client[dataset]'"
|
|
60
61
|
)
|
|
61
62
|
|
|
62
|
-
#: 旧 SDK(无 ``api_base``/``cluster`` 构造参数)时的升级提示,一行 stderr。
|
|
63
|
-
_LEGACY_SDK_NOTE = (
|
|
64
|
-
"note: installed instance-repo does not accept Repo(api_base=/cluster=); "
|
|
65
|
-
"falling back to INSTANCEREPO_* environment injection. "
|
|
66
|
-
"Upgrade with: pip install -U 'instance-repo[oss]>=1.1.0' "
|
|
67
|
-
"for cluster<->storage_env validation and symmetric read/write paths."
|
|
68
|
-
)
|
|
69
|
-
|
|
70
63
|
# storage_env 解析顺序:显式参数 > 规范环境变量(1.0.0) > 旧别名(0.8.x)。
|
|
71
64
|
# **没有默认值**——猜一个 storage_env 会把数据写到错误的存储环境。
|
|
72
65
|
_STORAGE_ENV_VARS = ("INSTANCE_REPO_STORAGE_ENV", "IR_STORAGE_ENV")
|
|
@@ -128,6 +121,97 @@ def _setdefault_env(name: str, value: str) -> None:
|
|
|
128
121
|
os.environ[name] = value
|
|
129
122
|
|
|
130
123
|
|
|
124
|
+
def _sdk_log_value(raw: bytes | str | None, cfg: Any) -> str:
|
|
125
|
+
if raw is None:
|
|
126
|
+
return ""
|
|
127
|
+
text = raw.decode("utf-8", errors="replace") if isinstance(raw, bytes) else raw
|
|
128
|
+
try:
|
|
129
|
+
value = json.loads(text)
|
|
130
|
+
except (TypeError, ValueError):
|
|
131
|
+
if cfg.verbose_redaction:
|
|
132
|
+
return "<non-JSON body omitted>"
|
|
133
|
+
rendered = text
|
|
134
|
+
else:
|
|
135
|
+
from .api import _redact_sensitive_data, _safe_json
|
|
136
|
+
|
|
137
|
+
value = _redact_sensitive_data(value, cfg.verbose_redaction)
|
|
138
|
+
if cfg.verbose_redaction:
|
|
139
|
+
value = _redact_sdk_tokens(value)
|
|
140
|
+
rendered = _safe_json(value)
|
|
141
|
+
if cfg.verbose_full_body or len(rendered) <= cfg.verbose_body_limit:
|
|
142
|
+
return rendered
|
|
143
|
+
return rendered[: cfg.verbose_body_limit] + "..."
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _redact_sdk_tokens(value: Any) -> Any:
|
|
147
|
+
if isinstance(value, dict):
|
|
148
|
+
redacted = {}
|
|
149
|
+
for key, item in value.items():
|
|
150
|
+
normalized = str(key).lower().replace("_", "").replace("-", "")
|
|
151
|
+
redacted[key] = (
|
|
152
|
+
"<redacted>" if normalized.endswith("token") else _redact_sdk_tokens(item)
|
|
153
|
+
)
|
|
154
|
+
return redacted
|
|
155
|
+
if isinstance(value, list):
|
|
156
|
+
return [_redact_sdk_tokens(item) for item in value]
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _instrument_sdk_transport(transport: Any, cfg: Any) -> bool:
|
|
161
|
+
sender = getattr(transport, "_send", None)
|
|
162
|
+
if not callable(sender):
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
from .api import _redact_headers, _redact_url_query_secrets, _safe_json, _write_stderr
|
|
166
|
+
|
|
167
|
+
def logged_send(method: str, url: str, headers: dict, body: bytes | None, timeout: float):
|
|
168
|
+
shown_url = _redact_url_query_secrets(url) if cfg.verbose_redaction else url
|
|
169
|
+
lines = [
|
|
170
|
+
f">> Request {method} {shown_url}",
|
|
171
|
+
f" headers={_safe_json(_redact_headers(headers, cfg.verbose_redaction))}",
|
|
172
|
+
]
|
|
173
|
+
if body is not None:
|
|
174
|
+
lines.extend((f" body_size={len(body)}", f" body={_sdk_log_value(body, cfg)}"))
|
|
175
|
+
_write_stderr(lines)
|
|
176
|
+
started = time.perf_counter()
|
|
177
|
+
try:
|
|
178
|
+
status, text = sender(method, url, headers, body, timeout)
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
duration_ms = (time.perf_counter() - started) * 1000
|
|
181
|
+
_write_stderr(
|
|
182
|
+
[
|
|
183
|
+
f"!! Error {method} {shown_url}",
|
|
184
|
+
f" duration_ms={duration_ms:.1f}",
|
|
185
|
+
f" error={type(exc).__name__}: {exc}",
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
raise
|
|
189
|
+
duration_ms = (time.perf_counter() - started) * 1000
|
|
190
|
+
encoded = text.encode("utf-8", errors="replace")
|
|
191
|
+
response_lines = [
|
|
192
|
+
f"<< Response {method} {shown_url}",
|
|
193
|
+
f" status={status}",
|
|
194
|
+
f" duration_ms={duration_ms:.1f}",
|
|
195
|
+
f" body_size={len(encoded)}",
|
|
196
|
+
]
|
|
197
|
+
if text:
|
|
198
|
+
response_lines.append(f" body={_sdk_log_value(text, cfg)}")
|
|
199
|
+
_write_stderr(response_lines)
|
|
200
|
+
return status, text
|
|
201
|
+
|
|
202
|
+
transport._send = logged_send
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _verbose_sdk_transport(cfg: Any, api_env: str) -> Any | None:
|
|
207
|
+
try:
|
|
208
|
+
from instance_repo.transport import Transport
|
|
209
|
+
except ImportError:
|
|
210
|
+
return None
|
|
211
|
+
transport = Transport(cfg.base_url, cfg.token_key or "", env=api_env)
|
|
212
|
+
return transport if _instrument_sdk_transport(transport, cfg) else None
|
|
213
|
+
|
|
214
|
+
|
|
131
215
|
def load_repo(*, cluster: str | None = None, storage_env: str | None = None) -> "Repo":
|
|
132
216
|
"""按 AP ``Config`` 构造 ``instance_repo.Repo``。
|
|
133
217
|
|
|
@@ -150,7 +234,10 @@ def load_repo(*, cluster: str | None = None, storage_env: str | None = None) ->
|
|
|
150
234
|
|
|
151
235
|
# 多 key 的 AP_API_KEYS 在这里直接抛 ConfigurationError:数据面要用这把 key 去
|
|
152
236
|
# 换 STS,拿不到确定的单 key 就没法继续(与控制面同一口径)。
|
|
153
|
-
cfg =
|
|
237
|
+
cfg = get_config(
|
|
238
|
+
verbose=_api._verbose_override,
|
|
239
|
+
cluster=cluster if cluster is not None else _api._cluster_override,
|
|
240
|
+
)
|
|
154
241
|
|
|
155
242
|
effective_cluster = _cluster_from_headers(cfg.headers)
|
|
156
243
|
effective_storage_env = _resolve_storage_env(storage_env)
|
|
@@ -168,13 +255,16 @@ def load_repo(*, cluster: str | None = None, storage_env: str | None = None) ->
|
|
|
168
255
|
kwargs["storage_env"] = effective_storage_env
|
|
169
256
|
if "api_env" in supported and api_env:
|
|
170
257
|
kwargs["api_env"] = api_env
|
|
258
|
+
if cfg.verbose and "transport" in supported:
|
|
259
|
+
transport = _verbose_sdk_transport(cfg, api_env)
|
|
260
|
+
if transport is not None:
|
|
261
|
+
kwargs["transport"] = transport
|
|
171
262
|
|
|
172
263
|
if "api_base" not in supported or "cluster" not in supported:
|
|
173
264
|
# 0.8.x 兼容路径:构造参数缺位,只能退回环境变量契约。
|
|
174
265
|
_setdefault_env(_LEGACY_API_BASE_VAR, cfg.base_url)
|
|
175
266
|
_setdefault_env(_LEGACY_TOKEN_VAR, cfg.token_key or "")
|
|
176
267
|
_setdefault_env(_LEGACY_STORAGE_ENV_VAR, effective_storage_env)
|
|
177
|
-
typer.echo(_LEGACY_SDK_NOTE, err=True)
|
|
178
268
|
|
|
179
269
|
return _Repo(**kwargs)
|
|
180
270
|
|
|
@@ -191,7 +281,9 @@ def _init_parameters(repo_cls: type) -> frozenset[str]:
|
|
|
191
281
|
continue
|
|
192
282
|
if parameter.kind is inspect.Parameter.VAR_KEYWORD:
|
|
193
283
|
# **kwargs 会吞下任何参数,当作全部支持(1.0.0 之后的宽松签名)。
|
|
194
|
-
return frozenset(
|
|
284
|
+
return frozenset(
|
|
285
|
+
{"token", "api_base", "cluster", "storage_env", "api_env", "transport"}
|
|
286
|
+
)
|
|
195
287
|
if parameter.kind is not inspect.Parameter.VAR_POSITIONAL:
|
|
196
288
|
names.add(name)
|
|
197
289
|
return frozenset(names)
|
|
@@ -216,7 +308,7 @@ _BIZ_CODE_RULES: dict[int, tuple[str, int]] = {
|
|
|
216
308
|
),
|
|
217
309
|
94018: ("当前角色只读,写操作需要 writer 角色;让 admin 执行 ap dataset access grant", 1),
|
|
218
310
|
94019: ("dataset 标识冲突:检查 --dataset 的取值是否与已有 dataset 重名", 1),
|
|
219
|
-
94020: ("请求未带 dataset 标识(SDK 过旧),升级:pip install -U 'instance-repo[oss]>=1.1.
|
|
311
|
+
94020: ("请求未带 dataset 标识(SDK 过旧),升级:pip install -U 'instance-repo[oss]>=1.1.2'", 1),
|
|
220
312
|
94021: ("该 dataset 不存在,先执行:ap dataset create <dataset> --benchmark <b>", 1),
|
|
221
313
|
94022: (
|
|
222
314
|
"无该用户存储空间的授权,让空间主人执行:ap fs access grant --user <你的工号> --role reader",
|
|
@@ -238,7 +330,7 @@ _CODE_RULES: dict[str, tuple[str, int]] = {
|
|
|
238
330
|
"E_DIGEST_MISMATCH": ("内容校验失败,重新拉取或重新打包该 instance 后再试", 1),
|
|
239
331
|
"E_IMMUTABLE": ("已发布的版本不可修改,创建新版本再操作", 1),
|
|
240
332
|
"E_FORBIDDEN": ("当前身份无权执行该操作,确认 AP_API_KEY 与 dataset 角色", 1),
|
|
241
|
-
"E_OWNER_REF_MISSING": ("缺少上游来源引用,升级:pip install -U 'instance-repo[oss]>=1.1.
|
|
333
|
+
"E_OWNER_REF_MISSING": ("缺少上游来源引用,升级:pip install -U 'instance-repo[oss]>=1.1.2'", 1),
|
|
242
334
|
"E_LAYOUT": ("instance 目录结构不合规,先执行 ap instance validate <path> 定位问题", 1),
|
|
243
335
|
"E_SCHEMA": ("instance 元数据不符合 schema,先执行 ap instance validate <path> 定位问题", 1),
|
|
244
336
|
"E_CRED_EXPIRED": ("临时凭据已过期,重试该命令即可重新换取", 1),
|
|
@@ -259,6 +351,7 @@ _ADDRESSING_FIELDS = (
|
|
|
259
351
|
"oss_prefix",
|
|
260
352
|
"scaffold_bucket",
|
|
261
353
|
"scaffold_root",
|
|
354
|
+
"storage_path",
|
|
262
355
|
)
|
|
263
356
|
_ADDRESSING_HINT = (
|
|
264
357
|
"当前 AP 集群未下发数据面存储配置,确认 AP_CLUSTER 指向支持 dataset 的集群,或联系 Dataset 管理员"
|
|
@@ -297,24 +390,20 @@ def update_version_detail(
|
|
|
297
390
|
body: dict,
|
|
298
391
|
*,
|
|
299
392
|
environment: str,
|
|
393
|
+
metadata_model: str = "",
|
|
300
394
|
) -> None:
|
|
301
|
-
"""``PATCH /apis/v1/datasets/versions/detail
|
|
302
|
-
|
|
303
|
-
为什么不用 ``repo.versions.update``(临时 shim,已反馈 SDK):它内部用
|
|
304
|
-
``_version_extra_q()`` **无参**调用,只能下发 profile 派生值(dev/staging 集群
|
|
305
|
-
→ ``pre``);而 CLI 的读路径与 ``versions.create`` 都在 ``online``,于是出现
|
|
306
|
-
"``version get`` 找得到、PATCH 却 404 dataset version not found"。SDK 的
|
|
307
|
-
``get``/``list_paged``/``create``/``ingest`` 都接受显式 ``environment``,
|
|
308
|
-
只有 ``update``/``status`` 没有。SDK 补上参数后这里会自动改走 SDK 方法。
|
|
309
|
-
"""
|
|
395
|
+
"""``PATCH /apis/v1/datasets/versions/detail`` with explicit routing selectors."""
|
|
310
396
|
update = repo.versions.update
|
|
311
397
|
try:
|
|
312
398
|
accepted = inspect.signature(update).parameters
|
|
313
399
|
except (TypeError, ValueError): # pragma: no cover - 内建/装饰过的可调用
|
|
314
400
|
accepted = {}
|
|
315
|
-
if "environment" in accepted:
|
|
316
|
-
|
|
317
|
-
|
|
401
|
+
if "environment" in accepted and "metadata_model" in accepted:
|
|
402
|
+
kwargs = dict(body)
|
|
403
|
+
if environment:
|
|
404
|
+
kwargs["environment"] = environment
|
|
405
|
+
if metadata_model:
|
|
406
|
+
kwargs["metadata_model"] = metadata_model
|
|
318
407
|
update(dataset, version, **kwargs)
|
|
319
408
|
return
|
|
320
409
|
|
|
@@ -323,6 +412,8 @@ def update_version_detail(
|
|
|
323
412
|
query = f"dataset_name={quote(dataset, safe='')}&version={quote(version, safe='')}"
|
|
324
413
|
if environment:
|
|
325
414
|
query += f"&environment={quote(environment, safe='')}"
|
|
415
|
+
if metadata_model:
|
|
416
|
+
query += f"&metadata_model={quote(metadata_model, safe='')}"
|
|
326
417
|
repo.transport.patch(f"/apis/v1/datasets/versions/detail?{query}", dict(body))
|
|
327
418
|
|
|
328
419
|
|
|
@@ -349,23 +440,17 @@ def _resolve_rule(exc: Exception) -> tuple[str, int]:
|
|
|
349
440
|
|
|
350
441
|
|
|
351
442
|
def sdk_error_exit(exc: Exception, *, what: str, stage: str) -> NoReturn:
|
|
352
|
-
"""把 SDK
|
|
353
|
-
|
|
354
|
-
非零退出走 ``error: <what> <stage>失败:<reason>``;设计 §5.1 里判定为幂等
|
|
355
|
-
(24401/24404,已达目标态)的码退出 0,此时首行改用 ``note:``——把"成功"
|
|
356
|
-
印成 ``error:`` 会让脚本与人都误判。第二行恒为 ``hint: <可执行下一步>``。
|
|
443
|
+
"""把 SDK 异常翻译成 ``error:`` / ``hint:`` 文案与 ``typer.Exit``。
|
|
357
444
|
|
|
358
|
-
|
|
445
|
+
设计 §5.1 里判定为幂等的 24401/24404 直接以 0 退出;非零失败输出固定
|
|
446
|
+
``error: <what> <stage>失败:<reason>`` 与可执行 ``hint:``。
|
|
359
447
|
"""
|
|
360
448
|
hint, exit_code = _resolve_rule(exc)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
typer.echo(f"
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
typer.echo(f"hint: {hint}", err=True)
|
|
367
|
-
if _is_verbose():
|
|
368
|
-
import traceback
|
|
449
|
+
if exit_code != 0:
|
|
450
|
+
typer.echo(f"error: {what} {stage}失败:{_reason(exc)}", err=True)
|
|
451
|
+
typer.echo(f"hint: {hint}", err=True)
|
|
452
|
+
if _is_verbose():
|
|
453
|
+
import traceback
|
|
369
454
|
|
|
370
|
-
|
|
455
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
|
371
456
|
raise typer.Exit(exit_code)
|
|
@@ -8,7 +8,7 @@ from typing import Optional
|
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
10
|
|
|
11
|
-
from .dataset_commands import _emit_detail, _fail, _fmt
|
|
11
|
+
from .dataset_commands import _emit_detail, _fail, _fmt
|
|
12
12
|
|
|
13
13
|
#: SDK 等待循环返回后不再变化的状态;其余视为等待超时。
|
|
14
14
|
_TERMINAL = ("succeeded", "failed", "canceled", "awaiting_approval")
|
|
@@ -93,7 +93,6 @@ def dataset_split_publish(
|
|
|
93
93
|
if not workflow_id:
|
|
94
94
|
raise _fail("发布响应缺少 workflow id", "重跑同一命令可安全重试")
|
|
95
95
|
hint = f"ap dataset split publish-status {workflow_id}"
|
|
96
|
-
_note(f"发布工作流:{workflow_id};查询进度:{hint}")
|
|
97
96
|
_emit_detail(workflow, fmt)
|
|
98
97
|
|
|
99
98
|
status = str(workflow.get("status") or "")
|
|
@@ -107,8 +106,6 @@ def dataset_split_publish(
|
|
|
107
106
|
if status not in _TERMINAL:
|
|
108
107
|
# 等待窗口内未到终态:SDK 返回最后一帧,服务端工作流不受影响。
|
|
109
108
|
raise _fail("发布等待超时,服务端工作流仍继续运行", hint)
|
|
110
|
-
if status == "awaiting_approval":
|
|
111
|
-
_note("发布工作流等待审批,尚未完成发布;审批信息见 approval 字段")
|
|
112
109
|
|
|
113
110
|
|
|
114
111
|
def dataset_split_publish_status(
|
|
@@ -132,5 +129,3 @@ def dataset_split_publish_status(
|
|
|
132
129
|
f"发布工作流 {workflow_id} {status}:{(workflow or {}).get('error_msg') or ''}",
|
|
133
130
|
"检查输出中的 steps 和 outputs,修正失败原因后再提交发布",
|
|
134
131
|
)
|
|
135
|
-
if status == "awaiting_approval":
|
|
136
|
-
_note("发布工作流等待审批,尚未完成发布;审批信息见 approval 字段")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ap-client"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Agent Platform API Client & CLI"
|
|
9
9
|
readme = { text = "A lightweight Python SDK and command line interface for Agent Platform. It provides helpers for configuring API access and managing templates, datasets, jobs, and groups.", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -18,10 +18,10 @@ dependencies = [
|
|
|
18
18
|
|
|
19
19
|
[project.optional-dependencies]
|
|
20
20
|
dataset = [
|
|
21
|
-
"instance-repo[oss]>=1.1.0",
|
|
21
|
+
"instance-repo[oss]>=1.1.2,<2.0.0",
|
|
22
22
|
]
|
|
23
23
|
all = [
|
|
24
|
-
"instance-repo[oss]>=1.1.0",
|
|
24
|
+
"instance-repo[oss]>=1.1.2,<2.0.0",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[project.scripts]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|