ap-client 0.5.1.dev4__tar.gz → 0.6.0.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.
Files changed (22) hide show
  1. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/PKG-INFO +4 -3
  2. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/api.py +3 -3
  3. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/cli.py +83 -3
  4. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/dataset_commands.py +53 -27
  5. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/dataset_register.py +53 -6
  6. ap_client-0.6.0.dev0/ap_client/exporter.py +729 -0
  7. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/instance_commands.py +18 -3
  8. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/irepo_sdk.py +133 -37
  9. ap_client-0.6.0.dev0/ap_client/managed_ak.py +177 -0
  10. ap_client-0.6.0.dev0/ap_client/manifest_view.py +105 -0
  11. ap_client-0.6.0.dev0/ap_client/repo_read_guard.py +45 -0
  12. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/pyproject.toml +4 -3
  13. ap_client-0.5.1.dev4/ap_client/exporter.py +0 -934
  14. ap_client-0.5.1.dev4/ap_client/managed_ak.py +0 -86
  15. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/__init__.py +0 -0
  16. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/akpool_commands.py +0 -0
  17. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/config.py +0 -0
  18. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/fs_commands.py +0 -0
  19. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/profile_commands.py +0 -0
  20. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/split_publish.py +0 -0
  21. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/tbb.py +0 -0
  22. {ap_client-0.5.1.dev4 → ap_client-0.6.0.dev0}/ap_client/waiter.py +0 -0
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.5.1.dev4
3
+ Version: 0.6.0.dev0
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
+ Requires-Dist: irepo-artifact==0.2.1
6
7
  Requires-Dist: pyyaml>=6.0
7
8
  Requires-Dist: requests>=2.28.0
8
9
  Requires-Dist: rich>=13.0.0
9
10
  Requires-Dist: typer>=0.9.0
10
11
  Requires-Dist: websockets>=13.0
11
12
  Provides-Extra: all
12
- Requires-Dist: instance-repo[oss]==1.3.2.dev5; extra == 'all'
13
+ Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.2; extra == 'all'
13
14
  Provides-Extra: dataset
14
- Requires-Dist: instance-repo[oss]==1.3.2.dev5; extra == 'dataset'
15
+ Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.2; extra == 'dataset'
15
16
  Description-Content-Type: text/markdown
16
17
 
17
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.
@@ -539,11 +539,11 @@ class APIClient:
539
539
 
540
540
  def _post(self, path: str, data: Any, timeout: TimeoutType = None) -> Any:
541
541
  """Send a POST request."""
542
- from .managed_ak import parameter_ak_ids, require_parameter_ak_ack
542
+ from .managed_ak import parameter_ak_usage, require_parameter_ak_ack
543
543
 
544
- references = parameter_ak_ids(data) if path == "/jobs" else set()
544
+ references, selectors = parameter_ak_usage(data) if path == "/jobs" else (set(), set())
545
545
  result = self._request("POST", path, json_body=data, timeout=timeout)
546
- require_parameter_ak_ack(result, references)
546
+ require_parameter_ak_ack(result, references, selectors)
547
547
  return result
548
548
 
549
549
  def _delete(self, path: str, *, params: Optional[dict] = None) -> Any:
@@ -25,7 +25,7 @@ from ap_client.api import (
25
25
  )
26
26
  from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
27
27
  from ap_client.dataset_commands import register as _register_dataset_commands
28
- from ap_client.exporter import export_group, export_job
28
+ from ap_client.exporter import export_group, export_job, export_jobs
29
29
  from ap_client.fs_commands import register as _register_fs_commands
30
30
  from ap_client.instance_commands import register as _register_instance_commands
