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.
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/PKG-INFO +2 -1
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/api.py +7 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/cli.py +104 -2
- ap_client-0.5.0.dev1/ap_client/exporter.py +729 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/pyproject.toml +2 -1
- ap_client-0.5.0.dev0/ap_client/exporter.py +0 -934
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/__init__.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/akpool_commands.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/config.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/dataset_commands.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/fs_commands.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/instance_commands.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/irepo_sdk.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/managed_ak.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/profile_commands.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/split_publish.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/tbb.py +0 -0
- {ap_client-0.5.0.dev0 → ap_client-0.5.0.dev1}/ap_client/waiter.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.5.0.
|
|
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
|
|