ap-client 0.2.0.dev0__tar.gz → 0.2.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.2.0.dev0
3
+ Version: 0.2.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]>=0.7.0; extra == 'all'
12
+ Requires-Dist: instance-repo[oss]>=1.0.3; extra == 'all'
13
13
  Provides-Extra: dataset
14
- Requires-Dist: instance-repo[oss]>=0.7.0; extra == 'dataset'
14
+ Requires-Dist: instance-repo[oss]>=1.0.3; 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.
@@ -279,6 +279,48 @@ def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
279
279
  return {"workspace_id": workspace_id} if workspace_id else None
280
280
 
281
281
 
282
+ def _unwrap_data(payload: Any) -> Any:
283
+ """Unwrap the apiserver ``{code,message,data}`` envelope when present.
284
+
285
+ The dataset-domain detail/create/patch endpoints return a bare JSON body,
286
+ but the same handlers are occasionally wrapped by the generic success
287
+ envelope. Tolerate both so command code never has to branch: a mapping that
288
+ carries ``data`` alongside ``code``/``message`` is treated as an envelope,
289
+ anything else is returned verbatim.
290
+ """
291
+ if (
292
+ isinstance(payload, dict)
293
+ and "data" in payload
294
+ and ("code" in payload or "message" in payload)
295
+ ):
296
+ return payload["data"]
297
+ return payload
298
+
299
+
300
+ def _split_paged(payload: Any) -> tuple[list[dict], dict]:
301
+ """Split a ``PagedSuccess`` envelope into ``(records, pagination)``.
302
+
303
+ ``PagedSuccess`` is ``{code,message,data:[...],pagination:{total,page,page_size}}``.
304
+ A bare list (or a null ``data``) yields an empty pagination dict so callers
305
+ can render results without probing the response shape.
306
+ """
307
+ if isinstance(payload, list):
308
+ return [item for item in payload if isinstance(item, dict)], {}
309
+ if not isinstance(payload, dict):
310
+ return [], {}
311
+ records = payload.get("data")
312
+ pagination = payload.get("pagination")
313
+ return (
314
+ [item for item in (records or []) if isinstance(item, dict)],
315
+ pagination if isinstance(pagination, dict) else {},
316
+ )
317
+
318
+
319
+ def _as_record_list(payload: Any) -> list[dict]:
320
+ """Coerce a list-returning endpoint to ``list[dict]``, envelope or not."""
321
+ return _split_paged(payload)[0]
322
+
323
+
282
324
  class APIClient:
283
325
  """Agent Platform API client."""
284
326
 
@@ -830,6 +872,211 @@ class APIClient:
830
872
  "instance_ids": all_instance_ids,
831
873
  }
832
874
 
