ap-client 0.3.0.dev0__tar.gz → 0.3.1.dev1__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.3.0.dev0
3
+ Version: 0.3.1.dev1
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.1.0; 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.1.0; 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.
@@ -15,6 +15,7 @@ from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
15
15
  import requests
16
16
 
17
17
  from .config import Config, ConfigurationError, get_config, has_cluster_header
18
+ from .managed_ak import require_managed_ak_ack, validate_ak_id, validate_ak_selection
18
19
 
19
20
  _DEFAULT_CONNECT_TIMEOUT = 300
20
21
  _DEFAULT_READ_TIMEOUT = 86400
@@ -293,6 +294,43 @@ def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
293
294
  return {"workspace_id": workspace_id} if workspace_id else None
294
295
 
295
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
+ def _split_paged(payload: Any) -> tuple[list[dict], dict]:
316
+ """Split a ``PagedSuccess`` envelope into ``(records, pagination)``.
317
+
318
+ ``PagedSuccess`` is ``{code,message,data:[...],pagination:{total,page,page_size}}``.
319
+ A bare list (or a null ``data``) yields an empty pagination dict so callers
320
+ can render results without probing the response shape.
321
+ """
322
+ if isinstance(payload, list):
323
+ return [item for item in payload if isinstance(item, dict)], {}
324
+ if not isinstance(payload, dict):
325
+ return [], {}
326
+ records = payload.get("data")
327
+ pagination = payload.get("pagination")
328
+ return (
329
+ [item for item in (records or []) if isinstance(item, dict)],
330
+ pagination if isinstance(pagination, dict) else {},
331
+ )
332
+
333
+
296
334
  class APIClient:
297
335
  """Agent Platform API client."""
298
336
 
@@ -459,7 +497,12 @@ class APIClient:
459
497
 
460
498
  def _post(self, path: str, data: Any, timeout: TimeoutType = None) -> Any:
461
499
  """Send a POST request."""
462
- return self._request("POST", path, json_body=data, timeout=timeout)
500
+ from .managed_ak import parameter_ak_ids, require_parameter_ak_ack
501
+
502
+ references = parameter_ak_ids(data) if path == "/jobs" else set()
503
+ result = self._request("POST", path, json_body=data, timeout=timeout)
504
+ require_parameter_ak_ack(result, references)
505
+ return result
463
506
 
464
507
  def _delete(self, path: str, *, params: Optional[dict] = None) -> Any:
465
508
  """Send a DELETE request."""
@@ -708,8 +751,35 @@ class APIClient:
708
751
  return self._get(f"/templates/{quote(name)}", params=params)
709
752
 
710
753
  def list_benchmarks(self) -> list:
711
- """List all active benchmarks from the local benchmark registry."""
712
- return self._get("/benchmarks")
754
+ """List all benchmarks from the central registry, preserving legacy IDs."""
755
+ benchmarks = []
756
+ page = 1
757
+ page_size = 200
758
+ # 与 CLI dataset list 的自动翻页同口径:100 页上限,防分页异常导致死循环。
759
+ max_pages = 100
760
+ while page <= max_pages:
761
+ payload = self._get(
762
+ "/apis/v1/benchmarks", params={"page": page, "page_size": page_size}
763
+ )
764
+ records, pagination = _split_paged(payload)
765
+ for record in records:
766
+ item = dict(record)
767
+ if "source_id" in item:
768
+ item.setdefault("id", item["source_id"])
769
+ benchmarks.append(item)
770
+ if not records or not pagination:
771
+ break
772
+ try:
773
+ total = int(pagination.get("total"))
774
+ except (TypeError, ValueError):
775
+ total = None
776
+ if total is not None:
777
+ if len(benchmarks) >= total:
778
+ break
779
+ elif len(records) < page_size:
780
+ break
781
+ page += 1
782
+ return benchmarks
713
783
 
714
784
  def get_benchmark(self, name: str) -> dict:
715
785
  """Get a single benchmark by exact name."""
@@ -844,7 +914,16 @@ class APIClient:
844
914
  "instance_ids": all_instance_ids,
845
915
  }
846
916
 
847
- # ==================== Meta operations ====================
917
+ # ==================== Dataset series (apiserver dataset domain) ====================
918
+ #
919
+ # 这一节对接 Go apiserver 的 dataset 领域(/apis/v1/datasets/...),与上面
920
+ # ossdata 目录的 /api/datasets 路由完全无关。
921
+ #
922
+ # **dataset 领域的控制面读写全部在 InstanceRepo SDK 上**(见
923
+ # docs/ap-cli-dataset-refactor-design.md §9.5):迁移期加进这里的 registry 方法、
924
+ # 以及最后三条"push 前检查 + publish-status"的旧读,都已随迁移删除。
925
+ # 本文件现在只服务旧 ossdata 路径(`dataset list --legacy`、弃用的
926
+ # `dataset versions/instances`、`job create --dataset`)。
848
927
 
849
928
  def list_meta_models(self) -> dict:
850
929
  """模型词表(apiserver 权威源 GET /apis/v1/meta-models,返回 {items:[...]})。"""
