ap-client 0.7.0.dev3__tar.gz → 0.7.0.dev5__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.
Files changed (28) hide show
  1. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/PKG-INFO +4 -4
  2. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/cli.py +17 -5
  3. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/dataset_commands.py +205 -117
  4. ap_client-0.7.0.dev5/ap_client/dataset_name.py +104 -0
  5. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/dataset_register.py +76 -7
  6. ap_client-0.7.0.dev5/ap_client/dfs_commands.py +705 -0
  7. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/fs_commands.py +199 -20
  8. ap_client-0.7.0.dev5/ap_client/fs_storage.py +146 -0
  9. ap_client-0.7.0.dev5/ap_client/fs_transfer.py +329 -0
  10. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/instance_commands.py +519 -52
  11. ap_client-0.7.0.dev5/ap_client/instance_options.py +89 -0
  12. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/irepo_sdk.py +129 -4
  13. ap_client-0.7.0.dev5/ap_client/ossutil_hash.py +329 -0
  14. ap_client-0.7.0.dev5/ap_client/ossutil_io.py +1271 -0
  15. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/repo_read_guard.py +6 -2
  16. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/split_publish.py +1 -1
  17. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/pyproject.toml +5 -12
  18. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/__init__.py +0 -0
  19. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/akpool_commands.py +0 -0
  20. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/api.py +0 -0
  21. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/config.py +0 -0
  22. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/exporter.py +0 -0
  23. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/managed_ak.py +0 -0
  24. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/manifest_view.py +0 -0
  25. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/profile_commands.py +0 -0
  26. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/share_api.py +0 -0
  27. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/tbb.py +0 -0
  28. {ap_client-0.7.0.dev3 → ap_client-0.7.0.dev5}/ap_client/waiter.py +0 -0
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.7.0.dev3
3
+ Version: 0.7.0.dev5
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
- Requires-Dist: irepo-artifact==0.2.4.dev1
6
+ Requires-Dist: irepo-artifact==0.2.4
7
7
  Requires-Dist: pyyaml>=6.0
8
8
  Requires-Dist: requests>=2.28.0
9
9
  Requires-Dist: rich>=13.0.0
10
10
  Requires-Dist: typer>=0.9.0
11
11
  Requires-Dist: websockets>=13.0
12
12
  Provides-Extra: all
13
- Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.5; extra == 'all'
13
+ Requires-Dist: instance-repo[oss]<2.0.0,>=1.4.0; extra == 'all'
14
14
  Provides-Extra: dataset
15
- Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.5; extra == 'dataset'
15
+ Requires-Dist: instance-repo[oss]<2.0.0,>=1.4.0; extra == 'dataset'
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  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.
@@ -23,6 +23,7 @@ from ap_client.api import (
23
23
  )
24
24
  from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
25
25
  from ap_client.dataset_commands import register as _register_dataset_commands
26
+ from ap_client.dfs_commands import register as _register_dfs_commands
26
27
  from ap_client.exporter import export_group, export_job, export_jobs
27
28
  from ap_client.fs_commands import register as _register_fs_commands
28
29
  from ap_client.instance_commands import register as _register_instance_commands
@@ -165,6 +166,7 @@ app.add_typer(checkpoint_app, name="checkpoint")
165
166
  # ap dataset version/split/access 嵌套在 dataset 下;instance/fs 是一等资源。
166
167
  _register_dataset_commands(dataset_app)
167
168
  _register_instance_commands(app)
169
+ _register_dfs_commands(app)
168
170
  _register_fs_commands(app)
169
171
  _register_akpool_commands(app)
170
172
 
