ap-client 0.2.1.dev0__tar.gz → 0.3.0.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.2.1.dev0 → ap_client-0.3.0.dev0}/PKG-INFO +3 -3
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/api.py +88 -266
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/cli.py +104 -24
- ap_client-0.3.0.dev0/ap_client/irepo_commands.py +131 -0
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/waiter.py +1 -1
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/pyproject.toml +3 -3
- ap_client-0.2.1.dev0/ap_client/dataset_commands.py +0 -1374
- ap_client-0.2.1.dev0/ap_client/fs_commands.py +0 -566
- ap_client-0.2.1.dev0/ap_client/instance_commands.py +0 -506
- ap_client-0.2.1.dev0/ap_client/irepo_sdk.py +0 -326
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/__init__.py +0 -0
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/config.py +0 -0
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/exporter.py +0 -0
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/profile_commands.py +0 -0
- {ap_client-0.2.1.dev0 → ap_client-0.3.0.dev0}/ap_client/tbb.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0.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]>=
|
|
12
|
+
Requires-Dist: instance-repo[oss]>=0.7.0; extra == 'all'
|
|
13
13
|
Provides-Extra: dataset
|
|
14
|
-
Requires-Dist: instance-repo[oss]>=
|
|
14
|
+
Requires-Dist: instance-repo[oss]>=0.7.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.
|
|
@@ -268,6 +268,20 @@ class APIError(Exception):
|
|
|
268
268
|
return f"API Error {self.status_code}: {self.detail} (request_id={self.request_id})"
|
|
269
269
|
|
|
270
270
|
|
|
271
|
+
class PaginationProtocolError(RuntimeError):
|
|
272
|
+
"""The server stopped honoring a pagination protocol already in use."""
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _ensure_next_token_honored(response: dict, *, next_token: Optional[str], endpoint: str) -> None:
|
|
276
|
+
# An old server ignores the unknown next_token param and answers with its
|
|
277
|
+
# default first page, so a `response.get("next_token")` loop would silently
|
|
278
|
+
# repeat that page and drop the rest of the collection.
|
|
279
|
+
if next_token and "next_token" not in response:
|
|
280
|
+
raise PaginationProtocolError(
|
|
281
|
+
f"server stopped returning next_token while paginating {endpoint}; upgrade the server"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
|
|
271
285
|
def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
|
|
272
286
|
"""Build the ``?workspace_id=`` query for name-addressed secret endpoints.
|
|
273
287
|
|
|
@@ -279,48 +293,6 @@ def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
|
|
|
279
293
|
return {"workspace_id": workspace_id} if workspace_id else None
|
|
280
294
|
|
|
281
295
|
|
|
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
|
-
|
|
324
296
|
class APIClient:
|
|
325
297
|
"""Agent Platform API client."""
|
|
326
298
|
|
|
@@ -872,211 +844,6 @@ class APIClient:
|
|
|
872
844
|
"instance_ids": all_instance_ids,
|
|
873
845
|
}
|
|
874
846
|
|
|
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
|
-
|
|
1080
847
|
# ==================== Meta operations ====================
|
|
1081
848
|
|
|
1082
849
|
def list_meta_models(self) -> dict:
|
|
@@ -1224,6 +991,7 @@ class APIClient:
|
|
|
1224
991
|
runner: Optional[str] = None,
|
|
1225
992
|
runner_options: Optional[dict] = None,
|
|
1226
993
|
idempotency_key: Optional[str] = None,
|
|
994
|
+
max_failure_retries: int = 0,
|
|
1227
995
|
# ─── Scheduling hints ─────────────────────────────────────────
|
|
1228
996
|
priority: Optional[str] = None,
|
|
1229
997
|
priority_weight: Optional[int] = None,
|
|
@@ -1276,6 +1044,7 @@ class APIClient:
|
|
|
1276
1044
|
runner=runner,
|
|
1277
1045
|
runner_options=runner_options,
|
|
1278
1046
|
idempotency_key=idempotency_key,
|
|
1047
|
+
max_failure_retries=max_failure_retries,
|
|
1279
1048
|
priority=priority,
|
|
1280
1049
|
priority_weight=priority_weight,
|
|
1281
1050
|
resource_mode=resource_mode,
|
|
@@ -1326,6 +1095,7 @@ class APIClient:
|
|
|
1326
1095
|
runner: Optional[str] = None,
|
|
1327
1096
|
runner_options: Optional[dict] = None,
|
|
1328
1097
|
idempotency_key: Optional[str] = None,
|
|
1098
|
+
max_failure_retries: int = 0,
|
|
1329
1099
|
# ─── Scheduling hints ─────────────────────────────────────────
|
|
1330
1100
|
priority: Optional[str] = None,
|
|
1331
1101
|
priority_weight: Optional[int] = None,
|
|
@@ -1412,6 +1182,8 @@ class APIClient:
|
|
|
1412
1182
|
body["runner_options"] = runner_options
|
|
1413
1183
|
if idempotency_key is not None:
|
|
1414
1184
|
body["idempotency_key"] = idempotency_key
|
|
1185
|
+
if max_failure_retries:
|
|
1186
|
+
body["max_failure_retries"] = max_failure_retries
|
|
1415
1187
|
|
|
1416
1188
|
# Scheduling hints
|
|
1417
1189
|
if priority is not None:
|
|
@@ -1514,6 +1286,9 @@ class APIClient:
|
|
|
1514
1286
|
meta_job_type: Optional[str] = None,
|
|
1515
1287
|
upstream_platform: Optional[str] = None,
|
|
1516
1288
|
upstream_job_id: Optional[str] = None,
|
|
1289
|
+
next_token: Optional[str] = None,
|
|
1290
|
+
pagination: Optional[str] = None,
|
|
1291
|
+
include_total: bool = True,
|
|
1517
1292
|
) -> dict:
|
|
1518
1293
|
"""List jobs."""
|
|
1519
1294
|
params = {"skip": skip, "limit": limit}
|
|
@@ -1571,7 +1346,15 @@ class APIClient:
|
|
|
1571
1346
|
params["created_at_sort"] = created_at_sort
|
|
1572
1347
|
if finished_at_sort is not None:
|
|
1573
1348
|
params["finished_at_sort"] = finished_at_sort
|
|
1574
|
-
|
|
1349
|
+
if next_token:
|
|
1350
|
+
params["next_token"] = next_token
|
|
1351
|
+
if pagination:
|
|
1352
|
+
params["pagination"] = pagination
|
|
1353
|
+
if not include_total:
|
|
1354
|
+
params["include_total"] = False
|
|
1355
|
+
response = self._get("/jobs", params=params)
|
|
1356
|
+
_ensure_next_token_honored(response, next_token=next_token, endpoint="/jobs")
|
|
1357
|
+
return response
|
|
1575
1358
|
|
|
1576
1359
|
# ==================== Group operations ====================
|
|
1577
1360
|
|
|
@@ -1762,6 +1545,9 @@ class APIClient:
|
|
|
1762
1545
|
meta_job_type: Optional[str] = None,
|
|
1763
1546
|
upstream_platform: Optional[str] = None,
|
|
1764
1547
|
upstream_job_id: Optional[str] = None,
|
|
1548
|
+
next_token: Optional[str] = None,
|
|
1549
|
+
pagination: Optional[str] = None,
|
|
1550
|
+
include_total: bool = True,
|
|
1765
1551
|
) -> dict:
|
|
1766
1552
|
"""List jobs in a group."""
|
|
1767
1553
|
params: dict = {"skip": skip, "limit": limit}
|
|
@@ -1785,7 +1571,16 @@ class APIClient:
|
|
|
1785
1571
|
params["upstream_job_id"] = upstream_job_id
|
|
1786
1572
|
if include_post_process:
|
|
1787
1573
|
params["include_post_process"] = True
|
|
1788
|
-
|
|
1574
|
+
if next_token:
|
|
1575
|
+
params["next_token"] = next_token
|
|
1576
|
+
if pagination:
|
|
1577
|
+
params["pagination"] = pagination
|
|
1578
|
+
if not include_total:
|
|
1579
|
+
params["include_total"] = False
|
|
1580
|
+
endpoint = f"/groups/{quote(group_id)}/jobs"
|
|
1581
|
+
response = self._get(endpoint, params=params)
|
|
1582
|
+
_ensure_next_token_honored(response, next_token=next_token, endpoint=endpoint)
|
|
1583
|
+
return response
|
|
1789
1584
|
|
|
1790
1585
|
def get_group_post_process_job_id(
|
|
1791
1586
|
self, group_id: str, timeout: TimeoutType = None
|
|
@@ -2263,19 +2058,30 @@ class APIClient:
|
|
|
2263
2058
|
skip: int = 0,
|
|
2264
2059
|
limit: int = _GROUP_ARTIFACTS_PAGE_SIZE,
|
|
2265
2060
|
include_post_process: bool = False,
|
|
2061
|
+
next_token: Optional[str] = None,
|
|
2062
|
+
pagination: Optional[str] = None,
|
|
2063
|
+
include_total: bool = True,
|
|
2266
2064
|
) -> dict:
|
|
2267
2065
|
"""Get one page of artifact download links for a group."""
|
|
2268
2066
|
params: dict[str, object] = {"skip": skip, "limit": limit}
|
|
2269
2067
|
if include_post_process:
|
|
2270
2068
|
params["include_post_process"] = True
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2069
|
+
if next_token:
|
|
2070
|
+
params["next_token"] = next_token
|
|
2071
|
+
if pagination:
|
|
2072
|
+
params["pagination"] = pagination
|
|
2073
|
+
if not include_total:
|
|
2074
|
+
params["include_total"] = False
|
|
2075
|
+
endpoint = f"/groups/{quote(group_id)}/artifacts"
|
|
2076
|
+
response = self._get(endpoint, params=params)
|
|
2077
|
+
_ensure_next_token_honored(response, next_token=next_token, endpoint=endpoint)
|
|
2078
|
+
return response
|
|
2275
2079
|
|
|
2276
2080
|
def get_group_artifacts(self, group_id: str, include_post_process: bool = False) -> dict:
|
|
2277
2081
|
"""Get artifact download links for a group."""
|
|
2082
|
+
next_token: str | None = None
|
|
2278
2083
|
skip = 0
|
|
2084
|
+
mode: str | None = None
|
|
2279
2085
|
artifacts: list[dict] = []
|
|
2280
2086
|
last_page: dict | None = None
|
|
2281
2087
|
|
|
@@ -2285,25 +2091,41 @@ class APIClient:
|
|
|
2285
2091
|
skip=skip,
|
|
2286
2092
|
limit=_GROUP_ARTIFACTS_PAGE_SIZE,
|
|
2287
2093
|
include_post_process=include_post_process,
|
|
2094
|
+
next_token=next_token,
|
|
2095
|
+
pagination="cursor" if mode in {None, "cursor"} else None,
|
|
2096
|
+
include_total=False,
|
|
2288
2097
|
)
|
|
2289
2098
|
last_page = page
|
|
2290
2099
|
page_artifacts = page.get("artifacts") or []
|
|
2291
2100
|
artifacts.extend(page_artifacts)
|
|
2101
|
+
has_token_field = "next_token" in page
|
|
2292
2102
|
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
break
|
|
2296
|
-
if len(page_artifacts) < _GROUP_ARTIFACTS_PAGE_SIZE:
|
|
2297
|
-
break
|
|
2103
|
+
if mode is None:
|
|
2104
|
+
mode = "cursor" if has_token_field else "offset"
|
|
2298
2105
|
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2106
|
+
if mode == "cursor":
|
|
2107
|
+
next_token = page.get("next_token")
|
|
2108
|
+
if not next_token:
|
|
2109
|
+
break
|
|
2110
|
+
else:
|
|
2111
|
+
skip += len(page_artifacts)
|
|
2112
|
+
total = page.get("total")
|
|
2113
|
+
if not page_artifacts or len(page_artifacts) < _GROUP_ARTIFACTS_PAGE_SIZE:
|
|
2114
|
+
break
|
|
2115
|
+
if total is not None and skip >= total:
|
|
2116
|
+
break
|
|
2117
|
+
|
|
2118
|
+
result = dict(last_page or {})
|
|
2119
|
+
result.pop("next_token", None)
|
|
2120
|
+
result.update(
|
|
2121
|
+
{
|
|
2122
|
+
"total": len(artifacts),
|
|
2123
|
+
"skip": 0,
|
|
2124
|
+
"limit": len(artifacts),
|
|
2125
|
+
"artifacts": artifacts,
|
|
2126
|
+
}
|
|
2127
|
+
)
|
|
2128
|
+
return result
|
|
2307
2129
|
|
|
2308
2130
|
def get_job_artifacts(self, job_ids: list) -> list:
|
|
2309
2131
|
"""Get artifact download links for one or more jobs."""
|