ap-client 0.5.1.dev5__tar.gz → 0.5.3.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 (25) hide show
  1. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/PKG-INFO +4 -3
  2. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/api.py +12 -4
  3. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/cli.py +188 -25
  4. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/dataset_commands.py +53 -27
  5. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/dataset_register.py +53 -6
  6. ap_client-0.5.3.dev0/ap_client/exporter.py +987 -0
  7. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/instance_commands.py +7 -2
  8. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/irepo_sdk.py +183 -37
  9. ap_client-0.5.3.dev0/ap_client/managed_ak.py +177 -0
  10. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/manifest_view.py +5 -3
  11. ap_client-0.5.3.dev0/ap_client/ossutil_commands.py +286 -0
  12. ap_client-0.5.3.dev0/ap_client/ossutil_io.py +1034 -0
  13. ap_client-0.5.3.dev0/ap_client/repo_read_guard.py +49 -0
  14. ap_client-0.5.3.dev0/ap_client/share_api.py +229 -0
  15. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/pyproject.toml +4 -3
  16. ap_client-0.5.1.dev5/ap_client/exporter.py +0 -934
  17. ap_client-0.5.1.dev5/ap_client/managed_ak.py +0 -86
  18. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/__init__.py +0 -0
  19. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/akpool_commands.py +0 -0
  20. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/config.py +0 -0
  21. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/fs_commands.py +0 -0
  22. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/profile_commands.py +0 -0
  23. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/split_publish.py +0 -0
  24. {ap_client-0.5.1.dev5 → ap_client-0.5.3.dev0}/ap_client/tbb.py +0 -0
  25. {ap_client-0.5.1.dev5 → ap_client-0.5.3.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.dev5
3
+ Version: 0.5.3.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.5.dev2; 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.5.dev2; 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:
@@ -709,13 +709,16 @@ class APIClient:
709
709
  request_id: str,
710
710
  duration_ms: float,
711
711
  exc: Exception,
712
+ *,
713
+ error_detail: str | None = None,
712
714
  ) -> None:
715
+ detail = str(exc) if error_detail is None else error_detail
713
716
  _write_stderr(
714
717
  [
715
718
  f"!! Error {method} {url}",
716
719
  f" request_id={request_id}",
717
720
  f" duration_ms={duration_ms:.1f}",
718
- f" error={type(exc).__name__}: {exc}",
721
+ f" error={type(exc).__name__}: {detail}",
719
722
  ]
720
723
  )
721
724
 
@@ -1878,9 +1881,12 @@ class APIClient:
1878
1881
  meta_job_type: Optional[str] = None,
1879
1882
  meta_teacher_direction: Optional[str] = None,
1880
1883
  meta_train_stage: Optional[str] = None,
1884
+ benchmark: Optional[str] = None,
1881
1885
  ) -> dict:
