ap-client 0.3.1.dev1__tar.gz → 0.3.2.dev0__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.dev1 → ap_client-0.3.2.dev0}/PKG-INFO +1 -1
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/api.py +0 -18
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/cli.py +29 -10
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/dataset_commands.py +172 -84
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/fs_commands.py +1 -12
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/instance_commands.py +70 -35
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/irepo_sdk.py +135 -44
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/split_publish.py +5 -7
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/pyproject.toml +1 -1
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/__init__.py +0 -0
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/config.py +0 -0
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/exporter.py +0 -0
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/managed_ak.py +0 -0
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/profile_commands.py +0 -0
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/tbb.py +0 -0
- {ap_client-0.3.1.dev1 → ap_client-0.3.2.dev0}/ap_client/waiter.py +0 -0
|
@@ -294,24 +294,6 @@ def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
|
|
|
294
294
|
return {"workspace_id": workspace_id} if workspace_id else None
|
|
295
295
|
|
|
296
296
|
|
|
297
|
-
def _unwrap_data(payload: Any) -> Any:
|
|
298
|
-
"""Unwrap the apiserver ``{code,message,data}`` envelope when present.
|
|
299
|
-
|
|
300
|
-
The dataset-domain detail/create/patch endpoints return a bare JSON body,
|
|
301
|
-
but the same handlers are occasionally wrapped by the generic success
|
|
302
|
-
envelope. Tolerate both so command code never has to branch: a mapping that
|
|
303
|
-
carries ``data`` alongside ``code``/``message`` is treated as an envelope,
|
|
304
|
-
anything else is returned verbatim.
|
|
305
|
-
"""
|
|
306
|
-
if (
|
|
307
|
-
isinstance(payload, dict)
|
|
308
|
-
and "data" in payload
|
|
309
|
-
and ("code" in payload or "message" in payload)
|
|
310
|
-
):
|
|
311
|
-
return payload["data"]
|
|
312
|
-
return payload
|
|
313
|
-
|
|
314
|
-
|
|
315
297
|
def _split_paged(payload: Any) -> tuple[list[dict], dict]:
|
|
316
298
|
"""Split a ``PagedSuccess`` envelope into ``(records, pagination)``.
|
|
317
299
|
|
|
@@ -815,26 +815,45 @@ def _print_json_block(data: Any) -> None:
|
|
|
815
815
|
typer.echo(json.dumps(data, ensure_ascii=False, indent=2))
|
|
816
816
|
|
|
817
817
|
|
|
818
|
+
def _format_nested_detail(value: Any) -> str:
|
|
819
|
+
"""Nested dict/list for detail views: compact one-liner, pretty JSON when long.
|
|
820
|
+
|
|
821
|
+
A detail view must not silently drop data - the manifest in ``ap instance
|
|
822
|
+
get`` used to be cut mid-JSON by the one-liner preview.
|
|
823
|
+
"""
|
|
824
|
+
compact = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
825
|
+
if len(compact) <= _PLAIN_VALUE_PREVIEW:
|
|
826
|
+
return compact
|
|
827
|
+
return json.dumps(value, ensure_ascii=False, indent=2)
|
|
828
|
+
|
|
829
|
+
|
|
818
830
|
def _print_detail(data: object) -> None:
|
|
819
831
|
"""Render a single object as an aligned key/value detail view.
|
|
820
832
|
|
|
821
833
|
Top-level scalars become key/value rows (``image`` fields shortened); nested
|
|
822
|
-
dict/list fields are shown as compact one-liners
|
|
834
|
+
dict/list fields are shown as compact one-liners, or pretty-printed across
|
|
835
|
+
multiple lines when the one-liner would not fit. Falls back to JSON for
|
|
823
836
|
non-dict payloads.
|
|
824
837
|
"""
|
|
825
838
|
if not isinstance(data, dict):
|
|
826
839
|
_print_json(data)
|
|
827
840
|
return
|
|
828
|
-
|
|
829
|
-
nested_rows: list[tuple[str, Any]] = []
|
|
841
|
+
rows: list[tuple[str, str]] = []
|
|
830
842
|
for key, value in data.items():
|
|
831
843
|
if isinstance(value, (dict, list)):
|
|
832
|
-
|
|
844
|
+
rows.append((key, _format_nested_detail(value)))
|
|
833
845
|
elif key == "image" and isinstance(value, str):
|
|
834
|
-
|
|
846
|
+
rows.append((key, _shorten_image(value)))
|
|
847
|
+
elif value is None:
|
|
848
|
+
continue
|
|
835
849
|
else:
|
|
836
|
-
|
|
837
|
-
|
|
850
|
+
rows.append((key, _format_plain_value(value, max_len=0)))
|
|
851
|
+
width = max((len(label) for label, _text in rows), default=0)
|
|
852
|
+
indent = " " * (width + 4)
|
|
853
|
+
for label, text in rows:
|
|
854
|
+
if "\n" in text:
|
|
855
|
+
text = text.replace("\n", f"\n{indent}")
|
|
856
|
+
typer.echo(f" {label.ljust(width)}: {text}")
|
|
838
857
|
|
|
839
858
|
|
|
840
859
|
def _print_template_get_plain(t: dict) -> None:
|
|
@@ -944,7 +963,7 @@ def _print_field_value(value: Any, output_format: str) -> None:
|
|
|
944
963
|
_print_formatted(value, "json" if output_format == "plain" else output_format)
|
|
945
964
|
|
|
946
965
|
|
|
947
|
-
def _format_plain_value(value: Any) -> str:
|
|
966
|
+
def _format_plain_value(value: Any, *, max_len: int = _PLAIN_VALUE_PREVIEW) -> str:
|
|
948
967
|
if value is None:
|
|
949
968
|
return ""
|
|
950
969
|
if isinstance(value, (dict, list)):
|
|
@@ -952,9 +971,9 @@ def _format_plain_value(value: Any) -> str:
|
|
|
952
971
|
else:
|
|
953
972
|
text = str(value)
|
|
954
973
|
text = text.replace("\r", "\\r").replace("\n", "\\n")
|
|
955
|
-
if len(text) <=
|
|
974
|
+
if not max_len or len(text) <= max_len:
|
|
956
975
|
return text
|
|
957
|
-
return f"{text[:
|
|
976
|
+
return f"{text[: max_len - 3]}..."
|
|
958
977
|
|
|
959
978
|
|
|
960
979
|
def _print_key_values(rows: list[tuple[str, Any]], *, skip_empty: bool = True) -> None:
|
|
@@ -17,6 +17,8 @@ import typer
|
|
|
17
17
|
if TYPE_CHECKING: # pragma: no cover - 仅用于类型标注,运行时不 import
|
|
18
18
|
from datetime import datetime
|
|
19
19
|
|
|
20
|
+
from instance_repo import Repo
|
|
21
|
+
|
|
20
22
|
__all__ = [
|
|
21
23
|
"dataset_version_app",
|
|
22
24
|
"dataset_split_app",
|
|
@@ -118,6 +120,7 @@ _NO_BENCHMARK = "none"
|
|
|
118
120
|
_FORMAT_HELP = "Output format: plain/table/json/yaml (default: AP_FORMAT or command default)"
|
|
119
121
|
|
|
120
122
|
_METADATA_MODELS = ("version_first", "split_first")
|
|
123
|
+
_VERSION_SPLIT_METADATA_MODEL = "version_first"
|
|
121
124
|
|
|
122
125
|
#: CLI **不替 SDK 决定元数据模型**:不下发 ``metadata_model``,由 SDK 按 profile 归一
|
|
123
126
|
#: (当前为 ``split_first``)。
|
|
@@ -153,10 +156,10 @@ _NO_VERSION_NOTE = "未指定 --version 表示无版本空间(split_first),不
|
|
|
153
156
|
# ---------------------------------------------------------------------------
|
|
154
157
|
|
|
155
158
|
|
|
156
|
-
def _fmt(output_format: Optional[str]) -> str:
|
|
159
|
+
def _fmt(output_format: Optional[str], *, default: str = "plain") -> str:
|
|
157
160
|
from .cli import _normalize_output_format
|
|
158
161
|
|
|
159
|
-
return _normalize_output_format(output_format, keep_table=True)
|
|
162
|
+
return _normalize_output_format(output_format, default=default, keep_table=True)
|
|
160
163
|
|
|
161
164
|
|
|
162
165
|
def _fail(message: str, hint: str, exit_code: int = 1) -> "typer.Exit":
|
|
@@ -166,11 +169,6 @@ def _fail(message: str, hint: str, exit_code: int = 1) -> "typer.Exit":
|
|
|
166
169
|
return typer.Exit(exit_code)
|
|
167
170
|
|
|
168
171
|
|
|
169
|
-
def _note(message: str) -> None:
|
|
170
|
-
"""退出码 0 的路径只用 ``note:``(设计 §5.1);一律 stderr。"""
|
|
171
|
-
typer.echo(f"note: {message}", err=True)
|
|
172
|
-
|
|
173
|
-
|
|
174
172
|
def _describe(exc: BaseException) -> str:
|
|
175
173
|
message = getattr(exc, "message", None)
|
|
176
174
|
text = str(message or "").strip() or str(exc).strip()
|
|
@@ -410,6 +408,21 @@ def _api_biz_code(exc: BaseException) -> Optional[int]:
|
|
|
410
408
|
return None
|
|
411
409
|
|
|
412
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
|
+
|
|
413
426
|
#: apiserver dataset 控制面业务码 → 固定 hint(设计 §5.1)。
|
|
414
427
|
_DATASET_CODE_HINTS: dict[int, str] = {
|
|
415
428
|
92008: "该环境未开启 dataset 能力,请联系管理员",
|
|
@@ -420,7 +433,7 @@ _DATASET_CODE_HINTS: dict[int, str] = {
|
|
|
420
433
|
}
|
|
421
434
|
|
|
422
435
|
|
|
423
|
-
def _resolve_benchmark_id(repo:
|
|
436
|
+
def _resolve_benchmark_id(repo: "Repo", value: str) -> str:
|
|
424
437
|
"""``--benchmark`` 接受名称或 ``source_id``,统一解析成 ``source_id``(设计 §9)。
|
|
425
438
|
|
|
426
439
|
走 SDK ``benchmarks``:先按 source_id 点查,再按名称模糊搜一遍精确比对
|
|
@@ -522,7 +535,12 @@ def _updated_range(from_value: Optional[str], to_value: Optional[str]) -> tuple[
|
|
|
522
535
|
|
|
523
536
|
|
|
524
537
|
def _fetch_version(
|
|
525
|
-
dataset: str,
|
|
538
|
+
dataset: str,
|
|
539
|
+
version: str,
|
|
540
|
+
*,
|
|
541
|
+
stage: str,
|
|
542
|
+
environment: Optional[str] = None,
|
|
543
|
+
metadata_model: str = "",
|
|
526
544
|
) -> dict:
|
|
527
545
|
"""读 version 详情(含内嵌 ``splits[]``);空 version 的报错必须点明"无版本空间"语义。
|
|
528
546
|
|
|
@@ -536,10 +554,18 @@ def _fetch_version(
|
|
|
536
554
|
env = _environment(environment)
|
|
537
555
|
repo = _dataset_repo()
|
|
538
556
|
try:
|
|
539
|
-
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
|
+
)
|
|
540
563
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
541
564
|
if version:
|
|
542
|
-
hint =
|
|
565
|
+
hint = (
|
|
566
|
+
f"确认版本名:ap dataset version list --dataset {dataset}"
|
|
567
|
+
";版本也可能在另一个元数据 environment,加 --environment online/pre 重试"
|
|
568
|
+
)
|
|
543
569
|
else:
|
|
544
570
|
hint = f"{_NO_VERSION_NOTE};要操作具体版本请加 --version <version>"
|
|
545
571
|
raise _fail(
|
|
@@ -634,7 +660,7 @@ def _split_names(detail: dict) -> list[str]:
|
|
|
634
660
|
return names
|
|
635
661
|
|
|
636
662
|
|
|
637
|
-
def _dataset_repo() ->
|
|
663
|
+
def _dataset_repo() -> "Repo":
|
|
638
664
|
"""dataset 写路径统一走 InstanceRepo SDK;未装 ``dataset`` extra 时退出 2。"""
|
|
639
665
|
from .irepo_sdk import SdkMissing, load_repo
|
|
640
666
|
|
|
@@ -653,6 +679,7 @@ def _patch_version(
|
|
|
653
679
|
stage: str,
|
|
654
680
|
fallback_hint: str = "",
|
|
655
681
|
environment: Optional[str] = None,
|
|
682
|
+
metadata_model: str = "",
|
|
656
683
|
) -> None:
|
|
657
684
|
"""split 写操作统一走 SDK;body 为 splits/split_run_types 单键。
|
|
658
685
|
|
|
@@ -664,7 +691,14 @@ def _patch_version(
|
|
|
664
691
|
|
|
665
692
|
repo = _dataset_repo()
|
|
666
693
|
try:
|
|
667
|
-
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
|
+
)
|
|
668
702
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
669
703
|
code = getattr(exc, "biz_code", None)
|
|
670
704
|
if code == 94018:
|
|
@@ -800,14 +834,16 @@ def _bindings(repo, dataset: str) -> list[dict]:
|
|
|
800
834
|
|
|
801
835
|
|
|
802
836
|
def _bindings_of(repo, dataset: str, principal_id: str, principal_type: str) -> list[dict]:
|
|
803
|
-
"""某 principal 在此 dataset
|
|
837
|
+
"""某 principal 在此 dataset 上的现存绑定;读失败时停止变更。"""
|
|
804
838
|
from .irepo_sdk import _reason
|
|
805
839
|
|
|
806
840
|
try:
|
|
807
841
|
rows = _bindings(repo, dataset)
|
|
808
|
-
except Exception as exc: # noqa: BLE001
|
|
809
|
-
|
|
810
|
-
|
|
842
|
+
except Exception as exc: # noqa: BLE001
|
|
843
|
+
raise _fail(
|
|
844
|
+
f"dataset {dataset} 现有授权查询失败:{_reason(exc)}",
|
|
845
|
+
"确认 dataset-admin 权限和当前集群后重试",
|
|
846
|
+
) from exc
|
|
811
847
|
return [
|
|
812
848
|
row
|
|
813
849
|
for row in rows
|
|
@@ -888,7 +924,7 @@ def dataset_list(
|
|
|
888
924
|
"""List datasets from the registry. Defaults to JSON; use --format table for a table."""
|
|
889
925
|
from .cli import _print_formatted, get_client
|
|
890
926
|
|
|
891
|
-
output_format = _fmt(output_format)
|
|
927
|
+
output_format = _fmt(output_format, default="json")
|
|
892
928
|
resolved_query = str(query or "").strip()
|
|
893
929
|
legacy_search = str(search or "").strip()
|
|
894
930
|
if legacy_search:
|
|
@@ -901,10 +937,6 @@ def dataset_list(
|
|
|
901
937
|
f"或 ap dataset list --query {query}",
|
|
902
938
|
exit_code=2,
|
|
903
939
|
)
|
|
904
|
-
if not resolved_query:
|
|
905
|
-
_note(
|
|
906
|
-
f"位置参数搜索已兼容为 --query 的等价形式:ap dataset list --query {legacy_search}"
|
|
907
|
-
)
|
|
908
940
|
resolved_query = legacy_search
|
|
909
941
|
|
|
910
942
|
if legacy:
|
|
@@ -935,7 +967,6 @@ def dataset_list(
|
|
|
935
967
|
exit_code=2,
|
|
936
968
|
)
|
|
937
969
|
result = get_client().list_all_datasets(resolved_query or None)
|
|
938
|
-
_note("--legacy 使用旧数据源(与 ap job create --dataset 读取的一致),已自动翻页拉全量")
|
|
939
970
|
if output_format == "table":
|
|
940
971
|
from .cli import _print_records_table
|
|
941
972
|
|
|
@@ -1001,14 +1032,6 @@ def dataset_list(
|
|
|
1001
1032
|
records, result = fetch(resolved_page)
|
|
1002
1033
|
pagination = _pagination_envelope(result)
|
|
1003
1034
|
|
|
1004
|
-
total = _pagination_total(pagination)
|
|
1005
|
-
if total is not None and total > len(records):
|
|
1006
|
-
_note(
|
|
1007
|
-
f"共 {total} 个 dataset,本次返回第 {resolved_page} 页的 {len(records)} 条"
|
|
1008
|
-
f"(每页 {resolved_page_size});继续翻页用 --page/--page-size,"
|
|
1009
|
-
f"拉全量用 --legacy"
|
|
1010
|
-
)
|
|
1011
|
-
|
|
1012
1035
|
payload: dict[str, Any] = {}
|
|
1013
1036
|
if mine:
|
|
1014
1037
|
payload["owned_by_me"] = True
|
|
@@ -1017,11 +1040,44 @@ def dataset_list(
|
|
|
1017
1040
|
payload["datasets"] = [_legacy_dataset_record(record) for record in records]
|
|
1018
1041
|
payload["total"] = _pagination_total(pagination, fallback=len(records))
|
|
1019
1042
|
payload["pagination"] = pagination
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1043
|
+
_emit(payload, rows, _DATASET_COLUMNS, output_format)
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
#: ``dataset get`` 尾部 environment 摘要表的列。
|
|
1047
|
+
_ENVIRONMENT_SUMMARY_COLUMNS: tuple[tuple[str, str], ...] = (
|
|
1048
|
+
("environment", "ENVIRONMENT"),
|
|
1049
|
+
("versions", "VERSIONS"),
|
|
1050
|
+
("splits", "SPLITS"),
|
|
1051
|
+
("latest_version", "LATEST_VERSION"),
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
def _print_dataset_environments(repo: "Repo", dataset: str, detail: dict) -> None:
|
|
1056
|
+
"""``dataset get`` 尾部按 environment 汇总 version/split,告诉用户该去哪个 env 指定。
|
|
1057
|
+
|
|
1058
|
+
仅人读视图(plain/table)追加;``json``/``yaml`` 恒为服务端原样。单个 env 读失败
|
|
1059
|
+
(旧 SDK 不接受 environment、权限差异)显示 ``?``,不影响主输出。
|
|
1060
|
+
"""
|
|
1061
|
+
envs = [str(env).strip() for env in detail.get("available_envs") or [] if str(env).strip()]
|
|
1062
|
+
if not envs:
|
|
1063
|
+
return
|
|
1064
|
+
rows: list[dict] = []
|
|
1065
|
+
for env in envs:
|
|
1066
|
+
try:
|
|
1067
|
+
env_detail = _model_dict(repo.datasets.get_detail(dataset, environment=env))
|
|
1068
|
+
except Exception: # noqa: BLE001 - 摘要尽力而为
|
|
1069
|
+
rows.append({"environment": env, "versions": "?", "splits": "?", "latest_version": "?"})
|
|
1070
|
+
continue
|
|
1071
|
+
rows.append(
|
|
1072
|
+
{
|
|
1073
|
+
"environment": env,
|
|
1074
|
+
"versions": env_detail.get("version_count", ""),
|
|
1075
|
+
"splits": env_detail.get("split_count", ""),
|
|
1076
|
+
"latest_version": env_detail.get("latest_version") or "",
|
|
1077
|
+
}
|
|
1078
|
+
)
|
|
1079
|
+
typer.echo("\nEnvironments (add --environment <env> to target one)")
|
|
1080
|
+
_print_table(rows, _ENVIRONMENT_SUMMARY_COLUMNS, "plain")
|
|
1025
1081
|
|
|
1026
1082
|
|
|
1027
1083
|
def dataset_get(
|
|
@@ -1032,6 +1088,13 @@ def dataset_get(
|
|
|
1032
1088
|
"""Show dataset detail, including version / split summary stats."""
|
|
1033
1089
|
from .irepo_sdk import _reason
|
|
1034
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
|
+
)
|
|
1035
1098
|
output_format = _fmt(output_format)
|
|
1036
1099
|
env = _environment(environment)
|
|
1037
1100
|
repo = _dataset_repo()
|
|
@@ -1042,12 +1105,10 @@ def dataset_get(
|
|
|
1042
1105
|
f"dataset {dataset} 查询失败:{_reason(exc)}",
|
|
1043
1106
|
f"确认 dataset 名称:ap dataset list --query {dataset}",
|
|
1044
1107
|
) from exc
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
always=("status",),
|
|
1050
|
-
)
|
|
1108
|
+
rendered = _model_dict(detail)
|
|
1109
|
+
_emit_detail(rendered, output_format, order=_DATASET_DETAIL_ORDER, always=("status",))
|
|
1110
|
+
if output_format not in ("json", "yaml"):
|
|
1111
|
+
_print_dataset_environments(repo, dataset, rendered)
|
|
1051
1112
|
|
|
1052
1113
|
|
|
1053
1114
|
def dataset_create(
|
|
@@ -1182,7 +1243,6 @@ def dataset_claim(
|
|
|
1182
1243
|
if not detail.get("claimable", False):
|
|
1183
1244
|
# 幂等:已认领就不是错误(设计 §4.1),stdout 仍按 --format 输出现状。
|
|
1184
1245
|
owner = str(detail.get("owner") or "").strip() or "<unknown>"
|
|
1185
|
-
_note(f"dataset {dataset} 已被认领(owner={owner}),无需重复认领")
|
|
1186
1246
|
_emit_detail(
|
|
1187
1247
|
{
|
|
1188
1248
|
"dataset": dataset,
|
|
@@ -1198,21 +1258,30 @@ def dataset_claim(
|
|
|
1198
1258
|
|
|
1199
1259
|
if not (workspace or "").strip():
|
|
1200
1260
|
candidates: list[Any] = []
|
|
1261
|
+
candidate_error = ""
|
|
1201
1262
|
try:
|
|
1202
1263
|
candidates = repo.datasets.list_claim_workspaces() or []
|
|
1203
1264
|
except Exception as exc: # noqa: BLE001 - 列不出来不影响报用法错
|
|
1204
|
-
|
|
1265
|
+
candidate_error = _reason(exc)
|
|
1266
|
+
choices: list[str] = []
|
|
1205
1267
|
for item in candidates:
|
|
1206
1268
|
queue_id = str(getattr(item, "queue_id", "") or getattr(item, "id", "") or "").strip()
|
|
1207
1269
|
name = str(getattr(item, "name", "") or "").strip()
|
|
1208
1270
|
if not queue_id and isinstance(item, dict):
|
|
1209
1271
|
queue_id = str(item.get("queue_id") or item.get("id") or "")
|
|
1210
1272
|
name = str(item.get("name") or "")
|
|
1211
|
-
|
|
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>"
|
|
1212
1282
|
raise _fail(
|
|
1213
1283
|
f"dataset {dataset} 认领失败:服务端要求 workspace_id,但没给 --workspace",
|
|
1214
|
-
|
|
1215
|
-
f"{dataset} --workspace <queue-id> --benchmark <benchmark>",
|
|
1284
|
+
hint,
|
|
1216
1285
|
exit_code=2,
|
|
1217
1286
|
)
|
|
1218
1287
|
|
|
@@ -1230,9 +1299,7 @@ def dataset_claim(
|
|
|
1230
1299
|
require_benchmark=bool(benchmark_id),
|
|
1231
1300
|
)
|
|
1232
1301
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
1233
|
-
code =
|
|
1234
|
-
if code is None:
|
|
1235
|
-
code = _api_biz_code(exc)
|
|
1302
|
+
code = _business_code(exc)
|
|
1236
1303
|
if code == 92010:
|
|
1237
1304
|
# 已被认领:以服务端 92010 为准(不本地推断),幂等 exit 0。
|
|
1238
1305
|
owner = str(detail.get("owner") or "").strip()
|
|
@@ -1243,7 +1310,6 @@ def dataset_claim(
|
|
|
1243
1310
|
except Exception: # noqa: BLE001 - 取不到 owner 不影响幂等退出
|
|
1244
1311
|
owner = ""
|
|
1245
1312
|
owner = owner or "<unknown>"
|
|
1246
|
-
_note(f"dataset {dataset} 已被认领(owner={owner}),无需重复认领")
|
|
1247
1313
|
_emit_detail(
|
|
1248
1314
|
{
|
|
1249
1315
|
"dataset": dataset,
|
|
@@ -1341,7 +1407,13 @@ def dataset_access_grant(
|
|
|
1341
1407
|
try:
|
|
1342
1408
|
repo.datasets.revoke(dataset, principal_id, stale, principal_type=resolved_type)
|
|
1343
1409
|
except Exception as exc: # noqa: BLE001
|
|
1344
|
-
|
|
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:
|
|
1345
1417
|
raise _fail(
|
|
1346
1418
|
f"dataset {dataset} 旧角色 {stale} 撤销失败:{_reason(exc)}",
|
|
1347
1419
|
f"先手动撤销:ap dataset access revoke {dataset} "
|
|
@@ -1349,19 +1421,14 @@ def dataset_access_grant(
|
|
|
1349
1421
|
) from exc
|
|
1350
1422
|
else:
|
|
1351
1423
|
replaced.append(stale)
|
|
1352
|
-
_note(f"已撤销 {principal_id} 在 {dataset} 上的旧角色 {stale}")
|
|
1353
1424
|
|
|
1354
1425
|
already = False
|
|
1355
1426
|
try:
|
|
1356
1427
|
repo.datasets.grant(dataset, principal_id, resolved_role, principal_type=resolved_type)
|
|
1357
1428
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
1358
|
-
code =
|
|
1429
|
+
code = _business_code(exc)
|
|
1359
1430
|
if code == 24404:
|
|
1360
1431
|
already = True
|
|
1361
|
-
_note(
|
|
1362
|
-
f"dataset {dataset} 上 {principal_id} 的 {resolved_role} 绑定已存在"
|
|
1363
|
-
f"({_reason(exc)})"
|
|
1364
|
-
)
|
|
1365
1432
|
elif code == 24402:
|
|
1366
1433
|
raise _fail(
|
|
1367
1434
|
f"dataset {dataset} 授权失败:入参非法({_reason(exc)})",
|
|
@@ -1435,7 +1502,6 @@ def dataset_access_revoke(
|
|
|
1435
1502
|
if row["role"] in _ROLE_IDS
|
|
1436
1503
|
]
|
|
1437
1504
|
if not targets:
|
|
1438
|
-
_note(f"dataset {dataset} 上 {principal_id} 已无绑定,无需撤销")
|
|
1439
1505
|
_emit_detail(
|
|
1440
1506
|
{
|
|
1441
1507
|
"dataset": dataset,
|
|
@@ -1455,14 +1521,9 @@ def dataset_access_revoke(
|
|
|
1455
1521
|
try:
|
|
1456
1522
|
repo.datasets.revoke(dataset, principal_id, target, principal_type=resolved_type)
|
|
1457
1523
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
1458
|
-
code =
|
|
1524
|
+
code = _business_code(exc)
|
|
1459
1525
|
if code == 24401:
|
|
1460
|
-
# 幂等:目标态已达成(设计 §5.1),不用 error: 前缀。
|
|
1461
1526
|
missing.append(target)
|
|
1462
|
-
_note(
|
|
1463
|
-
f"dataset {dataset} 上 {principal_id} 的 {target} 绑定不存在,"
|
|
1464
|
-
f"已处于目标状态({_reason(exc)})"
|
|
1465
|
-
)
|
|
1466
1527
|
continue
|
|
1467
1528
|
if code == 24402:
|
|
1468
1529
|
raise _fail(
|
|
@@ -1470,6 +1531,11 @@ def dataset_access_revoke(
|
|
|
1470
1531
|
"检查工号/用户组 id 与 --principal-type 是否匹配",
|
|
1471
1532
|
exit_code=2,
|
|
1472
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
|
|
1473
1539
|
raise _fail(
|
|
1474
1540
|
f"dataset {dataset} 撤销 {target} 失败:{_reason(exc)}",
|
|
1475
1541
|
"撤销需要 dataset-admin;确认该环境已开启 dataset RBAC",
|
|
@@ -1534,14 +1600,6 @@ def dataset_version_list(
|
|
|
1534
1600
|
) from exc
|
|
1535
1601
|
|
|
1536
1602
|
records = [_model_dict(record) for record in (getattr(result, "items", None) or [])]
|
|
1537
|
-
if records and all(
|
|
1538
|
-
"split_count" not in record and "instance_count" not in record for record in records
|
|
1539
|
-
):
|
|
1540
|
-
# 旧 SDK(1.0.9.dev0 及以前)的版本列表投影丢服务端统计字段:如实说明,不静默空列。
|
|
1541
|
-
_note(
|
|
1542
|
-
"已安装的 instance-repo 未返回版本统计(split_count/instance_count),"
|
|
1543
|
-
"SPLITS/INSTANCES 两列为空;升级到 1.1.0 及以上可恢复"
|
|
1544
|
-
)
|
|
1545
1603
|
rows = [_version_row(record) for record in records]
|
|
1546
1604
|
_emit(
|
|
1547
1605
|
{"dataset": dataset, "versions": records, "pagination": _pagination_envelope(result)},
|
|
@@ -1569,10 +1627,15 @@ def dataset_version_get(
|
|
|
1569
1627
|
def dataset_version_create(
|
|
1570
1628
|
version: str = typer.Argument(..., help="Dataset version to create"),
|
|
1571
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
|
+
),
|
|
1572
1635
|
output_format: str = typer.Option(None, "--format", help=_FORMAT_HELP),
|
|
1573
1636
|
):
|
|
1574
1637
|
"""Create a draft version. storage_path is auto-computed from data-plane addressing."""
|
|
1575
|
-
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
|
|
1576
1639
|
|
|
1577
1640
|
output_format = _fmt(output_format)
|
|
1578
1641
|
resolved = _version_value(version)
|
|
@@ -1590,9 +1653,19 @@ def dataset_version_create(
|
|
|
1590
1653
|
except SdkMissing as exc:
|
|
1591
1654
|
typer.echo(str(exc), err=True)
|
|
1592
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
|
|
1593
1660
|
try:
|
|
1594
|
-
record = repo.versions.create(dataset, resolved)
|
|
1661
|
+
record = repo.versions.create(dataset, resolved, **create_kwargs)
|
|
1595
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
|
|
1596
1669
|
sdk_error_exit(exc, what=f"dataset {dataset} 版本 {resolved}", stage="创建")
|
|
1597
1670
|
# 与 ``version get`` 同一个视图:同一个对象在两个命令里字段集、字段顺序、空值处理
|
|
1598
1671
|
# 必须一致(``to_dict()`` 原样输出会把 SDK 补的 False/[]/{} 也打出来)。
|
|
@@ -1642,7 +1715,11 @@ def dataset_split_list(
|
|
|
1642
1715
|
|
|
1643
1716
|
if resolved_version:
|
|
1644
1717
|
detail = _fetch_version(
|
|
1645
|
-
dataset,
|
|
1718
|
+
dataset,
|
|
1719
|
+
resolved_version,
|
|
1720
|
+
stage="split 查询",
|
|
1721
|
+
environment=environment,
|
|
1722
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1646
1723
|
)
|
|
1647
1724
|
records = _split_records(detail)
|
|
1648
1725
|
rows = [_split_row(record, detail) for record in records]
|
|
@@ -1673,7 +1750,11 @@ def dataset_split_get(
|
|
|
1673
1750
|
|
|
1674
1751
|
if resolved_version:
|
|
1675
1752
|
detail = _fetch_version(
|
|
1676
|
-
dataset,
|
|
1753
|
+
dataset,
|
|
1754
|
+
resolved_version,
|
|
1755
|
+
stage="split 查询",
|
|
1756
|
+
environment=environment,
|
|
1757
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1677
1758
|
)
|
|
1678
1759
|
for record in _split_records(detail):
|
|
1679
1760
|
if str(record.get("name") or "") == split:
|
|
@@ -1738,20 +1819,22 @@ def dataset_split_create(
|
|
|
1738
1819
|
)
|
|
1739
1820
|
|
|
1740
1821
|
detail = _fetch_version(
|
|
1741
|
-
dataset,
|
|
1822
|
+
dataset,
|
|
1823
|
+
resolved_version,
|
|
1824
|
+
stage="split 创建前查询",
|
|
1825
|
+
environment=environment,
|
|
1826
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1742
1827
|
)
|
|
1743
1828
|
|
|
1744
1829
|
existing = _split_names(detail)
|
|
1745
1830
|
if split in existing:
|
|
1746
1831
|
# 幂等:已存在同名 split(设计 §4.3)。
|
|
1747
|
-
_note(f"dataset {dataset} 版本 {_version_label(resolved_version)} 已有 split {split}")
|
|
1748
|
-
if resolved_run_type:
|
|
1749
|
-
_note(f"要改 run_type 请用:ap dataset split update {split} --dataset {dataset}")
|
|
1750
1832
|
for record in _split_records(detail):
|
|
1751
1833
|
if str(record.get("name") or "") == split:
|
|
1752
1834
|
payload = dict(record)
|
|
1753
1835
|
payload.setdefault("dataset", dataset)
|
|
1754
1836
|
payload.setdefault("version", resolved_version)
|
|
1837
|
+
payload["created"] = False
|
|
1755
1838
|
_emit_detail(payload, output_format)
|
|
1756
1839
|
return
|
|
1757
1840
|
_emit_detail(
|
|
@@ -1768,6 +1851,7 @@ def dataset_split_create(
|
|
|
1768
1851
|
{"splits": names},
|
|
1769
1852
|
stage="split 创建",
|
|
1770
1853
|
environment=environment,
|
|
1854
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1771
1855
|
)
|
|
1772
1856
|
|
|
1773
1857
|
# ② run_type 只能第二次 PATCH:服务端 splits 与 split_run_types 互斥。
|
|
@@ -1785,6 +1869,7 @@ def dataset_split_create(
|
|
|
1785
1869
|
# 第一段 PATCH 已建出 split,失败后的恢复动作是补打 run_type,不是重跑 create。
|
|
1786
1870
|
fallback_hint=f"split {split} 已创建但 run_type 未设置,补打:{retry}",
|
|
1787
1871
|
environment=environment,
|
|
1872
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1788
1873
|
)
|
|
1789
1874
|
|
|
1790
1875
|
_emit_detail(
|
|
@@ -1872,14 +1957,16 @@ def dataset_split_update(
|
|
|
1872
1957
|
return
|
|
1873
1958
|
|
|
1874
1959
|
detail = _fetch_version(
|
|
1875
|
-
dataset,
|
|
1960
|
+
dataset,
|
|
1961
|
+
resolved_version,
|
|
1962
|
+
stage="split 更新前查询",
|
|
1963
|
+
environment=environment,
|
|
1964
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1876
1965
|
)
|
|
1877
1966
|
|
|
1878
1967
|
if split not in _split_names(detail):
|
|
1879
1968
|
known = ", ".join(_split_names(detail)) or "<无>"
|
|
1880
1969
|
hint = f"先创建:ap dataset split create {split} --dataset {dataset}(现有:{known})"
|
|
1881
|
-
if not resolved_version:
|
|
1882
|
-
hint = f"{hint};{_NO_VERSION_NOTE}"
|
|
1883
1970
|
raise _fail(
|
|
1884
1971
|
f"dataset {dataset} 版本 {_version_label(resolved_version)} split {split} "
|
|
1885
1972
|
f"更新失败:不存在",
|
|
@@ -1892,6 +1979,7 @@ def dataset_split_update(
|
|
|
1892
1979
|
{"split_run_types": [{"name": split, "run_type": resolved_run_type}]},
|
|
1893
1980
|
stage="split 更新",
|
|
1894
1981
|
environment=environment,
|
|
1982
|
+
metadata_model=_VERSION_SPLIT_METADATA_MODEL,
|
|
1895
1983
|
)
|
|
1896
1984
|
_emit_detail(
|
|
1897
1985
|
{
|
|
@@ -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
|
|
|
@@ -120,11 +120,6 @@ def _fail(message: str, hint: str, exit_code: int = 1) -> "typer.Exit":
|
|
|
120
120
|
return typer.Exit(exit_code)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def _note(message: str) -> None:
|
|
124
|
-
"""退出码 0 的路径只用 ``note:``(设计 §5.1);一律 stderr。"""
|
|
125
|
-
typer.echo(f"note: {message}", err=True)
|
|
126
|
-
|
|
127
|
-
|
|
128
123
|
def _load_repo_or_exit():
|
|
129
124
|
"""懒加载 SDK;缺 extra → 安装提示 + 退出码 2(设计 §2)。"""
|
|
130
125
|
from .irepo_sdk import SdkMissing, load_repo
|
|
@@ -136,6 +131,25 @@ def _load_repo_or_exit():
|
|
|
136
131
|
raise typer.Exit(2) from exc
|
|
137
132
|
|
|
138
133
|
|
|
134
|
+
def _probe_pull_private_endpoint(repo: Any, dataset: str) -> bool | None:
|
|
135
|
+
content = getattr(repo, "_content", None)
|
|
136
|
+
instances = getattr(repo, "instances", None)
|
|
137
|
+
credentials = getattr(content, "_credentials", None)
|
|
138
|
+
probe = getattr(content, "_probe_private_endpoint", None)
|
|
139
|
+
layout_factory = getattr(instances, "_layout", None)
|
|
140
|
+
if not all(callable(value) for value in (credentials, probe, layout_factory)):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
from instance_repo._routing import _plan_oss_route
|
|
144
|
+
|
|
145
|
+
layout = layout_factory()
|
|
146
|
+
sts = credentials(layout.sts_prefix(dataset), dataset)
|
|
147
|
+
route = _plan_oss_route(sts.get("endpoint", ""))
|
|
148
|
+
if not route.standard:
|
|
149
|
+
return None
|
|
150
|
+
return bool(probe(route.private_endpoint))
|
|
151
|
+
|
|
152
|
+
|
|
139
153
|
#: SDK ``instance_repo.validate.detect_format`` 的取值 → 对应校验函数名。分派必须与 SDK
|
|
140
154
|
#: ``InstancesClient.validate`` 一致;认不出的格式回退 ``check_layout``(与 SDK 同样的兜底)。
|
|
141
155
|
_VALIDATOR_BY_FORMAT: dict[str, str] = {
|
|
@@ -217,12 +231,7 @@ def _describe(exc: BaseException) -> str:
|
|
|
217
231
|
|
|
218
232
|
|
|
219
233
|
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
|
-
"""
|
|
234
|
+
"""按 SDK 能力下发 environment / metadata_model,不允许静默忽略显式值。"""
|
|
226
235
|
fn = getattr(sdk_obj, method, None)
|
|
227
236
|
accepted: set[str] = set()
|
|
228
237
|
if fn is not None:
|
|
@@ -237,14 +246,18 @@ def _sdk_read_kwargs(sdk_obj: Any, method: str, *, environment: str, metadata_mo
|
|
|
237
246
|
accepted.add(name)
|
|
238
247
|
|
|
239
248
|
kwargs: dict[str, str] = {}
|
|
249
|
+
dropped: list[str] = []
|
|
240
250
|
for key, value in (("environment", environment), ("metadata_model", metadata_model)):
|
|
241
251
|
# 空值不下发:CLI 不替 SDK 决定 metadata_model(见 dataset_commands._metadata_model)。
|
|
242
252
|
if key in accepted and value:
|
|
243
253
|
kwargs[key] = value
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
elif value:
|
|
255
|
+
dropped.append(key)
|
|
256
|
+
if dropped:
|
|
257
|
+
raise _fail(
|
|
258
|
+
f"已安装的 instance-repo 不支持按调用指定 {'/'.join(dropped)}",
|
|
259
|
+
"升级到 instance-repo 1.1.0 及以上后重试",
|
|
260
|
+
exit_code=2,
|
|
248
261
|
)
|
|
249
262
|
return kwargs
|
|
250
263
|
|
|
@@ -390,6 +403,12 @@ def instance_list(
|
|
|
390
403
|
output_format = _fmt(output_format)
|
|
391
404
|
resolved_version = _version_value(version)
|
|
392
405
|
repo = _load_repo_or_exit()
|
|
406
|
+
read_kwargs = _sdk_read_kwargs(
|
|
407
|
+
repo.instances,
|
|
408
|
+
"list_paged",
|
|
409
|
+
environment=_environment(environment),
|
|
410
|
+
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
411
|
+
)
|
|
393
412
|
try:
|
|
394
413
|
result = repo.instances.list_paged(
|
|
395
414
|
dataset,
|
|
@@ -397,12 +416,7 @@ def instance_list(
|
|
|
397
416
|
split=split,
|
|
398
417
|
page=page,
|
|
399
418
|
page_size=page_size,
|
|
400
|
-
**
|
|
401
|
-
repo.instances,
|
|
402
|
-
"list_paged",
|
|
403
|
-
environment=_environment(environment),
|
|
404
|
-
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
405
|
-
),
|
|
419
|
+
**read_kwargs,
|
|
406
420
|
)
|
|
407
421
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
408
422
|
raise _fail(
|
|
@@ -441,18 +455,19 @@ def instance_get(
|
|
|
441
455
|
output_format = _fmt(output_format)
|
|
442
456
|
resolved_version = _version_value(version)
|
|
443
457
|
repo = _load_repo_or_exit()
|
|
458
|
+
read_kwargs = _sdk_read_kwargs(
|
|
459
|
+
repo.instances,
|
|
460
|
+
"get",
|
|
461
|
+
environment=_environment(environment),
|
|
462
|
+
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
463
|
+
)
|
|
444
464
|
try:
|
|
445
465
|
record = repo.instances.get(
|
|
446
466
|
dataset,
|
|
447
467
|
resolved_version,
|
|
448
468
|
instance,
|
|
449
469
|
split=split,
|
|
450
|
-
**
|
|
451
|
-
repo.instances,
|
|
452
|
-
"get",
|
|
453
|
-
environment=_environment(environment),
|
|
454
|
-
metadata_model=_metadata_model_for_split(metadata_model, split=split),
|
|
455
|
-
),
|
|
470
|
+
**read_kwargs,
|
|
456
471
|
)
|
|
457
472
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
458
473
|
raise _fail(
|
|
@@ -467,9 +482,12 @@ def instance_get(
|
|
|
467
482
|
_emit_detail(_instance_record(record), output_format)
|
|
468
483
|
|
|
469
484
|
|
|
485
|
+
def _confirm_version_autocreate(repo, dataset: str, version: str, split: str, *, yes: bool) -> None:
|
|
486
|
+
"""version 不存在时 push 会触发服务端自动创建;要求二次确认(可 ``-y/--yes`` 跳过)。
|
|
470
487
|
|
|
471
|
-
|
|
472
|
-
|
|
488
|
+
``repo`` 由调用方构造并复用:``Repo.__init__`` 会做一次 best-effort 的
|
|
489
|
+
repo-config 自动发现(HTTP),确认与推送共用一个实例,不重复触网。
|
|
490
|
+
"""
|
|
473
491
|
import sys
|
|
474
492
|
|
|
475
493
|
from .dataset_commands import _env_kwargs, _environment, _version_label
|
|
@@ -478,7 +496,6 @@ def _confirm_version_autocreate(dataset: str, version: str, split: str, *, yes:
|
|
|
478
496
|
if not str(version or "").strip():
|
|
479
497
|
# 无版本空间:没有"版本会被自动创建"这回事(数据面由 ingest 处理)。
|
|
480
498
|
return
|
|
481
|
-
repo = _load_repo_or_exit()
|
|
482
499
|
try:
|
|
483
500
|
repo.versions.get(dataset, version, **_env_kwargs(_environment(None)))
|
|
484
501
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
@@ -493,7 +510,6 @@ def _confirm_version_autocreate(dataset: str, version: str, split: str, *, yes:
|
|
|
493
510
|
|
|
494
511
|
label = _version_label(version)
|
|
495
512
|
if yes:
|
|
496
|
-
_note(f"版本 {label} 不存在,上传时将由服务端自动创建(已用 --yes 跳过确认)")
|
|
497
513
|
return
|
|
498
514
|
if not sys.stdin.isatty():
|
|
499
515
|
raise _fail(
|
|
@@ -547,13 +563,13 @@ def instance_push(
|
|
|
547
563
|
# ② 版本不存在时 push 会在服务端自动创建;先确认,避免误建。
|
|
548
564
|
# (split 已发布不可写由**服务端**裁决——pre 环境里 ingest 产出的 split 记录状态
|
|
549
565
|
# 就是 published,客户端按它预判会把正常的二次上传全拦掉。)
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
# ③ 数据面推送。
|
|
566
|
+
# ③ 数据面推送(repo 只构造一次,确认与推送复用,见 _confirm_version_autocreate)。
|
|
553
567
|
repo = _load_repo_or_exit()
|
|
568
|
+
_confirm_version_autocreate(repo, dataset, resolved, split, yes=yes)
|
|
554
569
|
pushed: list[dict] = []
|
|
555
570
|
failed: list[dict] = []
|
|
556
571
|
unconfirmed: list[str] = []
|
|
572
|
+
result_count_mismatch = False
|
|
557
573
|
if len(targets) == 1:
|
|
558
574
|
try:
|
|
559
575
|
result = repo.instances.push(
|
|
@@ -603,10 +619,19 @@ def instance_push(
|
|
|
603
619
|
exc, what=f"dataset {dataset} 的 {len(targets)} 个 instance", stage="上传"
|
|
604
620
|
)
|
|
605
621
|
else:
|
|
606
|
-
|
|
622
|
+
results_list = list(results_many or [])
|
|
623
|
+
result_count_mismatch = len(results_list) != len(targets)
|
|
624
|
+
if len(results_list) < len(targets):
|
|
625
|
+
unconfirmed.extend(targets[len(results_list) :])
|
|
626
|
+
for path, result in zip(targets, results_list):
|
|
607
627
|
pushed.append(_push_row(path, result))
|
|
608
628
|
|
|
609
629
|
_print_push_summary(dataset, resolved, split, pushed, failed, unconfirmed, output_format)
|
|
630
|
+
if result_count_mismatch:
|
|
631
|
+
raise _fail(
|
|
632
|
+
f"SDK 返回了 {len(results_list)}/{len(targets)} 个上传结果,无法确认完整写入状态",
|
|
633
|
+
f"用 ap instance list --dataset {dataset} --split {split} 核对实际内容",
|
|
634
|
+
)
|
|
610
635
|
|
|
611
636
|
|
|
612
637
|
def _push_row(path: str, result: Any) -> dict:
|
|
@@ -679,6 +704,16 @@ def instance_pull(
|
|
|
679
704
|
)
|
|
680
705
|
|
|
681
706
|
repo = _load_repo_or_exit()
|
|
707
|
+
try:
|
|
708
|
+
private_endpoint_reachable = _probe_pull_private_endpoint(repo, dataset)
|
|
709
|
+
except Exception as exc: # noqa: BLE001
|
|
710
|
+
sdk_error_exit(exc, what=f"instance {instance}", stage="拉取前私网探测")
|
|
711
|
+
if private_endpoint_reachable is False:
|
|
712
|
+
raise _fail(
|
|
713
|
+
f"instance {instance} 拉取失败:当前网络无法访问私网 OSS endpoint",
|
|
714
|
+
"instance pull 不支持公网下载;请在 AP dev cluster / ROCK VPC 内执行",
|
|
715
|
+
)
|
|
716
|
+
|
|
682
717
|
try:
|
|
683
718
|
root.mkdir(parents=True, exist_ok=True)
|
|
684
719
|
written = repo.instances.pull(dataset, resolved, instance, root, split)
|
|
@@ -24,24 +24,31 @@
|
|
|
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
|
|
43
44
|
|
|
44
|
-
__all__ = [
|
|
45
|
+
__all__ = [
|
|
46
|
+
"SdkMissing",
|
|
47
|
+
"is_already_satisfied",
|
|
48
|
+
"load_repo",
|
|
49
|
+
"sdk_error_exit",
|
|
50
|
+
"update_version_detail",
|
|
51
|
+
]
|
|
45
52
|
|
|
46
53
|
#: 数据集元数据的环境枚举(服务端 ``dataset environment``)。
|
|
47
54
|
DATASET_ENVIRONMENTS = ("online", "pre")
|
|
@@ -53,14 +60,6 @@ _INSTALL_HINT = (
|
|
|
53
60
|
"Install it with: pip install 'ap-client[dataset]'"
|
|
54
61
|
)
|
|
55
62
|
|
|
56
|
-
#: 旧 SDK(无 ``api_base``/``cluster`` 构造参数)时的升级提示,一行 stderr。
|
|
57
|
-
_LEGACY_SDK_NOTE = (
|
|
58
|
-
"note: installed instance-repo does not accept Repo(api_base=/cluster=); "
|
|
59
|
-
"falling back to INSTANCEREPO_* environment injection. "
|
|
60
|
-
"Upgrade with: pip install -U 'instance-repo[oss]>=1.0.9' "
|
|
61
|
-
"for cluster<->storage_env validation and symmetric read/write paths."
|
|
62
|
-
)
|
|
63
|
-
|
|
64
63
|
# storage_env 解析顺序:显式参数 > 规范环境变量(1.0.0) > 旧别名(0.8.x)。
|
|
65
64
|
# **没有默认值**——猜一个 storage_env 会把数据写到错误的存储环境。
|
|
66
65
|
_STORAGE_ENV_VARS = ("INSTANCE_REPO_STORAGE_ENV", "IR_STORAGE_ENV")
|
|
@@ -122,6 +121,97 @@ def _setdefault_env(name: str, value: str) -> None:
|
|
|
122
121
|
os.environ[name] = value
|
|
123
122
|
|
|
124
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
|
+
|
|
125
215
|
def load_repo(*, cluster: str | None = None, storage_env: str | None = None) -> "Repo":
|
|
126
216
|
"""按 AP ``Config`` 构造 ``instance_repo.Repo``。
|
|
127
217
|
|
|
@@ -144,7 +234,10 @@ def load_repo(*, cluster: str | None = None, storage_env: str | None = None) ->
|
|
|
144
234
|
|
|
145
235
|
# 多 key 的 AP_API_KEYS 在这里直接抛 ConfigurationError:数据面要用这把 key 去
|
|
146
236
|
# 换 STS,拿不到确定的单 key 就没法继续(与控制面同一口径)。
|
|
147
|
-
cfg =
|
|
237
|
+
cfg = get_config(
|
|
238
|
+
verbose=_api._verbose_override,
|
|
239
|
+
cluster=cluster if cluster is not None else _api._cluster_override,
|
|
240
|
+
)
|
|
148
241
|
|
|
149
242
|
effective_cluster = _cluster_from_headers(cfg.headers)
|
|
150
243
|
effective_storage_env = _resolve_storage_env(storage_env)
|
|
@@ -162,13 +255,16 @@ def load_repo(*, cluster: str | None = None, storage_env: str | None = None) ->
|
|
|
162
255
|
kwargs["storage_env"] = effective_storage_env
|
|
163
256
|
if "api_env" in supported and api_env:
|
|
164
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
|
|
165
262
|
|
|
166
263
|
if "api_base" not in supported or "cluster" not in supported:
|
|
167
264
|
# 0.8.x 兼容路径:构造参数缺位,只能退回环境变量契约。
|
|
168
265
|
_setdefault_env(_LEGACY_API_BASE_VAR, cfg.base_url)
|
|
169
266
|
_setdefault_env(_LEGACY_TOKEN_VAR, cfg.token_key or "")
|
|
170
267
|
_setdefault_env(_LEGACY_STORAGE_ENV_VAR, effective_storage_env)
|
|
171
|
-
typer.echo(_LEGACY_SDK_NOTE, err=True)
|
|
172
268
|
|
|
173
269
|
return _Repo(**kwargs)
|
|
174
270
|
|
|
@@ -185,7 +281,9 @@ def _init_parameters(repo_cls: type) -> frozenset[str]:
|
|
|
185
281
|
continue
|
|
186
282
|
if parameter.kind is inspect.Parameter.VAR_KEYWORD:
|
|
187
283
|
# **kwargs 会吞下任何参数,当作全部支持(1.0.0 之后的宽松签名)。
|
|
188
|
-
return frozenset(
|
|
284
|
+
return frozenset(
|
|
285
|
+
{"token", "api_base", "cluster", "storage_env", "api_env", "transport"}
|
|
286
|
+
)
|
|
189
287
|
if parameter.kind is not inspect.Parameter.VAR_POSITIONAL:
|
|
190
288
|
names.add(name)
|
|
191
289
|
return frozenset(names)
|
|
@@ -210,7 +308,7 @@ _BIZ_CODE_RULES: dict[int, tuple[str, int]] = {
|
|
|
210
308
|
),
|
|
211
309
|
94018: ("当前角色只读,写操作需要 writer 角色;让 admin 执行 ap dataset access grant", 1),
|
|
212
310
|
94019: ("dataset 标识冲突:检查 --dataset 的取值是否与已有 dataset 重名", 1),
|
|
213
|
-
94020: ("请求未带 dataset 标识(SDK 过旧),升级:pip install -U 'instance-repo[oss]>=1.0
|
|
311
|
+
94020: ("请求未带 dataset 标识(SDK 过旧),升级:pip install -U 'instance-repo[oss]>=1.1.0'", 1),
|
|
214
312
|
94021: ("该 dataset 不存在,先执行:ap dataset create <dataset> --benchmark <b>", 1),
|
|
215
313
|
94022: (
|
|
216
314
|
"无该用户存储空间的授权,让空间主人执行:ap fs access grant --user <你的工号> --role reader",
|
|
@@ -232,7 +330,7 @@ _CODE_RULES: dict[str, tuple[str, int]] = {
|
|
|
232
330
|
"E_DIGEST_MISMATCH": ("内容校验失败,重新拉取或重新打包该 instance 后再试", 1),
|
|
233
331
|
"E_IMMUTABLE": ("已发布的版本不可修改,创建新版本再操作", 1),
|
|
234
332
|
"E_FORBIDDEN": ("当前身份无权执行该操作,确认 AP_API_KEY 与 dataset 角色", 1),
|
|
235
|
-
"E_OWNER_REF_MISSING": ("缺少上游来源引用,升级:pip install -U 'instance-repo[oss]>=1.0
|
|
333
|
+
"E_OWNER_REF_MISSING": ("缺少上游来源引用,升级:pip install -U 'instance-repo[oss]>=1.1.0'", 1),
|
|
236
334
|
"E_LAYOUT": ("instance 目录结构不合规,先执行 ap instance validate <path> 定位问题", 1),
|
|
237
335
|
"E_SCHEMA": ("instance 元数据不符合 schema,先执行 ap instance validate <path> 定位问题", 1),
|
|
238
336
|
"E_CRED_EXPIRED": ("临时凭据已过期,重试该命令即可重新换取", 1),
|
|
@@ -253,6 +351,7 @@ _ADDRESSING_FIELDS = (
|
|
|
253
351
|
"oss_prefix",
|
|
254
352
|
"scaffold_bucket",
|
|
255
353
|
"scaffold_root",
|
|
354
|
+
"storage_path",
|
|
256
355
|
)
|
|
257
356
|
_ADDRESSING_HINT = (
|
|
258
357
|
"当前 AP 集群未下发数据面存储配置,确认 AP_CLUSTER 指向支持 dataset 的集群,或联系 Dataset 管理员"
|
|
@@ -291,24 +390,20 @@ def update_version_detail(
|
|
|
291
390
|
body: dict,
|
|
292
391
|
*,
|
|
293
392
|
environment: str,
|
|
393
|
+
metadata_model: str = "",
|
|
294
394
|
) -> None:
|
|
295
|
-
"""``PATCH /apis/v1/datasets/versions/detail
|
|
296
|
-
|
|
297
|
-
为什么不用 ``repo.versions.update``(临时 shim,已反馈 SDK):它内部用
|
|
298
|
-
``_version_extra_q()`` **无参**调用,只能下发 profile 派生值(dev/staging 集群
|
|
299
|
-
→ ``pre``);而 CLI 的读路径与 ``versions.create`` 都在 ``online``,于是出现
|
|
300
|
-
"``version get`` 找得到、PATCH 却 404 dataset version not found"。SDK 的
|
|
301
|
-
``get``/``list_paged``/``create``/``ingest`` 都接受显式 ``environment``,
|
|
302
|
-
只有 ``update``/``status`` 没有。SDK 补上参数后这里会自动改走 SDK 方法。
|
|
303
|
-
"""
|
|
395
|
+
"""``PATCH /apis/v1/datasets/versions/detail`` with explicit routing selectors."""
|
|
304
396
|
update = repo.versions.update
|
|
305
397
|
try:
|
|
306
398
|
accepted = inspect.signature(update).parameters
|
|
307
399
|
except (TypeError, ValueError): # pragma: no cover - 内建/装饰过的可调用
|
|
308
400
|
accepted = {}
|
|
309
|
-
if "environment" in accepted:
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
312
407
|
update(dataset, version, **kwargs)
|
|
313
408
|
return
|
|
314
409
|
|
|
@@ -317,6 +412,8 @@ def update_version_detail(
|
|
|
317
412
|
query = f"dataset_name={quote(dataset, safe='')}&version={quote(version, safe='')}"
|
|
318
413
|
if environment:
|
|
319
414
|
query += f"&environment={quote(environment, safe='')}"
|
|
415
|
+
if metadata_model:
|
|
416
|
+
query += f"&metadata_model={quote(metadata_model, safe='')}"
|
|
320
417
|
repo.transport.patch(f"/apis/v1/datasets/versions/detail?{query}", dict(body))
|
|
321
418
|
|
|
322
419
|
|
|
@@ -343,23 +440,17 @@ def _resolve_rule(exc: Exception) -> tuple[str, int]:
|
|
|
343
440
|
|
|
344
441
|
|
|
345
442
|
def sdk_error_exit(exc: Exception, *, what: str, stage: str) -> NoReturn:
|
|
346
|
-
"""把 SDK
|
|
347
|
-
|
|
348
|
-
非零退出走 ``error: <what> <stage>失败:<reason>``;设计 §5.1 里判定为幂等
|
|
349
|
-
(24401/24404,已达目标态)的码退出 0,此时首行改用 ``note:``——把"成功"
|
|
350
|
-
印成 ``error:`` 会让脚本与人都误判。第二行恒为 ``hint: <可执行下一步>``。
|
|
443
|
+
"""把 SDK 异常翻译成 ``error:`` / ``hint:`` 文案与 ``typer.Exit``。
|
|
351
444
|
|
|
352
|
-
|
|
445
|
+
设计 §5.1 里判定为幂等的 24401/24404 直接以 0 退出;非零失败输出固定
|
|
446
|
+
``error: <what> <stage>失败:<reason>`` 与可执行 ``hint:``。
|
|
353
447
|
"""
|
|
354
448
|
hint, exit_code = _resolve_rule(exc)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
typer.echo(f"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
typer.echo(f"hint: {hint}", err=True)
|
|
361
|
-
if _is_verbose():
|
|
362
|
-
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
|
|
363
454
|
|
|
364
|
-
|
|
455
|
+
traceback.print_exception(type(exc), exc, exc.__traceback__)
|
|
365
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")
|
|
@@ -44,7 +44,10 @@ def dataset_split_publish(
|
|
|
44
44
|
if not yes:
|
|
45
45
|
if not sys.stdin.isatty():
|
|
46
46
|
raise _fail("非交互发布需要确认", "加 --yes 确认将该 split 发布到 online", 2)
|
|
47
|
-
|
|
47
|
+
target = (
|
|
48
|
+
f"{dataset}/{resolved_version}/{split}" if resolved_version else f"{dataset}/{split}"
|
|
49
|
+
)
|
|
50
|
+
if not typer.confirm(f"Publish {target} to online?"):
|
|
48
51
|
raise _fail("发布已取消,未提交工作流", "确认后重跑", 1)
|
|
49
52
|
|
|
50
53
|
try:
|
|
@@ -90,7 +93,6 @@ def dataset_split_publish(
|
|
|
90
93
|
if not workflow_id:
|
|
91
94
|
raise _fail("发布响应缺少 workflow id", "重跑同一命令可安全重试")
|
|
92
95
|
hint = f"ap dataset split publish-status {workflow_id}"
|
|
93
|
-
_note(f"发布工作流:{workflow_id};查询进度:{hint}")
|
|
94
96
|
_emit_detail(workflow, fmt)
|
|
95
97
|
|
|
96
98
|
status = str(workflow.get("status") or "")
|
|
@@ -104,8 +106,6 @@ def dataset_split_publish(
|
|
|
104
106
|
if status not in _TERMINAL:
|
|
105
107
|
# 等待窗口内未到终态:SDK 返回最后一帧,服务端工作流不受影响。
|
|
106
108
|
raise _fail("发布等待超时,服务端工作流仍继续运行", hint)
|
|
107
|
-
if status == "awaiting_approval":
|
|
108
|
-
_note("发布工作流等待审批,尚未完成发布;审批信息见 approval 字段")
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
def dataset_split_publish_status(
|
|
@@ -129,5 +129,3 @@ def dataset_split_publish_status(
|
|
|
129
129
|
f"发布工作流 {workflow_id} {status}:{(workflow or {}).get('error_msg') or ''}",
|
|
130
130
|
"检查输出中的 steps 和 outputs,修正失败原因后再提交发布",
|
|
131
131
|
)
|
|
132
|
-
if status == "awaiting_approval":
|
|
133
|
-
_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.dev0"
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|