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.
@@ -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
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pyyaml>=6.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
- return self._get("/jobs", params=params)
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
- return self._get(f"/groups/{quote(group_id)}/jobs", params=params)
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
- return self._get(
2025
- f"/groups/{quote(group_id)}/artifacts",
2026
- params=params,
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
- total = page.get("total")
2047
- if total is not None and len(artifacts) >= total:
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
- return {
2055
- **last_page,
2056
- "skip": 0,
2057
- "limit": len(artifacts),
2058
- "artifacts": artifacts,
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.0.dev0"
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"