ap-client 0.3.1.dev2__tar.gz → 0.3.2.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.1.dev2
3
+ Version: 0.3.2.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]>=1.1.0; extra == 'all'
12
+ Requires-Dist: instance-repo[oss]>=0.7.0; extra == 'all'
13
13
  Provides-Extra: dataset
14
- Requires-Dist: instance-repo[oss]>=1.1.0; extra == 'dataset'
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.
@@ -294,25 +294,6 @@ def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
294
294
  return {"workspace_id": workspace_id} if workspace_id else None
295
295
 
296
296
 
297
- def _split_paged(payload: Any) -> tuple[list[dict], dict]:
298
- """Split a ``PagedSuccess`` envelope into ``(records, pagination)``.
299
-
300
- ``PagedSuccess`` is ``{code,message,data:[...],pagination:{total,page,page_size}}``.
301
- A bare list (or a null ``data``) yields an empty pagination dict so callers
302
- can render results without probing the response shape.
303
- """
304
- if isinstance(payload, list):
305
- return [item for item in payload if isinstance(item, dict)], {}
306
- if not isinstance(payload, dict):
307
- return [], {}
308
- records = payload.get("data")
309
- pagination = payload.get("pagination")
310
- return (
311
- [item for item in (records or []) if isinstance(item, dict)],
312
- pagination if isinstance(pagination, dict) else {},
313
- )
314
-
315
-
316
297
  class APIClient:
317
298
  """Agent Platform API client."""
318
299
 
@@ -733,35 +714,8 @@ class APIClient:
733
714
  return self._get(f"/templates/{quote(name)}", params=params)
734
715
 
735
716
  def list_benchmarks(self) -> list:
736
- """List all benchmarks from the central registry, preserving legacy IDs."""
737
- benchmarks = []
738
- page = 1
739
- page_size = 200
740
- # 与 CLI dataset list 的自动翻页同口径:100 页上限,防分页异常导致死循环。
741
- max_pages = 100
742
- while page <= max_pages:
743
- payload = self._get(
744
- "/apis/v1/benchmarks", params={"page": page, "page_size": page_size}
745
- )
746
- records, pagination = _split_paged(payload)
747
- for record in records:
748
- item = dict(record)
749
- if "source_id" in item:
750
- item.setdefault("id", item["source_id"])
751
- benchmarks.append(item)
752
- if not records or not pagination:
753
- break
754
- try:
755
- total = int(pagination.get("total"))
756
- except (TypeError, ValueError):
757
- total = None
758
- if total is not None:
759
- if len(benchmarks) >= total:
760
- break
761
- elif len(records) < page_size:
762
- break
763
- page += 1
764
- return benchmarks
717
+ """List all active benchmarks from the local benchmark registry."""
718
+ return self._get("/benchmarks")
765
719
 
766
720
  def get_benchmark(self, name: str) -> dict:
767
721
  """Get a single benchmark by exact name."""
@@ -896,16 +850,7 @@ class APIClient:
896
850
  "instance_ids": all_instance_ids,
897
851
  }
898
852
 
899
- # ==================== Dataset series (apiserver dataset domain) ====================
900
- #
901
- # 这一节对接 Go apiserver 的 dataset 领域(/apis/v1/datasets/...),与上面
902
- # ossdata 目录的 /api/datasets 路由完全无关。
903
- #
904
- # **dataset 领域的控制面读写全部在 InstanceRepo SDK 上**(见
905
- # docs/ap-cli-dataset-refactor-design.md §9.5):迁移期加进这里的 registry 方法、
906
- # 以及最后三条"push 前检查 + publish-status"的旧读,都已随迁移删除。
907
- # 本文件现在只服务旧 ossdata 路径(`dataset list --legacy`、弃用的
908
- # `dataset versions/instances`、`job create --dataset`)。
853
+ # ==================== Meta operations ====================
909
854
 
910
855
  def list_meta_models(self) -> dict:
911
856
  """模型词表(apiserver 权威源 GET /apis/v1/meta-models,返回 {items:[...]})。"""
