ap-client 0.6.1.dev13__tar.gz → 0.7.0__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 (29) hide show
  1. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/PKG-INFO +4 -4
  2. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/cli.py +19 -0
  3. ap_client-0.7.0/ap_client/dataset_name.py +104 -0
  4. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/dfs_commands.py +5 -2
  5. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/instance_commands.py +2 -2
  6. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/instance_options.py +4 -9
  7. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/irepo_sdk.py +5 -3
  8. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/ossutil_io.py +43 -19
  9. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/pyproject.toml +4 -4
  10. ap_client-0.6.1.dev13/ap_client/dataset_name.py +0 -53
  11. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/__init__.py +0 -0
  12. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/akpool_commands.py +0 -0
  13. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/api.py +0 -0
  14. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/config.py +0 -0
  15. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/dataset_commands.py +0 -0
  16. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/dataset_register.py +0 -0
  17. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/exporter.py +0 -0
  18. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/fs_commands.py +0 -0
  19. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/fs_storage.py +0 -0
  20. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/fs_transfer.py +0 -0
  21. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/managed_ak.py +0 -0
  22. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/manifest_view.py +0 -0
  23. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/ossutil_hash.py +0 -0
  24. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/profile_commands.py +0 -0
  25. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/repo_read_guard.py +0 -0
  26. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/share_api.py +0 -0
  27. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/split_publish.py +0 -0
  28. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/tbb.py +0 -0
  29. {ap_client-0.6.1.dev13 → ap_client-0.7.0}/ap_client/waiter.py +0 -0
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.6.1.dev13
3
+ Version: 0.7.0
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
- Requires-Dist: irepo-artifact==0.2.2
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.4.1.dev0; 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.4.1.dev0; 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.
@@ -955,6 +955,25 @@ def _print_job_get_plain(job: dict) -> None:
955
955
  ]
956
956
  )
957
957
 
