ap-client 0.5.1.dev4__tar.gz → 0.5.2.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 (24) hide show
  1. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/PKG-INFO +4 -3
  2. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/api.py +12 -4
  3. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/cli.py +186 -25
  4. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/dataset_commands.py +53 -27
  5. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/dataset_register.py +53 -6
  6. ap_client-0.5.2.dev0/ap_client/exporter.py +987 -0
  7. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/instance_commands.py +467 -44
  8. ap_client-0.5.2.dev0/ap_client/instance_options.py +94 -0
  9. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/irepo_sdk.py +151 -37
  10. ap_client-0.5.2.dev0/ap_client/managed_ak.py +177 -0
  11. ap_client-0.5.2.dev0/ap_client/manifest_view.py +105 -0
  12. ap_client-0.5.2.dev0/ap_client/repo_read_guard.py +45 -0
  13. ap_client-0.5.2.dev0/ap_client/share_api.py +229 -0
  14. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/pyproject.toml +4 -3
  15. ap_client-0.5.1.dev4/ap_client/exporter.py +0 -934
  16. ap_client-0.5.1.dev4/ap_client/managed_ak.py +0 -86
  17. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/__init__.py +0 -0
  18. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/akpool_commands.py +0 -0
  19. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/config.py +0 -0
  20. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/fs_commands.py +0 -0
  21. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/profile_commands.py +0 -0
  22. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/split_publish.py +0 -0
  23. {ap_client-0.5.1.dev4 → ap_client-0.5.2.dev0}/ap_client/tbb.py +0 -0
  24. {ap_client-0.5.1.dev4 → ap_client-0.5.2.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.5.2.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.dev1; 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.dev1; 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 (
@@ -359,6 +359,22 @@ def _emit_error(message: str) -> None:
359
359
  error_console.print(f"[red]error: {message}[/]")
360
360
 
361
361
 
362
+ def _read_client(share_token: Optional[str] = None, group_id: Optional[str] = None):
363
+ if group_id and not share_token:
364
+ raise typer.BadParameter("--group-id requires --share-token")
365
+ client = get_client()
366
+ if share_token is None:
367
+ return client
368
+ from ap_client.share_api import ShareAPIClient
369
+
370
+ # Typer verbose tracebacks must not render the share token in locals.
371
+ app.pretty_exceptions_show_locals = False
372
+ try:
373
+ return ShareAPIClient.from_client(client, share_token, group_id=group_id)
374
+ except ValueError as exc:
375
+ raise typer.BadParameter(str(exc)) from exc
376
+
377
+
362
378
  def _reject_account_pool_template_credential_cli(
363
379
  account_pool: Optional[str],
364
380
  *credential_types: Any,
@@ -3564,8 +3580,8 @@ def job_create(
3564
3580
  if dry_run:
3565
3581
  _emit_dry_run_warnings(dry_run_warnings)
3566
3582
  body["dry_run"] = True
3567
- result = client._post("/jobs", body, timeout=timeout)
3568
3583
  try:
3584
+ result = client._post("/jobs", body, timeout=timeout)
3569
3585
  require_managed_ak_ack(result, ak_id)
3570
3586
  except ManagedAKProtocolError as exc:
3571
3587
  _emit_error(str(exc))
@@ -4427,6 +4443,9 @@ def job_create(
4427
4443
  @job_app.command("get")
4428
4444
  def job_get(
4429
4445
  job_id: str = typer.Argument(..., help="Job ID"),
4446
+ share_token: Optional[str] = typer.Option(
4447
+ None, "--share-token", help="Read with a share token"
4448
+ ),
4430
4449
  include_params: bool = typer.Option(
4431
4450
  False,
4432
4451
  "--include-params",
@@ -4463,7 +4482,7 @@ def job_get(
4463
4482
  include_spec = include_spec or _field_path_requires_spec(field)
4464
4483
  if field == "job_spec_data.spec_yaml":
4465
4484
  spec_format = "yaml"
4466
- client = get_client()
4485
+ client = _read_client(share_token)
4467
4486
  result = client.get_job(
4468
4487
  job_id,
4469
4488
  include_params=include_params,
@@ -4489,6 +4508,9 @@ def job_get(
4489
4508
  @job_app.command("containers")
4490
4509
  def job_containers(
4491
4510
  job_id: str = typer.Argument(..., help="Job ID"),
4511
+ share_token: Optional[str] = typer.Option(
4512
+ None, "--share-token", help="Read with a share token"
4513
+ ),
4492
4514
  output_format: str = typer.Option(
4493
4515
  None,
4494
4516
  "--format",
@@ -4497,7 +4519,7 @@ def job_containers(
4497
4519
  ):
4498
4520
  """List container names for a job."""
4499
4521
  output_format = _normalize_output_format(output_format, keep_table=True)
4500
- client = get_client()
4522
+ client = _read_client(share_token)
4501
4523
  result = client.get_job_containers(job_id)
4502
4524
  if output_format == "table":
4503
4525
  _print_records_table(result)
@@ -4559,6 +4581,9 @@ def _print_tail_logs(logs, tail: int) -> None:
4559
4581
  @job_app.command("logs")
4560
4582
  def job_logs(
4561
4583
  job_id: str = typer.Argument(..., help="Job ID"),
4584
+ share_token: Optional[str] = typer.Option(
4585
+ None, "--share-token", help="Read with a share token"
4586
+ ),
4562
4587
  container: Optional[str] = typer.Option("main", "--container", "-c", help="Container name"),
4563
4588
  tail: Optional[int] = typer.Option(None, "--tail", "-n", help="Show only the last N log lines"),
4564
4589
  follow: bool = typer.Option(
@@ -4570,7 +4595,7 @@ def job_logs(
4570
4595
  ),
4571
4596
  ):
4572
4597
  """Show job logs."""
4573
- client = get_client()
4598
+ client = _read_client(share_token)
4574
4599
 
4575
4600
  if tail is not None:
4576
4601
  if tail <= 0:
@@ -4634,6 +4659,9 @@ def job_logs(
4634
4659
  @job_app.command("events")
4635
4660
  def job_events(
4636
4661
  job_id: str = typer.Argument(..., help="Job ID"),
4662
+ share_token: Optional[str] = typer.Option(
4663
+ None, "--share-token", help="Read with a share token"
4664
+ ),
4637
4665
  container: Optional[str] = typer.Option(None, "--container", "-c", help="Container name"),
4638
4666
  output_format: str = typer.Option(
4639
4667
  None,
@@ -4643,7 +4671,7 @@ def job_events(
4643
4671
  ):
4644
4672
  """Show job events."""
4645
4673
  output_format = _normalize_output_format(output_format)
4646
- client = get_client()
4674
+ client = _read_client(share_token)
4647
4675
  result = client.get_job_events(job_id, container=container)
4648
4676
  if output_format == "plain":
4649
4677
  _print_job_events_plain(result)
@@ -4654,6 +4682,9 @@ def job_events(
4654
4682
  @job_app.command("metrics")
4655
4683
  def job_metrics(
4656
4684
  job_id: str = typer.Argument(..., help="Job ID"),
4685
+ share_token: Optional[str] = typer.Option(
4686
+ None, "--share-token", help="Read with a share token"
4687
+ ),
4657
4688
  output_format: str = typer.Option(
4658
4689
  None,
4659
4690
  "--format",
@@ -4662,7 +4693,7 @@ def job_metrics(
4662
4693
  ):
4663
4694
  """Show job metrics."""
4664
4695
  output_format = _normalize_output_format(output_format)
4665
- client = get_client()
4696
+ client = _read_client(share_token)
4666
4697
  result = client.get_job_metrics(job_id)
4667
4698
  if output_format == "plain":
4668
4699
  _print_json(result)
@@ -4859,6 +4890,9 @@ def job_retry(
4859
4890
  @job_app.command("artifacts")
4860
4891
  def job_artifacts(
4861
4892
  job_ids: str = typer.Argument(..., help="Job IDs (comma-separated for multiple)"),
4893
+ share_token: Optional[str] = typer.Option(
4894
+ None, "--share-token", help="Read with a share token"
4895
+ ),
4862
4896
  output_format: str = typer.Option(
4863
4897
  None,
4864
4898
  "--format",
@@ -4867,7 +4901,7 @@ def job_artifacts(
4867
4901
  ):
4868
4902
  """Show job artifact download links."""
4869
4903
  output_format = _normalize_output_format(output_format, keep_table=True)
4870
- client = get_client()
4904
+ client = _read_client(share_token)
4871
4905
  ids = [jid.strip() for jid in job_ids.split(",")]
4872
4906
  results = client.get_job_artifacts(ids)
4873
4907
  if output_format == "table":
@@ -4876,10 +4910,50 @@ def job_artifacts(
4876
4910
  _print_formatted(results, output_format)
4877
4911
 
4878
4912
 
4913
+ def _validate_export_timeout(value: float) -> float:
4914
+ import math
4915
+
4916
+ if not math.isfinite(value) or value <= 0:
4917
+ raise typer.BadParameter("Timeout must be a finite number greater than zero")
4918
+ return value
4919
+
4920
+
4921
+ def _export_channel_reporter(emit):
4922
+ """Report completed Jobs, including callbacks from Group worker threads."""
4923
+ from threading import Lock
4924
+
4925
+ from rich.text import Text
4926
+
4927
+ counts = {"oss": 0, "proxy": 0}
4928
+ lock = Lock()
4929
+
4930
+ def report(job_id, status, channel):
4931
+ with lock:
4932
+ suffix = ""
4933
+ if status == "downloaded" and channel in counts:
4934
+ counts[channel] += 1
4935
+ suffix = f" channel={channel}"
4936
+ emit(Text(f"{job_id} {status}{suffix}"))
4937
+
4938
+ return report, counts
4939
+
4940
+
4879
4941
  @job_app.command("export")
4880
4942
  def job_export(
4881
- job_id: str = typer.Argument(..., help="Job ID"),
4943
+ job_id: str = typer.Argument(..., help="Job ID or comma-separated Job IDs"),
4944
+ share_token: Optional[str] = typer.Option(
4945
+ None, "--share-token", help="Export with a share token"
4946
+ ),
4882
4947
  output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
4948
+ workers: int = typer.Option(
4949
+ 1, "--workers", "-w", min=1, help="Number of concurrent export workers"
4950
+ ),
4951
+ timeout: float = typer.Option(
4952
+ 300,
4953
+ "--timeout",
4954
+ callback=_validate_export_timeout,
4955
+ help="SDK request timeout in seconds, not the total export duration",
4956
+ ),
4883
4957
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
4884
4958
  events: bool = typer.Option(False, "--events", help="Include events"),
4885
4959
  no_extract_artifacts: bool = typer.Option(
@@ -4889,17 +4963,50 @@ def job_export(
4889
4963
  ),
4890
4964
  ):
4891
4965
  """Export job artifacts to a local directory. Use --logs/--events to include logs and events."""
4892
- client = get_client()
4966
+ ids = list(dict.fromkeys(value.strip() for value in job_id.split(",")))
4967
+ if not all(ids):
4968
+ raise typer.BadParameter("Job IDs must not be empty", param_hint="job_id")
4969
+ client = _read_client(share_token)
4970
+ if len(ids) > 1:
4971
+ report, channels = _export_channel_reporter(print)
4972
+ dest, summary = export_jobs(
4973
+ client,
4974
+ ids,
4975
+ output,
4976
+ workers=workers,
4977
+ result_callback=report,
4978
+ timeout=timeout,
4979
+ include_logs=logs,
4980
+ include_events=events,
4981
+ extract_artifacts=not no_extract_artifacts,
4982
+ )
4983
+ print(
4984
+ f"Jobs exported: downloaded={summary.downloaded} "
4985
+ f"skipped={summary.skipped} failed={len(summary.failures)}"
4986
+ )
4987
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
4988
+ print(f" path: {dest}")
4989
+ for failed_id, error in summary.failures.items():
4990
+ print(f"[red]Failed job {failed_id}:[/] {error}")
4991
+ if summary.failures:
4992
+ raise typer.Exit(code=1)
4993
+ return
4994
+ job_id = ids[0]
4995
+ result = {}
4893
4996
  dest = export_job(
4894
4997
  client,
4895
4998
  job_id,
4896
4999
  output,
5000
+ result_callback=lambda jid, status, channel: result.update(status=status, channel=channel),
5001
+ timeout=timeout,
4897
5002
  include_logs=logs,
4898
5003
  include_events=events,
4899
5004
  extract_artifacts=not no_extract_artifacts,
4900
5005
  )
4901
5006
  print(f"[green]Job exported:[/] {job_id}")
4902
5007
  print(f" path: {dest}")
5008
+ if result.get("channel") in {"oss", "proxy"} and result.get("status") == "downloaded":
5009
+ print(f" channel: {result['channel']}")
4903
5010
 
4904
5011
 
4905
5012
  @job_app.command("wait")
@@ -5674,6 +5781,9 @@ def group_list(
5674
5781
  @group_app.command("get")
5675
5782
  def group_get(
5676
5783
  group_id: str = typer.Argument(..., help="Group ID"),
5784
+ share_token: Optional[str] = typer.Option(
5785
+ None, "--share-token", help="Read with a share token"
5786
+ ),
5677
5787
  output_format: str = typer.Option(
5678
5788
  None,
5679
5789
  "--format",
@@ -5682,7 +5792,7 @@ def group_get(
5682
5792
  ):
5683
5793
  """Show Group details."""
5684
5794
  output_format = _normalize_output_format(output_format, keep_table=True)
5685
- client = get_client()
5795
+ client = _read_client(share_token, group_id if share_token else None)
5686
5796
  result = client.get_group(group_id)
5687
5797
  if output_format == "table":
5688
5798
  _print_detail(result)
@@ -5956,6 +6066,9 @@ def group_update(
5956
6066
  scaffold: Optional[str] = typer.Option(
5957
6067
  None, "--scaffold", "-s", help="Scaffold/agent name (benchmark perspective)"
5958
6068
  ),
6069
+ benchmark: Optional[str] = typer.Option(
6070
+ None, "--benchmark", help="Benchmark name; pass an empty string to clear"
6071
+ ),
5959
6072
  tags: Optional[str] = typer.Option(
5960
6073
  None,
5961
6074
  "--tags",
@@ -6017,6 +6130,7 @@ def group_update(
6017
6130
  dataset,
6018
6131
  model_name,
6019
6132
  scaffold,
6133
+ benchmark,
6020
6134
  tags_list,
6021
6135
  )
6022
6136
  ):
@@ -6031,6 +6145,7 @@ def group_update(
6031
6145
  dataset=dataset,
6032
6146
  model=model_name,
6033
6147
  scaffold=scaffold,
6148
+ **({"benchmark": benchmark} if benchmark is not None else {}),
6034
6149
  **({"tags": tags_list} if tags_list is not None else {}),
6035
6150
  )
6036
6151
  if output_format == "plain":
@@ -6045,6 +6160,7 @@ def group_update(
6045
6160
  "dataset",
6046
6161
  "model",
6047
6162
  "scaffold",
6163
+ "benchmark",
6048
6164
  "updated_at",
6049
6165
  ],
6050
6166
  )
@@ -6162,6 +6278,9 @@ def group_resume(
6162
6278
  @group_app.command("jobs")
6163
6279
  def group_jobs(
6164
6280
  group_id: str = typer.Argument(..., help="Group ID"),
6281
+ share_token: Optional[str] = typer.Option(
6282
+ None, "--share-token", help="Read with a share token"
6283
+ ),
6165
6284
  tag: Optional[list[str]] = typer.Option(
6166
6285
  None, "--tag", help="Filter by tag (repeatable; multiple tags are combined with AND)"
6167
6286
  ),
@@ -6200,17 +6319,36 @@ def group_jobs(
6200
6319
  if cursor and skip > 0:
6201
6320
  raise typer.BadParameter("--cursor cannot be used with --skip greater than 0")
6202
6321
  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
- )
6322
+ if share_token is not None:
6323
+ unsupported = (
6324
+ ("--tag", bool(tag)),
6325
+ ("--include-post-process", include_post_process),
6326
+ ("--next-token", bool(next_token)),
6327
+ ("--cursor", cursor),
6328
+ ("--no-total", no_total),
6329
+ )
6330
+ for option, used in unsupported:
6331
+ if used:
6332
+ _emit_error(f"{option} is not supported with --share-token")
6333
+ raise typer.Exit(1)
6334
+ if skip < 0 or not 1 <= limit <= 500:
6335
+ raise typer.BadParameter(
6336
+ "share pagination requires --skip >= 0 and 1 <= --limit <= 500"
6337
+ )
6338
+ client = _read_client(share_token, group_id if share_token else None)
6339
+ result = client.list_jobs(group_id=group_id, skip=skip, limit=limit)
6340
+ else:
6341
+ client = get_client()
6342
+ result = client.list_group_jobs(
6343
+ group_id,
6344
+ tag=tag,
6345
+ include_post_process=include_post_process,
6346
+ skip=skip,
6347
+ limit=limit,
6348
+ **({"next_token": next_token} if next_token else {}),
6349
+ **({"pagination": "cursor"} if cursor_request else {}),
6350
+ include_total=not no_total,
6351
+ )
6214
6352
  if output_format == "table":
6215
6353
  _print_job_list_plain(result, rich=True)
6216
6354
  elif output_format == "plain":
@@ -6225,6 +6363,9 @@ def group_jobs(
6225
6363
  @group_app.command("stats")
6226
6364
  def group_stats(
6227
6365
  group_id: str = typer.Argument(..., help="Group ID"),
6366
+ share_token: Optional[str] = typer.Option(
6367
+ None, "--share-token", help="Read with a share token"
6368
+ ),
6228
6369
  output_format: str = typer.Option(
6229
6370
  None,
6230
6371
  "--format",
@@ -6233,7 +6374,7 @@ def group_stats(
6233
6374
  ):
6234
6375
  """Show Group progress."""
6235
6376
  output_format = _normalize_output_format(output_format, keep_table=True)
6236
- client = get_client()
6377
+ client = _read_client(share_token, group_id if share_token else None)
6237
6378
  result = client.get_group_stats(group_id)
6238
6379
  if output_format == "table":
6239
6380
  _print_detail(result)
@@ -6244,6 +6385,9 @@ def group_stats(
6244
6385
  @group_app.command("eval")
6245
6386
  def group_eval(
6246
6387
  group_id: str = typer.Argument(..., help="Group ID"),
6388
+ share_token: Optional[str] = typer.Option(
6389
+ None, "--share-token", help="Read with a share token"
6390
+ ),
6247
6391
  summary_only: bool = typer.Option(
6248
6392
  False,
6249
6393
  "--summary-only",
@@ -6257,7 +6401,7 @@ def group_eval(
6257
6401
  ):
6258
6402
  """Show Group aggregated evaluation results."""
6259
6403
  output_format = _normalize_output_format(output_format)
6260
- client = get_client()
6404
+ client = _read_client(share_token, group_id if share_token else None)
6261
6405
  result = (
6262
6406
  client.get_group_eval_summary(group_id) if summary_only else client.get_group_eval(group_id)
6263
6407
  )
@@ -6370,10 +6514,19 @@ def group_access_list(group_id: str = typer.Argument(..., help="Group ID")):
6370
6514
  @group_app.command("export")
6371
6515
  def group_export(
6372
6516
  group_id: str = typer.Argument(..., help="Group ID"),
6517
+ share_token: Optional[str] = typer.Option(
6518
+ None, "--share-token", help="Export with a share token"
6519
+ ),
6373
6520
  output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
6374
6521
  workers: int = typer.Option(
6375
6522
  4, "--workers", "-w", min=1, help="Number of concurrent export workers"
6376
6523
  ),
6524
+ timeout: float = typer.Option(
6525
+ 300,
6526
+ "--timeout",
6527
+ callback=_validate_export_timeout,
6528
+ help="SDK request timeout in seconds, not the total export duration",
6529
+ ),
6377
6530
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
6378
6531
  events: bool = typer.Option(False, "--events", help="Include events"),
6379
6532
  no_extract_artifacts: bool = typer.Option(
@@ -6405,7 +6558,11 @@ def group_export(
6405
6558
  _emit_error("--include-files/--exclude-files cannot be used with --no-extract-artifacts")
6406
6559
  raise typer.Exit(1)
6407
6560
 
6408
- client = get_client()
6561
+ if share_token is not None and (include_file_selectors or exclude_file_selectors):
6562
+ _emit_error("--include-files/--exclude-files are not supported with --share-token")
6563
+ raise typer.Exit(1)
6564
+
6565
+ client = _read_client(share_token, group_id if share_token else None)
6409
6566
  from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn
6410
6567
 
6411
6568
  with Progress(
@@ -6434,12 +6591,15 @@ def group_export(
6434
6591
  current_stage=current_stage,
6435
6592
  )
6436
6593
 
6594
+ report, channels = _export_channel_reporter(progress.console.print)
6437
6595
  dest, summary = export_group(
6438
6596
  client,
6439
6597
  group_id,
6440
6598
  output,
6441
6599
  progress_callback=_on_progress,
6442
6600
  workers=workers,
6601
+ result_callback=report,
6602
+ timeout=timeout,
6443
6603
  include_logs=logs,
6444
6604
  include_events=events,
6445
6605
  extract_artifacts=not no_extract_artifacts,
@@ -6452,6 +6612,7 @@ def group_export(
6452
6612
  f" [green]{summary.downloaded}[/] jobs downloaded, "
6453
6613
  f"[yellow]{summary.skipped}[/] jobs skipped (no artifact)"
6454
6614
  )
6615
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
6455
6616
  if summary.skipped_job_names:
6456
6617
  print(f" Skipped: {', '.join(summary.skipped_job_names)}")
6457
6618