ap-client 0.3.1.dev1__tar.gz → 0.3.1.dev2__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.dev1
3
+ Version: 0.3.1.dev2
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pyyaml>=6.0
@@ -294,24 +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 _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
297
  def _split_paged(payload: Any) -> tuple[list[dict], dict]:
316
298
  """Split a ``PagedSuccess`` envelope into ``(records, pagination)``.
317
299
 
@@ -815,26 +815,45 @@ def _print_json_block(data: Any) -> None:
815
815
  typer.echo(json.dumps(data, ensure_ascii=False, indent=2))
816
816
 
817
817
 
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
+
818
830
  def _print_detail(data: object) -> None:
819
831
  """Render a single object as an aligned key/value detail view.
820
832
 
821
833
  Top-level scalars become key/value rows (``image`` fields shortened); nested
822
- dict/list fields are shown as compact one-liners. Falls back to JSON for
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
823
836
  non-dict payloads.
824
837
  """
825
838
  if not isinstance(data, dict):
826
839
  _print_json(data)
827
840
  return
828
- scalar_rows: list[tuple[str, Any]] = []
829
- nested_rows: list[tuple[str, Any]] = []
841
+ rows: list[tuple[str, str]] = []
830
842
  for key, value in data.items():
831
843
  if isinstance(value, (dict, list)):
832
- nested_rows.append((key, value))
844
+ rows.append((key, _format_nested_detail(value)))
833
845
  elif key == "image" and isinstance(value, str):
834
- scalar_rows.append((key, _shorten_image(value)))
846
+ rows.append((key, _shorten_image(value)))
847
+ elif value is None:
848
+ continue
835
849
  else:
836
- scalar_rows.append((key, value))
837
- _print_key_values(scalar_rows + nested_rows, skip_empty=False)
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}")
838
857
 
839
858
 
840
859
  def _print_template_get_plain(t: dict) -> None:
@@ -944,7 +963,7 @@ def _print_field_value(value: Any, output_format: str) -> None:
944
963
  _print_formatted(value, "json" if output_format == "plain" else output_format)
945
964
 
946
965
 
947
- def _format_plain_value(value: Any) -> str:
966
+ def _format_plain_value(value: Any, *, max_len: int = _PLAIN_VALUE_PREVIEW) -> str:
948
967
  if value is None:
949
968
  return ""
950
969
  if isinstance(value, (dict, list)):
@@ -952,9 +971,9 @@ def _format_plain_value(value: Any) -> str:
952
971
  else:
953
972
  text = str(value)
954
973
  text = text.replace("\r", "\\r").replace("\n", "\\n")
955
- if len(text) <= _PLAIN_VALUE_PREVIEW:
974
+ if not max_len or len(text) <= max_len:
956
975
  return text
957
- return f"{text[: _PLAIN_VALUE_PREVIEW - 3]}..."
976
+ return f"{text[: max_len - 3]}..."
958
977
 
959
978
 
960
979
  def _print_key_values(rows: list[tuple[str, Any]], *, skip_empty: bool = True) -> None:
@@ -17,6 +17,8 @@ import typer
17
17
  if TYPE_CHECKING: # pragma: no cover - 仅用于类型标注,运行时不 import
18
18
  from datetime import datetime
19
19
 
20
+ from instance_repo import Repo
21
+
20
22
  __all__ = [
21
23
  "dataset_version_app",
22
24
  "dataset_split_app",
@@ -420,7 +422,7 @@ _DATASET_CODE_HINTS: dict[int, str] = {
420
422
  }
421
423
 
422
424
 
423
- def _resolve_benchmark_id(repo: Any, value: str) -> str:
425
+ def _resolve_benchmark_id(repo: "Repo", value: str) -> str:
424
426
  """``--benchmark`` 接受名称或 ``source_id``,统一解析成 ``source_id``(设计 §9)。
425
427
 
426
428
  走 SDK ``benchmarks``:先按 source_id 点查,再按名称模糊搜一遍精确比对