958
+ bindings = job.get("ak_bindings") or []
959
+ rows = [
960
+ {**parameter, "ak_id": binding["ak_id"]}
961
+ for binding in bindings
962
+ for parameter in binding.get("parameters", [])
963
+ ]
964
+ if "ak_bindings.parameters" in (job.get("restricted_fields") or []):
965
+ typer.echo("\nManaged AK parameters: content access required")
966
+ elif rows:
967
+ typer.echo("\nManaged AK parameters")
968
+ for row in rows:
969
+ typer.echo(f" {row['parameter']}: {row['value']} -> {row['ak_id']}")
970
+ if any("parameters" not in binding for binding in bindings):
971
+ typer.echo(" Some historical parameter mappings are unavailable")
972
+ elif response_ak_ids(job):
973
+ typer.echo("\nManaged AK parameters: historical mapping unavailable")
974
+ elif "ak_bindings" in job:
975
+ typer.echo("\nManaged AK parameters\n No managed AK used")
976
+
958
977
  meta_pairs = [
959
978
  ("meta_model", job.get("meta_model")),
960
979
  ("meta_job_type", job.get("meta_job_type")),
@@ -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
@@ -272,13 +272,16 @@ def _fail(exc: Exception) -> None:
272
272
  if isinstance(exc, SdkMissing):
273
273
  typer.echo(str(exc), err=True)
274
274
  raise typer.Exit(2) from exc
275
- message = str(exc) if isinstance(exc, ConfigurationError) else safe_error(exc)
275
+ input_errors = (OssUsageError, ConfigurationError, typer.BadParameter)
276
+ message = (
277
+ str(exc) if isinstance(exc, (ConfigurationError, typer.BadParameter)) else safe_error(exc)
278
+ )
276
279
  typer.echo("error: " + _display(message), err=True)
277
280
  if getattr(exc, "biz_code", None):
278
281
  typer.echo(f"hint: Check the Dataset access grant (API code {exc.biz_code})", err=True)
279
282
  elif getattr(exc, "code", None) == "AccessDenied":
280
283
  typer.echo("hint: Check the Dataset reader/writer grant for this operation", err=True)
281
- raise typer.Exit(2 if isinstance(exc, (OssUsageError, ConfigurationError)) else 1) from exc
284
+ raise typer.Exit(2 if isinstance(exc, input_errors) else 1) from exc
282
285
 
283
286
 
284
287
  @dfs_app.command("ls")
@@ -709,7 +709,7 @@ def instance_push(
709
709
  prune: bool = typer.Option(
710
710
  False,
711
711
  "--prune",
712
- help="After delivery, remove remote metadata absent from the supplied cases; OSS files remain",
712
+ help="Disabled: SDK safe pruning is unavailable; delete obsolete metadata explicitly",
713
713
  ),
714
714
  yes: bool = typer.Option(
715
715
  False,
@@ -1018,7 +1018,7 @@ def instance_pull(
1018
1018
  if private_endpoint_reachable is False:
1019
1019
  raise _fail(
1020
1020
  f"Failed to pull instance {instance}: the private OSS endpoint is unreachable from this network",
1021
- "instance pull does not support public download",
1021
+ "instance pull does not support public download; run inside an AP dev cluster / ROCK VPC",
1022
1022
  )
1023
1023
 
1024
1024
  # SDK 的 swe/custom 下载会先往输出目录写 <id>.json 与 .probe 中间文件,再由
@@ -4,7 +4,6 @@ from __future__ import annotations
4
4
 
5
5
  import inspect
6
6
  import json
7
- from importlib import metadata
8
7
  from pathlib import Path
9
8
  from typing import Any
10
9
 
@@ -75,14 +74,10 @@ def upload_hashes(records: list[dict], supplied: dict[str, str], skip: bool) ->
75
74
 
76
75
 
77
76
  def require_working_prune() -> None:
78
- try:
79
- version = metadata.version("instance-repo")
80
- except metadata.PackageNotFoundError:
81
- return
82
- if version in {"1.3.5.dev1", "1.3.5.dev2"}:
83
- raise ValueError(
84
- f"instance-repo {version} cannot complete --prune; upgrade to a fixed SDK before uploading"
85
- )
77
+ raise ValueError(
78
+ "--prune is disabled before uploading: instance-repo does not support safe pruning. "
79
+ "Upload without --prune; review and delete obsolete instance metadata explicitly."
80
+ )
86
81
 
87
82
 
88
83
  def progress_callback(completed: int, total: int, result: Any) -> None:
@@ -946,7 +946,7 @@ def load_repo(
946
946
  if request_timeout is not None:
947
947
  transport.timeout = request_timeout
948
948
  kwargs["transport"] = transport
949
- elif request_timeout is not None and "transport" in supported:
949
+ if request_timeout is not None and "transport" in supported and "transport" not in kwargs:
950
950
  # Inject a pre-configured Transport so the custom timeout is effective
951
951
  # from the very first HTTP call inside Repo.__init__ / _bind_dataset_profile.
952
952
  try:
@@ -1161,7 +1161,7 @@ _BIZ_CODE_RULES: dict[int, tuple[str, int]] = {
1161
1161
  # 字符串 ``code`` → (hint, 退出码)。SDK 的 ``InstanceRepoError.code``。
1162
1162
  _CODE_RULES: dict[str, tuple[str, int]] = {
1163
1163
  "E_PRIVATE_NETWORK_REQUIRED": (
1164
- "This operation requires private-network access; run inside an intranet environment",
1164
+ "This operation requires private-network access; run inside an AP dev cluster / ROCK VPC",
1165
1165
  1,
1166
1166
  ),
1167
1167
  "E_DIGEST_MISMATCH": ("Content digest check failed; pull or repack the instance again", 1),
@@ -1195,7 +1195,9 @@ _CODE_RULES: dict[str, tuple[str, int]] = {
1195
1195
  }
1196
1196
 
1197
1197
  #: 无法归类时的兜底文案。
1198
- _FALLBACK_HINT = "Rerun with -v/--verbose for the full traceback"
1198
+ _FALLBACK_HINT = (
1199
+ "Rerun with -v/--verbose for the full traceback; contact the Agent Platform on-call if needed"
1200
+ )
1199
1201
 
1200
1202
  # 数据面寻址字段:``/apis/v1/repo-config`` 在当前 apiserver 里不存在(设计 §8.3),SDK 的
1201
1203
  # 自动发现是 best-effort、构造期不抛错,真正用到某字段时才由 ``bootstrap.require`` 抛
@@ -7,6 +7,7 @@ import hashlib
7
7
  import json
8
8
  import os
9
9
  import re
10
+ import stat
10
11
  import tempfile
11
12
  import time
12
13
  import unicodedata
@@ -300,9 +301,9 @@ class OssFiles:
300
301
 
301
302
  凭据请求携带 ``dataset_name``:普通调用方在控制面按它走 dataset RBAC(旧 prod
302
303
  构建也要求显式上下文)。ans:// 路径是 dataset 相对的,dataset 名就是最长前缀
303
- (两段 L1/L2 优先,单段 L1 兜底);由于 ``A``(L1)与 ``A/B`` 可能同时是合法布局,
304
+ (任意深度的最长已注册前缀优先);由于 ``A``(L1)与 ``A/B`` 可能同时是合法布局,
304
305
  这里以**注册表为准**:候选按"最长优先"查注册,第一个命中者胜出;都未注册时退回
305
- 最长候选(系统白名单/平台管理员的 raw 路径与服务端的 list-only 发现都会校验
306
+ 原有最多两段上下文(系统身份的 raw 路径与服务端的 list-only 发现都会校验
306
307
  claimed 命名空间,写操作仍由服务端拒绝)。结果按相对路径缓存,一次命令内多次
307
308
  签发只查一次。
308
309
  """
@@ -322,18 +323,16 @@ class OssFiles:
322
323
  segments = [segment for segment in relative.split("/") if segment]
323
324
  if not segments:
324
325
  return None
325
- candidates = (
326
- ["/".join(segments[:2]), segments[0]] if len(segments) >= 2 else [segments[0]]
327
- )
326
+ # Validate each segment with the SDK. Older SDKs only parse one or two
327
+ # levels at a time; that limit must not truncate registry lookup paths.
328
328
  valid: list[str] = []
329
- for candidate in candidates:
329
+ for index, segment in enumerate(segments):
330
330
  try:
331
- parse_dataset_name(candidate)
331
+ parse_dataset_name(segment)
332
332
  except DatasetNameError:
333
- # 段本身合法(``_relative`` 已挡空/./..、反斜杠、通配与控制字符),
334
- # 这里只是防御 SDK 契约收紧;候选不合法就跳过,不整条失败。
335
- continue
336
- valid.append(candidate)
333
+ break
334
+ valid.append("/".join(segments[: index + 1]))
335
+ valid.reverse()
337
336
  if not valid:
338
337
  return None
339
338
  for candidate in valid:
@@ -344,8 +343,9 @@ class OssFiles:
344
343
  raise
345
344
  continue
346
345
  return candidate
347
- # 无任何注册命中:退回最长候选,list-only 发现与写操作的授权判定交给服务端。
348
- return valid[0]
346
+ # 未注册路径的 list-only 发现与写操作仍由服务端授权。
347
+ # Preserve the legacy raw-OSS discovery namespace when none is registered.
348
+ return valid[-2] if len(valid) >= 2 else valid[0]
349
349
 
350
350
  def _credentials(self, scope: str, permission: str, list_only: bool) -> dict:
351
351
  kwargs: dict[str, Any] = {}
@@ -559,7 +559,10 @@ class OssFiles:
559
559
  def cat(self, remote: RemotePath, output: Any) -> None:
560
560
  result = self.bucket(remote.key, "reader", private_only=True).get_object(remote.key)
561
561
  try:
562
- while data := result.read(1024 * 1024):
562
+ from .ossutil_hash import _stored_content_stream
563
+
564
+ stream = _stored_content_stream(result)
565
+ while data := stream.read(1024 * 1024):
563
566
  output.write(data)
564
567
  finally:
565
568
  result.resp.response.close()
@@ -654,11 +657,25 @@ class OssFiles:
654
657
  if target.exists() and not overwrite:
655
658
  raise OssOperationError(f"Local target already exists: {target}; use --force")
656
659
  suffix = hashlib.sha256((remote.bucket + remote.key).encode()).hexdigest()[:16]
657
- partial = target.with_name(f".ap-{target.name}-{suffix}.part")
658
- for path in (partial, Path(str(partial) + ".tmp")):
659
- fd = os.open(path, os.O_CREAT | os.O_WRONLY | getattr(os, "O_NOFOLLOW", 0), 0o600)
660
+ # oss2 creates random .tmp-* siblings, so protect their directory
661
+ # throughout the download, including interruptions and retries.
662
+ staging = target.with_name(f".ap-{target.name}-{suffix}.download")
663
+ staging.mkdir(mode=0o700, exist_ok=True)
664
+ directory_fd = os.open(
665
+ staging, os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
666
+ )
667
+ try:
668
+ if os.fstat(directory_fd).st_uid != os.getuid():
669
+ raise OssOperationError("Download staging directory must belong to this user")
670
+ os.fchmod(directory_fd, 0o700)
671
+ finally:
672
+ os.close(directory_fd)
673
+ partial = staging / "content.part"
674
+ fd = os.open(partial, os.O_CREAT | os.O_WRONLY | getattr(os, "O_NOFOLLOW", 0), 0o600)
675
+ try:
676
+ os.fchmod(fd, 0o600)
677
+ finally:
660
678
  os.close(fd)
661
- path.chmod(0o600)
662
679
  if self.byte_range is not None:
663
680
  result = bucket.get_object(
664
681
  remote.key,
@@ -682,13 +699,20 @@ class OssFiles:
682
699
  part_size=self.part_size,
683
700
  progress_callback=self._progress(remote.path, progress),
684
701
  )
702
+ fd = os.open(partial, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
703
+ try:
704
+ if not stat.S_ISREG(os.fstat(fd).st_mode):
705
+ raise OssOperationError("Download staging content must be a regular file")
706
+ os.fchmod(fd, 0o600)
707
+ finally:
708
+ os.close(fd)
685
709
  self._check_local_path(target)
686
710
  if overwrite:
687
711
  os.replace(partial, target)
688
712
  else:
689
713
  os.link(partial, target)
690
714
  partial.unlink()
691
- Path(str(partial) + ".tmp").unlink(missing_ok=True)
715
+ staging.rmdir()
692
716
 
693
717
  @staticmethod
694
718
  def _check_local_path(path: Path) -> None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ap-client"
7
- version = "0.6.1.dev13"
7
+ version = "0.7.0"
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"
@@ -14,15 +14,15 @@ dependencies = [
14
14
  "rich>=13.0.0",
15
15
  "PyYAML>=6.0",
16
16
  "websockets>=13.0",
17
- "irepo-artifact==0.2.2",
17
+ "irepo-artifact==0.2.4",
18
18
  ]
19
19
 
20
20
  [project.optional-dependencies]
21
21
  dataset = [
22
- "instance-repo[oss]>=1.4.1.dev0,<2.0.0",
22
+ "instance-repo[oss]>=1.4.0,<2.0.0",
23
23
  ]
24
24
  all = [
25
- "instance-repo[oss]>=1.4.1.dev0,<2.0.0",
25
+ "instance-repo[oss]>=1.4.0,<2.0.0",
26
26
  ]
27
27
 
28
28
  [project.scripts]
@@ -1,53 +0,0 @@
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
- from typing import Any
12
-
13
- __all__ = [
14
- "DatasetNameError",
15
- "DatasetNameUpgradeRequired",
16
- "parse_dataset_name",
17
- ]
18
-
19
- #: SDK 缺失/无 DatasetName 能力时的升级提示(与 irepo_sdk 的 SdkMissing 同范式)。
20
- _UPGRADE_HINT = (
21
- "The installed instance-repo SDK does not provide DatasetName parsing "
22
- "(single-segment L1 dataset names need it).\n"
23
- "Install it with: pip install 'ap-client[dataset]' --upgrade"
24
- )
25
-
26
-
27
- class DatasetNameUpgradeRequired(RuntimeError):
28
- """已安装的 instance-repo SDK 缺少 DatasetName 能力,必须升级。"""
29
-
30
-
31
- class DatasetNameError(ValueError):
32
- """Dataset 名不符合 IR DatasetName 契约(单段或两段、无非法段)。"""
33
-
34
-
35
- def parse_dataset_name(value: str) -> Any:
36
- """Parse ``value`` through the installed IR ``DatasetName`` capability.
37
-
38
- Returns the SDK ``DatasetName`` (``canonical`` / ``l1`` / ``l2`` /
39
- ``segments`` / ``leaf``)。Raises :class:`DatasetNameUpgradeRequired` when the
40
- installed SDK lacks the capability; raises :class:`DatasetNameError` when
41
- the value is not a valid L1 or L1/L2 name.
42
- """
43
- try:
44
- from instance_repo.paths import DatasetName
45
- except ImportError as exc:
46
- raise DatasetNameUpgradeRequired(_UPGRADE_HINT) from exc
47
- parse = getattr(DatasetName, "parse", None)
48
- if not callable(parse):
49
- raise DatasetNameUpgradeRequired(_UPGRADE_HINT)
50
- try:
51
- return parse(value)
52
- except Exception as exc: # noqa: BLE001 - SDK 校验失败统一转成 CLI 契约
53
- raise DatasetNameError(str(exc)) from exc