@@ -911,9 +990,19 @@ class APIClient:
911
990
  checkpoint: Optional[dict] = None,
912
991
  benchmark: Optional[str] = None,
913
992
  experiment: Optional[str] = None,
993
+ ak_ids: Optional[list[str]] = None,
914
994
  ) -> dict:
915
995
  """Create a group."""
996
+ if ak_ids is not None and not isinstance(ak_ids, list):
997
+ raise ValueError("ak_ids must be a list")
998
+ references = list(ak_ids or [])
999
+ for reference in references:
1000
+ if validate_ak_id(reference) is None:
1001
+ raise ValueError("ak_ids entries must be nonempty strings")
1002
+ references = list(dict.fromkeys(references))
916
1003
  body: dict = {}
1004
+ if references:
1005
+ body["ak_ids"] = references
917
1006
  if name:
918
1007
  body["name"] = name
919
1008
  effective_tags = _merge_meta_tags(
@@ -965,11 +1054,14 @@ class APIClient:
965
1054
  body["benchmark"] = benchmark
966
1055
  if experiment is not None:
967
1056
  body["experiment"] = experiment
968
- return self._post("/groups", body)
1057
+ result = self._post("/groups", body)
1058
+ for reference in references:
1059
+ require_managed_ak_ack(result, reference)
1060
+ return result
969
1061
 
970
1062
  def create_job(
971
1063
  self,
972
- template: str,
1064
+ template: Optional[str] = None,
973
1065
  params: Optional[dict] = None,
974
1066
  params_list: Optional[list] = None,
975
1067
  suite_name: Optional[str] = None,
@@ -1022,6 +1114,7 @@ class APIClient:
1022
1114
  checkpoint: Optional[dict] = None,
1023
1115
  benchmark: Optional[str] = None,
1024
1116
  experiment: Optional[str] = None,
1117
+ ak_id: Optional[str] = None,
1025
1118
  ) -> dict:
1026
1119
  """Create a job."""
1027
1120
  body = self.build_create_job_body(
@@ -1068,13 +1161,16 @@ class APIClient:
1068
1161
  checkpoint=checkpoint,
1069
1162
  benchmark=benchmark,
1070
1163
  experiment=experiment,
1164
+ ak_id=ak_id,
1071
1165
  )
1072
1166
 
1073
- return self._post("/jobs", body, timeout=timeout)
1167
+ result = self._post("/jobs", body, timeout=timeout)
1168
+ require_managed_ak_ack(result, ak_id)
1169
+ return result
1074
1170
 
1075
1171
  def build_create_job_body(
1076
1172
  self,
1077
- template: str,
1173
+ template: Optional[str] = None,
1078
1174
  params: Optional[dict] = None,
1079
1175
  params_list: Optional[list] = None,
1080
1176
  suite_name: Optional[str] = None,
@@ -1126,6 +1222,7 @@ class APIClient:
1126
1222
  checkpoint: Optional[dict] = None,
1127
1223
  benchmark: Optional[str] = None,
1128
1224
  experiment: Optional[str] = None,
1225
+ ak_id: Optional[str] = None,
1129
1226
  ) -> dict:
1130
1227
  """Build the /jobs request body for job creation."""
1131
1228
  _validate_credential_source_selection(
@@ -1134,7 +1231,14 @@ class APIClient:
1134
1231
  params=params,
1135
1232
  params_list=params_list,
1136
1233
  )
1137
- body: dict = {"template": template}
1234
+ if not template and not profile_id:
1235
+ raise ValueError("Either template or profile_id is required")
1236
+ validate_ak_selection(ak_id, params, params_list, overrides)
1237
+ body: dict = {}
1238
+ if template is not None:
1239
+ body["template"] = template
1240
+ if ak_id is not None:
1241
+ body["ak_id"] = ak_id
1138
1242
 
1139
1243
  if params is not None:
1140
1244
  body["params"] = params
@@ -1289,9 +1393,13 @@ class APIClient:
1289
1393
  next_token: Optional[str] = None,
1290
1394
  pagination: Optional[str] = None,
1291
1395
  include_total: bool = True,
1396
+ ak_id: Optional[str] = None,
1292
1397
  ) -> dict:
1293
1398
  """List jobs."""
1399
+ validate_ak_id(ak_id)
1294
1400
  params = {"skip": skip, "limit": limit}
1401
+ if ak_id is not None:
1402
+ params["ak_id"] = ak_id
1295
1403
  if template:
1296
1404
  params["template"] = template
1297
1405
  if group_id:
@@ -1485,9 +1593,13 @@ class APIClient:
1485
1593
  meta_job_type: Optional[str] = None,
1486
1594
  upstream_platform: Optional[str] = None,
1487
1595
  upstream_job_id: Optional[str] = None,
1596
+ ak_id: Optional[str] = None,
1488
1597
  ) -> dict:
1489
1598
  """List groups."""
1599
+ validate_ak_id(ak_id)
1490
1600
  params: dict = {"skip": skip, "limit": limit}
1601
+ if ak_id is not None:
1602
+ params["ak_id"] = ak_id
1491
1603
  if name is not None:
1492
1604
  params["name"] = name
1493
1605
  if group_id is not None: