ap-client 0.5.0.dev1__tar.gz → 0.5.1.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.
@@ -1,18 +1,17 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.5.0.dev1
3
+ Version: 0.5.1.dev0
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
- Requires-Dist: irepo-artifact==0.2.0
7
6
  Requires-Dist: pyyaml>=6.0
8
7
  Requires-Dist: requests>=2.28.0
9
8
  Requires-Dist: rich>=13.0.0
10
9
  Requires-Dist: typer>=0.9.0
11
10
  Requires-Dist: websockets>=13.0
12
11
  Provides-Extra: all
13
- Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.4; extra == 'all'
12
+ Requires-Dist: instance-repo[oss]==1.3.2.dev0; extra == 'all'
14
13
  Provides-Extra: dataset
15
- Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.4; extra == 'dataset'
14
+ Requires-Dist: instance-repo[oss]==1.3.2.dev0; extra == 'dataset'
16
15
  Description-Content-Type: text/markdown
17
16
 
18
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.
@@ -63,9 +63,7 @@ def _fmt(output_format: Optional[str]) -> str:
63
63
  def _check_time_range(value: str) -> str:
64
64
  normalized = str(value or "").strip().lower()
65
65
  if not _TIME_RANGE_RE.fullmatch(normalized):
66
- raise typer.BadParameter(
67
- f"--time-range must be a duration like 30s/5m/1h, got {value!r}"
68
- )
66
+ raise typer.BadParameter(f"--time-range must be a duration like 30s/5m/1h, got {value!r}")
69
67
  return normalized
70
68
 
71
69
 
@@ -123,14 +123,6 @@ _UNSET: Any = object()
123
123
  # when aggregation runs later.
124
124
  EXTRA_AGG_OPS = ("avg", "sum")
125
125
 
126
- # Others 占位:apiserver 词表不返回,CLI 展示与 get_meta_model("Others") 兜底用。
127
- _OTHERS_MODEL = {
128
- "name": "Others",
129
- "series_name": "",
130
- "source_id": "",
131
- "series_id": "",
132
- }
133
-
134
126
 
135
127
  def _merge_meta_tags(
136
128
  tags: Optional[list],
@@ -976,18 +968,19 @@ class APIClient:
976
968
  # `dataset versions/instances`、`job create --dataset`)。
977
969
 
978
970
  def list_meta_models(self) -> dict:
979
- """模型词表(apiserver 权威源 GET /apis/v1/meta-models,返回 {items:[...]})。"""
980
- return self._get_central("/meta-models")
971
+ """从 meta-model 标签目录获取可用模型,保留 list 的 items 响应形状。"""
972
+ items = self.get_meta_tags("meta-model").get("items") or []
973
+ return {
974
+ "items": [
975
+ {**item, "name": item["value"]}
976
+ for item in items
977
+ if item.get("value") and item.get("is_enabled", True)
978
+ ]
979
+ }
981
980
 
982
981
  def get_meta_model(self, name: str) -> dict:
983
- """单个模型详情(按 name 或 source_id 从 apiserver list 过滤;Others 返回占位)。
984
-
985
- apiserver 没有单模型详情接口,客户端从 list 结果过滤;找不到 raise
986
- APIError 404,与原 cluster AP /meta/models/{name} 行为一致。
987
- """
988
- if name == "Others":
989
- return dict(_OTHERS_MODEL)
990
- result = self._get_central("/meta-models")
982
+ """从启用的 meta-model 标签值中按 value 或 source_id 查找。"""
983
+ result = self.list_meta_models()
991
984
  for item in result.get("items") or []:
992
985
  if item.get("name") == name or item.get("source_id") == name:
993
986
  return item
@@ -1053,43 +1046,13 @@ class APIClient:
1053
1046
  return result
1054
1047
 
1055
1048
  def list_meta_job_types(self) -> dict:
1056
- """任务类型词表(目录驱动:GET /apis/v1/meta-tags?key=meta-job-type)。
1057
-
1058
- 仅取目录中 ``is_enabled`` 的值(缺失按启用解析)+ 末尾 ``Others``
1059
- 兜底行;``default`` 为 ``Eval-Eval``(在目录中时,否则空串)。
1060
- 目录请求失败或为空 → 回退本地常量(Eval-Eval + Others,fail-open),
1061
- 并在 stderr 打一行告警,词表降级可见但不阻塞命令。
1062
- """
1063
- error: Exception | None = None
1064
- try:
1065
- items = self.get_meta_tags("meta-job-type").get("items") or []
1066
- except Exception as exc: # fail-open:目录不可用不阻塞词表命令
1067
- error = exc
1068
- items = []
1069
- if items:
1070
- options = []
1071
- for item in items:
1072
- value = str(item.get("value") or "").strip()
1073
- if value and item.get("is_enabled", True):
1074
- options.append({"value": value})
1075
- if options:
1076
- default = "Eval-Eval" if any(o["value"] == "Eval-Eval" for o in options) else ""
1077
- return {
1078
- "meta_job_type": {
1079
- "default": default,
1080
- "options": [*options, {"value": "Others"}],
1081
- }
1082
- }
1083
- _write_stderr(
1084
- [
1085
- "warning: meta-job-type catalog unavailable or empty, "
1086
- f"falling back to local constants (reason={error or 'empty catalog'})"
1087
- ]
1088
- )
1049
+ """从 meta-job-type 标签目录获取启用的任务类型,不补本地展示值。"""
1050
+ items = self.get_meta_tags("meta-job-type").get("items") or []
1089
1051
  return {
1090
1052
  "meta_job_type": {
1091
- "default": "Eval-Eval",
1092
- "options": [{"value": "Eval-Eval"}, {"value": "Others"}],
1053
+ "options": [
1054
+ item for item in items if item.get("value") and item.get("is_enabled", True)
1055
+ ],
1093
1056
  }
1094
1057
  }
1095
1058
 
@@ -15,7 +15,6 @@ import typer
15
15
  from ap_client import __version__, get_client, get_config, tbb
16
16
  from ap_client.akpool_commands import register as _register_akpool_commands
17
17
  from ap_client.api import (
18
- _OTHERS_MODEL,
19
18
  APIError,
20
19
  PaginationProtocolError,
21
20
  _is_sensitive_key,
@@ -26,7 +25,7 @@ from ap_client.api import (
26
25
  )
27
26
  from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
28
27
  from ap_client.dataset_commands import register as _register_dataset_commands
29
- from ap_client.exporter import export_group, export_job, export_jobs
28
+ from ap_client.exporter import export_group, export_job
30
29
  from ap_client.fs_commands import register as _register_fs_commands
31
30
  from ap_client.instance_commands import register as _register_instance_commands
32
31
  from ap_client.managed_ak import (
@@ -2689,23 +2688,22 @@ def meta_model_list(
2689
2688
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2690
2689
  ),
2691
2690
  ):
2692
- """List meta model options (apiserver data + Others as the last row)."""
2691
+ """List enabled meta-model tag values."""
2693
2692
  output_format = _normalize_output_format(output_format, keep_table=True)
2694
2693
  result = get_client().list_meta_models()
2695
2694
  items = result.get("items") or []
2696
- options = [*items, dict(_OTHERS_MODEL)] # 末尾固定追加 Others 兜底手动输入
2695
+ options = items
2697
2696
  if output_format in ("plain", "table"):
2698
2697
  rows = [
2699
2698
  {
2700
2699
  "name": m.get("name") or "-",
2701
- "series": m.get("series_name") or "-",
2700
+ "desc": m.get("desc_l1") or "-",
2702
2701
  }
2703
2702
  for m in options
2704
2703
  ]
2705
- _print_plain_table(rows, [("name", "name"), ("series", "series")])
2704
+ _print_plain_table(rows, [("name", "name"), ("desc", "desc")])
2706
2705
  else:
2707
- # json/yaml 包装成 {meta_model:{options}}(原 cluster AP 格式;
2708
- # default 已随 meta-models default 逻辑移除而下线)
2706
+ # JSON/YAML 保持 {meta_model:{options}};每项保留目录字段及 name 别名。
2709
2707
  _print_formatted({"meta_model": {"options": options}}, output_format)
2710
2708
 
2711
2709
 
@@ -2796,21 +2794,20 @@ def meta_job_type_list(
2796
2794
  help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
2797
2795
  ),
2798
2796
  ):
2799
- """List meta job type options (catalog-driven; falls back to Eval-Eval/Others)."""
2797
+ """List enabled meta-job-type tag values."""
2800
2798
  output_format = _normalize_output_format(output_format, keep_table=True)
2801
2799
  result = get_client().list_meta_job_types()
2802
2800
  info = result.get("meta_job_type") or {}
2803
2801
  options = info.get("options") or []
2804
- default = info.get("default") or ""
2805
2802
  if output_format in ("plain", "table"):
2806
2803
  rows = [
2807
2804
  {
2808
2805
  "value": o.get("value") or "-",
2809
- "default": "*" if o.get("value") and o.get("value") == default else "",
2806
+ "desc": o.get("desc_l1") or "-",
2810
2807
  }
2811
2808
  for o in options
2812
2809
  ]
2813
- _print_plain_table(rows, [("value", "value"), ("default", "default")])
2810
+ _print_plain_table(rows, [("value", "value"), ("desc", "desc")])
2814
2811
  else:
2815
2812
  _print_formatted(result, output_format)
2816
2813
 
@@ -4879,47 +4876,10 @@ def job_artifacts(
4879
4876
  _print_formatted(results, output_format)
4880
4877
 
4881
4878
 
4882
- def _validate_export_timeout(value: float) -> float:
4883
- import math
4884
-
4885
- if not math.isfinite(value) or value <= 0:
4886
- raise typer.BadParameter("Timeout must be a finite number greater than zero")
4887
- return value
4888
-
4889
-
4890
- def _export_channel_reporter(emit):
4891
- """Report completed Jobs, including callbacks from Group worker threads."""
4892
- from threading import Lock
4893
-
4894
- from rich.text import Text
4895
-
4896
- counts = {"oss": 0, "proxy": 0}
4897
- lock = Lock()
4898
-
4899
- def report(job_id, status, channel):
4900
- with lock:
4901
- suffix = ""
4902
- if status == "downloaded" and channel in counts:
4903
- counts[channel] += 1
4904
- suffix = f" channel={channel}"
4905
- emit(Text(f"{job_id} {status}{suffix}"))
4906
-
4907
- return report, counts
4908
-
4909
-
4910
4879
  @job_app.command("export")
4911
4880
  def job_export(
4912
- job_id: str = typer.Argument(..., help="Job ID or comma-separated Job IDs"),
4881
+ job_id: str = typer.Argument(..., help="Job ID"),
4913
4882
  output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
4914
- workers: int = typer.Option(
4915
- 1, "--workers", "-w", min=1, help="Number of concurrent export workers"
4916
- ),
4917
- timeout: float = typer.Option(
4918
- 300,
4919
- "--timeout",
4920
- callback=_validate_export_timeout,
4921
- help="SDK request timeout in seconds, not the total export duration",
4922
- ),
4923
4883
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
4924
4884
  events: bool = typer.Option(False, "--events", help="Include events"),
4925
4885
  no_extract_artifacts: bool = typer.Option(
@@ -4929,50 +4889,17 @@ def job_export(
4929
4889
  ),
4930
4890
  ):
4931
4891
  """Export job artifacts to a local directory. Use --logs/--events to include logs and events."""
4932
- ids = list(dict.fromkeys(value.strip() for value in job_id.split(",")))
4933
- if not all(ids):
4934
- raise typer.BadParameter("Job IDs must not be empty", param_hint="job_id")
4935
4892
  client = get_client()
4936
- if len(ids) > 1:
4937
- report, channels = _export_channel_reporter(print)
4938
- dest, summary = export_jobs(
4939
- client,
4940
- ids,
4941
- output,
4942
- workers=workers,
4943
- result_callback=report,
4944
- timeout=timeout,
4945
- include_logs=logs,
4946
- include_events=events,
4947
- extract_artifacts=not no_extract_artifacts,
4948
- )
4949
- print(
4950
- f"Jobs exported: downloaded={summary.downloaded} "
4951
- f"skipped={summary.skipped} failed={len(summary.failures)}"
4952
- )
4953
- print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
4954
- print(f" path: {dest}")
4955
- for failed_id, error in summary.failures.items():
4956
- print(f"[red]Failed job {failed_id}:[/] {error}")
4957
- if summary.failures:
4958
- raise typer.Exit(code=1)
4959
- return
4960
- job_id = ids[0]
4961
- result = {}
4962
4893
  dest = export_job(
4963
4894
  client,
4964
4895
  job_id,
4965
4896
  output,
4966
- result_callback=lambda jid, status, channel: result.update(status=status, channel=channel),
4967
- timeout=timeout,
4968
4897
  include_logs=logs,
4969
4898
  include_events=events,
4970
4899
  extract_artifacts=not no_extract_artifacts,
4971
4900
  )
4972
4901
  print(f"[green]Job exported:[/] {job_id}")
4973
4902
  print(f" path: {dest}")
4974
- if result.get("channel") in {"oss", "proxy"} and result.get("status") == "downloaded":
4975
- print(f" channel: {result['channel']}")
4976
4903
 
4977
4904
 
4978
4905
  @job_app.command("wait")
@@ -6447,12 +6374,6 @@ def group_export(
6447
6374
  workers: int = typer.Option(
6448
6375
  4, "--workers", "-w", min=1, help="Number of concurrent export workers"
6449
6376
  ),
6450
- timeout: float = typer.Option(
6451
- 300,
6452
- "--timeout",
6453
- callback=_validate_export_timeout,
6454
- help="SDK request timeout in seconds, not the total export duration",
6455
- ),
6456
6377
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
6457
6378
  events: bool = typer.Option(False, "--events", help="Include events"),
6458
6379
  no_extract_artifacts: bool = typer.Option(
@@ -6513,15 +6434,12 @@ def group_export(
6513
6434
  current_stage=current_stage,
6514
6435
  )
6515
6436
 
6516
- report, channels = _export_channel_reporter(progress.console.print)
6517
6437
  dest, summary = export_group(
6518
6438
  client,
6519
6439
  group_id,
6520
6440
  output,
6521
6441
  progress_callback=_on_progress,
6522
6442
  workers=workers,
6523
- result_callback=report,
6524
- timeout=timeout,
6525
6443
  include_logs=logs,
6526
6444
  include_events=events,
6527
6445
  extract_artifacts=not no_extract_artifacts,
@@ -6534,7 +6452,6 @@ def group_export(
6534
6452
  f" [green]{summary.downloaded}[/] jobs downloaded, "
6535
6453
  f"[yellow]{summary.skipped}[/] jobs skipped (no artifact)"
6536
6454
  )
6537
- print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
6538
6455
  if summary.skipped_job_names:
6539
6456
  print(f" Skipped: {', '.join(summary.skipped_job_names)}")
6540
6457
 
@@ -50,18 +50,15 @@ def register(dataset_app: typer.Typer) -> None:
50
50
 
51
51
 
52
52
  def _register_dataset_commands(dataset_app: typer.Typer) -> None:
53
- """挂载 dataset 级命令(``list`` / ``get`` / ``create`` / ``update`` / ``claim``)。
53
+ """挂载 dataset 级命令。"""
54
+ from .dataset_register import dataset_register
54
55
 
55
- 实现方式:把命令写成本模块的模块级函数,再在这里用
56
- ``dataset_app.command("<name>")(<func>)`` 挂上去。``list`` 需要**替换**
57
- ``cli.py`` 里现存的 ossdata 版本(设计 §4.1),挂载时用同名 ``"list"`` 覆盖,
58
- 并同步删掉 ``cli.py`` 中的旧实现。
59
- """
60
56
  dataset_app.command("list")(dataset_list)
61
57
  dataset_app.command("get")(dataset_get)
62
58
  dataset_app.command("create")(dataset_create)
63
59
  dataset_app.command("update")(dataset_update)
64
60
  dataset_app.command("claim")(dataset_claim)
61
+ dataset_app.command("register")(dataset_register)
65
62
 
66
63
 
67
64
  # ---------------------------------------------------------------------------
@@ -86,6 +83,7 @@ _VERSION_COLUMNS: tuple[tuple[str, str], ...] = (
86
83
  ("splits", "SPLITS"),
87
84
  ("instances", "INSTANCES"),
88
85
  ("updated_at", "UPDATED_AT"),
86
+ ("registered", "REGISTERED"),
89
87
  )
90
88
 
91
89
  #: 上游 §8.2 的默认列(设计 §5)。
@@ -94,6 +92,7 @@ _SPLIT_COLUMNS: tuple[tuple[str, str], ...] = (
94
92
  ("run_type", "RUN_TYPE"),
95
93
  ("instances", "INSTANCES"),
96
94
  ("updated_at", "UPDATED_AT"),
95
+ ("registered", "REGISTERED"),
97
96
  )
98
97
 
99
98
  _ACCESS_COLUMNS: tuple[tuple[str, str], ...] = (
@@ -398,6 +397,7 @@ def _model_dict(record: Any) -> dict:
398
397
  ``{}`` / 缺省都不会出现在响应里),而 SDK 模型会把它们补成默认值;留着这些默认值会让
399
398
  JSON 凭空多出字段(迁移前是原样透出服务端响应,契约必须一致)。
400
399
 
400
+ ``registered`` 是显式注册状态,必须保留 ``false``。
401
401
  只清洗**顶层**:嵌套结构(如 version 详情里的 ``splits[].is_default``)按模型原样输出,
402
402
  服务端在那里确实是显式返回 ``false``。
403
403
  """
@@ -412,13 +412,24 @@ def _model_dict(record: Any) -> dict:
412
412
  return {}
413
413
  if not isinstance(payload, dict):
414
414
  return {}
415
+ registered = getattr(record, "registered", None)
416
+ if isinstance(registered, bool):
417
+ payload = {**payload, "registered": registered}
415
418
  return {
416
419
  key: value
417
420
  for key, value in payload.items()
418
- if not (value is None or value is False or value == "" or value == [] or value == {})
421
+ if value is not None
422
+ and (key == "registered" or not (value is False or value == "" or value == [] or value == {}))
419
423
  }
420
424
 
421
425
 
426
+ def _registered_dict(record: Any) -> dict:
427
+ """Preserve SDK registration state; older metadata-only SDK reads imply true."""
428
+ payload = _model_dict(record)
429
+ payload.setdefault("registered", True)
430
+ return payload
431
+
432
+
422
433
  def _api_biz_code(exc: BaseException) -> Optional[int]:
423
434
  """从 ``APIError`` 里取 apiserver 业务码(设计 §5.1),不 import 数值异常类。
424
435
 
@@ -814,6 +825,7 @@ def _version_row(record: dict) -> dict:
814
825
  "splits": splits,
815
826
  "instances": record.get("instance_count", ""),
816
827
  "updated_at": record.get("updated_at") or "",
828
+ "registered": record.get("registered", True),
817
829
  }
818
830
 
819
831
 
@@ -833,6 +845,7 @@ def _split_row(record: dict, detail: dict) -> dict:
833
845
  "run_type": record.get("run_type") or detail.get("run_type") or "",
834
846
  "instances": "" if instances is None else instances,
835
847
  "updated_at": record.get("updated_at") or detail.get("updated_at") or "",
848
+ "registered": record.get("registered", detail.get("registered", True)),
836
849
  }
837
850
 
838
851
 
@@ -1144,11 +1157,22 @@ def dataset_get(
1144
1157
  try:
1145
1158
  detail = repo.datasets.get_detail(dataset, **_env_kwargs(env))
1146
1159
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
1147
- raise _fail(
1148
- f"Failed to query dataset {dataset}: {_reason(exc)}",
1149
- f"Check the dataset name: ap dataset list --query {dataset}",
1150
- ) from exc
1151
- rendered = _model_dict(detail)
1160
+ if _business_code(exc) not in (92001, 94021):
1161
+ raise _fail(
1162
+ f"Failed to query dataset {dataset}: {_reason(exc)}",
1163
+ f"Check the dataset name: ap dataset list --query {dataset}",
1164
+ ) from exc
1165
+ detail = None
1166
+ if detail is None:
1167
+ l1, _, l2 = dataset.partition("/")
1168
+ rendered = {
1169
+ "dataset_name": dataset,
1170
+ "dataset_l1": l1,
1171
+ "dataset_l2": l2,
1172
+ "registered": False,
1173
+ }
1174
+ else:
1175
+ rendered = _registered_dict(detail)
1152
1176
  _emit_detail(rendered, output_format, order=_DATASET_DETAIL_ORDER, always=("status",))
1153
1177
  if output_format not in ("json", "yaml"):
1154
1178
  _print_dataset_environments(repo, dataset, rendered)
@@ -1642,7 +1666,7 @@ def dataset_version_list(
1642
1666
  f"Check the dataset name: ap dataset list --query {dataset}",
1643
1667
  ) from exc
1644
1668
 
1645
- records = [_model_dict(record) for record in (getattr(result, "items", None) or [])]
1669
+ records = [_registered_dict(record) for record in (getattr(result, "items", None) or [])]
1646
1670
  rows = [_version_row(record) for record in records]
1647
1671
  _emit(
1648
1672
  {"dataset": dataset, "versions": records, "pagination": _pagination_envelope(result)},
@@ -1663,7 +1687,7 @@ def dataset_version_get(
1663
1687
  output_format = _fmt(output_format)
1664
1688
  resolved = _version_value(version)
1665
1689
  detail = _fetch_version(dataset, resolved, stage="query", environment=environment)
1666
- _emit_detail(detail, output_format, order=_VERSION_DETAIL_ORDER)
1690
+ _emit_detail(_registered_dict(detail), output_format, order=_VERSION_DETAIL_ORDER)
1667
1691
 
1668
1692
 
1669
1693
  @dataset_version_app.command("create")
@@ -1810,6 +1834,7 @@ def dataset_split_get(
1810
1834
  payload = dict(record)
1811
1835
  payload.setdefault("dataset", dataset)
1812
1836
  payload.setdefault("version", resolved_version)
1837
+ payload.setdefault("registered", detail.get("registered", True))
1813
1838
  _emit_detail(payload, output_format)
1814
1839
  return
1815
1840
  known = ", ".join(_split_names(detail)) or "<none>"
@@ -1819,6 +1844,7 @@ def dataset_split_get(
1819
1844
  payload = dict(record)
1820
1845
  payload.setdefault("dataset", dataset)
1821
1846
  payload.setdefault("version", "")
1847
+ payload.setdefault("registered", True)
1822
1848
  _emit_detail(payload, output_format)
1823
1849
  return
1824
1850
  known = (