@@ -24,10 +24,8 @@ from ap_client.api import (
24
24
  set_verbose_override,
25
25
  )
26
26
  from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
27
- from ap_client.dataset_commands import register as _register_dataset_commands
28
27
  from ap_client.exporter import export_group, export_job
29
- from ap_client.fs_commands import register as _register_fs_commands
30
- from ap_client.instance_commands import register as _register_instance_commands
28
+ from ap_client.irepo_commands import register as _register_dataset_repo
31
29
  from ap_client.managed_ak import (
32
30
  ManagedAKProtocolError,
33
31
  require_managed_ak_ack,
@@ -155,11 +153,8 @@ app.add_typer(meta_job_type_app, name="meta-job-type")
155
153
  app.add_typer(benchmark_app, name="benchmark")
156
154
  app.add_typer(checkpoint_app, name="checkpoint")
157
155
 
158
- # Dataset 领域(见 docs/ap-cli-dataset-refactor-design.md):
159
- # ap dataset version/split/access 嵌套在 dataset 下;instance/fs 是一等资源。
160
- _register_dataset_commands(dataset_app)
161
- _register_instance_commands(app)
162
- _register_fs_commands(app)
156
+ # ap dataset repo:透传 instance_repo CLI(见 ap_client/irepo_commands.py)
157
+ _register_dataset_repo(dataset_app)
163
158
 
164
159
  _PAI_RUNTIME_ENV_TAGS: tuple[tuple[str, str], ...] = (
165
160
  ("DLC_JOB_ID", "dlc_job_id"),
@@ -815,45 +810,26 @@ def _print_json_block(data: Any) -> None:
815
810
  typer.echo(json.dumps(data, ensure_ascii=False, indent=2))
816
811
 
817
812
 
818
- def _format_nested_detail(value: Any) -> str:
819
- """Nested dict/list for detail views: compact one-liner, pretty JSON when long.
820
-
821
- A detail view must not silently drop data - the manifest in ``ap instance
822
- get`` used to be cut mid-JSON by the one-liner preview.
823
- """
824
- compact = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
825
- if len(compact) <= _PLAIN_VALUE_PREVIEW:
826
- return compact
827
- return json.dumps(value, ensure_ascii=False, indent=2)
828
-
829
-
830
813
  def _print_detail(data: object) -> None:
831
814
  """Render a single object as an aligned key/value detail view.
832
815
 
833
816
  Top-level scalars become key/value rows (``image`` fields shortened); nested
834
- dict/list fields are shown as compact one-liners, or pretty-printed across
835
- multiple lines when the one-liner would not fit. Falls back to JSON for
817
+ dict/list fields are shown as compact one-liners. Falls back to JSON for
836
818
  non-dict payloads.
837
819
  """
838
820
  if not isinstance(data, dict):
839
821
  _print_json(data)
840
822
  return
841
- rows: list[tuple[str, str]] = []
823
+ scalar_rows: list[tuple[str, Any]] = []
824
+ nested_rows: list[tuple[str, Any]] = []
842
825
  for key, value in data.items():
843
826
  if isinstance(value, (dict, list)):
844
- rows.append((key, _format_nested_detail(value)))
827
+ nested_rows.append((key, value))
845
828
  elif key == "image" and isinstance(value, str):
846
- rows.append((key, _shorten_image(value)))
847
- elif value is None:
848
- continue
829
+ scalar_rows.append((key, _shorten_image(value)))
849
830
  else:
850
- rows.append((key, _format_plain_value(value, max_len=0)))
851
- width = max((len(label) for label, _text in rows), default=0)
852
- indent = " " * (width + 4)
853
- for label, text in rows:
854
- if "\n" in text:
855
- text = text.replace("\n", f"\n{indent}")
856
- typer.echo(f" {label.ljust(width)}: {text}")
831
+ scalar_rows.append((key, value))
832
+ _print_key_values(scalar_rows + nested_rows, skip_empty=False)
857
833
 
858
834
 
859
835
  def _print_template_get_plain(t: dict) -> None:
@@ -963,7 +939,7 @@ def _print_field_value(value: Any, output_format: str) -> None:
963
939
  _print_formatted(value, "json" if output_format == "plain" else output_format)
964
940
 
965
941
 
966
- def _format_plain_value(value: Any, *, max_len: int = _PLAIN_VALUE_PREVIEW) -> str:
942
+ def _format_plain_value(value: Any) -> str:
967
943
  if value is None:
968
944
  return ""
969
945
  if isinstance(value, (dict, list)):
@@ -971,9 +947,9 @@ def _format_plain_value(value: Any, *, max_len: int = _PLAIN_VALUE_PREVIEW) -> s
971
947
  else:
972
948
  text = str(value)
973
949
  text = text.replace("\r", "\\r").replace("\n", "\\n")
974
- if not max_len or len(text) <= max_len:
950
+ if len(text) <= _PLAIN_VALUE_PREVIEW:
975
951
  return text
976
- return f"{text[: max_len - 3]}..."
952
+ return f"{text[: _PLAIN_VALUE_PREVIEW - 3]}..."
977
953
 
978
954
 
979
955
  def _print_key_values(rows: list[tuple[str, Any]], *, skip_empty: bool = True) -> None:
@@ -2092,17 +2068,24 @@ def template_fetch(
2092
2068
 
2093
2069
  # ==================== Dataset operations ====================
2094
2070
 
2095
- # 设计 §6 / 上游 §13.2:`ap dataset versions` 与 `ap dataset instances` 行为保持不变
2096
- # (仍打 ossdata 路由,`ap job create` 的配套流程依赖它们),只追加固定弃用文案。
2097
- # 文案**只写 stderr**——JSON/YAML 的 stdout 必须仍可被 json.loads 直接消费。
2098
- _DATASET_VERSIONS_DEPRECATION = (
2099
- "Warning: `ap dataset versions` is deprecated.\n"
2100
- "Use `ap dataset version list --dataset <dataset>` instead."
2101
- )
2102
- _DATASET_INSTANCES_DEPRECATION = (
2103
- "Warning: `ap dataset instances` is deprecated.\n"
2104
- "Use `ap instance list --dataset <dataset> --split <split> [--version <version>]` instead."
2105
- )
2071
+
2072
+ @dataset_app.command("list")
2073
+ def dataset_list(
2074
+ search: Optional[str] = typer.Argument(None, help="Search keyword"),
2075
+ output_format: str = typer.Option(
2076
+ None,
2077
+ "--format",
2078
+ help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2079
+ ),
2080
+ ):
2081
+ """List all datasets."""
2082
+ output_format = _normalize_output_format(output_format, keep_table=True)
2083
+ client = get_client()
2084
+ result = client.list_all_datasets(search)
2085
+ if output_format == "table":
2086
+ _print_records_table(result)
2087
+ else:
2088
+ _print_formatted(result, output_format)
2106
2089
 
2107
2090
 
2108
2091
  @dataset_app.command("versions")
@@ -2114,9 +2097,8 @@ def dataset_versions(
2114
2097
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2115
2098
  ),
2116
2099
  ):
2117
- """List dataset versions (deprecated: use 'ap dataset version list')."""
2100
+ """List dataset versions."""
2118
2101
  output_format = _normalize_output_format(output_format, keep_table=True)
2119
- _emit_progress(_DATASET_VERSIONS_DEPRECATION)
2120
2102
  client = get_client()
2121
2103
  versions = client.list_dataset_versions(dataset)
2122
2104
  if output_format == "table":
@@ -2136,9 +2118,8 @@ def dataset_instances(
2136
2118
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2137
2119
  ),
2138
2120
  ):
2139
- """List dataset instances (deprecated: use 'ap instance list')."""
2121
+ """List dataset instances."""
2140
2122
  output_format = _normalize_output_format(output_format, keep_table=True)
2141
- _emit_progress(_DATASET_INSTANCES_DEPRECATION)
2142
2123
  client = get_client()
2143
2124
  result = client.list_all_dataset_instances(dataset_version)
2144
2125
  if output_format == "table":
@@ -4931,7 +4912,7 @@ def benchmark_list(
4931
4912
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
4932
4913
  ),
4933
4914
  ):