1882
1886
  """Update group properties.
1883
1887
 
1888
+ ``benchmark`` is omitted when None; pass an empty string to clear it.
1889
+
1884
1890
  ``tags``(`key:value` 数组)在客户端先与 Group 现有 tags 合并,再以
1885
1891
  全量替换语义提交(服务端 PATCH 是整体替换);同名 key 覆盖,未提及
1886
1892
  的 key 保留。服务端会从 tags 同步 meta_* 等派生字段。
@@ -1903,6 +1909,8 @@ class APIClient:
1903
1909
  body["model"] = model
1904
1910
  if scaffold is not None:
1905
1911
  body["scaffold"] = scaffold
1912
+ if benchmark is not None:
1913
+ body["benchmark"] = benchmark
1906
1914
  meta_attr_tags = _merge_meta_tags(
1907
1915
  None,
1908
1916
  meta_model=meta_model,
@@ -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 (
@@ -35,6 +35,7 @@ from ap_client.managed_ak import (
35
35
  validate_ak_id,
36
36
  validate_ak_selection,
37
37
  )
38
+ from ap_client.ossutil_commands import register as _register_ossutil_commands
38
39
  from ap_client.profile_commands import profile_app
39
40
  from ap_client.waiter import (
40
41
  WaitTimeoutError,
@@ -167,6 +168,7 @@ app.add_typer(checkpoint_app, name="checkpoint")
167
168
  # ap dataset version/split/access 嵌套在 dataset 下;instance/fs 是一等资源。
168
169
  _register_dataset_commands(dataset_app)
169
170
  _register_instance_commands(app)
171
+ _register_ossutil_commands(app)
170
172
  _register_fs_commands(app)
171
173
  _register_akpool_commands(app)
172
174
 
@@ -359,6 +361,22 @@ def _emit_error(message: str) -> None:
359
361
  error_console.print(f"[red]error: {message}[/]")
360
362
 
361
363
 
364
+ def _read_client(share_token: Optional[str] = None, group_id: Optional[str] = None):
365
+ if group_id and not share_token:
366
+ raise typer.BadParameter("--group-id requires --share-token")
367
+ client = get_client()
368
+ if share_token is None:
369
+ return client
370
+ from ap_client.share_api import ShareAPIClient
371
+
372
+ # Typer verbose tracebacks must not render the share token in locals.
373
+ app.pretty_exceptions_show_locals = False
374
+ try:
375
+ return ShareAPIClient.from_client(client, share_token, group_id=group_id)
376
+ except ValueError as exc:
377
+ raise typer.BadParameter(str(exc)) from exc
378
+
379
+
362
380
  def _reject_account_pool_template_credential_cli(
363
381
  account_pool: Optional[str],
364
382
  *credential_types: Any,
@@ -3564,8 +3582,8 @@ def job_create(
3564
3582
  if dry_run:
3565
3583
  _emit_dry_run_warnings(dry_run_warnings)
3566
3584
  body["dry_run"] = True
3567
- result = client._post("/jobs", body, timeout=timeout)
3568
3585
  try:
3586
+ result = client._post("/jobs", body, timeout=timeout)
3569
3587
  require_managed_ak_ack(result, ak_id)
3570
3588
  except ManagedAKProtocolError as exc:
3571
3589
  _emit_error(str(exc))
@@ -4427,6 +4445,9 @@ def job_create(
4427
4445
  @job_app.command("get")
4428
4446
  def job_get(
4429
4447
  job_id: str = typer.Argument(..., help="Job ID"),
4448
+ share_token: Optional[str] = typer.Option(
4449
+ None, "--share-token", help="Read with a share token"
4450
+ ),
4430
4451
  include_params: bool = typer.Option(
4431
4452
  False,
4432
4453
  "--include-params",
@@ -4463,7 +4484,7 @@ def job_get(
4463
4484
  include_spec = include_spec or _field_path_requires_spec(field)
4464
4485
  if field == "job_spec_data.spec_yaml":
4465
4486
  spec_format = "yaml"
4466
- client = get_client()
4487
+ client = _read_client(share_token)
4467
4488
  result = client.get_job(
4468
4489
  job_id,
4469
4490
  include_params=include_params,
@@ -4489,6 +4510,9 @@ def job_get(
4489
4510
  @job_app.command("containers")
4490
4511
  def job_containers(
4491
4512
  job_id: str = typer.Argument(..., help="Job ID"),
4513
+ share_token: Optional[str] = typer.Option(
4514
+ None, "--share-token", help="Read with a share token"
4515
+ ),
4492
4516
  output_format: str = typer.Option(
4493
4517
  None,
4494
4518
  "--format",
@@ -4497,7 +4521,7 @@ def job_containers(
4497
4521
  ):
4498
4522
  """List container names for a job."""
4499
4523
  output_format = _normalize_output_format(output_format, keep_table=True)
4500
- client = get_client()
4524
+ client = _read_client(share_token)
4501
4525
  result = client.get_job_containers(job_id)
4502
4526
  if output_format == "table":
4503
4527
  _print_records_table(result)
@@ -4559,6 +4583,9 @@ def _print_tail_logs(logs, tail: int) -> None:
4559
4583
  @job_app.command("logs")
4560
4584
  def job_logs(
4561
4585
  job_id: str = typer.Argument(..., help="Job ID"),
4586
+ share_token: Optional[str] = typer.Option(
4587
+ None, "--share-token", help="Read with a share token"
4588
+ ),
4562
4589
  container: Optional[str] = typer.Option("main", "--container", "-c", help="Container name"),
4563
4590
  tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log lines"),
4564
4591
  follow: bool = typer.Option(
@@ -4570,7 +4597,7 @@ def job_logs(
4570
4597
  ),
4571
4598
  ):
4572
4599
  """Show job logs."""
4573
- client = get_client()
4600
+ client = _read_client(share_token)
4574
4601
 
4575
4602
  if tail is not None:
4576
4603
  if tail <= 0:
@@ -4634,6 +4661,9 @@ def job_logs(
4634
4661
  @job_app.command("events")
4635
4662
  def job_events(
4636
4663
  job_id: str = typer.Argument(..., help="Job ID"),
4664
+ share_token: Optional[str] = typer.Option(
4665
+ None, "--share-token", help="Read with a share token"
4666
+ ),
4637
4667
  container: Optional[str] = typer.Option(None, "--container", "-c", help="Container name"),
4638
4668
  output_format: str = typer.Option(
4639
4669
  None,
@@ -4643,7 +4673,7 @@ def job_events(
4643
4673
  ):
4644
4674
  """Show job events."""
4645
4675
  output_format = _normalize_output_format(output_format)
4646
- client = get_client()
4676
+ client = _read_client(share_token)
4647
4677
  result = client.get_job_events(job_id, container=container)
4648
4678
  if output_format == "plain":
4649
4679
  _print_job_events_plain(result)
@@ -4654,6 +4684,9 @@ def job_events(
4654
4684
  @job_app.command("metrics")
4655
4685
  def job_metrics(
4656
4686
  job_id: str = typer.Argument(..., help="Job ID"),
4687
+ share_token: Optional[str] = typer.Option(
4688
+ None, "--share-token", help="Read with a share token"
4689
+ ),
4657
4690
  output_format: str = typer.Option(
4658
4691
  None,
4659
4692
  "--format",
@@ -4662,7 +4695,7 @@ def job_metrics(
4662
4695
  ):
4663
4696
  """Show job metrics."""
4664
4697
  output_format = _normalize_output_format(output_format)
4665
- client = get_client()
4698
+ client = _read_client(share_token)
4666
4699
  result = client.get_job_metrics(job_id)
4667
4700
  if output_format == "plain":
4668
4701
  _print_json(result)
@@ -4859,6 +4892,9 @@ def job_retry(
4859
4892
  @job_app.command("artifacts")
4860
4893
  def job_artifacts(
4861
4894
  job_ids: str = typer.Argument(..., help="Job IDs (comma-separated for multiple)"),
4895
+ share_token: Optional[str] = typer.Option(
4896
+ None, "--share-token", help="Read with a share token"
4897
+ ),
4862
4898
  output_format: str = typer.Option(
4863
4899
  None,
4864
4900
  "--format",
@@ -4867,7 +4903,7 @@ def job_artifacts(
4867
4903
  ):
4868
4904
  """Show job artifact download links."""
4869
4905
  output_format = _normalize_output_format(output_format, keep_table=True)
4870
- client = get_client()
4906
+ client = _read_client(share_token)
4871
4907
  ids = [jid.strip() for jid in job_ids.split(",")]
4872
4908
  results = client.get_job_artifacts(ids)
4873
4909
  if output_format == "table":
@@ -4876,10 +4912,50 @@ def job_artifacts(
4876
4912
  _print_formatted(results, output_format)
4877
4913
 
4878
4914
 
4915
+ def _validate_export_timeout(value: float) -> float:
4916
+ import math
4917
+
4918
+ if not math.isfinite(value) or value <= 0:
4919
+ raise typer.BadParameter("Timeout must be a finite number greater than zero")
4920
+ return value
4921
+
4922
+
4923
+ def _export_channel_reporter(emit):
4924
+ """Report completed Jobs, including callbacks from Group worker threads."""
4925
+ from threading import Lock
4926
+
4927
+ from rich.text import Text
4928
+
4929
+ counts = {"oss": 0, "proxy": 0}
4930
+ lock = Lock()
4931
+
4932
+ def report(job_id, status, channel):
4933
+ with lock:
4934
+ suffix = ""
4935
+ if status == "downloaded" and channel in counts:
4936
+ counts[channel] += 1
4937
+ suffix = f" channel={channel}"
4938
+ emit(Text(f"{job_id} {status}{suffix}"))
4939
+
4940
+ return report, counts
4941
+
4942
+
4879
4943
  @job_app.command("export")
4880
4944
  def job_export(
4881
- job_id: str = typer.Argument(..., help="Job ID"),
4945
+ job_id: str = typer.Argument(..., help="Job ID or comma-separated Job IDs"),
4946
+ share_token: Optional[str] = typer.Option(
4947
+ None, "--share-token", help="Export with a share token"
4948
+ ),
4882
4949
  output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
4950
+ workers: int = typer.Option(
4951
+ 1, "--workers", "-w", min=1, help="Number of concurrent export workers"
4952
+ ),
4953
+ timeout: float = typer.Option(
4954
+ 300,
4955
+ "--timeout",
4956
+ callback=_validate_export_timeout,
4957
+ help="SDK request timeout in seconds, not the total export duration",
4958
+ ),
4883
4959
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
4884
4960
  events: bool = typer.Option(False, "--events", help="Include events"),
4885
4961
  no_extract_artifacts: bool = typer.Option(
@@ -4889,17 +4965,50 @@ def job_export(
4889
4965
  ),
4890
4966
  ):
4891
4967
  """Export job artifacts to a local directory. Use --logs/--events to include logs and events."""
4892
- client = get_client()
4968
+ ids = list(dict.fromkeys(value.strip() for value in job_id.split(",")))
4969
+ if not all(ids):
4970
+ raise typer.BadParameter("Job IDs must not be empty", param_hint="job_id")
4971
+ client = _read_client(share_token)
4972
+ if len(ids) > 1:
4973
+ report, channels = _export_channel_reporter(print)
4974
+ dest, summary = export_jobs(
4975
+ client,
4976
+ ids,
4977
+ output,
4978
+ workers=workers,
4979
+ result_callback=report,
4980
+ timeout=timeout,
4981
+ include_logs=logs,
4982
+ include_events=events,
4983
+ extract_artifacts=not no_extract_artifacts,
4984
+ )
4985
+ print(
4986
+ f"Jobs exported: downloaded={summary.downloaded} "
4987
+ f"skipped={summary.skipped} failed={len(summary.failures)}"
4988
+ )
4989
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
4990
+ print(f" path: {dest}")
4991
+ for failed_id, error in summary.failures.items():
4992
+ print(f"[red]Failed job {failed_id}:[/] {error}")
4993
+ if summary.failures:
4994
+ raise typer.Exit(code=1)
4995
+ return
4996
+ job_id = ids[0]
4997
+ result = {}
4893
4998
  dest = export_job(
4894
4999
  client,
4895
5000
  job_id,
4896
5001
  output,
5002
+ result_callback=lambda jid, status, channel: result.update(status=status, channel=channel),
5003
+ timeout=timeout,
4897
5004
  include_logs=logs,
4898
5005
  include_events=events,
4899
5006
  extract_artifacts=not no_extract_artifacts,
4900
5007
  )
4901
5008
  print(f"[green]Job exported:[/] {job_id}")
4902
5009
  print(f" path: {dest}")
5010
+ if result.get("channel") in {"oss", "proxy"} and result.get("status") == "downloaded":
5011
+ print(f" channel: {result['channel']}")
4903
5012
 
4904
5013
 
4905
5014
  @job_app.command("wait")
@@ -5674,6 +5783,9 @@ def group_list(
5674
5783
  @group_app.command("get")
5675
5784
  def group_get(
5676
5785
  group_id: str = typer.Argument(..., help="Group ID"),
5786
+ share_token: Optional[str] = typer.Option(
5787
+ None, "--share-token", help="Read with a share token"
5788
+ ),
5677
5789
  output_format: str = typer.Option(
5678
5790
  None,
5679
5791
  "--format",
@@ -5682,7 +5794,7 @@ def group_get(
5682
5794
  ):
5683
5795
  """Show Group details."""
5684
5796
  output_format = _normalize_output_format(output_format, keep_table=True)
5685
- client = get_client()
5797
+ client = _read_client(share_token, group_id if share_token else None)
5686
5798
  result = client.get_group(group_id)
5687
5799
  if output_format == "table":
5688
5800
  _print_detail(result)
@@ -5956,6 +6068,9 @@ def group_update(
5956
6068
  scaffold: Optional[str] = typer.Option(
5957
6069
  None, "--scaffold", "-s", help="Scaffold/agent name (benchmark perspective)"
5958
6070
  ),
6071
+ benchmark: Optional[str] = typer.Option(
6072
+ None, "--benchmark", help="Benchmark name; pass an empty string to clear"
6073
+ ),
5959
6074
  tags: Optional[str] = typer.Option(
5960
6075
  None,
5961
6076
  "--tags",
@@ -6017,6 +6132,7 @@ def group_update(
6017
6132
  dataset,
6018
6133
  model_name,
6019
6134
  scaffold,
6135
+ benchmark,
6020
6136
  tags_list,
6021
6137
  )
6022
6138
  ):
@@ -6031,6 +6147,7 @@ def group_update(
6031
6147
  dataset=dataset,
6032
6148
  model=model_name,
6033
6149
  scaffold=scaffold,
6150
+ **({"benchmark": benchmark} if benchmark is not None else {}),
6034
6151
  **({"tags": tags_list} if tags_list is not None else {}),
6035
6152
  )
6036
6153
  if output_format == "plain":
@@ -6045,6 +6162,7 @@ def group_update(
6045
6162
  "dataset",
6046
6163
  "model",
6047
6164
  "scaffold",
6165
+ "benchmark",
6048
6166
  "updated_at",
6049
6167
  ],
6050
6168
  )
@@ -6162,6 +6280,9 @@ def group_resume(
6162
6280
  @group_app.command("jobs")
6163
6281
  def group_jobs(
6164
6282
  group_id: str = typer.Argument(..., help="Group ID"),
6283
+ share_token: Optional[str] = typer.Option(
6284
+ None, "--share-token", help="Read with a share token"
6285
+ ),
6165
6286
  tag: Optional[list[str]] = typer.Option(
6166
6287
  None, "--tag", help="Filter by tag (repeatable; multiple tags are combined with AND)"
6167
6288
  ),
@@ -6200,17 +6321,36 @@ def group_jobs(
6200
6321
  if cursor and skip > 0:
6201
6322
  raise typer.BadParameter("--cursor cannot be used with --skip greater than 0")
6202
6323
  cursor_request = bool(next_token) or cursor
6203
- client = get_client()
6204
- result = client.list_group_jobs(
6205
- group_id,
6206
- tag=tag,
6207
- include_post_process=include_post_process,
6208
- skip=skip,
6209
- limit=limit,
6210
- **({"next_token": next_token} if next_token else {}),
6211
- **({"pagination": "cursor"} if cursor_request else {}),
6212
- include_total=not no_total,
6213
- )
6324
+ if share_token is not None:
6325
+ unsupported = (
6326
+ ("--tag", bool(tag)),
6327
+ ("--include-post-process", include_post_process),
6328
+ ("--next-token", bool(next_token)),
6329
+ ("--cursor", cursor),
6330
+ ("--no-total", no_total),
6331
+ )
6332
+ for option, used in unsupported:
6333
+ if used:
6334
+ _emit_error(f"{option} is not supported with --share-token")
6335
+ raise typer.Exit(1)
6336
+ if skip < 0 or not 1 <= limit <= 500:
6337
+ raise typer.BadParameter(
6338
+ "share pagination requires --skip >= 0 and 1 <= --limit <= 500"
6339
+ )
6340
+ client = _read_client(share_token, group_id if share_token else None)
6341
+ result = client.list_jobs(group_id=group_id, skip=skip, limit=limit)
6342
+ else:
6343
+ client = get_client()
6344
+ result = client.list_group_jobs(
6345
+ group_id,
6346
+ tag=tag,
6347
+ include_post_process=include_post_process,
6348
+ skip=skip,
6349
+ limit=limit,
6350
+ **({"next_token": next_token} if next_token else {}),
6351
+ **({"pagination": "cursor"} if cursor_request else {}),
6352
+ include_total=not no_total,
6353
+ )
6214
6354
  if output_format == "table":
6215
6355
  _print_job_list_plain(result, rich=True)
6216
6356
  elif output_format == "plain":
@@ -6225,6 +6365,9 @@ def group_jobs(
6225
6365
  @group_app.command("stats")
6226
6366
  def group_stats(
6227
6367
  group_id: str = typer.Argument(..., help="Group ID"),
6368
+ share_token: Optional[str] = typer.Option(
6369
+ None, "--share-token", help="Read with a share token"
6370
+ ),
6228
6371
  output_format: str = typer.Option(
6229
6372
  None,
6230
6373
  "--format",
@@ -6233,7 +6376,7 @@ def group_stats(
6233
6376
  ):
6234
6377
  """Show Group progress."""
6235
6378
  output_format = _normalize_output_format(output_format, keep_table=True)
6236
- client = get_client()
6379
+ client = _read_client(share_token, group_id if share_token else None)
6237
6380
  result = client.get_group_stats(group_id)
6238
6381
  if output_format == "table":
6239
6382
  _print_detail(result)
@@ -6244,6 +6387,9 @@ def group_stats(
6244
6387
  @group_app.command("eval")
6245
6388
  def group_eval(
6246
6389
  group_id: str = typer.Argument(..., help="Group ID"),
6390
+ share_token: Optional[str] = typer.Option(
6391
+ None, "--share-token", help="Read with a share token"
6392
+ ),
6247
6393
  summary_only: bool = typer.Option(
6248
6394
  False,
6249
6395
  "--summary-only",
@@ -6257,7 +6403,7 @@ def group_eval(
6257
6403
  ):
6258
6404
  """Show Group aggregated evaluation results."""
6259
6405
  output_format = _normalize_output_format(output_format)
6260
- client = get_client()
6406
+ client = _read_client(share_token, group_id if share_token else None)
6261
6407
  result = (
6262
6408
  client.get_group_eval_summary(group_id) if summary_only else client.get_group_eval(group_id)
6263
6409
  )
@@ -6370,10 +6516,19 @@ def group_access_list(group_id: str = typer.Argument(..., help="Group ID")):
6370
6516
  @group_app.command("export")
6371
6517
  def group_export(
6372
6518
  group_id: str = typer.Argument(..., help="Group ID"),
6519
+ share_token: Optional[str] = typer.Option(
6520
+ None, "--share-token", help="Export with a share token"
6521
+ ),
6373
6522
  output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
6374
6523
  workers: int = typer.Option(
6375
6524
  4, "--workers", "-w", min=1, help="Number of concurrent export workers"
6376
6525
  ),
6526
+ timeout: float = typer.Option(
6527
+ 300,
6528
+ "--timeout",
6529
+ callback=_validate_export_timeout,
6530
+ help="SDK request timeout in seconds, not the total export duration",
6531
+ ),
6377
6532
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
6378
6533
  events: bool = typer.Option(False, "--events", help="Include events"),
6379
6534
  no_extract_artifacts: bool = typer.Option(
@@ -6405,7 +6560,11 @@ def group_export(
6405
6560
  _emit_error("--include-files/--exclude-files cannot be used with --no-extract-artifacts")
6406
6561
  raise typer.Exit(1)
6407
6562
 
6408
- client = get_client()
6563
+ if share_token is not None and (include_file_selectors or exclude_file_selectors):
6564
+ _emit_error("--include-files/--exclude-files are not supported with --share-token")
6565
+ raise typer.Exit(1)
6566
+
6567
+ client = _read_client(share_token, group_id if share_token else None)
6409
6568
  from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn
6410
6569
 
6411
6570
  with Progress(
@@ -6434,12 +6593,15 @@ def group_export(
6434
6593
  current_stage=current_stage,
6435
6594
  )
6436
6595
 
6596
+ report, channels = _export_channel_reporter(progress.console.print)
6437
6597
  dest, summary = export_group(
6438
6598
  client,
6439
6599
  group_id,
6440
6600
  output,
6441
6601
  progress_callback=_on_progress,
6442
6602
  workers=workers,
6603
+ result_callback=report,
6604
+ timeout=timeout,
6443
6605
  include_logs=logs,
6444
6606
  include_events=events,
6445
6607
  extract_artifacts=not no_extract_artifacts,
@@ -6452,6 +6614,7 @@ def group_export(
6452
6614
  f" [green]{summary.downloaded}[/] jobs downloaded, "
6453
6615
  f"[yellow]{summary.skipped}[/] jobs skipped (no artifact)"
6454
6616
  )
6617
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
6455
6618
  if summary.skipped_job_names:
6456
6619
  print(f" Skipped: {', '.join(summary.skipped_job_names)}")
6457
6620