ap-client 0.5.0.dev0__tar.gz → 0.5.1.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.5.0.dev0
3
+ Version: 0.5.1.dev0
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]<2.0.0,>=1.1.4; extra == 'all'
12
+ Requires-Dist: instance-repo[oss]==1.3.2.dev0; extra == 'all'
13
13
  Provides-Extra: dataset
14
- Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.4; extra == 'dataset'
14
+ Requires-Dist: instance-repo[oss]==1.3.2.dev0; 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.
@@ -63,9 +63,7 @@ def _fmt(output_format: Optional[str]) -> str:
63
63
  def _check_time_range(value: str) -> str:
64
64
  normalized = str(value or "").strip().lower()
65
65
  if not _TIME_RANGE_RE.fullmatch(normalized):
66
- raise typer.BadParameter(
67
- f"--time-range must be a duration like 30s/5m/1h, got {value!r}"
68
- )
66
+ raise typer.BadParameter(f"--time-range must be a duration like 30s/5m/1h, got {value!r}")
69
67
  return normalized
70
68
 
71
69
 
@@ -123,14 +123,6 @@ _UNSET: Any = object()
123
123
  # when aggregation runs later.
124
124
  EXTRA_AGG_OPS = ("avg", "sum")
125
125
 
126
- # Others 占位:apiserver 词表不返回,CLI 展示与 get_meta_model("Others") 兜底用。
127
- _OTHERS_MODEL = {
128
- "name": "Others",
129
- "series_name": "",
130
- "source_id": "",
131
- "series_id": "",
132
- }
133
-
134
126
 
135
127
  def _merge_meta_tags(
136
128
  tags: Optional[list],
@@ -976,18 +968,19 @@ class APIClient:
976
968
  # `dataset versions/instances`、`job create --dataset`)。
977
969
 
978
970
  def list_meta_models(self) -> dict:
979
- """模型词表(apiserver 权威源 GET /apis/v1/meta-models,返回 {items:[...]})。"""
980
- return self._get_central("/meta-models")
971
+ """从 meta-model 标签目录获取可用模型,保留 list 的 items 响应形状。"""
972
+ items = self.get_meta_tags("meta-model").get("items") or []
973
+ return {
974
+ "items": [
975
+ {**item, "name": item["value"]}
976
+ for item in items
977
+ if item.get("value") and item.get("is_enabled", True)
978
+ ]
979
+ }
981
980
 
982
981
  def get_meta_model(self, name: str) -> dict:
983
- """单个模型详情(按 name 或 source_id 从 apiserver list 过滤;Others 返回占位)。
984
-
985
- apiserver 没有单模型详情接口,客户端从 list 结果过滤;找不到 raise
986
- APIError 404,与原 cluster AP /meta/models/{name} 行为一致。
987
- """
988
- if name == "Others":
989
- return dict(_OTHERS_MODEL)
990
- result = self._get_central("/meta-models")
982
+ """从启用的 meta-model 标签值中按 value 或 source_id 查找。"""
983
+ result = self.list_meta_models()
991
984
  for item in result.get("items") or []:
992
985
  if item.get("name") == name or item.get("source_id") == name:
993
986
  return item
@@ -1053,43 +1046,13 @@ class APIClient:
1053
1046
  return result
1054
1047
 
1055
1048
  def list_meta_job_types(self) -> dict:
1056
- """任务类型词表(目录驱动:GET /apis/v1/meta-tags?key=meta-job-type)。
1057
-
1058
- 仅取目录中 ``is_enabled`` 的值(缺失按启用解析)+ 末尾 ``Others``
1059
- 兜底行;``default`` 为 ``Eval-Eval``(在目录中时,否则空串)。
1060
- 目录请求失败或为空 → 回退本地常量(Eval-Eval + Others,fail-open),
1061
- 并在 stderr 打一行告警,词表降级可见但不阻塞命令。
1062
- """
1063
- error: Exception | None = None
1064
- try:
1065
- items = self.get_meta_tags("meta-job-type").get("items") or []
1066
- except Exception as exc: # fail-open:目录不可用不阻塞词表命令
1067
- error = exc
1068
- items = []
1069
- if items:
1070
- options = []
1071
- for item in items:
1072
- value = str(item.get("value") or "").strip()
1073
- if value and item.get("is_enabled", True):
1074
- options.append({"value": value})
1075
- if options:
1076
- default = "Eval-Eval" if any(o["value"] == "Eval-Eval" for o in options) else ""
1077
- return {
1078
- "meta_job_type": {
1079
- "default": default,
1080
- "options": [*options, {"value": "Others"}],
1081
- }
1082
- }
1083
- _write_stderr(
1084
- [
1085
- "warning: meta-job-type catalog unavailable or empty, "
1086
- f"falling back to local constants (reason={error or 'empty catalog'})"
1087
- ]
1088
- )
1049
+ """从 meta-job-type 标签目录获取启用的任务类型,不补本地展示值。"""
1050
+ items = self.get_meta_tags("meta-job-type").get("items") or []
1089
1051
  return {
1090
1052
  "meta_job_type": {
1091
- "default": "Eval-Eval",
1092
- "options": [{"value": "Eval-Eval"}, {"value": "Others"}],
1053
+ "options": [
1054
+ item for item in items if item.get("value") and item.get("is_enabled", True)
1055
+ ],
1093
1056
  }
1094
1057
  }
1095
1058
 
@@ -1283,6 +1246,7 @@ class APIClient:
1283
1246
  benchmark: Optional[str] = None,
1284
1247
  experiment: Optional[str] = None,
1285
1248
  ak_id: Optional[str] = None,
1249
+ ak_mode: str = "auto",
1286
1250
  instance_range: Optional[str] = None,
1287
1251
  profile: Optional[str] = None,
1288
1252
  ) -> dict:
@@ -1334,6 +1298,7 @@ class APIClient:
1334
1298
  benchmark=benchmark,
1335
1299
  experiment=experiment,
1336
1300
  ak_id=ak_id,
1301
+ ak_mode=ak_mode,
1337
1302
  )
1338
1303
 
1339
1304
  result = self._post("/jobs", body, timeout=timeout)
@@ -1395,6 +1360,7 @@ class APIClient:
1395
1360
  benchmark: Optional[str] = None,
1396
1361
  experiment: Optional[str] = None,
1397
1362
  ak_id: Optional[str] = None,
1363
+ ak_mode: str = "auto",
1398
1364
  instance_range: Optional[str] = None,
1399
1365
  profile: Optional[str] = None,
1400
1366
  ) -> dict:
@@ -1410,11 +1376,15 @@ class APIClient:
1410
1376
  if not template and not profile_id and profile is None:
1411
1377
  raise ValueError("Either template, profile or profile_id is required")
1412
1378
  validate_ak_selection(ak_id, params, params_list, overrides)
1379
+ if ak_mode not in {"auto", "manual", "synced"}:
1380
+ raise ValueError("ak_mode must be auto, manual or synced")
1413
1381
  body: dict = {}
1414
1382
  if template is not None:
1415
1383
  body["template"] = template
1416
1384
  if ak_id is not None:
1417
1385
  body["ak_id"] = ak_id
1386
+ if ak_mode != "auto":
1387
+ body["ak_mode"] = ak_mode
1418
1388
 
1419
1389
  if params is not None:
1420
1390
  body["params"] = params
@@ -15,7 +15,6 @@ import typer
15
15
  from ap_client import __version__, get_client, get_config, tbb
16
16
  from ap_client.akpool_commands import register as _register_akpool_commands
17
17
  from ap_client.api import (
18
- _OTHERS_MODEL,
19
18
  APIError,
20
19
  PaginationProtocolError,
21
20
  _is_sensitive_key,
@@ -267,12 +266,15 @@ def _build_job_create_retry_command(
267
266
  resource_profile_id: Optional[str] = None,
268
267
  timeout: Optional[float] = None,
269
268
  ak_id: Optional[str] = None,
269
+ ak_mode: str = "auto",
270
270
  ) -> str:
271
271
  args = ["ap", "job", "create"]
272
272
  if template is not None:
273
273
  args.append(template)
274
274
  if ak_id is not None:
275
275
  args.extend(["--ak-id", ak_id])
276
+ if ak_mode != "auto":
277
+ args.extend(["--ak-mode", ak_mode])
276
278
  if profile is not None:
277
279
  args.extend(["--profile", profile])
278
280
  if resource_profile_id:
@@ -1240,6 +1242,7 @@ def _idempotency_item_key(
1240
1242
  resource_profile_id: Optional[str] = None,
1241
1243
  max_failure_retries: int = 0,
1242
1244
  ak_id: Optional[str] = None,
1245
+ ak_mode: str = "auto",
1243
1246
  ) -> str:
1244
1247
  # Client-supplied item keys are authoritative; server hashing is only a
1245
1248
  # fallback for non-CLI callers, so this payload intentionally need not
@@ -1259,6 +1262,8 @@ def _idempotency_item_key(
1259
1262
  }
1260
1263
  if ak_id is not None:
1261
1264
  payload_data["ak_id"] = ak_id
1265
+ if ak_mode != "auto":
1266
+ payload_data["ak_mode"] = ak_mode
1262
1267
  if resource_profile_id is not None:
1263
1268
  payload_data["resource_profile_id"] = resource_profile_id
1264
1269
  if max_failure_retries:
@@ -2683,23 +2688,22 @@ def meta_model_list(
2683
2688
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2684
2689
  ),
2685
2690
  ):
2686
- """List meta model options (apiserver data + Others as the last row)."""
2691
+ """List enabled meta-model tag values."""
2687
2692
  output_format = _normalize_output_format(output_format, keep_table=True)
2688
2693
  result = get_client().list_meta_models()
2689
2694
  items = result.get("items") or []
2690
- options = [*items, dict(_OTHERS_MODEL)] # 末尾固定追加 Others 兜底手动输入
2695
+ options = items
2691
2696
  if output_format in ("plain", "table"):
2692
2697
  rows = [
2693
2698
  {
2694
2699
  "name": m.get("name") or "-",
2695
- "series": m.get("series_name") or "-",
2700
+ "desc": m.get("desc_l1") or "-",
2696
2701
  }
2697
2702
  for m in options
2698
2703
  ]
2699
- _print_plain_table(rows, [("name", "name"), ("series", "series")])
2704
+ _print_plain_table(rows, [("name", "name"), ("desc", "desc")])
2700
2705
  else:
2701
- # json/yaml 包装成 {meta_model:{options}}(原 cluster AP 格式;
2702
- # default 已随 meta-models default 逻辑移除而下线)
2706
+ # JSON/YAML 保持 {meta_model:{options}};每项保留目录字段及 name 别名。
2703
2707
  _print_formatted({"meta_model": {"options": options}}, output_format)