4934
- """List benchmarks from the central benchmark registry."""
4915
+ """List benchmarks from the local benchmark registry."""
4935
4916
  output_format = _normalize_output_format(output_format, keep_table=True)
4936
4917
  client = get_client()
4937
4918
  result = client.list_benchmarks()
@@ -0,0 +1,131 @@
1
+ """``ap dataset repo`` — InstanceRepo CLI 的原样透传入口。
2
+
3
+ 设计见 docs/ap-cli-irepo-integration-design.md:``repo`` 之后的原始参数
4
+ 原样交给 ``instance_repo.cli.cli_main``,参数解析、help 文本、错误码、
5
+ 退出码全部来自 irepo 自身。irepo 发版新增/修改命令时本模块无需改动,
6
+ 升级 instance-repo 包即可。
7
+
8
+ Typer/Click 会在生成 ``ctx.args`` 前消费第一个 ``--`` 分隔符,因此
9
+ 透传前从原始 argv 把它恢复回去,保证 ``ap dataset repo validate -- -foo``
10
+ 这类"路径以 ``-`` 开头"的用法与原生 irepo 行为一致。
11
+
12
+ instance-repo 以带 OSS extra 的公网 PyPI 包作为 ap-client 的 dataset 可选依赖。
13
+ 这里采用懒加载,使未安装 extra 时其它 ap 子命令仍可正常使用,并给出
14
+ 明确的安装提示。
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import sys
21
+
22
+ import typer
23
+
24
+ _REPO_HELP = (
25
+ "Dataset repository operations. Arguments match the native InstanceRepo CLI exactly; "
26
+ "run 'ap dataset repo <cmd> --help' for details"
27
+ )
28
+
29
+ _INSTALL_HINT = (
30
+ "ap dataset repo requires Dataset authoring support, which is not installed in "
31
+ "this environment.\n"
32
+ "Install it with: pip install 'ap-client[dataset]'"
33
+ )
34
+
35
+ _AP_TOKEN_ENV_NAMES = ("AP_API_KEY", "AP_API_KEYS", "AP_TOKEN_KEY")
36
+
37
+
38
+ def _inject_irepo_env() -> None:
39
+ """把 ap 的连接/认证配置翻译成 irepo 的环境变量。
40
+
41
+ 注入语义均为 setdefault——用户显式设置的 irepo 变量(INSTANCEREPO_*、
42
+ IR_*)优先,ap 只兜底:
43
+
44
+ - ``IR_API_ENV`` ← ap 的 ``Env`` 头(由 --cluster / AP_CLUSTER 推导,
45
+ 或 AP_HEADERS 显式指定)。无条件注入:irepo 的 api_env 注册表会据此
46
+ 解析控制面 URL 和 Env 头,与 ap 自身的网关路由保持一致。
47
+ - ``INSTANCEREPO_API_BASE`` ← ap 解析后的 base_url。instance-repo 0.7
48
+ 不再内置控制面地址,因此默认、自定义和 local 地址都必须注入。
49
+ - ``INSTANCEREPO_TOKEN`` ← ap 解析出的单个 API Key。
50
+ """
51
+ from ap_client import api as _api
52
+ from ap_client.config import Config, ConfigurationError
53
+
54
+ try:
55
+ cfg = Config.from_env(cluster=_api._cluster_override)
56
+ token = cfg.token_key
57
+ except ConfigurationError:
58
+ # token 不合法(如服务端用的逗号分隔 AP_API_KEYS)不影响连接注入:
59
+ # 掩掉 token 变量重新解析,本次仅跳过 INSTANCEREPO_TOKEN 注入。
60
+ masked = {name: os.environ.pop(name) for name in _AP_TOKEN_ENV_NAMES if name in os.environ}
61
+ try:
62
+ cfg = Config.from_env(cluster=_api._cluster_override)
63
+ finally:
64
+ os.environ.update(masked)
65
+ token = None
66
+
67
+ env_header = next((v for k, v in cfg.headers.items() if k.lower() == "env"), None)
68
+ if env_header:
69
+ os.environ.setdefault("IR_API_ENV", env_header)
70
+ os.environ.setdefault("INSTANCEREPO_API_BASE", cfg.base_url)
71
+ if token:
72
+ os.environ.setdefault("INSTANCEREPO_TOKEN", token)
73
+
74
+
75
+ def _restore_double_dash(args: list[str]) -> list[str]:
76
+ """把被 Click 消费的 ``--`` 分隔符插回透传参数。
77
+
78
+ Click 生成 ``ctx.args`` 前会消费第一个 ``--``,于是
79
+ ``ap dataset repo validate -- -foo`` 实际只透传 ``["validate", "-foo"]``,
80
+ irepo 的 argparse 会把 ``-foo`` 识别成选项而报缺 ``dir`` 参数
81
+ (退出码 2);原生 irepo 则靠 ``--`` 把它解析为位置参数。
82
+
83
+ 恢复依据:Click 消费 ``--`` 后,其后的原始 token 会原样落在
84
+ ``ctx.args`` 尾部。据此从 ``sys.argv`` 找回 ``--`` 的位置并插回;
85
+ 从左到右尝试每个 ``--``,尾部对不上的(``--`` 与 irepo 无关的
86
+ 病态输入)跳过,全部对不上时原样返回,不改变既有行为。
87
+ """
88
+ for idx, token in enumerate(sys.argv):
89
+ if token != "--":
90
+ continue
91
+ tail = sys.argv[idx + 1 :]
92
+ if not tail:
93
+ return [*args, "--"]
94
+ if len(tail) <= len(args) and args[-len(tail) :] == tail:
95
+ split = len(args) - len(tail)
96
+ return [*args[:split], "--", *args[split:]]
97
+ return args
98
+
99
+
100
+ def dataset_repo_passthrough(ctx: typer.Context) -> None:
101
+ """把 ``ap dataset repo`` 之后的参数原样透传给 instance_repo CLI。"""
102
+ try:
103
+ from instance_repo.cli import cli_main
104
+ except ImportError:
105
+ typer.echo(_INSTALL_HINT, err=True)
106
+ raise typer.Exit(code=2) from None
107
+
108
+ _inject_irepo_env()
109
+ try:
110
+ rc = cli_main(_restore_double_dash(list(ctx.args)))
111
+ except SystemExit as exc:
112
+ # argparse 的 --help / 参数错误以 SystemExit(int) 抛出,原样透传退出码;
113
+ # irepo 自身也会 raise SystemExit(str)(如 ref 段数错误),按 CPython
114
+ # 语义把消息打到 stderr、退出码 1,与原生 irepo 行为逐字节对齐。
115
+ if isinstance(exc.code, int):
116
+ raise typer.Exit(code=exc.code) from None
117
+ if exc.code:
118
+ typer.echo(str(exc.code), err=True)
119
+ raise typer.Exit(code=1) from None
120
+ if rc:
121
+ raise typer.Exit(code=rc)
122
+
123
+
124
+ def register(app: typer.Typer) -> None:
125
+ """把 InstanceRepo 透传命令挂到 ``ap dataset`` 下。"""
126
+ app.command(
127
+ "repo",
128
+ help=_REPO_HELP,
129
+ context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
130
+ add_help_option=False, # --help 留给 InstanceRepo 的 argparse 处理
131
+ )(dataset_repo_passthrough)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.3.1.dev2"
7
+ version = "0.3.2.dev1"
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"
@@ -18,10 +18,10 @@ dependencies = [
18
18
 
19
19
  [project.optional-dependencies]
20
20
  dataset = [
21
- "instance-repo[oss]>=1.1.0",
21
+ "instance-repo[oss]>=0.7.0",
22
22
  ]
23
23
  all = [
24
- "instance-repo[oss]>=1.1.0",
24
+ "instance-repo[oss]>=0.7.0",
25
25
  ]
26
26
 
27
27
  [project.scripts]