ap-client 0.4.1__tar.gz → 0.4.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.
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/PKG-INFO +2 -1
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/api.py +40 -9
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/cli.py +116 -32
- ap_client-0.4.2.dev0/ap_client/exporter.py +728 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/profile_commands.py +19 -2
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/pyproject.toml +2 -1
- ap_client-0.4.1/ap_client/exporter.py +0 -934
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/__init__.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/akpool_commands.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/config.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/dataset_commands.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/fs_commands.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/instance_commands.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/irepo_sdk.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/managed_ak.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/split_publish.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/tbb.py +0 -0
- {ap_client-0.4.1 → ap_client-0.4.2.dev0}/ap_client/waiter.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2.dev0
|
|
4
4
|
Summary: Agent Platform API Client & CLI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: irepo-artifact==0.1.2
|
|
6
7
|
Requires-Dist: pyyaml>=6.0
|
|
7
8
|
Requires-Dist: requests>=2.28.0
|
|
8
9
|
Requires-Dist: rich>=13.0.0
|
|
@@ -9,7 +9,7 @@ import sys
|
|
|
9
9
|
import time
|
|
10
10
|
import uuid
|
|
11
11
|
from email.utils import parsedate_to_datetime
|
|
12
|
-
from typing import Any, Optional, Union
|
|
12
|
+
from typing import Any, Literal, Optional, Union
|
|
13
13
|
from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
|
|
14
14
|
|
|
15
15
|
import requests
|
|
@@ -1186,6 +1186,7 @@ class APIClient:
|
|
|
1186
1186
|
experiment: Optional[str] = None,
|
|
1187
1187
|
ak_id: Optional[str] = None,
|
|
1188
1188
|
instance_range: Optional[str] = None,
|
|
1189
|
+
profile: Optional[str] = None,
|
|
1189
1190
|
) -> dict:
|
|
1190
1191
|
"""Create a job."""
|
|
1191
1192
|
body = self.build_create_job_body(
|
|
@@ -1216,6 +1217,7 @@ class APIClient:
|
|
|
1216
1217
|
credential_type=credential_type,
|
|
1217
1218
|
account_pool=account_pool,
|
|
1218
1219
|
eval_config=eval_config,
|
|
1220
|
+
profile=profile,
|
|
1219
1221
|
profile_id=profile_id,
|
|
1220
1222
|
profile_version=profile_version,
|
|
1221
1223
|
instances=instances,
|
|
@@ -1296,16 +1298,19 @@ class APIClient:
|
|
|
1296
1298
|
experiment: Optional[str] = None,
|
|
1297
1299
|
ak_id: Optional[str] = None,
|
|
1298
1300
|
instance_range: Optional[str] = None,
|
|
1301
|
+
profile: Optional[str] = None,
|
|
1299
1302
|
) -> dict:
|
|
1300
1303
|
"""Build the /jobs request body for job creation."""
|
|
1304
|
+
if profile is not None and (profile_id is not None or profile_version is not None):
|
|
1305
|
+
raise ValueError("profile cannot be combined with profile_id or profile_version")
|
|
1301
1306
|
_validate_credential_source_selection(
|
|
1302
1307
|
account_pool=account_pool,
|
|
1303
1308
|
credential_type=credential_type,
|
|
1304
1309
|
params=params,
|
|
1305
1310
|
params_list=params_list,
|
|
1306
1311
|
)
|
|
1307
|
-
if not template and not profile_id:
|
|
1308
|
-
raise ValueError("Either template or profile_id is required")
|
|
1312
|
+
if not template and not profile_id and profile is None:
|
|
1313
|
+
raise ValueError("Either template, profile or profile_id is required")
|
|
1309
1314
|
validate_ak_selection(ak_id, params, params_list, overrides)
|
|
1310
1315
|
body: dict = {}
|
|
1311
1316
|
if template is not None:
|
|
@@ -1383,6 +1388,8 @@ class APIClient:
|
|
|
1383
1388
|
body["eval_config"] = eval_config
|
|
1384
1389
|
|
|
1385
1390
|
# Profile mode fields
|
|
1391
|
+
if profile is not None:
|
|
1392
|
+
body["profile"] = profile
|
|
1386
1393
|
if profile_id is not None:
|
|
1387
1394
|
body["profile_id"] = profile_id
|
|
1388
1395
|
if profile_version is not None:
|
|
@@ -2248,8 +2255,14 @@ class APIClient:
|
|
|
2248
2255
|
next_token: Optional[str] = None,
|
|
2249
2256
|
pagination: Optional[str] = None,
|
|
2250
2257
|
include_total: bool = True,
|
|
2258
|
+
download_mode: Optional[Literal["oss", "proxy", "both"]] = None,
|
|
2251
2259
|
) -> dict:
|
|
2252
|
-
"""Get one page of
|
|
2260
|
+
"""Get one page of archive links.
|
|
2261
|
+
|
|
2262
|
+
Omit download_mode for the server default and legacy response. Explicit
|
|
2263
|
+
oss/proxy/both modes add a downloads mapping to each artifact; both can
|
|
2264
|
+
return only one successful provider. This method does not download files.
|
|
2265
|
+
"""
|
|
2253
2266
|
params: dict[str, object] = {"skip": skip, "limit": limit}
|
|
2254
2267
|
if include_post_process:
|
|
2255
2268
|
params["include_post_process"] = True
|
|
@@ -2259,19 +2272,28 @@ class APIClient:
|
|
|
2259
2272
|
params["pagination"] = pagination
|
|
2260
2273
|
if not include_total:
|
|
2261
2274
|
params["include_total"] = False
|
|
2275
|
+
if download_mode is not None:
|
|
2276
|
+
params["download_mode"] = download_mode
|
|
2262
2277
|
endpoint = f"/groups/{quote(group_id)}/artifacts"
|
|
2263
2278
|
response = self._get(endpoint, params=params)
|
|
2264
2279
|
_ensure_next_token_honored(response, next_token=next_token, endpoint=endpoint)
|
|
2265
2280
|
return response
|
|
2266
2281
|
|
|
2267
|
-
def get_group_artifacts(
|
|
2268
|
-
|
|
2282
|
+
def get_group_artifacts(
|
|
2283
|
+
self,
|
|
2284
|
+
group_id: str,
|
|
2285
|
+
include_post_process: bool = False,
|
|
2286
|
+
*,
|
|
2287
|
+
download_mode: Optional[Literal["oss", "proxy", "both"]] = None,
|
|
2288
|
+
) -> dict:
|
|
2289
|
+
"""Collect all archive-link pages, preserving download_mode on every page."""
|
|
2269
2290
|
next_token: str | None = None
|
|
2270
2291
|
skip = 0
|
|
2271
2292
|
mode: str | None = None
|
|
2272
2293
|
artifacts: list[dict] = []
|
|
2273
2294
|
last_page: dict | None = None
|
|
2274
2295
|
|
|
2296
|
+
download_options = {"download_mode": download_mode} if download_mode is not None else {}
|
|
2275
2297
|
while True:
|
|
2276
2298
|
page = self.get_group_artifacts_page(
|
|
2277
2299
|
group_id,
|
|
@@ -2281,6 +2303,7 @@ class APIClient:
|
|
|
2281
2303
|
next_token=next_token,
|
|
2282
2304
|
pagination="cursor" if mode in {None, "cursor"} else None,
|
|
2283
2305
|
include_total=False,
|
|
2306
|
+
**download_options,
|
|
2284
2307
|
)
|
|
2285
2308
|
last_page = page
|
|
2286
2309
|
page_artifacts = page.get("artifacts") or []
|
|
@@ -2314,9 +2337,17 @@ class APIClient:
|
|
|
2314
2337
|
)
|
|
2315
2338
|
return result
|
|
2316
2339
|
|
|
2317
|
-
def get_job_artifacts(
|
|
2318
|
-
|
|
2319
|
-
|
|
2340
|
+
def get_job_artifacts(
|
|
2341
|
+
self,
|
|
2342
|
+
job_ids: list,
|
|
2343
|
+
*,
|
|
2344
|
+
download_mode: Optional[Literal["oss", "proxy", "both"]] = None,
|
|
2345
|
+
) -> list:
|
|
2346
|
+
"""Get archive links; explicit modes also return typed ``downloads`` entries."""
|
|
2347
|
+
body = {"job_ids": job_ids}
|
|
2348
|
+
if download_mode is not None:
|
|
2349
|
+
body["download_mode"] = download_mode
|
|
2350
|
+
return self._post("/jobs/artifacts", body)
|
|
2320
2351
|
|
|
2321
2352
|
def get_job_artifacts_manifest(
|
|
2322
2353
|
self,
|
|
@@ -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 (
|
|
@@ -265,7 +265,7 @@ def _build_job_create_retry_command(
|
|
|
265
265
|
args.append(template)
|
|
266
266
|
if ak_id is not None:
|
|
267
267
|
args.extend(["--ak-id", ak_id])
|
|
268
|
-
if profile:
|
|
268
|
+
if profile is not None:
|
|
269
269
|
args.extend(["--profile", profile])
|
|
270
270
|
if resource_profile_id:
|
|
271
271
|
args.extend(["--resource-profile", resource_profile_id])
|
|
@@ -565,6 +565,27 @@ def _require_resource_profile_ack(result: dict, resource_profile_id: Optional[st
|
|
|
565
565
|
raise typer.Exit(1)
|
|
566
566
|
|
|
567
567
|
|
|
568
|
+
def _require_profile_ack(result: dict, profile: str) -> None:
|
|
569
|
+
"""Detect servers that silently ignored the Profile reference (even in dry-run)."""
|
|
570
|
+
profile_id = result.get("profile_id")
|
|
571
|
+
version = result.get("profile_version")
|
|
572
|
+
if isinstance(profile_id, str) and profile_id.strip() and type(version) is int and version > 0:
|
|
573
|
+
return
|
|
574
|
+
identifiers = {key: result[key] for key in ("job_id", "group_id") if result.get(key)}
|
|
575
|
+
jobs = result.get("jobs")
|
|
576
|
+
if isinstance(jobs, list):
|
|
577
|
+
identifiers["job_ids"] = [
|
|
578
|
+
job["job_id"] for job in jobs if isinstance(job, dict) and job.get("job_id")
|
|
579
|
+
]
|
|
580
|
+
_emit_error(
|
|
581
|
+
f"server did not return a resolved profile_id and profile_version for {profile!r}. "
|
|
582
|
+
"Deploy an AP server supporting profile references before using this CLI. "
|
|
583
|
+
"Jobs may already have been created, even with --dry-run; check the returned IDs "
|
|
584
|
+
f"before retrying: {json.dumps(identifiers)}. No legacy-field retry was sent."
|
|
585
|
+
)
|
|
586
|
+
raise typer.Exit(1)
|
|
587
|
+
|
|
588
|
+
|
|
568
589
|
def _select_instance_ids(
|
|
569
590
|
instance_ids: list[str],
|
|
570
591
|
instance_range: Optional[str],
|
|
@@ -3032,7 +3053,7 @@ def job_create(
|
|
|
3032
3053
|
profile: Optional[str] = typer.Option(
|
|
3033
3054
|
None,
|
|
3034
3055
|
"--profile",
|
|
3035
|
-
help="Profile ID or name
|
|
3056
|
+
help="Profile ID or name with optional :latest, :verified, :release (no fallback), or :vN; resolved server-side",
|
|
3036
3057
|
),
|
|
3037
3058
|
resource_profile_id: Optional[str] = typer.Option(
|
|
3038
3059
|
None,
|
|
@@ -3242,39 +3263,19 @@ def job_create(
|
|
|
3242
3263
|
raise typer.Exit(1)
|
|
3243
3264
|
|
|
3244
3265
|
# Reject --trials when not in profile mode (profile-only override)
|
|
3245
|
-
if trials is not None and
|
|
3266
|
+
if trials is not None and profile is None:
|
|
3246
3267
|
_emit_error("--trials is only supported with --profile")
|
|
3247
3268
|
raise typer.Exit(1)
|
|
3248
3269
|
|
|
3249
3270
|
# Reject --eval-config when not in profile mode (use 'ap group create/update --eval-config' instead)
|
|
3250
|
-
if eval_config is not None and
|
|
3271
|
+
if eval_config is not None and profile is None:
|
|
3251
3272
|
_emit_error(
|
|
3252
3273
|
"--eval-config is only supported with --profile. Use 'ap group create --eval-config' or 'ap group update --eval-config' instead."
|
|
3253
3274
|
)
|
|
3254
3275
|
raise typer.Exit(1)
|
|
3255
3276
|
|
|
3256
3277
|
# Profile mode: early branch
|
|
3257
|
-
if profile:
|
|
3258
|
-
# Parse profile:version syntax (e.g. "sweagent-swe:v3" or "sweagent-swe:3")
|
|
3259
|
-
# Also supports :verified suffix (e.g. "sweagent-swe:verified") which stays
|
|
3260
|
-
# in profile_id and is interpreted server-side.
|
|
3261
|
-
profile_version_req: Optional[int] = None
|
|
3262
|
-
if ":" in profile:
|
|
3263
|
-
_, suffix = profile.rsplit(":", 1)
|
|
3264
|
-
if suffix.lower() == "verified":
|
|
3265
|
-
# Keep :verified in the profile string — server parses it from profile_id
|
|
3266
|
-
pass
|
|
3267
|
-
else:
|
|
3268
|
-
profile, version_str = profile.rsplit(":", 1)
|
|
3269
|
-
version_str = version_str.lstrip("v")
|
|
3270
|
-
try:
|
|
3271
|
-
profile_version_req = int(version_str)
|
|
3272
|
-
except ValueError:
|
|
3273
|
-
_emit_error(
|
|
3274
|
-
f"Invalid profile version: '{version_str}' (expected integer or 'verified', e.g. --profile myprofile:v3 or --profile myprofile:verified)"
|
|
3275
|
-
)
|
|
3276
|
-
raise typer.Exit(1)
|
|
3277
|
-
|
|
3278
|
+
if profile is not None:
|
|
3278
3279
|
# Mutual exclusion checks
|
|
3279
3280
|
if params_list_input:
|
|
3280
3281
|
_emit_error(
|
|
@@ -3300,7 +3301,7 @@ def job_create(
|
|
|
3300
3301
|
if not suite_name and not group_id:
|
|
3301
3302
|
from datetime import datetime as _dt
|
|
3302
3303
|
|
|
3303
|
-
suite_name = f"{template or 'profile'}-sp-{profile}-{_dt.now().strftime('%m%d-%H%M')}"
|
|
3304
|
+
suite_name = f"{template or 'profile'}-sp-{profile.replace(':', '-')}-{_dt.now().strftime('%m%d-%H%M')}"
|
|
3304
3305
|
|
|
3305
3306
|
# Parse IDs only; Profile range selection happens on the server.
|
|
3306
3307
|
instances_list: Optional[list[str]] = None
|
|
@@ -3338,10 +3339,9 @@ def job_create(
|
|
|
3338
3339
|
credential_type=credential_type,
|
|
3339
3340
|
account_pool=account_pool,
|
|
3340
3341
|
eval_config=eval_config_dict,
|
|
3341
|
-
|
|
3342
|
+
profile=profile,
|
|
3342
3343
|
resource_profile_id=resource_profile_id,
|
|
3343
3344
|
**({"ak_id": ak_id} if ak_id is not None else {}),
|
|
3344
|
-
profile_version=profile_version_req,
|
|
3345
3345
|
instances=instances_list,
|
|
3346
3346
|
instance_range=instance_range,
|
|
3347
3347
|
dataset=dataset,
|
|
@@ -3375,6 +3375,7 @@ def job_create(
|
|
|
3375
3375
|
except ManagedAKProtocolError as exc:
|
|
3376
3376
|
_emit_error(str(exc))
|
|
3377
3377
|
raise typer.Exit(1) from None
|
|
3378
|
+
_require_profile_ack(result, profile)
|
|
3378
3379
|
_print_formatted(result, "json" if output_format == "plain" else output_format)
|
|
3379
3380
|
return
|
|
3380
3381
|
|
|
@@ -3455,10 +3456,9 @@ def job_create(
|
|
|
3455
3456
|
credential_type=credential_type,
|
|
3456
3457
|
account_pool=account_pool,
|
|
3457
3458
|
eval_config=eval_config_dict,
|
|
3458
|
-
|
|
3459
|
+
profile=profile,
|
|
3459
3460
|
resource_profile_id=resource_profile_id,
|
|
3460
3461
|
**({"ak_id": ak_id} if ak_id is not None else {}),
|
|
3461
|
-
profile_version=profile_version_req,
|
|
3462
3462
|
instances=instances_list,
|
|
3463
3463
|
instance_range=instance_range,
|
|
3464
3464
|
dataset=dataset,
|
|
@@ -3481,6 +3481,8 @@ def job_create(
|
|
|
3481
3481
|
emit_profile_retry_hint_once()
|
|
3482
3482
|
raise
|
|
3483
3483
|
|
|
3484
|
+
_require_profile_ack(result, profile)
|
|
3485
|
+
|
|
3484
3486
|
# Use idempotency_key returned by server (may differ when auto-generated server-side)
|
|
3485
3487
|
effective_idem_key = result.get("idempotency_key") or submission_idempotency_key
|
|
3486
3488
|
|
|
@@ -3493,6 +3495,8 @@ def job_create(
|
|
|
3493
3495
|
print(f" profile: {profile}")
|
|
3494
3496
|
if ak_id is not None:
|
|
3495
3497
|
print(f" ak_ids: {response_ak_ids(result)}")
|
|
3498
|
+
print(f" profile_id: {result['profile_id']}")
|
|
3499
|
+
print(f" profile_version: {result['profile_version']}")
|
|
3496
3500
|
if resource_profile_id is not None:
|
|
3497
3501
|
print(f" resource_profile_id: {resource_profile_id}")
|
|
3498
3502
|
print(f" group_id: {result.get('group_id')}")
|
|
@@ -4667,10 +4671,47 @@ def job_artifacts(
|
|
|
4667
4671
|
_print_formatted(results, output_format)
|
|
4668
4672
|
|
|
4669
4673
|
|
|
4674
|
+
def _validate_export_timeout(value: float) -> float:
|
|
4675
|
+
import math
|
|
4676
|
+
|
|
4677
|
+
if not math.isfinite(value) or value <= 0:
|
|
4678
|
+
raise typer.BadParameter("Timeout must be a finite number greater than zero")
|
|
4679
|
+
return value
|
|
4680
|
+
|
|
4681
|
+
|
|
4682
|
+
def _export_channel_reporter(emit):
|
|
4683
|
+
"""Report completed Jobs, including callbacks from Group worker threads."""
|
|
4684
|
+
from threading import Lock
|
|
4685
|
+
|
|
4686
|
+
from rich.text import Text
|
|
4687
|
+
|
|
4688
|
+
counts = {"oss": 0, "proxy": 0}
|
|
4689
|
+
lock = Lock()
|
|
4690
|
+
|
|
4691
|
+
def report(job_id, status, channel):
|
|
4692
|
+
with lock:
|
|
4693
|
+
suffix = ""
|
|
4694
|
+
if status == "downloaded" and channel in counts:
|
|
4695
|
+
counts[channel] += 1
|
|
4696
|
+
suffix = f" channel={channel}"
|
|
4697
|
+
emit(Text(f"{job_id} {status}{suffix}"))
|
|
4698
|
+
|
|
4699
|
+
return report, counts
|
|
4700
|
+
|
|
4701
|
+
|
|
4670
4702
|
@job_app.command("export")
|
|
4671
4703
|
def job_export(
|
|
4672
|
-
job_id: str = typer.Argument(..., help="Job ID"),
|
|
4704
|
+
job_id: str = typer.Argument(..., help="Job ID or comma-separated Job IDs"),
|
|
4673
4705
|
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Export directory"),
|
|
4706
|
+
workers: int = typer.Option(
|
|
4707
|
+
1, "--workers", "-w", min=1, help="Number of concurrent export workers"
|
|
4708
|
+
),
|
|
4709
|
+
timeout: float = typer.Option(
|
|
4710
|
+
300,
|
|
4711
|
+
"--timeout",
|
|
4712
|
+
callback=_validate_export_timeout,
|
|
4713
|
+
help="SDK request timeout in seconds, not the total export duration",
|
|
4714
|
+
),
|
|
4674
4715
|
logs: bool = typer.Option(False, "--logs", help="Include container logs"),
|
|
4675
4716
|
events: bool = typer.Option(False, "--events", help="Include events"),
|
|
4676
4717
|
no_extract_artifacts: bool = typer.Option(
|
|
@@ -4680,17 +4721,50 @@ def job_export(
|
|
|
4680
4721
|
),
|
|
4681
4722
|
):
|
|
4682
4723
|
"""Export job artifacts to a local directory. Use --logs/--events to include logs and events."""
|
|
4724
|
+
ids = list(dict.fromkeys(value.strip() for value in job_id.split(",")))
|
|
4725
|
+
if not all(ids):
|
|
4726
|
+
raise typer.BadParameter("Job IDs must not be empty", param_hint="job_id")
|
|
4683
4727
|
client = get_client()
|
|
4728
|
+
if len(ids) > 1:
|
|
4729
|
+
report, channels = _export_channel_reporter(print)
|
|
4730
|
+
dest, summary = export_jobs(
|
|
4731
|
+
client,
|
|
4732
|
+
ids,
|
|
4733
|
+
output,
|
|
4734
|
+
workers=workers,
|
|
4735
|
+
result_callback=report,
|
|
4736
|
+
timeout=timeout,
|
|
4737
|
+
include_logs=logs,
|
|
4738
|
+
include_events=events,
|
|
4739
|
+
extract_artifacts=not no_extract_artifacts,
|
|
4740
|
+
)
|
|
4741
|
+
print(
|
|
4742
|
+
f"Jobs exported: downloaded={summary.downloaded} "
|
|
4743
|
+
f"skipped={summary.skipped} failed={len(summary.failures)}"
|
|
4744
|
+
)
|
|
4745
|
+
print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
|
|
4746
|
+
print(f" path: {dest}")
|
|
4747
|
+
for failed_id, error in summary.failures.items():
|
|
4748
|
+
print(f"[red]Failed job {failed_id}:[/] {error}")
|
|
4749
|
+
if summary.failures:
|
|
4750
|
+
raise typer.Exit(code=1)
|
|
4751
|
+
return
|
|
4752
|
+
job_id = ids[0]
|
|
4753
|
+
result = {}
|
|
4684
4754
|
dest = export_job(
|
|
4685
4755
|
client,
|
|
4686
4756
|
job_id,
|
|
4687
4757
|
output,
|
|
4758
|
+
result_callback=lambda jid, status, channel: result.update(status=status, channel=channel),
|
|
4759
|
+
timeout=timeout,
|
|
4688
4760
|
include_logs=logs,
|
|
4689
4761
|
include_events=events,
|
|
4690
4762
|
extract_artifacts=not no_extract_artifacts,
|
|
4691
4763
|
)
|
|
4692
4764
|
print(f"[green]Job exported:[/] {job_id}")
|
|
4693
4765
|
print(f" path: {dest}")
|
|
4766
|
+
if result.get("channel") in {"oss", "proxy"} and result.get("status") == "downloaded":
|
|
4767
|
+
print(f" channel: {result['channel']}")
|
|
4694
4768
|
|
|
4695
4769
|
|
|
4696
4770
|
@job_app.command("wait")
|
|
@@ -6138,6 +6212,12 @@ def group_export(
|
|
|
6138
6212
|
workers: int = typer.Option(
|
|
6139
6213
|
4, "--workers", "-w", min=1, help="Number of concurrent export workers"
|
|
6140
6214
|
),
|
|
6215
|
+
timeout: float = typer.Option(
|
|
6216
|
+
300,
|
|
6217
|
+
"--timeout",
|
|
6218
|
+
callback=_validate_export_timeout,
|
|
6219
|
+
help="SDK request timeout in seconds, not the total export duration",
|
|
6220
|
+
),
|
|
6141
6221
|
logs: bool = typer.Option(False, "--logs", help="Include container logs"),
|
|
6142
6222
|
events: bool = typer.Option(False, "--events", help="Include events"),
|
|
6143
6223
|
no_extract_artifacts: bool = typer.Option(
|
|
@@ -6198,12 +6278,15 @@ def group_export(
|
|
|
6198
6278
|
current_stage=current_stage,
|
|
6199
6279
|
)
|
|
6200
6280
|
|
|
6281
|
+
report, channels = _export_channel_reporter(progress.console.print)
|
|
6201
6282
|
dest, summary = export_group(
|
|
6202
6283
|
client,
|
|
6203
6284
|
group_id,
|
|
6204
6285
|
output,
|
|
6205
6286
|
progress_callback=_on_progress,
|
|
6206
6287
|
workers=workers,
|
|
6288
|
+
result_callback=report,
|
|
6289
|
+
timeout=timeout,
|
|
6207
6290
|
include_logs=logs,
|
|
6208
6291
|
include_events=events,
|
|
6209
6292
|
extract_artifacts=not no_extract_artifacts,
|
|
@@ -6216,6 +6299,7 @@ def group_export(
|
|
|
6216
6299
|
f" [green]{summary.downloaded}[/] jobs downloaded, "
|
|
6217
6300
|
f"[yellow]{summary.skipped}[/] jobs skipped (no artifact)"
|
|
6218
6301
|
)
|
|
6302
|
+
print(f"channels: oss={channels['oss']} proxy={channels['proxy']}")
|
|
6219
6303
|
if summary.skipped_job_names:
|
|
6220
6304
|
print(f" Skipped: {', '.join(summary.skipped_job_names)}")
|
|
6221
6305
|
|