@@ -3001,22 +3003,22 @@ def job_create(
3001
3003
  meta_model: Optional[str] = typer.Option(
3002
3004
  None,
3003
3005
  "--meta-model",
3004
- help="Meta model tag; omit to leave unset",
3006
+ help="Target model or training project this job serves; not the called model or Judge. Values: ap meta-tag get meta-model --format json",
3005
3007
  ),
3006
3008
  meta_job_type: Optional[str] = typer.Option(
3007
3009
  None,
3008
3010
  "--meta-job-type",
3009
- help="Meta job type tag: Eval-Eval (default) or Others",
3011
+ help="Business purpose of this job. Values: ap meta-tag get meta-job-type --format json",
3010
3012
  ),
3011
3013
  meta_teacher_direction: Optional[str] = typer.Option(
3012
3014
  None,
3013
3015
  "--meta-teacher-direction",
3014
- help="Meta teacher direction tag; omit to derive from dataset_series; options: ap meta-teacher-direction list",
3016
+ help="Teacher direction this job serves; verify any dataset-derived value. Values: ap meta-tag get meta-teacher-direction --format json",
3015
3017
  ),
3016
3018
  meta_train_stage: Optional[str] = typer.Option(
3017
3019
  None,
3018
3020
  "--meta-train-stage",
3019
- help="Meta train stage tag; omit to derive from dataset run_type; options: ap meta-train-stage list",
3021
+ help="Training stage this job serves; verify any dataset-derived value. Values: ap meta-tag get meta-train-stage --format json",
3020
3022
  ),
3021
3023
  overrides: Optional[str] = typer.Option(None, "--overrides", help="Override config (JSON)"),
3022
3024
  concurrency: Optional[int] = typer.Option(
@@ -3188,7 +3190,7 @@ def job_create(
3188
3190
  benchmark: Optional[str] = typer.Option(
3189
3191
  None,
3190
3192
  "--benchmark",
3191
- help="Benchmark name from the AP registry (see 'ap benchmark list')",
3193
+ help="Prefer the registered benchmark source ID for this run; name is also accepted. Find IDs: ap benchmark list --format json; inspect by name: ap benchmark get NAME --format json",
3192
3194
  ),
3193
3195
  checkpoint_id: Optional[str] = typer.Option(
3194
3196
  None,
@@ -3234,6 +3236,16 @@ def job_create(
3234
3236
  ):
3235
3237
  """Submit a job.
3236
3238
 
3239
+ For agents:
3240
+ \b
3241
+ Before submitting, resolve benchmark, target model, job purpose,
3242
+ training stage, and teacher direction using live catalogs and task context.
3243
+ Verify applicable inherited values: ap profile get ID --version N --format json;
3244
+ ap group get ID --format json. Job tags do not overwrite existing Group metadata.
3245
+ Do not infer target model from serving/Judge models or training stage from evaluation.
3246
+ Ask the user for missing or conflicting business context before choosing values.
3247
+ All five options remain optional; existing validation still applies.
3248
+
3237
3249
  Model configuration:
3238
3250
  Supply template parameters through --params/-p; select a managed credential with --ak-id.
3239
3251
 
@@ -123,7 +123,10 @@ _NO_BENCHMARK = "none"
123
123
  _FORMAT_HELP = "Output format: plain/table/json/yaml (default: AP_FORMAT or command default)"
124
124
 
125
125
  _METADATA_MODELS = ("version_first", "split_first")
126
- _VERSION_SPLIT_METADATA_MODEL = "version_first"
126
+ # IR SDK >= 1.4.0.dev3 的业务入口固定 split_first:business_metadata_model 拒绝
127
+ # version_first(仅保留给低层 legacy 读)。带 version 的 split 查询在两种模型下
128
+ # 结果一致(实测),显式 split_first 与 SDK 归一值一致且新旧 SDK 均接受。
129
+ _VERSION_SPLIT_METADATA_MODEL = "split_first"
127
130
 
128
131
  #: CLI **不替 SDK 决定元数据模型**:不下发 ``metadata_model``,由 SDK 按 profile 归一
129
132
  #: (当前为 ``split_first``)。
@@ -691,6 +694,74 @@ def _unversioned_split(
691
694
  return detail if detail is not None and not _version_value(detail.get("version")) else None
692
695
 
693
696
 
697
+ def _versioned_split(
698
+ dataset: str, version: str, split: str, *, environment: Optional[str] = None
699
+ ) -> Optional[dict]:
700
+ """取指定 version 里的单个 split 记录;不存在返回 ``None``(不当作错误)。
701
+
702
+ 与 ``_unversioned_split`` 同源,走 recordsvc 的 ``splits.get``(SDK 自带
703
+ version_first 内嵌投影的 legacy 回退)。
704
+ """
705
+ from .irepo_sdk import _reason
706
+
707
+ env = _environment(environment)
708
+ repo = _dataset_repo()
709
+ try:
710
+ record = repo.splits.get(
711
+ dataset, split, version=version, **_read_env_kwargs(repo, repo.splits, "get", env)
712
+ )
713
+ except Exception as exc: # noqa: BLE001
714
+ raise _fail(
715
+ f"dataset {dataset} version {version} split {split}: query failed: {_reason(exc)}",
716
+ f"Check the dataset name and permission: ap dataset get {dataset}",
717
+ ) from exc
718
+ detail = _model_dict(record) if record is not None else None
719
+ return detail if detail is not None and _version_value(detail.get("version")) == version else None
720
+
721
+
722
+ def _versioned_splits(
723
+ dataset: str, version: str, *, stage: str, environment: Optional[str] = None
724
+ ) -> list[dict]:
725
+ """读**指定 version**的 split——走 SDK ``repo.splits``(recordsvc)。
726
+
727
+ 与 ``_unversioned_splits`` 同源:version 文档不内嵌 ``splits[]``(服务端只给
728
+ ``split_count`` 聚合,split 记录由 recordsvc 的 ``/datasets/splits`` 承载),
729
+ SDK ``splits.list(version=...)`` 是唯一正确读法,且自带存量 version_first
730
+ 内嵌投影的 legacy 回退。
731
+ """
732
+ from .irepo_sdk import _reason
733
+
734
+ env = _environment(environment)
735
+ repo = _dataset_repo()
736
+ try:
737
+ records: list[dict] = []
738
+ page = 1
739
+ while True:
740
+ result = repo.splits.list(
741
+ dataset,
742
+ version=version,
743
+ page_size=_SPLIT_PAGE_SIZE,
744
+ **({"page": page} if page > 1 else {}),
745
+ **_read_env_kwargs(repo, repo.splits, "list", env),
746
+ )
747
+ items = getattr(result, "items", None) or []
748
+ for item in items:
749
+ record = _model_dict(item)
750
+ # 与 ``_versioned_split`` 同口径:只收 version 匹配的记录(服务端
751
+ # exact scope 保证;legacy 投影与 OSS 发现也会带 version)。
752
+ if _version_value(record.get("version")) == version:
753
+ records.append(record)
754
+ if not items or getattr(result, "has_more", False) is not True:
755
+ break
756
+ page += 1
757
+ except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
758
+ raise _fail(
759
+ f"dataset {dataset} version {version}: {stage} failed: {_reason(exc)}",
760
+ f"Check the version name: ap dataset version get {version} --dataset {dataset}",
761
+ ) from exc
762
+ return records
763
+
764
+
694
765
  def _split_records(detail: dict) -> list[dict]:
695
766
  """version 文档内嵌的 ``splits[]``(设计 §4.3:split 没有独立资源)。"""
696
767
  splits = detail.get("splits")
@@ -724,12 +795,12 @@ def _split_names(detail: dict) -> list[str]:
724
795
  return names
725
796
 
726
797
 
727
- def _dataset_repo() -> "Repo":
798
+ def _dataset_repo(*, request_timeout: float | None = None) -> "Repo":
728
799
  """dataset 写路径统一走 InstanceRepo SDK;未装 ``dataset`` extra 时退出 2。"""
729
800
  from .irepo_sdk import SdkMissing, load_repo
730
801
 
731
802
  try:
732
- return load_repo()
803
+ return load_repo(request_timeout=request_timeout)
733
804
  except SdkMissing as exc:
734
805
  typer.echo(str(exc), err=True)
735
806
  raise typer.Exit(2) from exc
@@ -1186,9 +1257,26 @@ def dataset_get(
1186
1257
  "dataset_name": dataset,
1187
1258
  "registered": False,
1188
1259
  }
1189
- parts = dataset.split("/")
1190
- if len(parts) == 2 and all(parts):
1191
- rendered.update(dataset_l1=parts[0], dataset_l2=parts[1])
1260
+ from .dataset_name import (
1261
+ DatasetNameError,
1262
+ DatasetNameUpgradeRequired,
1263
+ parse_dataset_name,
1264
+ )
1265
+
1266
+ try:
1267
+ name = parse_dataset_name(dataset)
1268
+ except DatasetNameUpgradeRequired as exc:
1269
+ raise _fail(
1270
+ f"Failed to query dataset {dataset}: {exc}",
1271
+ "pip install 'ap-client[dataset]' --upgrade",
1272
+ exit_code=2,
1273
+ ) from exc
1274
+ except DatasetNameError:
1275
+ # 非规范名:不凭空发明 level 字段(与历史行为一致)。
1276
+ pass
1277
+ else:
1278
+ rendered["dataset_l1"] = name.l1
1279
+ rendered["dataset_l2"] = name.l2
1192
1280
  else:
1193
1281
  rendered = _registered_dict(detail)
1194
1282
  _emit_detail(rendered, output_format, order=_DATASET_DETAIL_ORDER, always=("status",))
@@ -1197,7 +1285,10 @@ def dataset_get(
1197
1285
 
1198
1286
 
1199
1287
  def dataset_create(
1200
- dataset: str = typer.Argument(..., help="Dataset name (e.g. alibaba/my-bench)"),
1288
+ dataset: str = typer.Argument(
1289
+ ..., help="Dataset name: L1, L1/L2, or a deeper path "
1290
+ "(e.g. my-bench, alibaba/my-bench, or gdpval/synthetic/my-bench)"
1291
+ ),
1201
1292
  benchmark: str = typer.Option(..., "--benchmark", help="Benchmark name or source_id"),
1202
1293
  description: Optional[str] = typer.Option(None, "--description", help="Dataset description"),
1203
1294
  visibility: str = typer.Option(
@@ -1216,20 +1307,32 @@ def dataset_create(
1216
1307
 
1217
1308
  from instance_repo.models import DatasetSeries
1218
1309
 
1310
+ from .dataset_name import (
1311
+ DatasetNameError,
1312
+ DatasetNameUpgradeRequired,
1313
+ parse_dataset_name,
1314
+ )
1219
1315
  from .irepo_sdk import _reason
1220
1316
 
1221
- parts = dataset.split("/")
1222
- if len(parts) != 2 or not all(parts):
1317
+ try:
1318
+ name = parse_dataset_name(dataset)
1319
+ except DatasetNameUpgradeRequired as exc:
1223
1320
  raise _fail(
1224
- f"Failed to create dataset {dataset}: the name must be two segments <team>/<name>",
1225
- "e.g. your-team/my-bench",
1321
+ f"Failed to create dataset {dataset}: {exc}",
1322
+ "pip install 'ap-client[dataset]' --upgrade",
1323
+ exit_code=2,
1324
+ ) from exc
1325
+ except DatasetNameError:
1326
+ raise _fail(
1327
+ f"Failed to create dataset {dataset}: the name must be one or more "
1328
+ "canonical path segments",
1329
+ "e.g. my-bench, your-team/my-bench, or gdpval/synthetic/my-bench",
1226
1330
  exit_code=2,
1227
1331
  )
1228
- l1, l2 = parts
1229
1332
  series = DatasetSeries(
1230
1333
  dataset_name=dataset,
1231
- dataset_l1=l1,
1232
- dataset_l2=l2,
1334
+ dataset_l1=name.l1,
1335
+ dataset_l2=name.l2,
1233
1336
  benchmark_id=benchmark_id,
1234
1337
  visibility=resolved_visibility,
1235
1338
  description=description or None,
@@ -1813,15 +1916,10 @@ def dataset_split_list(
1813
1916
  resolved_run_type = _check_choice(run_type, _RUN_TYPES, "--run-type") if run_type else ""
1814
1917
 
1815
1918
  if resolved_version:
1816
- detail = _fetch_version(
1817
- dataset,
1818
- resolved_version,
1819
- stage="split query",
1820
- environment=environment,
1821
- metadata_model=_VERSION_SPLIT_METADATA_MODEL,
1919
+ records = _versioned_splits(
1920
+ dataset, resolved_version, stage="split query", environment=environment
1822
1921
  )
1823
- records = _split_records(detail)
1824
- rows = [_split_row(record, detail) for record in records]
1922
+ rows = [_split_row(record, {}) for record in records]
1825
1923
  else:
1826
1924
  records = _unversioned_splits(dataset, stage="split query", environment=environment)
1827
1925
  rows = [_split_row(record, {}) for record in records]
@@ -1848,22 +1946,26 @@ def dataset_split_get(
1848
1946
  resolved_version = _version_value(version)
1849
1947
 
1850
1948
  if resolved_version:
1851
- detail = _fetch_version(
1852
- dataset,
1853
- resolved_version,
1854
- stage="split query",
1855
- environment=environment,
1856
- metadata_model=_VERSION_SPLIT_METADATA_MODEL,
1949
+ record = _versioned_split(
1950
+ dataset, resolved_version, split, environment=environment
1951
+ )
1952
+ if record is not None:
1953
+ payload = dict(record)
1954
+ payload.setdefault("dataset", dataset)
1955
+ payload.setdefault("version", resolved_version)
1956
+ payload.setdefault("registered", True)
1957
+ _emit_detail(payload, output_format)
1958
+ return
1959
+ known = (
1960
+ ", ".join(
1961
+ _split_names_of(
1962
+ _versioned_splits(
1963
+ dataset, resolved_version, stage="split query", environment=environment
1964
+ )
1965
+ )
1966
+ )
1967
+ or "<none>"
1857
1968
  )
1858
- for record in _split_records(detail):
1859
- if str(record.get("name") or "") == split:
1860
- payload = dict(record)
1861
- payload.setdefault("dataset", dataset)
1862
- payload.setdefault("version", resolved_version)
1863
- payload.setdefault("registered", detail.get("registered", True))
1864
- _emit_detail(payload, output_format)
1865
- return
1866
- known = ", ".join(_split_names(detail)) or "<none>"
1867
1969
  else:
1868
1970
  record = _unversioned_split(dataset, split, environment=environment)
1869
1971
  if record is not None:
@@ -1920,71 +2022,40 @@ def dataset_split_create(
1920
2022
  exit_code=2,
1921
2023
  )
1922
2024
 
1923
- detail = _fetch_version(
1924
- dataset,
1925
- resolved_version,
1926
- stage="split create precheck",
1927
- environment=environment,
1928
- metadata_model=_VERSION_SPLIT_METADATA_MODEL,
1929
- )
1930
-
1931
- existing = _split_names(detail)
1932
- if split in existing:
2025
+ # 版本化创建走独立 Split API(POST /datasets/splits),与 split-first 服务端
2026
+ # 契约对齐——旧 Version PATCH(splits 全量替换 / split_run_types)已被服务端
2027
+ # 拒绝("split-first Version group updates are not supported")。
2028
+ env = _environment(environment)
2029
+ existing = _versioned_split(dataset, resolved_version, split, environment=environment)
2030
+ if existing is not None:
1933
2031
  # 幂等:已存在同名 split(设计 §4.3)。
1934
- for record in _split_records(detail):
1935
- if str(record.get("name") or "") == split:
1936
- payload = dict(record)
1937
- payload.setdefault("dataset", dataset)
1938
- payload.setdefault("version", resolved_version)
1939
- payload["created"] = False
1940
- _emit_detail(payload, output_format)
1941
- return
1942
- _emit_detail(
1943
- {"dataset": dataset, "version": resolved_version, "split": split, "created": False},
1944
- output_format,
1945
- )
2032
+ payload = dict(existing)
2033
+ payload.setdefault("dataset", dataset)
2034
+ payload.setdefault("version", resolved_version)
2035
+ payload["created"] = False
2036
+ _emit_detail(payload, output_format)
1946
2037
  return
1947
2038
 
1948
- # ① splits 全量 PATCH(现有 ∪ 新增)。
1949
- names = existing + [split]
1950
- _patch_version(
1951
- dataset,
1952
- resolved_version,
1953
- {"splits": names},
1954
- stage="split create",
1955
- environment=environment,
1956
- metadata_model=_VERSION_SPLIT_METADATA_MODEL,
1957
- )
2039
+ from .instance_commands import _sdk_read_kwargs
2040
+ from .irepo_sdk import _reason
1958
2041
 
1959
- # ② run_type 只能第二次 PATCH:服务端 splits 与 split_run_types 互斥。
2042
+ repo = _dataset_repo()
2043
+ create_kwargs = _sdk_read_kwargs(repo.splits, "create", environment=env, metadata_model="")
1960
2044
  if resolved_run_type:
1961
- retry = (
1962
- f"ap dataset split update {split} --dataset {dataset} --run-type {resolved_run_type}"
1963
- )
1964
- if resolved_version:
1965
- retry += f" --version {resolved_version}"
1966
- _patch_version(
1967
- dataset,
1968
- resolved_version,
1969
- {"split_run_types": [{"name": split, "run_type": resolved_run_type}]},
1970
- stage=f"split {split} set run_type",
1971
- # 第一段 PATCH 已建出 split,失败后的恢复动作是补打 run_type,不是重跑 create。
1972
- fallback_hint=f"split {split} was created but run_type is not set; set it now: {retry}",
1973
- environment=environment,
1974
- metadata_model=_VERSION_SPLIT_METADATA_MODEL,
1975
- )
1976
-
1977
- _emit_detail(
1978
- {
1979
- "dataset": dataset,
1980
- "version": resolved_version,
1981
- "split": split,
1982
- "run_type": resolved_run_type,
1983
- "splits": names,
1984
- "created": True,
1985
- },
1986
- output_format,
1987
- )
2045
+ create_kwargs["run_type"] = resolved_run_type
2046
+ try:
2047
+ record = repo.splits.create(dataset, split, version=resolved_version, **create_kwargs)
2048
+ except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
2049
+ raise _fail(
2050
+ f"Failed to create split {split} of dataset {dataset} version {resolved_version}: {_reason(exc)}",
2051
+ "Check the version name and write permission, then retry",
2052
+ ) from exc
2053
+ payload = _model_dict(record) if record is not None else {}
2054
+ payload.setdefault("dataset", dataset)
2055
+ payload.setdefault("version", resolved_version)
2056
+ payload.setdefault("split", split)
2057
+ payload["created"] = True
2058
+ _emit_detail(payload, output_format)
1988
2059
 
1989
2060
 
1990
2061
  @dataset_split_app.command("update")
@@ -2064,31 +2135,48 @@ def dataset_split_update(
2064
2135
  )
2065
2136
  return
2066
2137
 
2067
- detail = _fetch_version(
2068
- dataset,
2069
- resolved_version,
2070
- stage="split update precheck",
2071
- environment=environment,
2072
- metadata_model=_VERSION_SPLIT_METADATA_MODEL,
2073
- )
2074
-
2075
- if split not in _split_names(detail):
2076
- known = ", ".join(_split_names(detail)) or "<none>"
2077
- hint = f"Create it first: ap dataset split create {split} --dataset {dataset} (existing: {known})"
2138
+ # 版本化更新走独立 Split API(PATCH /datasets/splits/detail),预检用精确
2139
+ # split 身份(splits.get with version)——旧实现查 Version 内嵌 splits 再
2140
+ # PATCH Version,与独立 Split 记录不匹配,导致"已存在却报 not found"。
2141
+ env = _environment(environment)
2142
+ if env == "pre":
2143
+ raise _fail(
2144
+ f"Failed to update split {split} of dataset {dataset}: split records can only be changed in the online environment",
2145
+ "PATCH on split records only supports online; drop --environment pre",
2146
+ exit_code=2,
2147
+ )
2148
+ record = _versioned_split(dataset, resolved_version, split, environment=environment)
2149
+ if record is None:
2150
+ known = (
2151
+ ", ".join(
2152
+ _split_names_of(
2153
+ _versioned_splits(
2154
+ dataset, resolved_version, stage="split update precheck",
2155
+ environment=environment,
2156
+ )
2157
+ )
2158
+ )
2159
+ or "<none>"
2160
+ )
2078
2161
  raise _fail(
2079
2162
  f"dataset {dataset} version {_version_label(resolved_version)} split {split}: "
2080
2163
  f"update failed: not found",
2081
- hint,
2164
+ f"Create it first: ap dataset split create {split} --dataset {dataset} "
2165
+ f"--version {resolved_version} (existing: {known})",
2082
2166
  )
2083
2167
 
2084
- _patch_version(
2085
- dataset,
2086
- resolved_version,
2087
- {"split_run_types": [{"name": split, "run_type": resolved_run_type}]},
2088
- stage="split update",
2089
- environment=environment,
2090
- metadata_model=_VERSION_SPLIT_METADATA_MODEL,
2091
- )
2168
+ from .instance_commands import _sdk_read_kwargs
2169
+ from .irepo_sdk import _reason
2170
+
2171
+ repo = _dataset_repo()
2172
+ update_kwargs = _sdk_read_kwargs(repo.splits, "update", environment=env, metadata_model="")
2173
+ try:
2174
+ repo.splits.update(dataset, split, version=resolved_version, run_type=resolved_run_type, **update_kwargs)
2175
+ except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
2176
+ raise _fail(
2177
+ f"Failed to update split {split} of dataset {dataset} version {resolved_version}: {_reason(exc)}",
2178
+ "Write permission on this dataset is required; check the split name and the online environment, then retry",
2179
+ ) from exc
2092
2180
  _emit_detail(
2093
2181
  {
2094
2182
  "dataset": dataset,
@@ -0,0 +1,104 @@
1
+ """DatasetName 解析 Adapter:唯一入口委托给已安装的 InstanceRepo SDK。
2
+
3
+ AP CLI 不维护第三套 dataset 名解析:``instance_repo.paths.DatasetName`` 是客户端
4
+ 路径契约的唯一事实源(单段 ``A`` / 两段 ``A/B`` / 多段树形 ``A/B/C``,与
5
+ apiserver 的 namespace guard 及 conformance 契约对齐)。SDK 缺失或过旧(没有
6
+ ``DatasetName.parse`` 能力)时,给出明确的升级提示,不静默降级、不本地重写解析规则。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ import unicodedata
13
+ from typing import Any
14
+
15
+ __all__ = [
16
+ "DatasetNameError",
17
+ "DatasetNameUpgradeRequired",
18
+ "parse_dataset_name",
19
+ ]
20
+
21
+ #: SDK 缺失/无 DatasetName 能力时的升级提示(与 irepo_sdk 的 SdkMissing 同范式)。
22
+ _UPGRADE_HINT = (
23
+ "The installed instance-repo SDK does not provide DatasetName parsing "
24
+ "(single-segment L1 dataset names need it).\n"
25
+ "Install it with: pip install 'ap-client[dataset]' --upgrade"
26
+ )
27
+
28
+ #: 多段(3+)名能力的首个 SDK 版本。旧 SDK(如 1.4.0)有 parse 但只认 1-2 段,
29
+ #: 对多段名会抛"one or two canonical segments"——此时应提示升级而非报名称非法。
30
+ _MULTI_LEVEL_SDK_VERSION = (1, 4, 1)
31
+
32
+
33
+ def _sdk_supports_multi_level() -> bool:
34
+ """已安装 instance-repo 是否支持多段数据集名(>= 1.4.1)。版本不可读时保守视为不支持。"""
35
+ try:
36
+ from importlib.metadata import version
37
+
38
+ installed = version("instance-repo")
39
+ except Exception: # noqa: BLE001 - 元数据缺失与旧 SDK 同等对待
40
+ return False
41
+ match = re.match(r"(\d+)\.(\d+)\.(\d+)", str(installed))
42
+ if not match:
43
+ return False
44
+ return tuple(int(part) for part in match.groups()) >= _MULTI_LEVEL_SDK_VERSION
45
+
46
+
47
+ def _is_structurally_valid_multi_level(value: str) -> bool:
48
+ """值是否为"新 SDK 会接受"的 3+ 段名——即每个段都合法。
49
+
50
+ 旧 SDK 对非法名(空段 a//b、非法字符 a*b)与多段名报同一文案,不能只按
51
+ 段数判断:升级 SDK 也修不好空段/非法字符,必须保留原始校验错误。
52
+ 这里保守复刻 SDK 的非法段规则作为 gate,只放行结构上合法的多段名。
53
+ """
54
+ parts = value.split("/")
55
+ if len(parts) < 3 or any(not part for part in parts):
56
+ return False
57
+ for part in parts:
58
+ if (
59
+ part in {".", ".."}
60
+ or "\\" in part
61
+ or "*" in part
62
+ or "?" in part
63
+ or any(unicodedata.category(char) == "Cc" for char in part)
64
+ ):
65
+ return False
66
+ return True
67
+
68
+
69
+ class DatasetNameUpgradeRequired(RuntimeError):
70
+ """已安装的 instance-repo SDK 缺少 DatasetName 能力,必须升级。"""
71
+
72
+
73
+ class DatasetNameError(ValueError):
74
+ """Dataset 名不符合 IR DatasetName 契约(单段或两段、无非法段)。"""
75
+
76
+
77
+ def parse_dataset_name(value: str) -> Any:
78
+ """Parse ``value`` through the installed IR ``DatasetName`` capability.
79
+
80
+ Returns the SDK ``DatasetName`` (``canonical`` / ``l1`` / ``l2`` /
81
+ ``segments`` / ``leaf``)。Raises :class:`DatasetNameUpgradeRequired` when the
82
+ installed SDK lacks the capability (包括只认 1-2 段的旧 SDK 拒绝多段名);
83
+ raises :class:`DatasetNameError` when the value is not a valid name.
84
+ """
85
+ try:
86
+ from instance_repo.paths import DatasetName
87
+ except ImportError as exc:
88
+ raise DatasetNameUpgradeRequired(_UPGRADE_HINT) from exc
89
+ parse = getattr(DatasetName, "parse", None)
90
+ if not callable(parse):
91
+ raise DatasetNameUpgradeRequired(_UPGRADE_HINT)
92
+ try:
93
+ return parse(value)
94
+ except Exception as exc: # noqa: BLE001 - SDK 校验失败统一转成 CLI 契约
95
+ # 旧 SDK(1.4.0 及更早)只认 1-2 段:结构上合法的多段名被拒时给出
96
+ # 升级提示。非法名(空段/非法字符)无论新旧 SDK 都拒绝,保留原始
97
+ # 校验错误为 DatasetNameError——升级修不好这类名称。
98
+ if (
99
+ isinstance(value, str)
100
+ and _is_structurally_valid_multi_level(value)
101
+ and not _sdk_supports_multi_level()
102
+ ):
103
+ raise DatasetNameUpgradeRequired(_UPGRADE_HINT) from exc
104
+ raise DatasetNameError(str(exc)) from exc