ap-client 0.5.0.dev0__tar.gz → 0.5.0.dev1__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.
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ap-client
3
- Version: 0.5.0.dev0
3
+ Version: 0.5.0.dev1
4
4
  Summary: Agent Platform API Client & CLI
5
5
  Requires-Python: >=3.10
6
+ Requires-Dist: irepo-artifact==0.2.0
6
7
  Requires-Dist: pyyaml>=6.0
7
8
  Requires-Dist: requests>=2.28.0
8
9
  Requires-Dist: rich>=13.0.0
@@ -1283,6 +1283,7 @@ class APIClient:
1283
1283
  benchmark: Optional[str] = None,
1284
1284
  experiment: Optional[str] = None,
1285
1285
  ak_id: Optional[str] = None,
1286
+ ak_mode: str = "auto",
1286
1287
  instance_range: Optional[str] = None,
1287
1288
  profile: Optional[str] = None,
1288
1289
  ) -> dict:
@@ -1334,6 +1335,7 @@ class APIClient:
1334
1335
  benchmark=benchmark,
1335
1336
  experiment=experiment,
1336
1337
  ak_id=ak_id,
1338
+ ak_mode=ak_mode,
1337
1339
  )
1338
1340
 
1339
1341
  result = self._post("/jobs", body, timeout=timeout)
@@ -1395,6 +1397,7 @@ class APIClient:
1395
1397
  benchmark: Optional[str] = None,
1396
1398
  experiment: Optional[str] = None,
1397
1399
  ak_id: Optional[str] = None,
1400
+ ak_mode: str = "auto",
1398
1401
  instance_range: Optional[str] = None,
1399
1402
  profile: Optional[str] = None,
1400
1403
  ) -> dict:
@@ -1410,11 +1413,15 @@ class APIClient:
1410
1413
  if not template and not profile_id and profile is None:
1411
1414
  raise ValueError("Either template, profile or profile_id is required")
1412
1415
  validate_ak_selection(ak_id, params, params_list, overrides)
1416
+ if ak_mode not in {"auto", "manual", "synced"}:
1417
+ raise ValueError("ak_mode must be auto, manual or synced")
1413
1418
  body: dict = {}
1414
1419
  if template is not None:
1415
1420
  body["template"] = template
1416
1421
  if ak_id is not None:
1417
1422
  body["ak_id"] = ak_id
1423
+ if ak_mode != "auto":
1424
+ body["ak_mode"] = ak_mode
1418
1425
 
1419
1426
  if params is not None:
1420
1427
  body["params"] = params
@@ -26,7 +26,7 @@ from ap_client.api import (
26
26
  )
27
27
  from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
28
28
  from ap_client.dataset_commands import register as _register_dataset_commands
29
- from ap_client.exporter import export_group, export_job
29
+ from ap_client.exporter import export_group, export_job, export_jobs
30
30
  from ap_client.fs_commands import register as _register_fs_commands
31
31
  from ap_client.instance_commands import register as _register_instance_commands
