ap-client 0.2.0.dev0__tar.gz → 0.2.1__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.0.dev0 → ap_client-0.2.1}/PKG-INFO +1 -1
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/api.py +83 -19
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/cli.py +58 -1
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/pyproject.toml +1 -1
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/__init__.py +0 -0
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/config.py +0 -0
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/exporter.py +0 -0
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/irepo_commands.py +0 -0
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/profile_commands.py +0 -0
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/tbb.py +0 -0
- {ap_client-0.2.0.dev0 → ap_client-0.2.1}/ap_client/waiter.py +0 -0
|
@@ -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
|
|
|
@@ -1267,6 +1281,9 @@ class APIClient:
|
|
|
1267
1281
|
meta_job_type: Optional[str] = None,
|
|
1268
1282
|
upstream_platform: Optional[str] = None,
|
|
1269
1283
|
upstream_job_id: Optional[str] = None,
|
|
1284
|
+
next_token: Optional[str] = None,
|
|
1285
|
+
pagination: Optional[str] = None,
|
|
1286
|
+
include_total: bool = True,
|
|
1270
1287
|
) -> dict:
|
|
1271
1288
|
"""List jobs."""
|
|
1272
1289
|
params = {"skip": skip, "limit": limit}
|
|
@@ -1324,7 +1341,15 @@ class APIClient:
|
|
|
1324
1341
|
params["created_at_sort"] = created_at_sort
|
|
1325
1342
|
if finished_at_sort is not None:
|
|
1326
1343
|
params["finished_at_sort"] = finished_at_sort
|
|
1327
|
-
|
|
1344
|
+
if next_token:
|
|
1345
|
+
params["next_token"] = next_token
|
|
1346
|
+
if pagination:
|
|
1347
|
+
params["pagination"] = pagination
|
|
1348
|
+
if not include_total:
|
|
1349
|
+
params["include_total"] = False
|
|
1350
|
+
response = self._get("/jobs", params=params)
|
|
1351
|
+
_ensure_next_token_honored(response, next_token=next_token, endpoint="/jobs")
|
|
1352
|
+
return response
|
|
1328
1353
|
|
|
1329
1354
|
# ==================== Group operations ====================
|
|
1330
1355
|
|
|
@@ -1515,6 +1540,9 @@ class APIClient:
|
|
|
1515
1540
|
meta_job_type: Optional[str] = None,
|
|
1516
1541
|
upstream_platform: Optional[str] = None,
|
|
1517
1542
|
upstream_job_id: Optional[str] = None,
|
|
1543
|
+
next_token: Optional[str] = None,
|
|
1544
|
+
pagination: Optional[str] = None,
|
|
1545
|
+
include_total: bool = True,
|
|
1518
1546
|
) -> dict:
|
|
1519
1547
|
"""List jobs in a group."""
|
|
1520
1548
|
params: dict = {"skip": skip, "limit": limit}
|
|
@@ -1538,7 +1566,16 @@ class APIClient:
|
|
|
1538
1566
|
params["upstream_job_id"] = upstream_job_id
|
|
1539
1567
|
if include_post_process:
|
|
1540
1568
|
params["include_post_process"] = True
|
|
1541
|
-
|
|
1569
|
+
if next_token:
|
|
1570
|
+
params["next_token"] = next_token
|
|
1571
|
+
if pagination:
|
|
1572
|
+
params["pagination"] = pagination
|
|
1573
|
+
if not include_total:
|
|
1574
|
+
params["include_total"] = False
|
|
1575
|
+
endpoint = f"/groups/{quote(group_id)}/jobs"
|
|
1576
|
+
response = self._get(endpoint, params=params)
|
|
1577
|
+
_ensure_next_token_honored(response, next_token=next_token, endpoint=endpoint)
|
|
1578
|
+
return response
|
|
1542
1579
|
|
|
1543
1580
|
def get_group_post_process_job_id(
|
|
1544
1581
|
self, group_id: str, timeout: TimeoutType = None
|
|
@@ -2016,19 +2053,30 @@ class APIClient:
|
|
|
2016
2053
|
skip: int = 0,
|
|
2017
2054
|
limit: int = _GROUP_ARTIFACTS_PAGE_SIZE,
|
|
2018
2055
|
include_post_process: bool = False,
|
|
2056
|
+
next_token: Optional[str] = None,
|
|
2057
|
+
pagination: Optional[str] = None,
|
|
2058
|
+
include_total: bool = True,
|
|
2019
2059
|
) -> dict:
|
|
2020
2060
|
"""Get one page of artifact download links for a group."""
|
|
2021
2061
|
params: dict[str, object] = {"skip": skip, "limit": limit}
|
|
2022
2062
|
if include_post_process:
|
|
2023
2063
|
params["include_post_process"] = True
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2064
|
+
if next_token:
|
|
2065
|
+
params["next_token"] = next_token
|
|
2066
|
+
if pagination:
|
|
2067
|
+
params["pagination"] = pagination
|
|
2068
|
+
if not include_total:
|
|
2069
|
+
params["include_total"] = False
|
|
2070
|
+
endpoint = f"/groups/{quote(group_id)}/artifacts"
|
|
2071
|
+
response = self._get(endpoint, params=params)
|
|
2072
|
+
_ensure_next_token_honored(response, next_token=next_token, endpoint=endpoint)
|
|
2073
|
+
return response
|
|
2028
2074
|
|
|
2029
2075
|
def get_group_artifacts(self, group_id: str, include_post_process: bool = False) -> dict:
|
|
2030
2076
|
"""Get artifact download links for a group."""
|
|
2077
|
+
next_token: str | None = None
|
|
2031
2078
|
skip = 0
|
|
2079
|
+
mode: str | None = None
|
|
2032
2080
|
artifacts: list[dict] = []
|
|
2033
2081
|
last_page: dict | None = None
|
|
2034
2082
|
|
|
@@ -2038,25 +2086,41 @@ class APIClient:
|
|
|
2038
2086
|
skip=skip,
|
|
2039
2087
|
limit=_GROUP_ARTIFACTS_PAGE_SIZE,
|
|
2040
2088
|
include_post_process=include_post_process,
|
|
2089
|
+
next_token=next_token,
|
|
2090
|
+
pagination="cursor" if mode in {None, "cursor"} else None,
|
|
2091
|
+
include_total=False,
|
|
2041
2092
|
)
|
|
2042
2093
|
last_page = page
|
|
2043
2094
|
page_artifacts = page.get("artifacts") or []
|
|
2044
2095
|
artifacts.extend(page_artifacts)
|
|
2096
|
+
has_token_field = "next_token" in page
|
|
2045
2097
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
break
|
|
2049
|
-
if len(page_artifacts) < _GROUP_ARTIFACTS_PAGE_SIZE:
|
|
2050
|
-
break
|
|
2051
|
-
|
|
2052
|
-
skip += len(page_artifacts)
|
|
2098
|
+
if mode is None:
|
|
2099
|
+
mode = "cursor" if has_token_field else "offset"
|
|
2053
2100
|
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2101
|
+
if mode == "cursor":
|
|
2102
|
+
next_token = page.get("next_token")
|
|
2103
|
+
if not next_token:
|
|
2104
|
+
break
|
|
2105
|
+
else:
|
|
2106
|
+
skip += len(page_artifacts)
|
|
2107
|
+
total = page.get("total")
|
|
2108
|
+
if not page_artifacts or len(page_artifacts) < _GROUP_ARTIFACTS_PAGE_SIZE:
|
|
2109
|
+
break
|
|
2110
|
+
if total is not None and skip >= total:
|
|
2111
|
+
break
|
|
2112
|
+
|
|
2113
|
+
result = dict(last_page or {})
|
|
2114
|
+
result.pop("next_token", None)
|
|
2115
|
+
result.update(
|
|
2116
|
+
{
|
|
2117
|
+
"total": len(artifacts),
|
|
2118
|
+
"skip": 0,
|
|
2119
|
+
"limit": len(artifacts),
|
|
2120
|
+
"artifacts": artifacts,
|
|
2121
|
+
}
|
|
2122
|
+
)
|
|
2123
|
+
return result
|
|
2060
2124
|
|
|
2061
2125
|
def get_job_artifacts(self, job_ids: list) -> list:
|
|
2062
2126
|
"""Get artifact download links for one or more jobs."""
|
|
@@ -16,6 +16,7 @@ from ap_client import __version__, get_client, get_config, tbb
|
|
|
16
16
|
from ap_client.api import (
|
|
17
17
|
_OTHERS_MODEL,
|
|
18
18
|
APIError,
|
|
19
|
+
PaginationProtocolError,
|
|
19
20
|
_merge_meta_tags,
|
|
20
21
|
set_cluster_override,
|
|
21
22
|
set_verbose_override,
|
|
@@ -2633,6 +2634,21 @@ def job_list(
|
|
|
2633
2634
|
desc: bool = typer.Option(False, "--desc", help="Sort descending; requires --sort"),
|
|
2634
2635
|
skip: int = typer.Option(0, "--skip", help="Skip the first N entries"),
|
|
2635
2636
|
limit: int = typer.Option(100, "--limit", help="Maximum number of entries to return"),
|
|
2637
|
+
next_token: Optional[str] = typer.Option(
|
|
2638
|
+
None,
|
|
2639
|
+
"--next-token",
|
|
2640
|
+
help="Opaque token returned by the previous exact-group job list page; requires --group-id",
|
|
2641
|
+
),
|
|
2642
|
+
cursor: bool = typer.Option(
|
|
2643
|
+
False,
|
|
2644
|
+
"--cursor",
|
|
2645
|
+
help="Opt this page into next-token pagination; requires --group-id",
|
|
2646
|
+
),
|
|
2647
|
+
no_total: bool = typer.Option(
|
|
2648
|
+
False,
|
|
2649
|
+
"--no-total",
|
|
2650
|
+
help="Skip the server-side count; total becomes null in json/yaml output",
|
|
2651
|
+
),
|
|
2636
2652
|
columns: Optional[str] = typer.Option(
|
|
2637
2653
|
None,
|
|
2638
2654
|
"--columns",
|
|
@@ -2658,7 +2674,16 @@ def job_list(
|
|
|
2658
2674
|
raise typer.BadParameter("--tag-pattern cannot be used with --tag")
|
|
2659
2675
|
if normalized_tag_logic != "and" and not tag:
|
|
2660
2676
|
raise typer.BadParameter("--tag-logic requires --tag")
|
|
2677
|
+
if next_token and not group_id:
|
|
2678
|
+
raise typer.BadParameter("--next-token requires --group-id")
|
|
2679
|
+
if cursor and not group_id:
|
|
2680
|
+
raise typer.BadParameter("--cursor requires --group-id")
|
|
2681
|
+
if next_token and skip > 0:
|
|
2682
|
+
raise typer.BadParameter("--next-token cannot be used with --skip greater than 0")
|
|
2683
|
+
if cursor and skip > 0:
|
|
2684
|
+
raise typer.BadParameter("--cursor cannot be used with --skip greater than 0")
|
|
2661
2685
|
sort_kwargs = _job_list_sort_kwargs(sort, asc=asc, desc=desc)
|
|
2686
|
+
cursor_request = bool(next_token) or cursor
|
|
2662
2687
|
client = get_client()
|
|
2663
2688
|
result = client.list_jobs(
|
|
2664
2689
|
template=template,
|
|
@@ -2682,6 +2707,9 @@ def job_list(
|
|
|
2682
2707
|
**({"meta_model": meta_model} if meta_model is not None else {}),
|
|
2683
2708
|
**({"meta_job_type": meta_job_type} if meta_job_type is not None else {}),
|
|
2684
2709
|
**sort_kwargs,
|
|
2710
|
+
**({"next_token": next_token} if next_token else {}),
|
|
2711
|
+
**({"pagination": "cursor"} if cursor_request else {}),
|
|
2712
|
+
include_total=not no_total,
|
|
2685
2713
|
skip=skip,
|
|
2686
2714
|
limit=limit,
|
|
2687
2715
|
)
|
|
@@ -2691,6 +2719,9 @@ def job_list(
|
|
|
2691
2719
|
_print_job_list_plain(result, columns=selected_columns)
|
|
2692
2720
|
else:
|
|
2693
2721
|
_print_formatted(result, output_format)
|
|
2722
|
+
next_token = result.get("next_token")
|
|
2723
|
+
if output_format in {"table", "plain"} and next_token:
|
|
2724
|
+
typer.echo(f"Next token: {next_token}", err=True)
|
|
2694
2725
|
|
|
2695
2726
|
|
|
2696
2727
|
@job_app.command("create")
|
|
@@ -5743,6 +5774,21 @@ def group_jobs(
|
|
|
5743
5774
|
),
|
|
5744
5775
|
skip: int = typer.Option(0, "--skip", help="Skip the first N entries"),
|
|
5745
5776
|
limit: int = typer.Option(100, "--limit", help="Maximum number of entries to return"),
|
|
5777
|
+
next_token: Optional[str] = typer.Option(
|
|
5778
|
+
None,
|
|
5779
|
+
"--next-token",
|
|
5780
|
+
help="Opaque token returned by the previous Group jobs page",
|
|
5781
|
+
),
|
|
5782
|
+
cursor: bool = typer.Option(
|
|
5783
|
+
False,
|
|
5784
|
+
"--cursor",
|
|
5785
|
+
help="Opt this page into next-token pagination",
|
|
5786
|
+
),
|
|
5787
|
+
no_total: bool = typer.Option(
|
|
5788
|
+
False,
|
|
5789
|
+
"--no-total",
|
|
5790
|
+
help="Skip the server-side count; total becomes null in json/yaml output",
|
|
5791
|
+
),
|
|
5746
5792
|
output_format: str = typer.Option(
|
|
5747
5793
|
None,
|
|
5748
5794
|
"--format",
|
|
@@ -5751,6 +5797,11 @@ def group_jobs(
|
|
|
5751
5797
|
):
|
|
5752
5798
|
"""List jobs in a Group."""
|
|
5753
5799
|
output_format = _normalize_output_format(output_format, keep_table=True)
|
|
5800
|
+
if next_token and skip > 0:
|
|
5801
|
+
raise typer.BadParameter("--next-token cannot be used with --skip greater than 0")
|
|
5802
|
+
if cursor and skip > 0:
|
|
5803
|
+
raise typer.BadParameter("--cursor cannot be used with --skip greater than 0")
|
|
5804
|
+
cursor_request = bool(next_token) or cursor
|
|
5754
5805
|
client = get_client()
|
|
5755
5806
|
result = client.list_group_jobs(
|
|
5756
5807
|
group_id,
|
|
@@ -5758,6 +5809,9 @@ def group_jobs(
|
|
|
5758
5809
|
include_post_process=include_post_process,
|
|
5759
5810
|
skip=skip,
|
|
5760
5811
|
limit=limit,
|
|
5812
|
+
**({"next_token": next_token} if next_token else {}),
|
|
5813
|
+
**({"pagination": "cursor"} if cursor_request else {}),
|
|
5814
|
+
include_total=not no_total,
|
|
5761
5815
|
)
|
|
5762
5816
|
if output_format == "table":
|
|
5763
5817
|
_print_job_list_plain(result, rich=True)
|
|
@@ -5765,6 +5819,9 @@ def group_jobs(
|
|
|
5765
5819
|
_print_job_list_plain(result)
|
|
5766
5820
|
else:
|
|
5767
5821
|
_print_formatted(result, output_format)
|
|
5822
|
+
following_token = result.get("next_token")
|
|
5823
|
+
if output_format in {"table", "plain"} and following_token:
|
|
5824
|
+
typer.echo(f"Next token: {following_token}", err=True)
|
|
5768
5825
|
|
|
5769
5826
|
|
|
5770
5827
|
@group_app.command("stats")
|
|
@@ -6035,7 +6092,7 @@ def main() -> None:
|
|
|
6035
6092
|
except APIError as exc:
|
|
6036
6093
|
typer.echo(_format_api_error(exc), err=True)
|
|
6037
6094
|
raise SystemExit(1) from None
|
|
6038
|
-
except ConfigurationError as exc:
|
|
6095
|
+
except (ConfigurationError, PaginationProtocolError) as exc:
|
|
6039
6096
|
typer.echo(f"error: {exc}", err=True)
|
|
6040
6097
|
raise SystemExit(1) from None
|
|
6041
6098
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ap-client"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
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
|