2704
2708
 
2705
2709
 
@@ -2790,21 +2794,20 @@ def meta_job_type_list(
2790
2794
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2791
2795
  ),
2792
2796
  ):
2793
- """List meta job type options (catalog-driven; falls back to Eval-Eval/Others)."""
2797
+ """List enabled meta-job-type tag values."""
2794
2798
  output_format = _normalize_output_format(output_format, keep_table=True)
2795
2799
  result = get_client().list_meta_job_types()
2796
2800
  info = result.get("meta_job_type") or {}
2797
2801
  options = info.get("options") or []
2798
- default = info.get("default") or ""
2799
2802
  if output_format in ("plain", "table"):
2800
2803
  rows = [
2801
2804
  {
2802
2805
  "value": o.get("value") or "-",
2803
- "default": "*" if o.get("value") and o.get("value") == default else "",
2806
+ "desc": o.get("desc_l1") or "-",
2804
2807
  }
2805
2808
  for o in options
2806
2809
  ]
2807
- _print_plain_table(rows, [("value", "value"), ("default", "default")])
2810
+ _print_plain_table(rows, [("value", "value"), ("desc", "desc")])
2808
2811
  else:
2809
2812
  _print_formatted(result, output_format)
2810
2813
 
@@ -2990,6 +2993,9 @@ def job_create(
2990
2993
  ak_id: Optional[str] = typer.Option(
2991
2994
  None, "--ak-id", help="AP-managed AK ID; requires server support"
2992
2995
  ),
2996
+ ak_mode: str = typer.Option(
2997
+ "auto", "--ak-mode", help="AK credential source: auto, manual or synced"
2998
+ ),
2993
2999
  agenthub_ref: Optional[str] = typer.Option(
2994
3000
  None, "--agenthub-ref", help="Agent-Hub branch/commit; overrides AP_AGENTHUB_REF"
2995
3001
  ),
@@ -3332,6 +3338,8 @@ def job_create(
3332
3338
  output_format = _normalize_output_format(output_format)
3333
3339
  if not template and not profile:
3334
3340
  raise typer.BadParameter("TEMPLATE is required unless --profile is provided")
3341
+ if ak_mode not in {"auto", "manual", "synced"}:
3342
+ raise typer.BadParameter("Choose auto, manual or synced", param_hint="--ak-mode")
3335
3343
  try:
3336
3344
  validate_ak_id(ak_id)
3337
3345
  resource_profile_id = _normalize_resource_profile_id(resource_profile_id)
@@ -3528,6 +3536,7 @@ def job_create(
3528
3536
  profile=profile,
3529
3537
  resource_profile_id=resource_profile_id,
3530
3538
  **({"ak_id": ak_id} if ak_id is not None else {}),
3539
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3531
3540
  instances=instances_list,
3532
3541
  instance_range=instance_range,
3533
3542
  dataset=dataset,
@@ -3601,6 +3610,7 @@ def job_create(
3601
3610
  profile=profile,
3602
3611
  resource_profile_id=resource_profile_id,
3603
3612
  **({"ak_id": ak_id} if ak_id is not None else {}),
3613
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3604
3614
  timeout=timeout,
3605
3615
  )
3606
3616
  if idempotency_enabled and submission_idempotency_key is not None
@@ -3645,6 +3655,7 @@ def job_create(
3645
3655
  profile=profile,
3646
3656
  resource_profile_id=resource_profile_id,
3647
3657
  **({"ak_id": ak_id} if ak_id is not None else {}),
3658
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3648
3659
  instances=instances_list,
3649
3660
  instance_range=instance_range,
3650
3661
  dataset=dataset,
@@ -3860,6 +3871,7 @@ def job_create(
3860
3871
  idempotency_key=submission_idempotency_key,
3861
3872
  resource_profile_id=resource_profile_id,
3862
3873
  **({"ak_id": ak_id} if ak_id is not None else {}),
3874
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3863
3875
  timeout=timeout,
3864
3876
  )
3865
3877
  if not dry_run
@@ -3956,6 +3968,7 @@ def job_create(
3956
3968
  account_pool=account_pool,
3957
3969
  resource_profile_id=resource_profile_id,
3958
3970
  **({"ak_id": ak_id} if ak_id is not None else {}),
3971
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3959
3972
  max_failure_retries=max_failure_retries,
3960
3973
  )
3961
3974
  batch.append(item)
@@ -3989,6 +4002,7 @@ def job_create(
3989
4002
  account_pool=account_pool,
3990
4003
  resource_profile_id=resource_profile_id,
3991
4004
  **({"ak_id": ak_id} if ak_id is not None else {}),
4005
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3992
4006
  checkpoint_id=checkpoint_id,
3993
4007
  checkpoint_path=checkpoint_path,
3994
4008
  checkpoint=checkpoint_dict,
@@ -4032,6 +4046,7 @@ def job_create(
4032
4046
  account_pool=account_pool,
4033
4047
  resource_profile_id=resource_profile_id,
4034
4048
  **({"ak_id": ak_id} if ak_id is not None else {}),
4049
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4035
4050
  checkpoint_id=checkpoint_id,
4036
4051
  checkpoint_path=checkpoint_path,
4037
4052
  checkpoint=checkpoint_dict,
@@ -4089,6 +4104,7 @@ def job_create(
4089
4104
  account_pool=account_pool,
4090
4105
  resource_profile_id=resource_profile_id,
4091
4106
  **({"ak_id": ak_id} if ak_id is not None else {}),
4107
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4092
4108
  checkpoint_id=checkpoint_id,
4093
4109
  checkpoint_path=checkpoint_path,
4094
4110
  checkpoint=checkpoint_dict,
@@ -4195,6 +4211,7 @@ def job_create(
4195
4211
  account_pool=account_pool,
4196
4212
  resource_profile_id=resource_profile_id,
4197
4213
  **({"ak_id": ak_id} if ak_id is not None else {}),
4214
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4198
4215
  checkpoint_id=checkpoint_id,
4199
4216
  checkpoint_path=checkpoint_path,
4200
4217
  checkpoint=checkpoint_dict,
@@ -4232,6 +4249,7 @@ def job_create(
4232
4249
  account_pool=account_pool,
4233
4250
  resource_profile_id=resource_profile_id,
4234
4251
  **({"ak_id": ak_id} if ak_id is not None else {}),
4252
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4235
4253
  checkpoint_id=checkpoint_id,
4236
4254
  checkpoint_path=checkpoint_path,
4237
4255
  checkpoint=checkpoint_dict,
@@ -4279,6 +4297,7 @@ def job_create(
4279
4297
  account_pool=account_pool,
4280
4298
  resource_profile_id=resource_profile_id,
4281
4299
  **({"ak_id": ak_id} if ak_id is not None else {}),
4300
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4282
4301
  checkpoint_id=checkpoint_id,
4283
4302
  checkpoint_path=checkpoint_path,
4284
4303
  checkpoint=checkpoint_dict,
@@ -50,18 +50,15 @@ def register(dataset_app: typer.Typer) -> None:
50
50
 
51
51
 
52
52
  def _register_dataset_commands(dataset_app: typer.Typer) -> None:
53
- """挂载 dataset 级命令(``list`` / ``get`` / ``create`` / ``update`` / ``claim``)。
53
+ """挂载 dataset 级命令。"""
54
+ from .dataset_register import dataset_register
54
55
 
55
- 实现方式:把命令写成本模块的模块级函数,再在这里用
56
- ``dataset_app.command("<name>")(<func>)`` 挂上去。``list`` 需要**替换**
57
- ``cli.py`` 里现存的 ossdata 版本(设计 §4.1),挂载时用同名 ``"list"`` 覆盖,
58
- 并同步删掉 ``cli.py`` 中的旧实现。
59
- """
60
56
  dataset_app.command("list")(dataset_list)
61
57
  dataset_app.command("get")(dataset_get)
62
58
  dataset_app.command("create")(dataset_create)
63
59
  dataset_app.command("update")(dataset_update)
64
60
  dataset_app.command("claim")(dataset_claim)
61
+ dataset_app.command("register")(dataset_register)
65
62
 
66
63
 
67
64
  # ---------------------------------------------------------------------------
@@ -86,6 +83,7 @@ _VERSION_COLUMNS: tuple[tuple[str, str], ...] = (
86
83
  ("splits", "SPLITS"),
87
84
  ("instances", "INSTANCES"),
88
85
  ("updated_at", "UPDATED_AT"),
86
+ ("registered", "REGISTERED"),
89
87
  )
90
88
 
91
89
  #: 上游 §8.2 的默认列(设计 §5)。
@@ -94,6 +92,7 @@ _SPLIT_COLUMNS: tuple[tuple[str, str], ...] = (
94
92
  ("run_type", "RUN_TYPE"),
95
93
  ("instances", "INSTANCES"),
96
94
  ("updated_at", "UPDATED_AT"),
95
+ ("registered", "REGISTERED"),
97
96
  )
98
97
 
99
98
  _ACCESS_COLUMNS: tuple[tuple[str, str], ...] = (
@@ -398,6 +397,7 @@ def _model_dict(record: Any) -> dict:
398
397
  ``{}`` / 缺省都不会出现在响应里),而 SDK 模型会把它们补成默认值;留着这些默认值会让
399
398
  JSON 凭空多出字段(迁移前是原样透出服务端响应,契约必须一致)。
400
399
 
400
+ ``registered`` 是显式注册状态,必须保留 ``false``。
401
401
  只清洗**顶层**:嵌套结构(如 version 详情里的 ``splits[].is_default``)按模型原样输出,
402
402
  服务端在那里确实是显式返回 ``false``。
403
403
  """
@@ -412,13 +412,24 @@ def _model_dict(record: Any) -> dict:
412
412
  return {}
413
413
  if not isinstance(payload, dict):
414
414
  return {}
415
+ registered = getattr(record, "registered", None)
416
+ if isinstance(registered, bool):
417
+ payload = {**payload, "registered": registered}
415
418
  return {
416
419
  key: value
417
420
  for key, value in payload.items()
418
- if not (value is None or value is False or value == "" or value == [] or value == {})
421
+ if value is not None
422
+ and (key == "registered" or not (value is False or value == "" or value == [] or value == {}))
419
423
  }
420
424
 
421
425
 
426
+ def _registered_dict(record: Any) -> dict:
427
+ """Preserve SDK registration state; older metadata-only SDK reads imply true."""
428
+ payload = _model_dict(record)
429
+ payload.setdefault("registered", True)
430
+ return payload
431
+
432
+
422
433
  def _api_biz_code(exc: BaseException) -> Optional[int]:
423
434
  """从 ``APIError`` 里取 apiserver 业务码(设计 §5.1),不 import 数值异常类。
424
435
 
@@ -814,6 +825,7 @@ def _version_row(record: dict) -> dict:
814
825
  "splits": splits,
815
826
  "instances": record.get("instance_count", ""),
816
827
  "updated_at": record.get("updated_at") or "",
828
+ "registered": record.get("registered", True),
817
829
  }
818
830
 
819
831
 
@@ -833,6 +845,7 @@ def _split_row(record: dict, detail: dict) -> dict:
833
845
  "run_type": record.get("run_type") or detail.get("run_type") or "",
834
846
  "instances": "" if instances is None else instances,
835
847
  "updated_at": record.get("updated_at") or detail.get("updated_at") or "",
848
+ "registered": record.get("registered", detail.get("registered", True)),
836
849
  }
837
850
 
838
851
 
@@ -1144,11 +1157,22 @@ def dataset_get(
1144
1157
  try:
1145
1158
  detail = repo.datasets.get_detail(dataset, **_env_kwargs(env))
1146
1159
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
1147
- raise _fail(
1148
- f"Failed to query dataset {dataset}: {_reason(exc)}",
1149
- f"Check the dataset name: ap dataset list --query {dataset}",
1150
- ) from exc
1151
- rendered = _model_dict(detail)
1160
+ if _business_code(exc) not in (92001, 94021):
1161
+ raise _fail(
1162
+ f"Failed to query dataset {dataset}: {_reason(exc)}",
1163
+ f"Check the dataset name: ap dataset list --query {dataset}",
1164
+ ) from exc
1165
+ detail = None
1166
+ if detail is None:
1167
+ l1, _, l2 = dataset.partition("/")
1168
+ rendered = {
1169
+ "dataset_name": dataset,
1170
+ "dataset_l1": l1,
1171
+ "dataset_l2": l2,
1172
+ "registered": False,
1173
+ }
1174
+ else:
1175
+ rendered = _registered_dict(detail)
1152
1176
  _emit_detail(rendered, output_format, order=_DATASET_DETAIL_ORDER, always=("status",))
1153
1177
  if output_format not in ("json", "yaml"):
1154
1178
  _print_dataset_environments(repo, dataset, rendered)
@@ -1642,7 +1666,7 @@ def dataset_version_list(
1642
1666
  f"Check the dataset name: ap dataset list --query {dataset}",
1643
1667
  ) from exc
1644
1668
 
1645
- records = [_model_dict(record) for record in (getattr(result, "items", None) or [])]
1669
+ records = [_registered_dict(record) for record in (getattr(result, "items", None) or [])]
1646
1670
  rows = [_version_row(record) for record in records]
1647
1671
  _emit(
1648
1672
  {"dataset": dataset, "versions": records, "pagination": _pagination_envelope(result)},
@@ -1663,7 +1687,7 @@ def dataset_version_get(
1663
1687
  output_format = _fmt(output_format)
1664
1688
  resolved = _version_value(version)
1665
1689
  detail = _fetch_version(dataset, resolved, stage="query", environment=environment)
1666
- _emit_detail(detail, output_format, order=_VERSION_DETAIL_ORDER)
1690
+ _emit_detail(_registered_dict(detail), output_format, order=_VERSION_DETAIL_ORDER)
1667
1691
 
1668
1692
 
1669
1693
  @dataset_version_app.command("create")
@@ -1810,6 +1834,7 @@ def dataset_split_get(
1810
1834
  payload = dict(record)
1811
1835
  payload.setdefault("dataset", dataset)
1812
1836
  payload.setdefault("version", resolved_version)
1837
+ payload.setdefault("registered", detail.get("registered", True))
1813
1838
  _emit_detail(payload, output_format)
1814
1839
  return
1815
1840
  known = ", ".join(_split_names(detail)) or "<none>"
@@ -1819,6 +1844,7 @@ def dataset_split_get(
1819
1844
  payload = dict(record)
1820
1845
  payload.setdefault("dataset", dataset)
1821
1846
  payload.setdefault("version", "")
1847
+ payload.setdefault("registered", True)
1822
1848
  _emit_detail(payload, output_format)
1823
1849
  return
1824
1850
  known = (
@@ -0,0 +1,320 @@
1
+ """Dataset metadata registration command backed by Instance Repo ingest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import sys
8
+ from typing import Any, List, Optional
9
+ from urllib.parse import quote
10
+
11
+ import typer
12
+
13
+ from . import dataset_commands as datasets
14
+
15
+ _TERMINAL_SUCCESS = {"completed"}
16
+ _TERMINAL_FAILURE = {"failed"}
17
+ _TERMINAL_CANCELED = {"canceled", "cancelled"}
18
+
19
+
20
+ def _payload(outcome: str, dataset: str, result: Any) -> dict:
21
+ return {**datasets._model_dict(result), "outcome": outcome, "dataset": dataset}
22
+
23
+
24
+ def _emit_preview_stderr(payload: dict, output_format: str) -> None:
25
+ typer.echo("Preview:", err=True)
26
+ if output_format == "yaml":
27
+ try:
28
+ import yaml
29
+ except ImportError as exc:
30
+ raise typer.BadParameter("--format yaml requires PyYAML to be installed") from exc
31
+ typer.echo(yaml.safe_dump(payload, allow_unicode=True, sort_keys=False).rstrip(), err=True)
32
+ return
33
+ typer.echo(json.dumps(payload, ensure_ascii=False, indent=2), err=True)
34
+
35
+
36
+ def _confirm(dataset: str, *, yes: bool) -> None:
37
+ if yes:
38
+ return
39
+ if not sys.stdin.isatty():
40
+ raise datasets._fail(
41
+ f"Registering dataset {dataset} requires confirmation after preview",
42
+ "Add --yes (-y) to confirm the registration",
43
+ exit_code=2,
44
+ )
45
+ if not typer.confirm(f"Register the previewed data for dataset {dataset}?", default=False, err=True):
46
+ raise datasets._fail(
47
+ "Registration canceled; nothing was written remotely",
48
+ "Rerun the command and confirm, or add --yes (-y)",
49
+ )
50
+
51
+
52
+ def _claim_without_workspace(
53
+ repo: Any, dataset_id: str, current_user: str, benchmark_id: str
54
+ ) -> dict:
55
+ body = {"owner": current_user}
56
+ if benchmark_id:
57
+ body["benchmark_id"] = benchmark_id
58
+ return repo.transport.post(
59
+ f"/apis/v1/datasets/series/{quote(dataset_id, safe='')}/claim",
60
+ body,
61
+ )
62
+
63
+
64
+ def _create_owned_dataset(repo: Any, dataset: str, benchmark_id: str) -> dict:
65
+ parts = dataset.split("/")
66
+ if len(parts) != 2 or not all(parts):
67
+ raise datasets._fail(
68
+ f"Failed to create dataset {dataset}: the name must be two segments <team>/<name>",
69
+ "Use a dataset name such as your-team/my-bench",
70
+ exit_code=2,
71
+ )
72
+
73
+ from instance_repo.models import DatasetSeries
74
+
75
+ fields: dict[str, Any] = {
76
+ "dataset_name": dataset,
77
+ "dataset_l1": parts[0],
78
+ "dataset_l2": parts[1],
79
+ "visibility": "private",
80
+ }
81
+ if benchmark_id:
82
+ fields["benchmark_id"] = benchmark_id
83
+ record = repo.datasets.create(DatasetSeries(**fields), require_benchmark=False)
84
+ return datasets._model_dict(record)
85
+
86
+
87
+ def _ensure_current_user_owns(
88
+ repo: Any, dataset: str, environment: str, benchmark: Optional[str]
89
+ ) -> None:
90
+ from . import cli
91
+ from .irepo_sdk import _reason
92
+
93
+ try:
94
+ current_user = cli.get_client()._resolve_self_user_id()
95
+ except Exception as exc: # noqa: BLE001 - keep the CLI error contract
96
+ raise datasets._fail(
97
+ f"Failed to resolve the current user: {_reason(exc)}",
98
+ "Check AP_API_KEY and the current cluster, then retry",
99
+ ) from exc
100
+ if not current_user:
101
+ raise datasets._fail(
102
+ "Failed to resolve the current user for --owner @me",
103
+ "Check AP_API_KEY and the current cluster, then retry",
104
+ )
105
+
106
+ missing = False
107
+ try:
108
+ raw_detail = repo.datasets.get_detail(dataset, **datasets._env_kwargs(environment))
109
+ detail = datasets._model_dict(raw_detail)
110
+ missing = raw_detail is None or detail.get("registered") is False
111
+ except Exception as exc: # noqa: BLE001 - dataset absence permits create
112
+ if datasets._business_code(exc) not in (92001, 94021):
113
+ raise datasets._fail(
114
+ f"Failed to inspect ownership for dataset {dataset}: {_reason(exc)}",
115
+ f"Check access with: ap dataset get {dataset}",
116
+ ) from exc
117
+ missing = True
118
+ detail = {}
119
+ if missing:
120
+ benchmark_id = datasets._resolve_benchmark_id(repo, benchmark) if benchmark else ""
121
+ try:
122
+ created = _create_owned_dataset(repo, dataset, benchmark_id)
123
+ except typer.Exit:
124
+ raise
125
+ except Exception as create_exc: # noqa: BLE001 - server remains authoritative
126
+ raise datasets._fail(
127
+ f"Failed to create dataset {dataset}: {_reason(create_exc)}",
128
+ f"Check the name and benchmark, then inspect: ap dataset get {dataset}",
129
+ ) from create_exc
130
+ created_owner = str(created.get("owner") or "").strip()
131
+ if created_owner != current_user:
132
+ shown_owner = created_owner or "<unknown>"
133
+ raise datasets._fail(
134
+ f"Dataset {dataset} was created or claimed concurrently by {shown_owner}",
135
+ "Do not retry with --owner @me; ask the current dataset admin for access",
136
+ )
137
+ return
138
+
139
+ owner = str(detail.get("owner") or "").strip()
140
+ if owner == current_user:
141
+ return
142
+ if owner:
143
+ raise datasets._fail(
144
+ f"Dataset {dataset} is owned by {owner}; --owner @me cannot take it over",
145
+ "Ask the existing dataset admin to run the registration or grant access",
146
+ )
147
+ if not detail.get("claimable", False):
148
+ raise datasets._fail(
149
+ f"Dataset {dataset} cannot be claimed because it already has an admin",
150
+ "Ask an existing dataset admin to run the registration or grant access",
151
+ )
152
+ dataset_id = str(detail.get("dataset_id") or "").strip()
153
+ if not dataset_id:
154
+ raise datasets._fail(
155
+ f"Failed to claim dataset {dataset}: the server returned no dataset_id",
156
+ f"Inspect the dataset with: ap dataset get {dataset}",
157
+ )
158
+ benchmark_id = datasets._resolve_benchmark_id(repo, benchmark) if benchmark else ""
159
+ try:
160
+ _claim_without_workspace(repo, dataset_id, current_user, benchmark_id)
161
+ except Exception as exc: # noqa: BLE001 - preserve SDK transport error mapping
162
+ hint = datasets._DATASET_CODE_HINTS.get(datasets._business_code(exc)) or (
163
+ "Ask an existing dataset admin for access, or verify that the dataset is still claimable"
164
+ )
165
+ raise datasets._fail(
166
+ f"Failed to claim dataset {dataset}: {_reason(exc)}",
167
+ hint,
168
+ ) from exc
169
+
170
+
171
+ def dataset_register(
172
+ dataset: str = typer.Argument(..., help="Dataset name"),
173
+ version: Optional[str] = typer.Option(None, "--version", help=datasets._VERSION_HELP),
174
+ splits: Optional[List[str]] = typer.Option(None, "--split", help="Split to scan; repeatable"),
175
+ unversioned: bool = typer.Option(
176
+ False, "--unversioned", help="Scan the version-less space; mutually exclusive with --version"
177
+ ),
178
+ environment: Optional[str] = typer.Option(
179
+ None, "--environment", help=datasets._ENVIRONMENT_HELP
180
+ ),
181
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview without submitting registration"),
182
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip the registration confirmation"),
183
+ wait: bool = typer.Option(False, "--wait", help="Wait for the ingest task to finish"),
184
+ timeout: float = typer.Option(1800.0, "--timeout", help="Local wait timeout in seconds"),
185
+ output_format: Optional[str] = typer.Option(None, "--format", help=datasets._FORMAT_HELP),
186
+ owner: Optional[str] = typer.Option(
187
+ None, "--owner", help="Ensure ownership before ingest; the only supported value is @me"
188
+ ),
189
+ benchmark: Optional[str] = typer.Option(
190
+ None, "--benchmark", help="Benchmark for create/claim when using --owner @me"
191
+ ),
192
+ ) -> None:
193
+ """Preview and register existing dataset data through an ingest task."""
194
+ from .irepo_sdk import _reason
195
+
196
+ fmt = datasets._fmt(output_format)
197
+ selected_version = str(version or "").strip() or None
198
+ selected_splits = [str(value).strip() for value in (splits or []) if str(value).strip()]
199
+ if selected_version and unversioned:
200
+ raise datasets._fail(
201
+ "--version and --unversioned are mutually exclusive",
202
+ "Choose a version or the version-less space",
203
+ exit_code=2,
204
+ )
205
+ if unversioned and not selected_splits:
206
+ raise datasets._fail(
207
+ "--unversioned requires at least one --split",
208
+ "Repeat --split for each version-less split to register",
209
+ exit_code=2,
210
+ )
211
+ if owner is not None and owner != "@me":
212
+ raise datasets._fail(
213
+ "--owner only supports @me",
214
+ "Use --owner @me or omit --owner to leave ownership unchanged",
215
+ exit_code=2,
216
+ )
217
+ if benchmark and owner != "@me":
218
+ raise datasets._fail(
219
+ "--benchmark requires --owner @me because it is only used during create or claim",
220
+ "Add --owner @me or omit --benchmark",
221
+ exit_code=2,
222
+ )
223
+ if not math.isfinite(timeout) or timeout <= 0:
224
+ raise datasets._fail(
225
+ "--timeout must be a positive finite number",
226
+ "Pass a positive number of seconds",
227
+ exit_code=2,
228
+ )
229
+
230
+ env = datasets._environment(environment)
231
+ repo = datasets._dataset_repo()
232
+ datasets._check_storage_side(repo, env)
233
+ ingest = getattr(getattr(repo, "versions", None), "ingest", None)
234
+ if not callable(ingest):
235
+ raise datasets._fail(
236
+ "The installed Instance Repo SDK does not support dataset ingest",
237
+ "Install or upgrade ap-client[dataset] to a version with versions.ingest",
238
+ exit_code=2,
239
+ )
240
+
241
+ kwargs = {
242
+ "splits": selected_splits or None,
243
+ **datasets._env_kwargs(env),
244
+ "allow_layout_inference": selected_version is None and not selected_splits,
245
+ "dry_run": True,
246
+ "wait": False,
247
+ "timeout_sec": timeout,
248
+ "raise_on_failure": False,
249
+ }
250
+ try:
251
+ preview = datasets._model_dict(ingest(dataset, selected_version, **kwargs))
252
+ except Exception as exc: # noqa: BLE001 - SDK failures use the CLI contract
253
+ raise datasets._fail(
254
+ f"Failed to preview registration for dataset {dataset}: {_reason(exc)}",
255
+ "Check the dataset scope, cluster storage, and environment, then retry",
256
+ ) from exc
257
+
258
+ preview_payload = _payload("preview", dataset, preview)
259
+ if dry_run:
260
+ datasets._emit_detail(preview_payload, fmt)
261
+ return
262
+
263
+ _emit_preview_stderr(preview_payload, fmt)
264
+ _confirm(dataset, yes=yes)
265
+ if owner == "@me":
266
+ _ensure_current_user_owns(repo, dataset, env, benchmark)
267
+
268
+ submission_kwargs = {**kwargs, "dry_run": False, "wait": wait}
269
+ if wait:
270
+ typer.echo(
271
+ f"Waiting up to {timeout:g} seconds for the registration task to finish...",
272
+ err=True,
273
+ )
274
+ try:
275
+ result = datasets._model_dict(ingest(dataset, selected_version, **submission_kwargs))
276
+ except Exception as exc: # noqa: BLE001 - SDK failures use the CLI contract
277
+ if wait:
278
+ failed_job = datasets._model_dict(getattr(exc, "job", None))
279
+ ingest_id = str(failed_job.get("ingest_id") or failed_job.get("id") or "").strip()
280
+ task_hint = f" task {ingest_id}" if ingest_id else " the task"
281
+ raise datasets._fail(
282
+ f"Failed to submit or wait for registration of dataset {dataset}: {_reason(exc)}",
283
+ f"The request may have been accepted and{task_hint} may still be running; "
284
+ "inspect dataset metadata before deciding whether to submit again",
285
+ ) from exc
286
+ raise datasets._fail(
287
+ f"Failed to submit registration for dataset {dataset}: {_reason(exc)}",
288
+ "The preview did not write metadata; inspect the error and retry the command",
289
+ ) from exc
290
+
291
+ status = str(result.get("status") or "").strip().lower()
292
+ if status in _TERMINAL_SUCCESS:
293
+ outcome = "completed"
294
+ elif status in _TERMINAL_FAILURE:
295
+ outcome = "failed"
296
+ elif status in _TERMINAL_CANCELED:
297
+ outcome = "canceled"
298
+ elif wait:
299
+ outcome = "timed_out"
300
+ else:
301
+ outcome = "submitted"
302
+
303
+ datasets._emit_detail(_payload(outcome, dataset, result), fmt)
304
+ ingest_id = str(result.get("ingest_id") or result.get("id") or "").strip()
305
+ if outcome == "failed":
306
+ raise datasets._fail(
307
+ f"Registration task {ingest_id or '<unknown>'} failed: "
308
+ f"{result.get('error_message') or ''}",
309
+ "Fix the reported data or metadata issue, then run the same registration command again",
310
+ )
311
+ if outcome == "canceled":
312
+ raise datasets._fail(
313
+ f"Registration task {ingest_id or '<unknown>'} was canceled",
314
+ "Inspect the dataset metadata before deciding whether to submit again",
315
+ )
316
+ if outcome == "timed_out":
317
+ raise datasets._fail(
318
+ "Registration wait timed out; the background task continues running",
319
+ f"Inspect dataset metadata after task {ingest_id or '<unknown>'} finishes",
320
+ )
@@ -17,7 +17,7 @@
17
17
  错误);SDK 抛出的业务异常 → ``sdk_error_exit(exc, what=..., stage=...)``。
18
18
 
19
19
  渲染与退出码沿用 ``ap_client.cli`` 的既有 helper,同样函数内部 import 以避免与
20
- ``cli`` 循环依赖。默认列(设计 §5):``INSTANCE SPLIT VERSION DIGEST UPDATED_AT``。
20
+ ``cli`` 循环依赖。默认列:``INSTANCE SPLIT VERSION DIGEST UPDATED_AT REGISTERED``。
21
21
  """
22
22
 
23
23
  from __future__ import annotations
@@ -52,6 +52,7 @@ _INSTANCE_COLUMNS: tuple[tuple[str, str], ...] = (
52
52
  ("version", "VERSION"),
53
53
  ("digest", "DIGEST"),
54
54
  ("updated_at", "UPDATED_AT"),
55
+ ("registered", "REGISTERED"),
55
56
  )
56
57
 
57
58
  _FORMAT_HELP = "Output format: plain/table/json/yaml (default: AP_FORMAT or command default)"
@@ -292,6 +293,7 @@ def _instance_record(instance: Any) -> dict:
292
293
  "manifest": served.get("manifest") or [],
293
294
  "created_at": served.get("created_at"),
294
295
  "updated_at": served.get("updated_at"),
296
+ "registered": served.get("registered", True),
295
297
  }
296
298
  if split and split != "default": # SDK 的 default 哨兵 = 无 split,服务端不返回该键
297
299
  record["split"] = split
@@ -309,6 +311,7 @@ def _instance_row(record: dict) -> dict:
309
311
  "version": record.get("dataset_version") or record.get("version") or "",
310
312
  "digest": _record_digest(record),
311
313
  "updated_at": record.get("updated_at") or "",
314
+ "registered": record.get("registered", True),
312
315
  }
313
316
 
314
317
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.5.0.dev0"
7
+ version = "0.5.1.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"
@@ -18,10 +18,10 @@ dependencies = [
18
18
 
19
19
  [project.optional-dependencies]
20
20
  dataset = [
21
- "instance-repo[oss]>=1.1.4,<2.0.0",
21
+ "instance-repo[oss]==1.3.2.dev0",
22
22
  ]
23
23
  all = [
24
- "instance-repo[oss]>=1.1.4,<2.0.0",
24
+ "instance-repo[oss]==1.3.2.dev0",
25
25
  ]
26
26
 
27
27
  [project.scripts]