32
32
  from ap_client.managed_ak import (
@@ -267,12 +267,15 @@ def _build_job_create_retry_command(
267
267
  resource_profile_id: Optional[str] = None,
268
268
  timeout: Optional[float] = None,
269
269
  ak_id: Optional[str] = None,
270
+ ak_mode: str = "auto",
270
271
  ) -> str:
271
272
  args = ["ap", "job", "create"]
272
273
  if template is not None:
273
274
  args.append(template)
274
275
  if ak_id is not None:
275
276
  args.extend(["--ak-id", ak_id])
277
+ if ak_mode != "auto":
278
+ args.extend(["--ak-mode", ak_mode])
276
279
  if profile is not None:
277
280
  args.extend(["--profile", profile])
278
281
  if resource_profile_id:
@@ -1240,6 +1243,7 @@ def _idempotency_item_key(
1240
1243
  resource_profile_id: Optional[str] = None,
1241
1244
  max_failure_retries: int = 0,
1242
1245
  ak_id: Optional[str] = None,
1246
+ ak_mode: str = "auto",
1243
1247
  ) -> str:
1244
1248
  # Client-supplied item keys are authoritative; server hashing is only a
1245
1249
  # fallback for non-CLI callers, so this payload intentionally need not
@@ -1259,6 +1263,8 @@ def _idempotency_item_key(
1259
1263
  }
1260
1264
  if ak_id is not None:
1261
1265
  payload_data["ak_id"] = ak_id
1266
+ if ak_mode != "auto":
1267
+ payload_data["ak_mode"] = ak_mode
1262
1268
  if resource_profile_id is not None:
1263
1269
  payload_data["resource_profile_id"] = resource_profile_id
1264
1270
  if max_failure_retries:
@@ -2990,6 +2996,9 @@ def job_create(
2990
2996
  ak_id: Optional[str] = typer.Option(
2991
2997
  None, "--ak-id", help="AP-managed AK ID; requires server support"
2992
2998
  ),
2999
+ ak_mode: str = typer.Option(
3000
+ "auto", "--ak-mode", help="AK credential source: auto, manual or synced"
3001
+ ),
2993
3002
  agenthub_ref: Optional[str] = typer.Option(
2994
3003
  None, "--agenthub-ref", help="Agent-Hub branch/commit; overrides AP_AGENTHUB_REF"
2995
3004
  ),
@@ -3332,6 +3341,8 @@ def job_create(
3332
3341
  output_format = _normalize_output_format(output_format)
3333
3342
  if not template and not profile:
3334
3343
  raise typer.BadParameter("TEMPLATE is required unless --profile is provided")
3344
+ if ak_mode not in {"auto", "manual", "synced"}:
3345
+ raise typer.BadParameter("Choose auto, manual or synced", param_hint="--ak-mode")
3335
3346
  try:
3336
3347
  validate_ak_id(ak_id)
3337
3348
  resource_profile_id = _normalize_resource_profile_id(resource_profile_id)
@@ -3528,6 +3539,7 @@ def job_create(
3528
3539
  profile=profile,
3529
3540
  resource_profile_id=resource_profile_id,
3530
3541
  **({"ak_id": ak_id} if ak_id is not None else {}),
3542
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3531
3543
  instances=instances_list,
3532
3544
  instance_range=instance_range,
3533
3545
  dataset=dataset,
@@ -3601,6 +3613,7 @@ def job_create(
3601
3613
  profile=profile,
3602
3614
  resource_profile_id=resource_profile_id,
3603
3615
  **({"ak_id": ak_id} if ak_id is not None else {}),
3616
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3604
3617
  timeout=timeout,
3605
3618
  )
3606
3619
  if idempotency_enabled and submission_idempotency_key is not None
@@ -3645,6 +3658,7 @@ def job_create(
3645
3658
  profile=profile,
3646
3659
  resource_profile_id=resource_profile_id,
3647
3660
  **({"ak_id": ak_id} if ak_id is not None else {}),
3661
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3648
3662
  instances=instances_list,
3649
3663
  instance_range=instance_range,
3650
3664
  dataset=dataset,
@@ -3860,6 +3874,7 @@ def job_create(
3860
3874
  idempotency_key=submission_idempotency_key,
3861
3875
  resource_profile_id=resource_profile_id,
3862
3876
  **({"ak_id": ak_id} if ak_id is not None else {}),
3877
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3863
3878
  timeout=timeout,
3864
3879
  )
3865
3880
  if not dry_run
@@ -3956,6 +3971,7 @@ def job_create(
3956
3971
  account_pool=account_pool,
3957
3972
  resource_profile_id=resource_profile_id,
3958
3973
  **({"ak_id": ak_id} if ak_id is not None else {}),
3974
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3959
3975
  max_failure_retries=max_failure_retries,
3960
3976
  )
3961
3977
  batch.append(item)
@@ -3989,6 +4005,7 @@ def job_create(
3989
4005
  account_pool=account_pool,
3990
4006
  resource_profile_id=resource_profile_id,
3991
4007
  **({"ak_id": ak_id} if ak_id is not None else {}),
4008
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
3992
4009
  checkpoint_id=checkpoint_id,
3993
4010
  checkpoint_path=checkpoint_path,
3994
4011
  checkpoint=checkpoint_dict,
@@ -4032,6 +4049,7 @@ def job_create(
4032
4049
  account_pool=account_pool,
4033
4050
  resource_profile_id=resource_profile_id,
4034
4051
  **({"ak_id": ak_id} if ak_id is not None else {}),
4052
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4035
4053
  checkpoint_id=checkpoint_id,
4036
4054
  checkpoint_path=checkpoint_path,
4037
4055
  checkpoint=checkpoint_dict,
@@ -4089,6 +4107,7 @@ def job_create(
4089
4107
  account_pool=account_pool,
4090
4108
  resource_profile_id=resource_profile_id,
4091
4109
  **({"ak_id": ak_id} if ak_id is not None else {}),
4110
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4092
4111
  checkpoint_id=checkpoint_id,
4093
4112
  checkpoint_path=checkpoint_path,
4094
4113
  checkpoint=checkpoint_dict,
@@ -4195,6 +4214,7 @@ def job_create(
4195
4214
  account_pool=account_pool,
4196
4215
  resource_profile_id=resource_profile_id,
4197
4216
  **({"ak_id": ak_id} if ak_id is not None else {}),
4217
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4198
4218
  checkpoint_id=checkpoint_id,
4199
4219
  checkpoint_path=checkpoint_path,
4200
4220
  checkpoint=checkpoint_dict,
@@ -4232,6 +4252,7 @@ def job_create(
4232
4252
  account_pool=account_pool,
4233
4253
  resource_profile_id=resource_profile_id,
4234
4254
  **({"ak_id": ak_id} if ak_id is not None else {}),
4255
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4235
4256
  checkpoint_id=checkpoint_id,
4236
4257
  checkpoint_path=checkpoint_path,
4237
4258
  checkpoint=checkpoint_dict,
@@ -4279,6 +4300,7 @@ def job_create(
4279
4300
  account_pool=account_pool,
4280
4301
  resource_profile_id=resource_profile_id,
4281
4302
  **({"ak_id": ak_id} if ak_id is not None else {}),
4303
+ **({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
4282
4304
  checkpoint_id=checkpoint_id,
4283
4305
  checkpoint_path=checkpoint_path,
4284
4306
  checkpoint=checkpoint_dict,
@@ -4857,10 +4879,47 @@ def job_artifacts(
4857
4879
  _print_formatted(results, output_format)
4858
4880
 
4859
4881
 
4882
+ def _validate_export_timeout(value: float) -> float:
4883
+ import math
4884
+
4885
+ if not math.isfinite(value) or value <= 0:
4886
+ raise typer.BadParameter("Timeout must be a finite number greater than zero")
4887
+ return value
4888
+
4889
+
4890
+ def _export_channel_reporter(emit):
4891
+ """Report completed Jobs, including callbacks from Group worker threads."""
4892
+ from threading import Lock
4893
+
4894
+ from rich.text import Text
4895
+
4896
+ counts = {"oss": 0, "proxy": 0}
4897
+ lock = Lock()
4898
+
4899
+ def report(job_id, status, channel):
4900
+ with lock:
4901
+ suffix = ""
4902
+ if status == "downloaded" and channel in counts:
4903
+ counts[channel] += 1
4904
+ suffix = f" channel={channel}"
4905
+ emit(Text(f"{job_id} {status}{suffix}"))
4906
+
4907
+ return report, counts
4908
+
4909
+
4860
4910
  @job_app.command("export")
4861
4911
  def job_export(
4862
- job_id: str = typer.Argument(..., help="Job ID"),
4912
+ job_id: str = typer.Argument(..., help="Job ID or comma-separated Job IDs"),
4863
4913
  output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
4914
+ workers: int = typer.Option(
4915
+ 1, "--workers", "-w", min=1, help="Number of concurrent export workers"
4916
+ ),
4917
+ timeout: float = typer.Option(
4918
+ 300,
4919
+ "--timeout",
4920
+ callback=_validate_export_timeout,
4921
+ help="SDK request timeout in seconds, not the total export duration",
4922
+ ),
4864
4923
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
4865
4924
  events: bool = typer.Option(False, "--events", help="Include events"),
4866
4925
  no_extract_artifacts: bool = typer.Option(
@@ -4870,17 +4929,50 @@ def job_export(
4870
4929
  ),
4871
4930
  ):
4872
4931
  """Export job artifacts to a local directory. Use --logs/--events to include logs and events."""
4932
+ ids = list(dict.fromkeys(value.strip() for value in job_id.split(",")))
4933
+ if not all(ids):
4934
+ raise typer.BadParameter("Job IDs must not be empty", param_hint="job_id")
4873
4935
  client = get_client()
4936
+ if len(ids) > 1:
4937
+ report, channels = _export_channel_reporter(print)
4938
+ dest, summary = export_jobs(
4939
+ client,
4940
+ ids,
4941
+ output,
4942
+ workers=workers,
4943
+ result_callback=report,
4944
+ timeout=timeout,
4945
+ include_logs=logs,
4946
+ include_events=events,
4947
+ extract_artifacts=not no_extract_artifacts,
4948
+ )
4949
+ print(
4950
+ f"Jobs exported: downloaded={summary.downloaded} "
4951
+ f"skipped={summary.skipped} failed={len(summary.failures)}"
4952
+ )
4953
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
4954
+ print(f" path: {dest}")
4955
+ for failed_id, error in summary.failures.items():
4956
+ print(f"[red]Failed job {failed_id}:[/] {error}")
4957
+ if summary.failures:
4958
+ raise typer.Exit(code=1)
4959
+ return
4960
+ job_id = ids[0]
4961
+ result = {}
4874
4962
  dest = export_job(
4875
4963
  client,
4876
4964
  job_id,
4877
4965
  output,
4966
+ result_callback=lambda jid, status, channel: result.update(status=status, channel=channel),
4967
+ timeout=timeout,
4878
4968
  include_logs=logs,
4879
4969
  include_events=events,
4880
4970
  extract_artifacts=not no_extract_artifacts,
4881
4971
  )
4882
4972
  print(f"[green]Job exported:[/] {job_id}")
4883
4973
  print(f" path: {dest}")
4974
+ if result.get("channel") in {"oss", "proxy"} and result.get("status") == "downloaded":
4975
+ print(f" channel: {result['channel']}")
4884
4976
 
4885
4977
 
4886
4978
  @job_app.command("wait")
@@ -6355,6 +6447,12 @@ def group_export(
6355
6447
  workers: int = typer.Option(
6356
6448
  4, "--workers", "-w", min=1, help="Number of concurrent export workers"
6357
6449
  ),
6450
+ timeout: float = typer.Option(
6451
+ 300,
6452
+ "--timeout",
6453
+ callback=_validate_export_timeout,
6454
+ help="SDK request timeout in seconds, not the total export duration",
6455
+ ),
6358
6456
  logs: bool = typer.Option(False, "--logs", help="Include container logs"),
6359
6457
  events: bool = typer.Option(False, "--events", help="Include events"),
6360
6458
  no_extract_artifacts: bool = typer.Option(
@@ -6415,12 +6513,15 @@ def group_export(
6415
6513
  current_stage=current_stage,
6416
6514
  )
6417
6515
 
6516
+ report, channels = _export_channel_reporter(progress.console.print)
6418
6517
  dest, summary = export_group(
6419
6518
  client,
6420
6519
  group_id,
6421
6520
  output,
6422
6521
  progress_callback=_on_progress,
6423
6522
  workers=workers,
6523
+ result_callback=report,
6524
+ timeout=timeout,
6424
6525
  include_logs=logs,
6425
6526
  include_events=events,
6426
6527
  extract_artifacts=not no_extract_artifacts,
@@ -6433,6 +6534,7 @@ def group_export(
6433
6534
  f" [green]{summary.downloaded}[/] jobs downloaded, "
6434
6535
  f"[yellow]{summary.skipped}[/] jobs skipped (no artifact)"
6435
6536
  )
6537
+ print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
6436
6538
  if summary.skipped_job_names:
6437
6539
  print(f" Skipped: {', '.join(summary.skipped_job_names)}")
6438
6540