875
+ # ==================== Dataset series (apiserver dataset domain) ====================
876
+ #
877
+ # 这一节对接 Go apiserver 的 dataset 领域(/apis/v1/datasets/...),与上面
878
+ # ossdata 目录的 /api/datasets 路由完全无关。方法名统一带 `_series`,
879
+ # 以免和 `ap job create --dataset` 依赖的 list_all_datasets /
880
+ # list_dataset_versions / list_all_dataset_instances 混淆。
881
+ #
882
+ # 列表端点返回 PagedSuccess envelope:
883
+ # {"code":0,"message":"ok","data":[...],"pagination":{"total","page","page_size"}}
884
+ # 详情/创建/更新端点返回裸 JSON body。
885
+
886
+ _DATASET_SERIES_BASE = "/apis/v1/datasets/series"
887
+ _DATASET_VERSIONS_BASE = "/apis/v1/datasets/versions"
888
+ _DATASET_INSTANCES_BASE = "/apis/v1/datasets/instances"
889
+
890
+ def list_dataset_series_page(
891
+ self,
892
+ *,
893
+ q: Optional[str] = None,
894
+ keyword: Optional[str] = None,
895
+ visibility: Optional[str] = None,
896
+ benchmark_id: Optional[str] = None,
897
+ include_deprecated: bool = False,
898
+ environment: Optional[str] = None,
899
+ page: int = 1,
900
+ page_size: int = 20,
901
+ ) -> dict:
902
+ """一页 dataset series(原始 PagedSuccess envelope)。
903
+
904
+ 服务端没有 owner/mine/l1 过滤参数,调用方需要时只能在客户端做页内过滤。
905
+ """
906
+ params: dict = {"page": page, "page_size": page_size}
907
+ if q:
908
+ params["q"] = q
909
+ if keyword:
910
+ params["keyword"] = keyword
911
+ if visibility:
912
+ params["visibility"] = visibility
913
+ if benchmark_id:
914
+ params["benchmark_id"] = benchmark_id
915
+ if include_deprecated:
916
+ params["include_deprecated"] = "true"
917
+ if environment:
918
+ params["environment"] = environment
919
+ return self._get(self._DATASET_SERIES_BASE, params=params)
920
+
921
+ def list_dataset_series(self, **kw) -> tuple[list[dict], dict]:
922
+ """列出 dataset series,返回 ``(records, pagination)``。"""
923
+ return _split_paged(self.list_dataset_series_page(**kw))
924
+
925
+ def get_dataset_series(self, dataset_name: str, *, environment: Optional[str] = None) -> dict:
926
+ """按名字取 dataset series 详情(含 dataset_id / claimable / stats)。"""
927
+ params: dict = {"dataset_name": dataset_name}
928
+ if environment:
929
+ params["environment"] = environment
930
+ return _unwrap_data(self._get(f"{self._DATASET_SERIES_BASE}/detail", params=params))
931
+
932
+ def create_dataset_series(self, body: dict) -> dict:
933
+ """创建 dataset series。owner 由服务端按调用者身份填充。"""
934
+ return _unwrap_data(self._post(self._DATASET_SERIES_BASE, body))
935
+
936
+ def update_dataset_series(self, dataset_name: str, body: dict) -> dict:
937
+ """PATCH dataset series 元数据(visibility/status 需要 dataset-admin)。"""
938
+ return _unwrap_data(
939
+ self._request(
940
+ "PATCH",
941
+ f"{self._DATASET_SERIES_BASE}/detail",
942
+ params={"dataset_name": dataset_name},
943
+ json_body=body,
944
+ )
945
+ )
946
+
947
+ def claim_dataset_series(self, dataset_id: str, body: dict) -> dict:
948
+ """认领 dataset series(``workspace_id`` 服务端必填);成功后调用方成为 dataset admin。"""
949
+ return _unwrap_data(
950
+ self._post(f"{self._DATASET_SERIES_BASE}/{quote(dataset_id, safe='')}/claim", body)
951
+ )
952
+
953
+ def list_claim_workspaces(self) -> list[dict]:
954
+ """列出当前用户可用于认领的 workspace。"""
955
+ return _as_record_list(self._get("/apis/v1/datasets/claim-workspaces"))
956
+
957
+ # ---- dataset versions ----
958
+
959
+ def list_dataset_series_versions(
960
+ self,
961
+ dataset_name: str,
962
+ *,
963
+ status: Optional[str] = None,
964
+ page: int = 1,
965
+ page_size: int = 20,
966
+ environment: Optional[str] = None,
967
+ ) -> tuple[list[dict], dict]:
968
+ """列出某 dataset 的版本,返回 ``(records, pagination)``。列表项不含 splits 数组。"""
969
+ params: dict = {"dataset_name": dataset_name, "page": page, "page_size": page_size}
970
+ if status:
971
+ params["status"] = status
972
+ if environment:
973
+ params["environment"] = environment
974
+ return _split_paged(self._get(self._DATASET_VERSIONS_BASE, params=params))
975
+
976
+ def get_dataset_series_version(
977
+ self,
978
+ dataset_name: str,
979
+ version: str,
980
+ *,
981
+ environment: Optional[str] = None,
982
+ ) -> dict:
983
+ """取单个版本详情(含 run_type / splits[] / manifest / published_at)。
984
+
985
+ ``version=""`` 表示 split_first 的**无版本空间**,原样下发空串——绝不隐式取 latest。
986
+ """
987
+ params: dict = {"dataset_name": dataset_name, "version": version}
988
+ if environment:
989
+ params["environment"] = environment
990
+ return _unwrap_data(self._get(f"{self._DATASET_VERSIONS_BASE}/detail", params=params))
991
+
992
+ def create_dataset_series_version(self, dataset_name: str, body: dict) -> dict:
993
+ """创建版本(``version``/``storage_type``/``storage_path``/``splits``/``status``)。"""
994
+ return _unwrap_data(
995
+ self._request(
996
+ "POST",
997
+ self._DATASET_VERSIONS_BASE,
998
+ params={"dataset_name": dataset_name},
999
+ json_body=body,
1000
+ )
1001
+ )
1002
+
1003
+ def update_dataset_series_version(self, dataset_name: str, version: str, body: dict) -> dict:
1004
+ """PATCH 版本(``status``/``splits``/``manifest``/``run_type``/``split_run_types``)。
1005
+
1006
+ 服务端要求 ``splits`` 与 ``split_run_types`` 互斥,调用方需分两次 PATCH。
1007
+ """
1008
+ return _unwrap_data(
1009
+ self._request(
1010
+ "PATCH",
1011
+ f"{self._DATASET_VERSIONS_BASE}/detail",
1012
+ params={"dataset_name": dataset_name, "version": version},
1013
+ json_body=body,
1014
+ )
1015
+ )
1016
+
1017
+ # ---- dataset instances (metadata only) ----
1018
+
1019
+ def list_dataset_series_instances(
1020
+ self,
1021
+ dataset_name: str,
1022
+ *,
1023
+ version: Optional[str] = None,
1024
+ split: Optional[str] = None,
1025
+ instance_id: Optional[str] = None,
1026
+ page: int = 1,
1027
+ page_size: int = 50,
1028
+ environment: Optional[str] = None,
1029
+ ) -> tuple[list[dict], dict]:
1030
+ """列出 instance 元数据,返回 ``(records, pagination)``。
1031
+
1032
+ ``version=""`` / ``split=""`` 原样下发(无版本空间语义),``None`` 才省略该参数。
1033
+ """
1034
+ params: dict = {"dataset_name": dataset_name, "page": page, "page_size": page_size}
1035
+ if version is not None:
1036
+ params["version"] = version
1037
+ if split is not None:
1038
+ params["split"] = split
1039
+ if instance_id:
1040
+ params["instance_id"] = instance_id
1041
+ if environment:
1042
+ params["environment"] = environment
1043
+ return _split_paged(self._get(self._DATASET_INSTANCES_BASE, params=params))
1044
+
1045
+ def get_dataset_series_instance(
1046
+ self,
1047
+ dataset_name: str,
1048
+ version: str,
1049
+ split: str,
1050
+ instance_id: str,
1051
+ *,
1052
+ environment: Optional[str] = None,
1053
+ ) -> dict:
1054
+ """取单个 instance 的元数据详情。``version=""`` 表示无版本空间。"""
1055
+ params: dict = {
1056
+ "dataset_name": dataset_name,
1057
+ "version": version,
1058
+ "split": split,
1059
+ "instance_id": instance_id,
1060
+ }
1061
+ if environment:
1062
+ params["environment"] = environment
1063
+ return _unwrap_data(self._get(f"{self._DATASET_INSTANCES_BASE}/detail", params=params))
1064
+
1065
+ # ---- permissions ----
1066
+
1067
+ def get_my_resource_permissions(self, resource_type: str, resource_id: str) -> list[str]:
1068
+ """当前用户在某资源上的 action 列表(``{"actions":[...]}``)。"""
1069
+ payload = self._get(
1070
+ "/apis/v1/me/resource-permissions",
1071
+ params={"resource_type": resource_type, "resource_id": resource_id},
1072
+ )
1073
+ payload = _unwrap_data(payload)
1074
+ if isinstance(payload, dict):
1075
+ actions = payload.get("actions")
1076
+ if isinstance(actions, list):
1077
+ return [str(action) for action in actions]
1078
+ return []
1079
+
833
1080
  # ==================== Meta operations ====================