31
31
  from ap_client.managed_ak import (
@@ -3564,8 +3564,8 @@ def job_create(
3564
3564
  if dry_run:
3565
3565
  _emit_dry_run_warnings(dry_run_warnings)
3566
3566
  body["dry_run"] = True
3567
- result = client._post("/jobs", body, timeout=timeout)
3568
3567
  try:
3568
+ result = client._post("/jobs", body, timeout=timeout)
3569
3569
  require_managed_ak_ack(result, ak_id)
3570
3570
  except ManagedAKProtocolError as exc:
3571
3571
  _emit_error(str(exc))
@@ -4876,10 +4876,47 @@ def job_artifacts(
4876
4876
  _print_formatted(results, output_format)
4877
4877
 
4878
4878
 
4879
+ def _validate_export_timeout(value: float) -> float:
4880
+ import math
4881
+
4882
+ if not math.isfinite(value) or value <= 0:
4883
+ raise typer.BadParameter("Timeout must be a finite number greater than zero")
4884
+ return value
4885
+
4886
+
4887
+ def _export_channel_reporter(emit):
4888
+ """Report completed Jobs, including callbacks from Group worker threads."""
4889
+ from threading import Lock
4890
+
4891
+ from rich.text import Text
4892
+
4893
+ counts = {"oss": 0, "proxy": 0}
4894
+ lock = Lock()
4895
+
4896
+ def report(job_id, status, channel):
4897
+ with lock:
4898
+ suffix = ""
4899
+ if status == "downloaded" and channel in counts:
4900
+ counts[channel] += 1
4901
+ suffix = f" channel={channel}"
4902
+ emit(Text(f"{job_id} {status}{suffix}"))
4903
+
4904
+ return report, counts
4905
+
4906
+
4879
4907
  @job_app.command("export")
4880
4908
  def job_export(
4881
- job_id: str = typer.Argument(..., help="Job ID"),
4909
+ job_id: str = typer.Argument(..., help="Job ID or comma-separated Job IDs"),
4882
4910
  output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
4911
+ workers: int = typer.Option(
4912
+ 1, "--workers", "-w", min=1, help="Number of concurrent export workers"
4913
+ ),
4914
+ timeout: float = typer.Option(
4915
+ 300,
4916
+ "--timeout",
4917
+ callback=_validate_export_timeout,
4918
+ help="SDK request timeout in seconds, not the total export duration",
4919
+ ),
4883
4920
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
4884
4921
  events: bool = typer.Option(False, "--events", help="Include events"),
4885
4922
  no_extract_artifacts: bool = typer.Option(
@@ -4889,17 +4926,50 @@ def job_export(
4889
4926
  ),
4890
4927
  ):
4891
4928
  """Export job artifacts to a local directory. Use --logs/--events to include logs and events."""
4929
+ ids = list(dict.fromkeys(value.strip() for value in job_id.split(",")))
4930
+ if not all(ids):
4931
+ raise typer.BadParameter("Job IDs must not be empty", param_hint="job_id")
4892
4932
  client = get_client()
4933
+ if len(ids) > 1:
4934
+ report, channels = _export_channel_reporter(print)
4935
+ dest, summary = export_jobs(
4936
+ client,
4937
+ ids,
4938
+ output,
4939
+ workers=workers,
4940
+ result_callback=report,
4941
+ timeout=timeout,
4942
+ include_logs=logs,
4943
+ include_events=events,
4944
+ extract_artifacts=not no_extract_artifacts,
4945
+ )
4946
+ print(
4947
+ f"Jobs exported: downloaded={summary.downloaded} "
4948
+ f"skipped={summary.skipped} failed={len(summary.failures)}"
4949
+ )
4950
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
4951
+ print(f" path: {dest}")
4952
+ for failed_id, error in summary.failures.items():
4953
+ print(f"[red]Failed job {failed_id}:[/] {error}")
4954
+ if summary.failures:
4955
+ raise typer.Exit(code=1)
4956
+ return
4957
+ job_id = ids[0]
4958
+ result = {}
4893
4959
  dest = export_job(
4894
4960
  client,
4895
4961
  job_id,
4896
4962
  output,
4963
+ result_callback=lambda jid, status, channel: result.update(status=status, channel=channel),
4964
+ timeout=timeout,
4897
4965
  include_logs=logs,
4898
4966
  include_events=events,
4899
4967
  extract_artifacts=not no_extract_artifacts,
4900
4968
  )
4901
4969
  print(f"[green]Job exported:[/] {job_id}")
4902
4970
  print(f" path: {dest}")
4971
+ if result.get("channel") in {"oss", "proxy"} and result.get("status") == "downloaded":
4972
+ print(f" channel: {result['channel']}")
4903
4973
 
4904
4974
 
4905
4975
  @job_app.command("wait")
@@ -6374,6 +6444,12 @@ def group_export(
6374
6444
  workers: int = typer.Option(
6375
6445
  4, "--workers", "-w", min=1, help="Number of concurrent export workers"
6376
6446
  ),
6447
+ timeout: float = typer.Option(
6448
+ 300,
6449
+ "--timeout",
6450
+ callback=_validate_export_timeout,
6451
+ help="SDK request timeout in seconds, not the total export duration",
6452
+ ),
6377
6453
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
6378
6454
  events: bool = typer.Option(False, "--events", help="Include events"),
6379
6455
  no_extract_artifacts: bool = typer.Option(
@@ -6434,12 +6510,15 @@ def group_export(
6434
6510
  current_stage=current_stage,
6435
6511
  )
6436
6512
 
6513
+ report, channels = _export_channel_reporter(progress.console.print)
6437
6514
  dest, summary = export_group(
6438
6515
  client,
6439
6516
  group_id,
6440
6517
  output,
6441
6518
  progress_callback=_on_progress,
6442
6519
  workers=workers,
6520
+ result_callback=report,
6521
+ timeout=timeout,
6443
6522
  include_logs=logs,
6444
6523
  include_events=events,
6445
6524
  extract_artifacts=not no_extract_artifacts,
@@ -6452,6 +6531,7 @@ def group_export(
6452
6531
  f" [green]{summary.downloaded}[/] jobs downloaded, "
6453
6532
  f"[yellow]{summary.skipped}[/] jobs skipped (no artifact)"
6454
6533
  )
6534
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
6455
6535
  if summary.skipped_job_names:
6456
6536
  print(f" Skipped: {', '.join(summary.skipped_job_names)}")
6457
6537
 
@@ -15,6 +15,9 @@ from typing import TYPE_CHECKING, Any, List, Optional
15
15
 
16
16
  import typer
17
17
 
18
+ from .repo_read_guard import STORAGE_SIDES as _STORAGE_SIDES
19
+ from .repo_read_guard import oss_fallback_kwargs
20
+
18
21
  if TYPE_CHECKING: # pragma: no cover - 仅用于类型标注,运行时不 import
19
22
  from datetime import datetime
20
23
 
@@ -316,13 +319,8 @@ def _env_kwargs(environment: str) -> dict:
316
319
  return {"environment": environment} if environment and environment != "auto" else {}
317
320
 
318
321
 
319
- #: 集群绑定存储 → 环境侧(dev 三个变体都是 pre 侧;元数据层它们归并到同一个 pre 集合)。
320
- _STORAGE_SIDES = {
321
- "dev": "pre",
322
- "internal-dev": "pre",
323
- "external-dev": "pre",
324
- "prod": "online",
325
- }
322
+ def _read_env_kwargs(repo: Any, client: Any, method: str, environment: str) -> dict:
323
+ return {**_env_kwargs(environment), **oss_fallback_kwargs(repo, client, method, environment)}
326
324
 
327
325
 
328
326
  def _check_storage_side(repo: Any, environment: str) -> None:
@@ -419,7 +417,9 @@ def _model_dict(record: Any) -> dict:
419
417
  key: value
420
418
  for key, value in payload.items()
421
419
  if value is not None
422
- and (key == "registered" or not (value is False or value == "" or value == [] or value == {}))
420
+ and (
421
+ key == "registered" or not (value is False or value == "" or value == [] or value == {})
422
+ )
423
423
  }
424
424
 
425
425
 
@@ -609,7 +609,7 @@ def _fetch_version(
609
609
  detail = repo.versions.get(
610
610
  dataset,
611
611
  version,
612
- **_env_kwargs(env),
612
+ **_read_env_kwargs(repo, repo.versions, "get", env),
613
613
  **({"metadata_model": metadata_model} if metadata_model else {}),
614
614
  )
615
615
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
@@ -646,19 +646,30 @@ def _unversioned_splits(
646
646
  env = _environment(environment)
647
647
  repo = _dataset_repo()
648
648
  try:
649
- result = repo.splits.list(
650
- dataset,
651
- # CLI 语义:省略 --version 就是无版本空间。SDK 的缺省是 all(跨全部版本),不一样。
652
- version_scope="unversioned",
653
- page_size=_SPLIT_PAGE_SIZE,
654
- **_env_kwargs(env),
655
- )
649
+ records: list[dict] = []
650
+ page = 1
651
+ while True:
652
+ result = repo.splits.list(
653
+ dataset,
654
+ version_scope="unversioned",
655
+ page_size=_SPLIT_PAGE_SIZE,
656
+ **({"page": page} if page > 1 else {}),
657
+ **_read_env_kwargs(repo, repo.splits, "list", env),
658
+ )
659
+ items = getattr(result, "items", None) or []
660
+ for item in items:
661
+ record = _model_dict(item)
662
+ if not _version_value(record.get("version")):
663
+ records.append(record)
664
+ if not items or getattr(result, "has_more", False) is not True:
665
+ break
666
+ page += 1
656
667
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
657
668
  raise _fail(
658
669
  f"dataset {dataset} (no version space): {stage} failed: {_reason(exc)}",
659
670
  f"Check the dataset name and permission: ap dataset get {dataset}",
660
671
  ) from exc
661
- return [_model_dict(item) for item in (getattr(result, "items", None) or [])]
672
+ return records
662
673
 
663
674
 
664
675
  def _unversioned_split(
@@ -670,13 +681,14 @@ def _unversioned_split(
670
681
  env = _environment(environment)
671
682
  repo = _dataset_repo()
672
683
  try:
673
- record = repo.splits.get(dataset, split, **_env_kwargs(env))
684
+ record = repo.splits.get(dataset, split, **_read_env_kwargs(repo, repo.splits, "get", env))
674
685
  except Exception as exc: # noqa: BLE001
675
686
  raise _fail(
676
687
  f"dataset {dataset} (no version space) split {split}: query failed: {_reason(exc)}",
677
688
  f"Check the dataset name and permission: ap dataset get {dataset}",
678
689
  ) from exc
679
- return _model_dict(record) if record is not None else None
690
+ detail = _model_dict(record) if record is not None else None
691
+ return detail if detail is not None and not _version_value(detail.get("version")) else None
680
692
 
681
693
 
682
694
  def _split_records(detail: dict) -> list[dict]:
@@ -1120,7 +1132,11 @@ def _print_dataset_environments(repo: "Repo", dataset: str, detail: dict) -> Non
1120
1132
  rows: list[dict] = []
1121
1133
  for env in envs:
1122
1134
  try:
1123
- env_detail = _model_dict(repo.datasets.get_detail(dataset, environment=env))
1135
+ env_detail = _model_dict(
1136
+ repo.datasets.get_detail(
1137
+ dataset, **_read_env_kwargs(repo, repo.datasets, "get_detail", env)
1138
+ )
1139
+ )
1124
1140
  except Exception: # noqa: BLE001 - 摘要尽力而为
1125
1141
  rows.append({"environment": env, "versions": "?", "splits": "?", "latest_version": "?"})
1126
1142
  continue
@@ -1155,7 +1171,9 @@ def dataset_get(
1155
1171
  env = _environment(environment)
1156
1172
  repo = _dataset_repo()
1157
1173
  try:
1158
- detail = repo.datasets.get_detail(dataset, **_env_kwargs(env))
1174
+ detail = repo.datasets.get_detail(
1175
+ dataset, **_read_env_kwargs(repo, repo.datasets, "get_detail", env)
1176
+ )
1159
1177
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
1160
1178
  if _business_code(exc) not in (92001, 94021):
1161
1179
  raise _fail(
@@ -1164,13 +1182,13 @@ def dataset_get(
1164
1182
  ) from exc
1165
1183
  detail = None
1166
1184
  if detail is None:
1167
- l1, _, l2 = dataset.partition("/")
1168
1185
  rendered = {
1169
1186
  "dataset_name": dataset,
1170
- "dataset_l1": l1,
1171
- "dataset_l2": l2,
1172
1187
  "registered": False,
1173
1188
  }
1189
+ parts = dataset.split("/")
1190
+ if len(parts) == 2 and all(parts):
1191
+ rendered.update(dataset_l1=parts[0], dataset_l2=parts[1])
1174
1192
  else:
1175
1193
  rendered = _registered_dict(detail)
1176
1194
  _emit_detail(rendered, output_format, order=_DATASET_DETAIL_ORDER, always=("status",))
@@ -1294,7 +1312,11 @@ def dataset_claim(
1294
1312
  env = _environment(environment)
1295
1313
  repo = _dataset_repo()
1296
1314
  try:
1297
- detail = _model_dict(repo.datasets.get_detail(dataset, **_env_kwargs(env)))
1315
+ detail = _model_dict(
1316
+ repo.datasets.get_detail(
1317
+ dataset, **_read_env_kwargs(repo, repo.datasets, "get_detail", env)
1318
+ )
1319
+ )
1298
1320
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
1299
1321
  raise _fail(
1300
1322
  f"dataset {dataset}: pre-claim query failed: {_reason(exc)}",
@@ -1372,7 +1394,11 @@ def dataset_claim(
1372
1394
  owner = str(detail.get("owner") or "").strip()
1373
1395
  if not owner:
1374
1396
  try:
1375
- fresh = _model_dict(repo.datasets.get_detail(dataset, **_env_kwargs(env)))
1397
+ fresh = _model_dict(
1398
+ repo.datasets.get_detail(
1399
+ dataset, **_read_env_kwargs(repo, repo.datasets, "get_detail", env)
1400
+ )
1401
+ )
1376
1402
  owner = str(fresh.get("owner") or "").strip()
1377
1403
  except Exception: # noqa: BLE001 - 取不到 owner 不影响幂等退出
1378
1404
  owner = ""
@@ -1658,7 +1684,7 @@ def dataset_version_list(
1658
1684
  page=page,
1659
1685
  page_size=page_size,
1660
1686
  **({"metadata_model": model} if model else {}),
1661
- **_env_kwargs(env),
1687
+ **_read_env_kwargs(repo, repo.versions, "list_paged", env),
1662
1688
  )
1663
1689
  except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
1664
1690
  raise _fail(
@@ -42,7 +42,9 @@ def _confirm(dataset: str, *, yes: bool) -> None:
42
42
  "Add --yes (-y) to confirm the registration",
43
43
  exit_code=2,
44
44
  )
45
- if not typer.confirm(f"Register the previewed data for dataset {dataset}?", default=False, err=True):
45
+ if not typer.confirm(
46
+ f"Register the previewed data for dataset {dataset}?", default=False, err=True
47
+ ):
46
48
  raise datasets._fail(
47
49
  "Registration canceled; nothing was written remotely",
48
50
  "Rerun the command and confirm, or add --yes (-y)",
@@ -52,6 +54,8 @@ def _confirm(dataset: str, *, yes: bool) -> None:
52
54
  def _claim_without_workspace(
53
55
  repo: Any, dataset_id: str, current_user: str, benchmark_id: str
54
56
  ) -> dict:
57
+ # API allows omitted workspace (ClaimAllowsOmittedWorkspace). SDK 1.3.2's
58
+ # claim() still rejects it locally, so keep the authenticated transport shim.
55
59
  body = {"owner": current_user}
56
60
  if benchmark_id:
57
61
  body["benchmark_id"] = benchmark_id
@@ -128,6 +132,22 @@ def _ensure_current_user_owns(
128
132
  f"Check the name and benchmark, then inspect: ap dataset get {dataset}",
129
133
  ) from create_exc
130
134
  created_owner = str(created.get("owner") or "").strip()
135
+ if not created_owner:
136
+ try:
137
+ refreshed = datasets._model_dict(
138
+ repo.datasets.get_detail(dataset, **datasets._env_kwargs(environment))
139
+ )
140
+ created_owner = str(refreshed.get("owner") or "").strip()
141
+ except Exception as exc: # noqa: BLE001 - creation may already have succeeded
142
+ raise datasets._fail(
143
+ f"Dataset {dataset} was created, but ownership could not be verified: {_reason(exc)}",
144
+ f"Inspect ownership before registering again: ap dataset get {dataset}",
145
+ ) from exc
146
+ if not created_owner:
147
+ raise datasets._fail(
148
+ f"Dataset {dataset} was created, but the server returned no owner",
149
+ f"Verify ownership before registering again: ap dataset get {dataset}",
150
+ )
131
151
  if created_owner != current_user:
132
152
  shown_owner = created_owner or "<unknown>"
133
153
  raise datasets._fail(
@@ -173,12 +193,16 @@ def dataset_register(
173
193
  version: Optional[str] = typer.Option(None, "--version", help=datasets._VERSION_HELP),
174
194
  splits: Optional[List[str]] = typer.Option(None, "--split", help="Split to scan; repeatable"),
175
195
  unversioned: bool = typer.Option(
176
- False, "--unversioned", help="Scan the version-less space; mutually exclusive with --version"
196
+ False,
197
+ "--unversioned",
198
+ help="Scan the version-less space; mutually exclusive with --version",
177
199
  ),
178
200
  environment: Optional[str] = typer.Option(
179
201
  None, "--environment", help=datasets._ENVIRONMENT_HELP
180
202
  ),
181
- dry_run: bool = typer.Option(False, "--dry-run", help="Preview without submitting registration"),
203
+ dry_run: bool = typer.Option(
204
+ False, "--dry-run", help="Preview without submitting registration"
205
+ ),
182
206
  yes: bool = typer.Option(False, "--yes", "-y", help="Skip the registration confirmation"),
183
207
  wait: bool = typer.Option(False, "--wait", help="Wait for the ingest task to finish"),
184
208
  timeout: float = typer.Option(1800.0, "--timeout", help="Local wait timeout in seconds"),
@@ -194,6 +218,13 @@ def dataset_register(
194
218
  from .irepo_sdk import _reason
195
219
 
196
220
  fmt = datasets._fmt(output_format)
221
+ dataset = dataset.strip()
222
+ if not dataset:
223
+ raise datasets._fail(
224
+ "Failed to register dataset: name is empty",
225
+ "Pass a non-empty dataset name, e.g. ap dataset register alibaba/a",
226
+ exit_code=2,
227
+ )
197
228
  selected_version = str(version or "").strip() or None
198
229
  selected_splits = [str(value).strip() for value in (splits or []) if str(value).strip()]
199
230
  if selected_version and unversioned:
@@ -258,9 +289,25 @@ def dataset_register(
258
289
  preview_payload = _payload("preview", dataset, preview)
259
290
  if dry_run:
260
291
  datasets._emit_detail(preview_payload, fmt)
292
+ else:
293
+ _emit_preview_stderr(preview_payload, fmt)
294
+ preview_status = str(preview.get("status") or "").strip().lower()
295
+ if preview_status in _TERMINAL_FAILURE | _TERMINAL_CANCELED:
296
+ raise datasets._fail(
297
+ f"Registration preview {preview_status}: "
298
+ f"{preview.get('error_message') or preview.get('error_msg') or 'no details returned'}",
299
+ "Fix the preview error before submitting registration",
300
+ )
301
+ if dry_run:
261
302
  return
262
-
263
- _emit_preview_stderr(preview_payload, fmt)
303
+ counts = preview.get("counts")
304
+ if preview.get("instance_count") == 0 or (
305
+ isinstance(counts, dict) and counts.get("instances") == 0
306
+ ):
307
+ raise datasets._fail(
308
+ "Registration preview found no instances",
309
+ "Check the dataset scope and storage layout before submitting registration",
310
+ )
264
311
  _confirm(dataset, yes=yes)
265
312
  if owner == "@me":
266
313
  _ensure_current_user_owns(repo, dataset, env, benchmark)
@@ -305,7 +352,7 @@ def dataset_register(
305
352
  if outcome == "failed":
306
353
  raise datasets._fail(
307
354
  f"Registration task {ingest_id or '<unknown>'} failed: "
308
- f"{result.get('error_message') or ''}",
355
+ f"{result.get('error_message') or result.get('error_msg') or 'no details returned'}",
309
356
  "Fix the reported data or metadata issue, then run the same registration command again",
310
357
  )
311
358
  if outcome == "canceled":