@@ -539,7 +541,10 @@ def _fetch_version(
539
541
  detail = repo.versions.get(dataset, version, **_env_kwargs(env))
540
542
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
541
543
  if version:
542
- hint = f"确认版本名:ap dataset version list --dataset {dataset}"
544
+ hint = (
545
+ f"确认版本名:ap dataset version list --dataset {dataset}"
546
+ ";版本也可能在另一个元数据 environment,加 --environment online/pre 重试"
547
+ )
543
548
  else:
544
549
  hint = f"{_NO_VERSION_NOTE};要操作具体版本请加 --version <version>"
545
550
  raise _fail(
@@ -634,7 +639,7 @@ def _split_names(detail: dict) -> list[str]:
634
639
  return names
635
640
 
636
641
 
637
- def _dataset_repo() -> Any:
642
+ def _dataset_repo() -> "Repo":
638
643
  """dataset 写路径统一走 InstanceRepo SDK;未装 ``dataset`` extra 时退出 2。"""
639
644
  from .irepo_sdk import SdkMissing, load_repo
640
645
 
@@ -645,6 +650,47 @@ def _dataset_repo() -> Any:
645
650
  raise typer.Exit(2) from exc
646
651
 
647
652
 
653
+ def _dataset_available_envs(repo: "Repo", dataset: str) -> list[str]:
654
+ """dataset 元数据分布的 environment 候选;详情读不到时退回固定枚举。"""
655
+ envs: list[str] = []
656
+ try:
657
+ detail = repo.datasets.get_detail(dataset)
658
+ except Exception: # noqa: BLE001 - 引导信息尽力而为,不打断主流程
659
+ detail = None
660
+ raw = detail.get("available_envs") if isinstance(detail, dict) else None
661
+ if isinstance(raw, list):
662
+ envs = [str(env).strip() for env in raw if str(env).strip()]
663
+ from .irepo_sdk import DATASET_ENVIRONMENTS
664
+
665
+ for fallback in DATASET_ENVIRONMENTS:
666
+ if fallback not in envs:
667
+ envs.append(fallback)
668
+ return envs
669
+
670
+
671
+ def _note_environment_distribution(repo: "Repo", dataset: str, probe, *, what: str) -> None:
672
+ """空结果时逐 environment 探测数据分布,引导用户换 ``--environment`` 重试。
673
+
674
+ ``probe(env)`` 返回该 environment 下的条数;读失败按 -1 计。全部为空/读失败
675
+ (dataset 真没数据)时不提示,避免噪声。
676
+ """
677
+ counts: list[tuple[str, int]] = []
678
+ for env in _dataset_available_envs(repo, dataset):
679
+ try:
680
+ counts.append((env, int(probe(env))))
681
+ except Exception: # noqa: BLE001 - 旧 SDK 不接受 environment 等情况
682
+ counts.append((env, -1))
683
+ if not any(count > 0 for _env, count in counts):
684
+ return
685
+ shown = ", ".join(f"{env}={count if count >= 0 else '读失败'}" for env, count in counts)
686
+ with_data = [env for env, count in counts if count > 0]
687
+ suggest = with_data[0] if len(with_data) == 1 else f"<{'/'.join(with_data)}>"
688
+ _note(
689
+ f"{what}在本次查询的 environment 下没有数据;各 environment 探测:{shown}。"
690
+ f"数据可能在另一个元数据 environment,加 --environment {suggest} 重试"
691
+ )
692
+
693
+
648
694
  def _patch_version(
649
695
  dataset: str,
650
696
  version: str,
@@ -1024,6 +1070,43 @@ def dataset_list(
1024
1070
  _print_formatted(payload, output_format)
1025
1071
 
1026
1072
 
1073
+ #: ``dataset get`` 尾部 environment 摘要表的列。
1074
+ _ENVIRONMENT_SUMMARY_COLUMNS: tuple[tuple[str, str], ...] = (
1075
+ ("environment", "ENVIRONMENT"),
1076
+ ("versions", "VERSIONS"),
1077
+ ("splits", "SPLITS"),
1078
+ ("latest_version", "LATEST_VERSION"),
1079
+ )
1080
+
1081
+
1082
+ def _print_dataset_environments(repo: "Repo", dataset: str, detail: dict) -> None:
1083
+ """``dataset get`` 尾部按 environment 汇总 version/split,告诉用户该去哪个 env 指定。
1084
+
1085
+ 仅人读视图(plain/table)追加;``json``/``yaml`` 恒为服务端原样。单个 env 读失败
1086
+ (旧 SDK 不接受 environment、权限差异)显示 ``?``,不影响主输出。
1087
+ """
1088
+ envs = [str(env).strip() for env in detail.get("available_envs") or [] if str(env).strip()]
1089
+ if not envs:
1090
+ return
1091
+ rows: list[dict] = []
1092
+ for env in envs:
1093
+ try:
1094
+ env_detail = _model_dict(repo.datasets.get_detail(dataset, environment=env))
1095
+ except Exception: # noqa: BLE001 - 摘要尽力而为
1096
+ rows.append({"environment": env, "versions": "?", "splits": "?", "latest_version": "?"})
1097
+ continue
1098
+ rows.append(
1099
+ {
1100
+ "environment": env,
1101
+ "versions": env_detail.get("version_count", ""),
1102
+ "splits": env_detail.get("split_count", ""),
1103
+ "latest_version": env_detail.get("latest_version") or "",
1104
+ }
1105
+ )
1106
+ typer.echo("\nEnvironments (add --environment <env> to target one)")
1107
+ _print_table(rows, _ENVIRONMENT_SUMMARY_COLUMNS, "plain")
1108
+
1109
+
1027
1110
  def dataset_get(
1028
1111
  dataset: str = typer.Argument(..., help="Dataset name"),
1029
1112
  environment: Optional[str] = typer.Option(None, "--environment", help=_ENVIRONMENT_HELP),
@@ -1042,12 +1125,10 @@ def dataset_get(
1042
1125
  f"dataset {dataset} 查询失败:{_reason(exc)}",
1043
1126
  f"确认 dataset 名称:ap dataset list --query {dataset}",
1044
1127
  ) from exc
1045
- _emit_detail(
1046
- _model_dict(detail),
1047
- output_format,
1048
- order=_DATASET_DETAIL_ORDER,
1049
- always=("status",),
1050
- )
1128
+ rendered = _model_dict(detail)
1129
+ _emit_detail(rendered, output_format, order=_DATASET_DETAIL_ORDER, always=("status",))
1130
+ if output_format not in ("json", "yaml"):
1131
+ _print_dataset_environments(repo, dataset, rendered)
1051
1132
 
1052
1133
 
1053
1134
  def dataset_create(
@@ -1534,6 +1615,29 @@ def dataset_version_list(
1534
1615
  ) from exc
1535
1616
 
1536
1617
  records = [_model_dict(record) for record in (getattr(result, "items", None) or [])]
1618
+ if not records:
1619
+ # 空结果常见根因是元数据 environment 不匹配(version 落在另一个 env),
1620
+ # 探测一遍并引导切换,而不是只回一张空表。
1621
+ _note_environment_distribution(
1622
+ repo,
1623
+ dataset,
1624
+ lambda env: (
1625
+ _pagination_total(
1626
+ _pagination_envelope(
1627
+ repo.versions.list_paged(
1628
+ dataset,
1629
+ status=resolved_status or None,
1630
+ page=1,
1631
+ page_size=1,
1632
+ environment=env,
1633
+ )
1634
+ ),
1635
+ fallback=0,
1636
+ )
1637
+ or 0
1638
+ ),
1639
+ what="version",
1640
+ )
1537
1641
  if records and all(
1538
1642
  "split_count" not in record and "instance_count" not in record for record in records
1539
1643
  ):
@@ -1651,6 +1755,27 @@ def dataset_split_list(
1651
1755
  rows = [_split_row(record, {}) for record in records]
1652
1756
  if resolved_run_type:
1653
1757
  rows = [row for row in rows if str(row["run_type"]).lower() == resolved_run_type]
1758
+ if not rows and not resolved_run_type:
1759
+ # 空结果常见根因是元数据 environment 不匹配,探测一遍并引导切换。
1760
+ probe_repo = _dataset_repo()
1761
+ if resolved_version:
1762
+
1763
+ def _probe(env: str) -> int:
1764
+ try:
1765
+ detail = probe_repo.versions.get(dataset, resolved_version, environment=env)
1766
+ except Exception: # noqa: BLE001 - 该 env 没有这个版本
1767
+ return -1
1768
+ return len(_split_records(_model_dict(detail)))
1769
+
1770
+ else:
1771
+
1772
+ def _probe(env: str) -> int:
1773
+ page = probe_repo.splits.list(
1774
+ dataset, version_scope="unversioned", page_size=1, environment=env
1775
+ )
1776
+ return _pagination_total(_pagination_envelope(page), fallback=0) or 0
1777
+
1778
+ _note_environment_distribution(probe_repo, dataset, _probe, what="split")
1654
1779
  _emit(
1655
1780
  {"dataset": dataset, "version": resolved_version, "splits": rows},
1656
1781
  rows,
@@ -1878,8 +2003,6 @@ def dataset_split_update(
1878
2003
  if split not in _split_names(detail):
1879
2004
  known = ", ".join(_split_names(detail)) or "<无>"
1880
2005
  hint = f"先创建:ap dataset split create {split} --dataset {dataset}(现有:{known})"
1881
- if not resolved_version:
1882
- hint = f"{hint};{_NO_VERSION_NOTE}"
1883
2006
  raise _fail(
1884
2007
  f"dataset {dataset} 版本 {_version_label(resolved_version)} split {split} "
1885
2008
  f"更新失败:不存在",
@@ -237,14 +237,18 @@ def _sdk_read_kwargs(sdk_obj: Any, method: str, *, environment: str, metadata_mo
237
237
  accepted.add(name)
238
238
 
239
239
  kwargs: dict[str, str] = {}
240
+ dropped: list[str] = []
240
241
  for key, value in (("environment", environment), ("metadata_model", metadata_model)):
241
242
  # 空值不下发:CLI 不替 SDK 决定 metadata_model(见 dataset_commands._metadata_model)。
242
243
  if key in accepted and value:
243
244
  kwargs[key] = value
244
- if "environment" not in accepted:
245
+ elif value:
246
+ dropped.append(key)
247
+ if dropped:
248
+ # 只有真正丢弃了调用方给的值才提示;无条件打印会暗示"想指定却被忽略"。
245
249
  _note(
246
- "已安装的 instance-repo 不支持按调用指定 environment/metadata_model,"
247
- "本次按 profile 派生值读取;需要精确指定请升级到 1.1.0 及以上"
250
+ "已安装的 instance-repo 不支持按调用指定 "
251
+ f"{'/'.join(dropped)},本次按 profile 派生值读取;需要精确指定请升级到 1.1.0 及以上"
248
252
  )
249
253
  return kwargs
250
254
 
@@ -467,9 +471,12 @@ def instance_get(
467
471
  _emit_detail(_instance_record(record), output_format)
468
472
 
469
473
 
474
+ def _confirm_version_autocreate(repo, dataset: str, version: str, split: str, *, yes: bool) -> None:
475
+ """version 不存在时 push 会触发服务端自动创建;要求二次确认(可 ``-y/--yes`` 跳过)。
470
476
 
471
- def _confirm_version_autocreate(dataset: str, version: str, split: str, *, yes: bool) -> None:
472
- """version 不存在时 push 会触发服务端自动创建;要求二次确认(可 ``-y/--yes`` 跳过)。"""
477
+ ``repo`` 由调用方构造并复用:``Repo.__init__`` 会做一次 best-effort 的
478
+ repo-config 自动发现(HTTP),确认与推送共用一个实例,不重复触网。
479
+ """
473
480
  import sys
474
481
 
475
482
  from .dataset_commands import _env_kwargs, _environment, _version_label
@@ -478,7 +485,6 @@ def _confirm_version_autocreate(dataset: str, version: str, split: str, *, yes:
478
485
  if not str(version or "").strip():
479
486
  # 无版本空间:没有"版本会被自动创建"这回事(数据面由 ingest 处理)。
480
487
  return
481
- repo = _load_repo_or_exit()
482
488
  try:
483
489
  repo.versions.get(dataset, version, **_env_kwargs(_environment(None)))
484
490
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
@@ -547,10 +553,9 @@ def instance_push(
547
553
  # ② 版本不存在时 push 会在服务端自动创建;先确认,避免误建。
548
554
  # (split 已发布不可写由**服务端**裁决——pre 环境里 ingest 产出的 split 记录状态
549
555
  # 就是 published,客户端按它预判会把正常的二次上传全拦掉。)
550
- _confirm_version_autocreate(dataset, resolved, split, yes=yes)
551
-
552
- # ③ 数据面推送。
556
+ # ③ 数据面推送(repo 只构造一次,确认与推送复用,见 _confirm_version_autocreate)。
553
557
  repo = _load_repo_or_exit()
558
+ _confirm_version_autocreate(repo, dataset, resolved, split, yes=yes)
554
559
  pushed: list[dict] = []
555
560
  failed: list[dict] = []
556
561
  unconfirmed: list[str] = []
@@ -603,7 +608,13 @@ def instance_push(
603
608
  exc, what=f"dataset {dataset} 的 {len(targets)} 个 instance", stage="上传"
604
609
  )
605
610
  else:
606
- for path, result in zip(targets, list(results_many or [])):
611
+ results_list = list(results_many or [])
612
+ if len(results_list) != len(targets):
613
+ _note(
614
+ f"SDK 返回了 {len(results_list)}/{len(targets)} 个上传结果,摘要可能不完整;"
615
+ f"用 ap instance list --dataset {dataset} --split {split} 核对实际内容"
616
+ )
617
+ for path, result in zip(targets, results_list):
607
618
  pushed.append(_push_row(path, result))
608
619
 
609
620
  _print_push_summary(dataset, resolved, split, pushed, failed, unconfirmed, output_format)
@@ -41,7 +41,13 @@ from .config import Config, _parse_bool
41
41
  if TYPE_CHECKING: # pragma: no cover - 仅供类型检查,运行时不 import SDK
42
42
  from instance_repo import Repo
43
43
 
44
- __all__ = ["SdkMissing", "is_already_satisfied", "load_repo", "sdk_error_exit"]
44
+ __all__ = [
45
+ "SdkMissing",
46
+ "is_already_satisfied",
47
+ "load_repo",
48
+ "sdk_error_exit",
49
+ "update_version_detail",
50
+ ]
45
51
 
46
52
  #: 数据集元数据的环境枚举(服务端 ``dataset environment``)。
47
53
  DATASET_ENVIRONMENTS = ("online", "pre")
@@ -57,7 +63,7 @@ _INSTALL_HINT = (
57
63
  _LEGACY_SDK_NOTE = (
58
64
  "note: installed instance-repo does not accept Repo(api_base=/cluster=); "
59
65
  "falling back to INSTANCEREPO_* environment injection. "
60
- "Upgrade with: pip install -U 'instance-repo[oss]>=1.0.9' "
66
+ "Upgrade with: pip install -U 'instance-repo[oss]>=1.1.0' "
61
67
  "for cluster<->storage_env validation and symmetric read/write paths."
62
68
  )
63
69
 
@@ -210,7 +216,7 @@ _BIZ_CODE_RULES: dict[int, tuple[str, int]] = {
210
216
  ),
211
217
  94018: ("当前角色只读,写操作需要 writer 角色;让 admin 执行 ap dataset access grant", 1),
212
218
  94019: ("dataset 标识冲突:检查 --dataset 的取值是否与已有 dataset 重名", 1),
213
- 94020: ("请求未带 dataset 标识(SDK 过旧),升级:pip install -U 'instance-repo[oss]>=1.0.9'", 1),
219
+ 94020: ("请求未带 dataset 标识(SDK 过旧),升级:pip install -U 'instance-repo[oss]>=1.1.0'", 1),
214
220
  94021: ("该 dataset 不存在,先执行:ap dataset create <dataset> --benchmark <b>", 1),
215
221
  94022: (
216
222
  "无该用户存储空间的授权,让空间主人执行:ap fs access grant --user <你的工号> --role reader",
@@ -232,7 +238,7 @@ _CODE_RULES: dict[str, tuple[str, int]] = {
232
238
  "E_DIGEST_MISMATCH": ("内容校验失败,重新拉取或重新打包该 instance 后再试", 1),
233
239
  "E_IMMUTABLE": ("已发布的版本不可修改,创建新版本再操作", 1),
234
240
  "E_FORBIDDEN": ("当前身份无权执行该操作,确认 AP_API_KEY 与 dataset 角色", 1),
235
- "E_OWNER_REF_MISSING": ("缺少上游来源引用,升级:pip install -U 'instance-repo[oss]>=1.0.9'", 1),
241
+ "E_OWNER_REF_MISSING": ("缺少上游来源引用,升级:pip install -U 'instance-repo[oss]>=1.1.0'", 1),
236
242
  "E_LAYOUT": ("instance 目录结构不合规,先执行 ap instance validate <path> 定位问题", 1),
237
243
  "E_SCHEMA": ("instance 元数据不符合 schema,先执行 ap instance validate <path> 定位问题", 1),
238
244
  "E_CRED_EXPIRED": ("临时凭据已过期,重试该命令即可重新换取", 1),
@@ -44,7 +44,10 @@ def dataset_split_publish(
44
44
  if not yes:
45
45
  if not sys.stdin.isatty():
46
46
  raise _fail("非交互发布需要确认", "加 --yes 确认将该 split 发布到 online", 2)
47
- if not typer.confirm(f"Publish {dataset}/{resolved_version}/{split} to online?"):
47
+ target = (
48
+ f"{dataset}/{resolved_version}/{split}" if resolved_version else f"{dataset}/{split}"
49
+ )
50
+ if not typer.confirm(f"Publish {target} to online?"):
48
51
  raise _fail("发布已取消,未提交工作流", "确认后重跑", 1)
49
52
 
50
53
  try:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.3.1.dev1"
7
+ version = "0.3.1.dev2"
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"