834
1081
 
835
1082
  def list_meta_models(self) -> dict:
@@ -21,8 +21,10 @@ from ap_client.api import (
21
21
  set_verbose_override,
22
22
  )
23
23
  from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
24
+ from ap_client.dataset_commands import register as _register_dataset_commands
24
25
  from ap_client.exporter import export_group, export_job
25
- from ap_client.irepo_commands import register as _register_dataset_repo
26
+ from ap_client.fs_commands import register as _register_fs_commands
27
+ from ap_client.instance_commands import register as _register_instance_commands
26
28
  from ap_client.profile_commands import profile_app
27
29
  from ap_client.waiter import (
28
30
  WaitTimeoutError,
@@ -143,8 +145,11 @@ app.add_typer(meta_job_type_app, name="meta-job-type")
143
145
  app.add_typer(benchmark_app, name="benchmark")
144
146
  app.add_typer(checkpoint_app, name="checkpoint")
145
147
 
146
- # ap dataset repo:透传 instance_repo CLI(见 ap_client/irepo_commands.py)
147
- _register_dataset_repo(dataset_app)
148
+ # Dataset 领域(见 docs/ap-cli-dataset-refactor-design.md):
149
+ # ap dataset version/split/access 嵌套在 dataset 下;instance/fs 是一等资源。
150
+ _register_dataset_commands(dataset_app)
151
+ _register_instance_commands(app)
152
+ _register_fs_commands(app)
148
153
 
149
154
  _PAI_RUNTIME_ENV_TAGS: tuple[tuple[str, str], ...] = (
150
155
  ("DLC_JOB_ID", "dlc_job_id"),
@@ -923,7 +928,10 @@ def _print_params_plain(params: Any, masked_params: list) -> None:
923
928
  rows: list[tuple[str, Any]] = []
924
929
  for key, value in params.items():
925
930
  display_value = _format_plain_value(value)
926
- if str(key) in masked_paths and value == "***":
931
+ # 直接信任 masked_params 路径:服务端掩码形态已扩展为保留首尾(如
932
+ # sk-***890 / ****),不能再按值严格等于 "***" 判断,否则形态变化后
933
+ # (masked) 标识静默丢失
934
+ if str(key) in masked_paths:
927
935
  display_value = f"{display_value} (masked)"
928
936
  rows.append((str(key), display_value))
929
937
  _print_key_values(rows, skip_empty=False)
@@ -2014,24 +2022,17 @@ def template_fetch(
2014
2022
 
2015
2023
  # ==================== Dataset operations ====================
2016
2024
 
2017
-
2018
- @dataset_app.command("list")
2019
- def dataset_list(
2020
- search: Optional[str] = typer.Argument(None, help="Search keyword"),
2021
- output_format: str = typer.Option(
2022
- None,
2023
- "--format",
2024
- help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2025
- ),
2026
- ):
2027
- """List all datasets."""
2028
- output_format = _normalize_output_format(output_format, keep_table=True)
2029
- client = get_client()
2030
- result = client.list_all_datasets(search)
2031
- if output_format == "table":
2032
- _print_records_table(result)
2033
- else:
2034
- _print_formatted(result, output_format)
2025
+ # 设计 §6 / 上游 §13.2:`ap dataset versions` 与 `ap dataset instances` 行为保持不变
2026
+ # (仍打 ossdata 路由,`ap job create` 的配套流程依赖它们),只追加固定弃用文案。
2027
+ # 文案**只写 stderr**——JSON/YAML 的 stdout 必须仍可被 json.loads 直接消费。
2028
+ _DATASET_VERSIONS_DEPRECATION = (
2029
+ "Warning: `ap dataset versions` is deprecated.\n"
2030
+ "Use `ap dataset version list --dataset <dataset>` instead."
2031
+ )
2032
+ _DATASET_INSTANCES_DEPRECATION = (
2033
+ "Warning: `ap dataset instances` is deprecated.\n"
2034
+ "Use `ap instance list --dataset <dataset> --split <split> [--version <version>]` instead."
2035
+ )
2035
2036
 
2036
2037
 
2037
2038
  @dataset_app.command("versions")
@@ -2043,8 +2044,9 @@ def dataset_versions(
2043
2044
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2044
2045
  ),
2045
2046
  ):
2046
- """List dataset versions."""
2047
+ """List dataset versions (deprecated: use 'ap dataset version list')."""
2047
2048
  output_format = _normalize_output_format(output_format, keep_table=True)
2049
+ _emit_progress(_DATASET_VERSIONS_DEPRECATION)
2048
2050
  client = get_client()
2049
2051
  versions = client.list_dataset_versions(dataset)
2050
2052
  if output_format == "table":
@@ -2064,8 +2066,9 @@ def dataset_instances(
2064
2066
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2065
2067
  ),
2066
2068
  ):
2067
- """List dataset instances."""
2069
+ """List dataset instances (deprecated: use 'ap instance list')."""
2068
2070
  output_format = _normalize_output_format(output_format, keep_table=True)
2071
+ _emit_progress(_DATASET_INSTANCES_DEPRECATION)
2069
2072
  client = get_client()
2070
2073
  result = client.list_all_dataset_instances(dataset_version)
2071
2074
  if output_format == "table":