ap-client 0.3.2.dev1__tar.gz → 0.4.0__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.3.2.dev1 → ap_client-0.4.0}/PKG-INFO +3 -3
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/api.py +184 -12
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/cli.py +72 -42
- ap_client-0.4.0/ap_client/dataset_commands.py +1994 -0
- ap_client-0.4.0/ap_client/fs_commands.py +665 -0
- ap_client-0.4.0/ap_client/instance_commands.py +771 -0
- ap_client-0.4.0/ap_client/irepo_sdk.py +456 -0
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/profile_commands.py +96 -1
- ap_client-0.4.0/ap_client/split_publish.py +131 -0
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/tbb.py +1 -1
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/pyproject.toml +3 -3
- ap_client-0.3.2.dev1/ap_client/irepo_commands.py +0 -131
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/__init__.py +0 -0
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/config.py +0 -0
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/exporter.py +0 -0
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/managed_ak.py +0 -0
- {ap_client-0.3.2.dev1 → ap_client-0.4.0}/ap_client/waiter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Agent Platform API Client & CLI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: pyyaml>=6.0
|
|
@@ -9,9 +9,9 @@ Requires-Dist: rich>=13.0.0
|
|
|
9
9
|
Requires-Dist: typer>=0.9.0
|
|
10
10
|
Requires-Dist: websockets>=13.0
|
|
11
11
|
Provides-Extra: all
|
|
12
|
-
Requires-Dist: instance-repo[oss]
|
|
12
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.2; extra == 'all'
|
|
13
13
|
Provides-Extra: dataset
|
|
14
|
-
Requires-Dist: instance-repo[oss]
|
|
14
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.2; extra == 'dataset'
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
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.
|
|
@@ -283,6 +283,36 @@ def _ensure_next_token_honored(response: dict, *, next_token: Optional[str], end
|
|
|
283
283
|
)
|
|
284
284
|
|
|
285
285
|
|
|
286
|
+
def _openapi_supports_instance_range(document: Any) -> bool:
|
|
287
|
+
"""Check the actual POST /jobs JSON request schema, failing closed on unknown shapes."""
|
|
288
|
+
|
|
289
|
+
def resolve(node: Any) -> dict:
|
|
290
|
+
seen = set()
|
|
291
|
+
while isinstance(node, dict) and "$ref" in node:
|
|
292
|
+
ref = node["$ref"]
|
|
293
|
+
if not isinstance(ref, str) or not ref.startswith("#/") or ref in seen:
|
|
294
|
+
return {}
|
|
295
|
+
seen.add(ref)
|
|
296
|
+
node = document
|
|
297
|
+
for part in ref[2:].split("/"):
|
|
298
|
+
if not isinstance(node, dict):
|
|
299
|
+
return {}
|
|
300
|
+
node = node.get(part.replace("~1", "/").replace("~0", "~"))
|
|
301
|
+
return node if isinstance(node, dict) else {}
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
path = resolve(document["paths"]["/jobs"])
|
|
305
|
+
body = resolve(path["post"]["requestBody"])
|
|
306
|
+
schema = resolve(body["content"]["application/json"]["schema"])
|
|
307
|
+
properties = schema["properties"]
|
|
308
|
+
except (KeyError, TypeError):
|
|
309
|
+
return False
|
|
310
|
+
if not isinstance(properties, dict) or "instance_range" not in properties:
|
|
311
|
+
return False
|
|
312
|
+
field = resolve(properties["instance_range"])
|
|
313
|
+
return bool(field) and field.get("readOnly") is not True
|
|
314
|
+
|
|
315
|
+
|
|
286
316
|
def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
|
|
287
317
|
"""Build the ``?workspace_id=`` query for name-addressed secret endpoints.
|
|
288
318
|
|
|
@@ -294,6 +324,25 @@ def _secret_ws_params(workspace_id: Optional[str]) -> Optional[dict]:
|
|
|
294
324
|
return {"workspace_id": workspace_id} if workspace_id else None
|
|
295
325
|
|
|
296
326
|
|
|
327
|
+
def _split_paged(payload: Any) -> tuple[list[dict], dict]:
|
|
328
|
+
"""Split a ``PagedSuccess`` envelope into ``(records, pagination)``.
|
|
329
|
+
|
|
330
|
+
``PagedSuccess`` is ``{code,message,data:[...],pagination:{total,page,page_size}}``.
|
|
331
|
+
A bare list (or a null ``data``) yields an empty pagination dict so callers
|
|
332
|
+
can render results without probing the response shape.
|
|
333
|
+
"""
|
|
334
|
+
if isinstance(payload, list):
|
|
335
|
+
return [item for item in payload if isinstance(item, dict)], {}
|
|
336
|
+
if not isinstance(payload, dict):
|
|
337
|
+
return [], {}
|
|
338
|
+
records = payload.get("data")
|
|
339
|
+
pagination = payload.get("pagination")
|
|
340
|
+
return (
|
|
341
|
+
[item for item in (records or []) if isinstance(item, dict)],
|
|
342
|
+
pagination if isinstance(pagination, dict) else {},
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
297
346
|
class APIClient:
|
|
298
347
|
"""Agent Platform API client."""
|
|
299
348
|
|
|
@@ -458,6 +507,28 @@ class APIClient:
|
|
|
458
507
|
"""Send a GET request."""
|
|
459
508
|
return self._request("GET", path, params=params, timeout=timeout)
|
|
460
509
|
|
|
510
|
+
def ensure_profile_range_supported(self, timeout: TimeoutType = None) -> None:
|
|
511
|
+
"""Check the target cluster before sending a profile range request.
|
|
512
|
+
|
|
513
|
+
Do not cache this across submissions: the server may have been rolled back.
|
|
514
|
+
Use the normal authenticated, cluster-scoped request path.
|
|
515
|
+
"""
|
|
516
|
+
try:
|
|
517
|
+
document = self._get(
|
|
518
|
+
"/openapi.json", timeout=timeout if timeout is not None else (10, 30)
|
|
519
|
+
)
|
|
520
|
+
except APIError as exc:
|
|
521
|
+
raise ValueError(
|
|
522
|
+
"Cannot verify server support for --profile --range via /openapi.json; "
|
|
523
|
+
f"no jobs were submitted. Check AP_BASE_URL, AP_CLUSTER and server access. {exc}"
|
|
524
|
+
) from exc
|
|
525
|
+
if not _openapi_supports_instance_range(document):
|
|
526
|
+
raise ValueError(
|
|
527
|
+
"Cannot confirm server support for --profile --range: /openapi.json must "
|
|
528
|
+
"declare instance_range in the POST /jobs JSON request schema. "
|
|
529
|
+
"No jobs were submitted. Upgrade the target server before using --range."
|
|
530
|
+
)
|
|
531
|
+
|
|
461
532
|
def _post(self, path: str, data: Any, timeout: TimeoutType = None) -> Any:
|
|
462
533
|
"""Send a POST request."""
|
|
463
534
|
from .managed_ak import parameter_ak_ids, require_parameter_ak_ack
|
|
@@ -714,8 +785,35 @@ class APIClient:
|
|
|
714
785
|
return self._get(f"/templates/{quote(name)}", params=params)
|
|
715
786
|
|
|
716
787
|
def list_benchmarks(self) -> list:
|
|
717
|
-
"""List all
|
|
718
|
-
|
|
788
|
+
"""List all benchmarks from the central registry, preserving legacy IDs."""
|
|
789
|
+
benchmarks = []
|
|
790
|
+
page = 1
|
|
791
|
+
page_size = 200
|
|
792
|
+
# 与 CLI dataset list 的自动翻页同口径:100 页上限,防分页异常导致死循环。
|
|
793
|
+
max_pages = 100
|
|
794
|
+
while page <= max_pages:
|
|
795
|
+
payload = self._get(
|
|
796
|
+
"/apis/v1/benchmarks", params={"page": page, "page_size": page_size}
|
|
797
|
+
)
|
|
798
|
+
records, pagination = _split_paged(payload)
|
|
799
|
+
for record in records:
|
|
800
|
+
item = dict(record)
|
|
801
|
+
if "source_id" in item:
|
|
802
|
+
item.setdefault("id", item["source_id"])
|
|
803
|
+
benchmarks.append(item)
|
|
804
|
+
if not records or not pagination:
|
|
805
|
+
break
|
|
806
|
+
try:
|
|
807
|
+
total = int(pagination.get("total"))
|
|
808
|
+
except (TypeError, ValueError):
|
|
809
|
+
total = None
|
|
810
|
+
if total is not None:
|
|
811
|
+
if len(benchmarks) >= total:
|
|
812
|
+
break
|
|
813
|
+
elif len(records) < page_size:
|
|
814
|
+
break
|
|
815
|
+
page += 1
|
|
816
|
+
return benchmarks
|
|
719
817
|
|
|
720
818
|
def get_benchmark(self, name: str) -> dict:
|
|
721
819
|
"""Get a single benchmark by exact name."""
|
|
@@ -850,7 +948,16 @@ class APIClient:
|
|
|
850
948
|
"instance_ids": all_instance_ids,
|
|
851
949
|
}
|
|
852
950
|
|
|
853
|
-
# ====================
|
|
951
|
+
# ==================== Dataset series (apiserver dataset domain) ====================
|
|
952
|
+
#
|
|
953
|
+
# 这一节对接 Go apiserver 的 dataset 领域(/apis/v1/datasets/...),与上面
|
|
954
|
+
# ossdata 目录的 /api/datasets 路由完全无关。
|
|
955
|
+
#
|
|
956
|
+
# **dataset 领域的控制面读写全部在 InstanceRepo SDK 上**(见
|
|
957
|
+
# docs/ap-cli-dataset-refactor-design.md §9.5):迁移期加进这里的 registry 方法、
|
|
958
|
+
# 以及最后三条"push 前检查 + publish-status"的旧读,都已随迁移删除。
|
|
959
|
+
# 本文件现在只服务旧 ossdata 路径(`dataset list --legacy`、弃用的
|
|
960
|
+
# `dataset versions/instances`、`job create --dataset`)。
|
|
854
961
|
|
|
855
962
|
def list_meta_models(self) -> dict:
|
|
856
963
|
"""模型词表(apiserver 权威源 GET /apis/v1/meta-models,返回 {items:[...]})。"""
|
|
@@ -1042,6 +1149,7 @@ class APIClient:
|
|
|
1042
1149
|
benchmark: Optional[str] = None,
|
|
1043
1150
|
experiment: Optional[str] = None,
|
|
1044
1151
|
ak_id: Optional[str] = None,
|
|
1152
|
+
instance_range: Optional[str] = None,
|
|
1045
1153
|
) -> dict:
|
|
1046
1154
|
"""Create a job."""
|
|
1047
1155
|
body = self.build_create_job_body(
|
|
@@ -1079,6 +1187,7 @@ class APIClient:
|
|
|
1079
1187
|
batch_size=batch_size,
|
|
1080
1188
|
trials=trials,
|
|
1081
1189
|
resource_profile_id=resource_profile_id,
|
|
1190
|
+
instance_range=instance_range,
|
|
1082
1191
|
meta_model=meta_model,
|
|
1083
1192
|
meta_job_type=meta_job_type,
|
|
1084
1193
|
upstream_platform=upstream_platform,
|
|
@@ -1150,6 +1259,7 @@ class APIClient:
|
|
|
1150
1259
|
benchmark: Optional[str] = None,
|
|
1151
1260
|
experiment: Optional[str] = None,
|
|
1152
1261
|
ak_id: Optional[str] = None,
|
|
1262
|
+
instance_range: Optional[str] = None,
|
|
1153
1263
|
) -> dict:
|
|
1154
1264
|
"""Build the /jobs request body for job creation."""
|
|
1155
1265
|
_validate_credential_source_selection(
|
|
@@ -1243,6 +1353,8 @@ class APIClient:
|
|
|
1243
1353
|
body["profile_version"] = profile_version
|
|
1244
1354
|
if instances is not None:
|
|
1245
1355
|
body["instances"] = instances
|
|
1356
|
+
if instance_range is not None:
|
|
1357
|
+
body["instance_range"] = instance_range
|
|
1246
1358
|
if dataset is not None:
|
|
1247
1359
|
body["dataset"] = dataset
|
|
1248
1360
|
if batch_size is not None:
|
|
@@ -2580,6 +2692,8 @@ class APIClient:
|
|
|
2580
2692
|
spec: dict,
|
|
2581
2693
|
description: str = "",
|
|
2582
2694
|
account_pool: Optional[str] = None,
|
|
2695
|
+
metadata: Optional[dict] = None,
|
|
2696
|
+
tags: Optional[list[str]] = None,
|
|
2583
2697
|
) -> dict:
|
|
2584
2698
|
"""Create a new submission profile."""
|
|
2585
2699
|
body: dict = {"name": name, "spec": spec}
|
|
@@ -2587,6 +2701,10 @@ class APIClient:
|
|
|
2587
2701
|
body["description"] = description
|
|
2588
2702
|
if account_pool is not None:
|
|
2589
2703
|
body["account_pool"] = account_pool
|
|
2704
|
+
if metadata is not None:
|
|
2705
|
+
body["metadata"] = metadata
|
|
2706
|
+
if tags is not None:
|
|
2707
|
+
body["tags"] = tags
|
|
2590
2708
|
return self._post(self._PROFILES_BASE, body)
|
|
2591
2709
|
|
|
2592
2710
|
def list_profiles(
|
|
@@ -2594,6 +2712,11 @@ class APIClient:
|
|
|
2594
2712
|
*,
|
|
2595
2713
|
keyword: Optional[str] = None,
|
|
2596
2714
|
template: Optional[str] = None,
|
|
2715
|
+
dataset: Optional[str] = None,
|
|
2716
|
+
scaffold: Optional[str] = None,
|
|
2717
|
+
owner: Optional[str] = None,
|
|
2718
|
+
profile_ids: Optional[list[str]] = None,
|
|
2719
|
+
tags: Optional[list[str]] = None,
|
|
2597
2720
|
show_all: bool = False,
|
|
2598
2721
|
page: int = 1,
|
|
2599
2722
|
page_size: int = 20,
|
|
@@ -2603,6 +2726,12 @@ class APIClient:
|
|
|
2603
2726
|
Args:
|
|
2604
2727
|
keyword: Filter by name keyword.
|
|
2605
2728
|
template: Filter by template name.
|
|
2729
|
+
dataset: Filter by metadata dataset using a case-insensitive substring match.
|
|
2730
|
+
scaffold: Filter by metadata scaffold using a case-insensitive substring match.
|
|
2731
|
+
owner: Filter by an explicit owner ID instead of the current caller.
|
|
2732
|
+
profile_ids: Filter by profile IDs (maximum 100; mutually exclusive
|
|
2733
|
+
with ``keyword``).
|
|
2734
|
+
tags: Filter by profile tags using AND semantics (maximum 10).
|
|
2606
2735
|
show_all: If False (default), return only profiles owned by the
|
|
2607
2736
|
current caller — the owner filter is resolved client-side
|
|
2608
2737
|
via the configured API key, mirroring how the web console
|
|
@@ -2612,21 +2741,40 @@ class APIClient:
|
|
|
2612
2741
|
page_size: Number of items per page.
|
|
2613
2742
|
|
|
2614
2743
|
Raises:
|
|
2615
|
-
ConfigurationError: When
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
the API key cannot be resolved to a user_id.
|
|
2744
|
+
ConfigurationError: When the default Mine view needs an API key
|
|
2745
|
+
but none is configured, or the key cannot be resolved.
|
|
2746
|
+
ValueError: When mutually exclusive filters are combined.
|
|
2619
2747
|
"""
|
|
2620
|
-
if
|
|
2621
|
-
raise
|
|
2622
|
-
|
|
2623
|
-
)
|
|
2748
|
+
if keyword and profile_ids:
|
|
2749
|
+
raise ValueError("profile_ids and keyword are mutually exclusive")
|
|
2750
|
+
if owner and show_all:
|
|
2751
|
+
raise ValueError("owner and show_all are mutually exclusive")
|
|
2752
|
+
if profile_ids and len(profile_ids) > 100:
|
|
2753
|
+
raise ValueError("profile_ids accepts at most 100 values")
|
|
2754
|
+
if tags and len(tags) > 10:
|
|
2755
|
+
raise ValueError("tags accepts at most 10 values")
|
|
2756
|
+
|
|
2624
2757
|
params: dict = {"page": page, "page_size": page_size}
|
|
2625
2758
|
if keyword:
|
|
2626
2759
|
params["keyword"] = keyword
|
|
2627
2760
|
if template:
|
|
2628
2761
|
params["template"] = template
|
|
2629
|
-
if
|
|
2762
|
+
if dataset:
|
|
2763
|
+
params["dataset"] = dataset
|
|
2764
|
+
if scaffold:
|
|
2765
|
+
params["scaffold"] = scaffold
|
|
2766
|
+
if profile_ids:
|
|
2767
|
+
params["profile_ids"] = ",".join(profile_ids)
|
|
2768
|
+
if tags:
|
|
2769
|
+
params["tags"] = ",".join(tags)
|
|
2770
|
+
if owner:
|
|
2771
|
+
params["owner"] = owner
|
|
2772
|
+
elif not show_all:
|
|
2773
|
+
if not self.config.token_key:
|
|
2774
|
+
raise ConfigurationError(
|
|
2775
|
+
"Cannot list own profiles without authentication. "
|
|
2776
|
+
"Set AP_API_KEY environment variable or use --all."
|
|
2777
|
+
)
|
|
2630
2778
|
try:
|
|
2631
2779
|
owner_id = self._resolve_self_user_id()
|
|
2632
2780
|
except APIError as exc:
|
|
@@ -2642,6 +2790,27 @@ class APIClient:
|
|
|
2642
2790
|
params["owner"] = owner_id
|
|
2643
2791
|
return self._get(self._PROFILES_BASE, params=params)
|
|
2644
2792
|
|
|
2793
|
+
def list_profile_tags(
|
|
2794
|
+
self,
|
|
2795
|
+
*,
|
|
2796
|
+
query: Optional[str] = None,
|
|
2797
|
+
limit: Optional[int] = None,
|
|
2798
|
+
) -> dict:
|
|
2799
|
+
"""List distinct tags for SDK consumers building tag selectors or completion UIs."""
|
|
2800
|
+
params: dict = {}
|
|
2801
|
+
if query:
|
|
2802
|
+
params["q"] = query
|
|
2803
|
+
if limit is not None:
|
|
2804
|
+
params["limit"] = limit
|
|
2805
|
+
return self._get(f"{self._PROFILES_BASE}/tags", params=params)
|
|
2806
|
+
|
|
2807
|
+
def replace_profile_tags(self, profile_id: str, tags: list[str]) -> dict:
|
|
2808
|
+
"""Replace all tags on a submission profile; an empty list clears them."""
|
|
2809
|
+
return self._put(
|
|
2810
|
+
f"{self._PROFILES_BASE}/{quote(profile_id)}/tags",
|
|
2811
|
+
{"tags": tags},
|
|
2812
|
+
)
|
|
2813
|
+
|
|
2645
2814
|
def get_profile(self, id_or_name: str, *, version: Optional[int] = None) -> dict:
|
|
2646
2815
|
"""Get a submission profile by ID or name.
|
|
2647
2816
|
|
|
@@ -2658,6 +2827,7 @@ class APIClient:
|
|
|
2658
2827
|
spec: dict,
|
|
2659
2828
|
description: str = "",
|
|
2660
2829
|
account_pool: Optional[str] = None,
|
|
2830
|
+
metadata: Optional[dict] = None,
|
|
2661
2831
|
) -> dict:
|
|
2662
2832
|
"""Update a submission profile, preserving an omitted default Pool."""
|
|
2663
2833
|
body: dict = {"name": name, "spec": spec}
|
|
@@ -2665,6 +2835,8 @@ class APIClient:
|
|
|
2665
2835
|
body["description"] = description
|
|
2666
2836
|
if account_pool is not None:
|
|
2667
2837
|
body["account_pool"] = account_pool
|
|
2838
|
+
if metadata is not None:
|
|
2839
|
+
body["metadata"] = metadata
|
|
2668
2840
|
return self._put(f"{self._PROFILES_BASE}/{quote(profile_id)}", body)
|
|
2669
2841
|
|
|
2670
2842
|
def delete_profile(self, profile_id: str) -> dict:
|
|
@@ -24,8 +24,10 @@ from ap_client.api import (
|
|
|
24
24
|
set_verbose_override,
|
|
25
25
|
)
|
|
26
26
|
from ap_client.config import ENV_VAR_SPECS, ConfigurationError, _parse_bool, normalize_output_format
|
|
27
|
+
from ap_client.dataset_commands import register as _register_dataset_commands
|
|
27
28
|
from ap_client.exporter import export_group, export_job
|
|
28
|
-
from ap_client.
|
|
29
|
+
from ap_client.fs_commands import register as _register_fs_commands
|
|
30
|
+
from ap_client.instance_commands import register as _register_instance_commands
|
|
29
31
|
from ap_client.managed_ak import (
|
|
30
32
|
ManagedAKProtocolError,
|
|
31
33
|
require_managed_ak_ack,
|
|
@@ -139,7 +141,7 @@ app.add_typer(config_app, name="config")
|
|
|
139
141
|
app.add_typer(template_app, name="template")
|
|
140
142
|
app.add_typer(dataset_app, name="dataset")
|
|
141
143
|
app.add_typer(job_app, name="job")
|
|
142
|
-
|
|
144
|
+
job_app.add_typer(job_tbb_app, name="tbb")
|
|
143
145
|
app.add_typer(group_app, name="group")
|
|
144
146
|
app.add_typer(plan_app, name="plan")
|
|
145
147
|
app.add_typer(datasource_app, name="datasource")
|
|
@@ -153,8 +155,11 @@ app.add_typer(meta_job_type_app, name="meta-job-type")
|
|
|
153
155
|
app.add_typer(benchmark_app, name="benchmark")
|
|
154
156
|
app.add_typer(checkpoint_app, name="checkpoint")
|
|
155
157
|
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
+
# Dataset 领域(见 docs/ap-cli-dataset-refactor-design.md):
|
|
159
|
+
# ap dataset version/split/access 嵌套在 dataset 下;instance/fs 是一等资源。
|
|
160
|
+
_register_dataset_commands(dataset_app)
|
|
161
|
+
_register_instance_commands(app)
|
|
162
|
+
_register_fs_commands(app)
|
|
158
163
|
|
|
159
164
|
_PAI_RUNTIME_ENV_TAGS: tuple[tuple[str, str], ...] = (
|
|
160
165
|
("DLC_JOB_ID", "dlc_job_id"),
|
|
@@ -810,26 +815,43 @@ def _print_json_block(data: Any) -> None:
|
|
|
810
815
|
typer.echo(json.dumps(data, ensure_ascii=False, indent=2))
|
|
811
816
|
|
|
812
817
|
|
|
818
|
+
def _format_nested_detail(value: Any) -> str:
|
|
819
|
+
"""Nested dict/list for detail views: compact one-liner, pretty JSON when long.
|
|
820
|
+
|
|
821
|
+
A detail view must not silently drop data - the manifest in ``ap instance
|
|
822
|
+
get`` used to be cut mid-JSON by the one-liner preview.
|
|
823
|
+
"""
|
|
824
|
+
compact = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
|
|
825
|
+
if len(compact) <= _PLAIN_VALUE_PREVIEW:
|
|
826
|
+
return compact
|
|
827
|
+
return json.dumps(value, ensure_ascii=False, indent=2)
|
|
828
|
+
|
|
829
|
+
|
|
813
830
|
def _print_detail(data: object) -> None:
|
|
814
831
|
"""Render a single object as an aligned key/value detail view.
|
|
815
832
|
|
|
816
833
|
Top-level scalars become key/value rows (``image`` fields shortened); nested
|
|
817
|
-
dict/list fields are shown as compact one-liners
|
|
834
|
+
dict/list fields are shown as compact one-liners, or pretty-printed across
|
|
835
|
+
multiple lines when the one-liner would not fit. Falls back to JSON for
|
|
818
836
|
non-dict payloads.
|
|
819
837
|
"""
|
|
820
838
|
if not isinstance(data, dict):
|
|
821
839
|
_print_json(data)
|
|
822
840
|
return
|
|
823
|
-
|
|
824
|
-
nested_rows: list[tuple[str, Any]] = []
|
|
841
|
+
rows: list[tuple[str, str]] = []
|
|
825
842
|
for key, value in data.items():
|
|
826
843
|
if isinstance(value, (dict, list)):
|
|
827
|
-
|
|
844
|
+
rows.append((key, _format_nested_detail(value)))
|
|
828
845
|
elif key == "image" and isinstance(value, str):
|
|
829
|
-
|
|
846
|
+
rows.append((key, _shorten_image(value)))
|
|
830
847
|
else:
|
|
831
|
-
|
|
832
|
-
|
|
848
|
+
rows.append((key, _format_plain_value(value, max_len=0)))
|
|
849
|
+
width = max((len(label) for label, _text in rows), default=0)
|
|
850
|
+
indent = " " * (width + 4)
|
|
851
|
+
for label, text in rows:
|
|
852
|
+
if "\n" in text:
|
|
853
|
+
text = text.replace("\n", f"\n{indent}")
|
|
854
|
+
typer.echo(f" {label.ljust(width)}: {text}")
|
|
833
855
|
|
|
834
856
|
|
|
835
857
|
def _print_template_get_plain(t: dict) -> None:
|
|
@@ -939,7 +961,7 @@ def _print_field_value(value: Any, output_format: str) -> None:
|
|
|
939
961
|
_print_formatted(value, "json" if output_format == "plain" else output_format)
|
|
940
962
|
|
|
941
963
|
|
|
942
|
-
def _format_plain_value(value: Any) -> str:
|
|
964
|
+
def _format_plain_value(value: Any, *, max_len: int = _PLAIN_VALUE_PREVIEW) -> str:
|
|
943
965
|
if value is None:
|
|
944
966
|
return ""
|
|
945
967
|
if isinstance(value, (dict, list)):
|
|
@@ -947,9 +969,9 @@ def _format_plain_value(value: Any) -> str:
|
|
|
947
969
|
else:
|
|
948
970
|
text = str(value)
|
|
949
971
|
text = text.replace("\r", "\\r").replace("\n", "\\n")
|
|
950
|
-
if len(text) <=
|
|
972
|
+
if not max_len or len(text) <= max_len:
|
|
951
973
|
return text
|
|
952
|
-
return f"{text[:
|
|
974
|
+
return f"{text[: max_len - 3]}..."
|
|
953
975
|
|
|
954
976
|
|
|
955
977
|
def _print_key_values(rows: list[tuple[str, Any]], *, skip_empty: bool = True) -> None:
|
|
@@ -2068,24 +2090,17 @@ def template_fetch(
|
|
|
2068
2090
|
|
|
2069
2091
|
# ==================== Dataset operations ====================
|
|
2070
2092
|
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
output_format = _normalize_output_format(output_format, keep_table=True)
|
|
2083
|
-
client = get_client()
|
|
2084
|
-
result = client.list_all_datasets(search)
|
|
2085
|
-
if output_format == "table":
|
|
2086
|
-
_print_records_table(result)
|
|
2087
|
-
else:
|
|
2088
|
-
_print_formatted(result, output_format)
|
|
2093
|
+
# 设计 §6 / 上游 §13.2:`ap dataset versions` 与 `ap dataset instances` 行为保持不变
|
|
2094
|
+
# (仍打 ossdata 路由,`ap job create` 的配套流程依赖它们),只追加固定弃用文案。
|
|
2095
|
+
# 文案**只写 stderr**——JSON/YAML 的 stdout 必须仍可被 json.loads 直接消费。
|
|
2096
|
+
_DATASET_VERSIONS_DEPRECATION = (
|
|
2097
|
+
"Warning: `ap dataset versions` is deprecated.\n"
|
|
2098
|
+
"Use `ap dataset version list --dataset <dataset>` instead."
|
|
2099
|
+
)
|
|
2100
|
+
_DATASET_INSTANCES_DEPRECATION = (
|
|
2101
|
+
"Warning: `ap dataset instances` is deprecated.\n"
|
|
2102
|
+
"Use `ap instance list --dataset <dataset> --split <split> [--version <version>]` instead."
|
|
2103
|
+
)
|
|
2089
2104
|
|
|
2090
2105
|
|
|
2091
2106
|
@dataset_app.command("versions")
|
|
@@ -2097,8 +2112,9 @@ def dataset_versions(
|
|
|
2097
2112
|
help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
|
|
2098
2113
|
),
|
|
2099
2114
|
):
|
|
2100
|
-
"""List dataset versions."""
|
|
2115
|
+
"""List dataset versions (deprecated: use 'ap dataset version list')."""
|
|
2101
2116
|
output_format = _normalize_output_format(output_format, keep_table=True)
|
|
2117
|
+
_emit_progress(_DATASET_VERSIONS_DEPRECATION)
|
|
2102
2118
|
client = get_client()
|
|
2103
2119
|
versions = client.list_dataset_versions(dataset)
|
|
2104
2120
|
if output_format == "table":
|
|
@@ -2118,8 +2134,9 @@ def dataset_instances(
|
|
|
2118
2134
|
help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
|
|
2119
2135
|
),
|
|
2120
2136
|
):
|
|
2121
|
-
"""List dataset instances."""
|
|
2137
|
+
"""List dataset instances (deprecated: use 'ap instance list')."""
|
|
2122
2138
|
output_format = _normalize_output_format(output_format, keep_table=True)
|
|
2139
|
+
_emit_progress(_DATASET_INSTANCES_DEPRECATION)
|
|
2123
2140
|
client = get_client()
|
|
2124
2141
|
result = client.list_all_dataset_instances(dataset_version)
|
|
2125
2142
|
if output_format == "table":
|
|
@@ -2424,7 +2441,7 @@ def job_tbb_list(
|
|
|
2424
2441
|
help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
|
|
2425
2442
|
),
|
|
2426
2443
|
) -> None:
|
|
2427
|
-
"""List pipeline jobs created by ap job
|
|
2444
|
+
"""List pipeline jobs created by ap job tbb run."""
|
|
2428
2445
|
output_format = _normalize_output_format(output_format, keep_table=True)
|
|
2429
2446
|
selected_columns = (
|
|
2430
2447
|
_parse_columns_option(columns) if output_format in {"plain", "table"} else None
|
|
@@ -2804,7 +2821,8 @@ def job_create(
|
|
|
2804
2821
|
"--range",
|
|
2805
2822
|
help=(
|
|
2806
2823
|
"Select a positional subset of instance IDs. Supports END, START,END, "
|
|
2807
|
-
"START,END,STEP. Also accepts START:END[:STEP] and range(START, END, STEP)"
|
|
2824
|
+
"START,END,STEP. Also accepts START:END[:STEP] and range(START, END, STEP). "
|
|
2825
|
+
"With --profile, the server slices instances before trials expansion"
|
|
2808
2826
|
),
|
|
2809
2827
|
),
|
|
2810
2828
|
exclude_instance_ids: Optional[str] = typer.Option(
|
|
@@ -3261,9 +3279,12 @@ def job_create(
|
|
|
3261
3279
|
"--profile is incompatible with --params-list (profile expands params server-side)"
|
|
3262
3280
|
)
|
|
3263
3281
|
raise typer.Exit(1)
|
|
3264
|
-
if instance_range:
|
|
3265
|
-
|
|
3266
|
-
|
|
3282
|
+
if instance_range is not None:
|
|
3283
|
+
try:
|
|
3284
|
+
_parse_instance_range(instance_range)
|
|
3285
|
+
except ValueError as exc:
|
|
3286
|
+
_emit_error(str(exc))
|
|
3287
|
+
raise typer.Exit(1)
|
|
3267
3288
|
if exclude_instance_ids:
|
|
3268
3289
|
_emit_error("--profile is incompatible with --exclude-instance-ids")
|
|
3269
3290
|
raise typer.Exit(1)
|
|
@@ -3279,7 +3300,7 @@ def job_create(
|
|
|
3279
3300
|
|
|
3280
3301
|
suite_name = f"{template or 'profile'}-sp-{profile}-{_dt.now().strftime('%m%d-%H%M')}"
|
|
3281
3302
|
|
|
3282
|
-
# Parse
|
|
3303
|
+
# Parse IDs only; Profile range selection happens on the server.
|
|
3283
3304
|
instances_list: Optional[list[str]] = None
|
|
3284
3305
|
if instance_id:
|
|
3285
3306
|
try:
|
|
@@ -3320,6 +3341,7 @@ def job_create(
|
|
|
3320
3341
|
**({"ak_id": ak_id} if ak_id is not None else {}),
|
|
3321
3342
|
profile_version=profile_version_req,
|
|
3322
3343
|
instances=instances_list,
|
|
3344
|
+
instance_range=instance_range,
|
|
3323
3345
|
dataset=dataset,
|
|
3324
3346
|
batch_size=batch_size,
|
|
3325
3347
|
trials=trials,
|
|
@@ -3335,6 +3357,13 @@ def job_create(
|
|
|
3335
3357
|
_print_formatted(body, "json" if output_format == "plain" else output_format)
|
|
3336
3358
|
return
|
|
3337
3359
|
|
|
3360
|
+
if instance_range is not None:
|
|
3361
|
+
try:
|
|
3362
|
+
client.ensure_profile_range_supported(timeout=timeout)
|
|
3363
|
+
except (ValueError, ConfigurationError) as exc:
|
|
3364
|
+
_emit_error(str(exc))
|
|
3365
|
+
raise typer.Exit(1) from None
|
|
3366
|
+
|
|
3338
3367
|
if dry_run:
|
|
3339
3368
|
_emit_dry_run_warnings(dry_run_warnings)
|
|
3340
3369
|
body["dry_run"] = True
|
|
@@ -3354,7 +3383,7 @@ def job_create(
|
|
|
3354
3383
|
agenthub_ref=agenthub_ref,
|
|
3355
3384
|
instance_id=instance_id,
|
|
3356
3385
|
dataset=dataset,
|
|
3357
|
-
instance_range=
|
|
3386
|
+
instance_range=instance_range,
|
|
3358
3387
|
exclude_instance_ids=None,
|
|
3359
3388
|
params=params,
|
|
3360
3389
|
params_list_input=None,
|
|
@@ -3429,6 +3458,7 @@ def job_create(
|
|
|
3429
3458
|
**({"ak_id": ak_id} if ak_id is not None else {}),
|
|
3430
3459
|
profile_version=profile_version_req,
|
|
3431
3460
|
instances=instances_list,
|
|
3461
|
+
instance_range=instance_range,
|
|
3432
3462
|
dataset=dataset,
|
|
3433
3463
|
batch_size=batch_size,
|
|
3434
3464
|
trials=trials,
|
|
@@ -4912,7 +4942,7 @@ def benchmark_list(
|
|
|
4912
4942
|
help="Output format: plain/table/json/yaml (default: AP_FORMAT or command default)",
|
|
4913
4943
|
),
|
|
4914
4944
|
):
|
|
4915
|
-
"""List benchmarks from the
|
|
4945
|
+
"""List benchmarks from the central benchmark registry."""
|
|
4916
4946
|
output_format = _normalize_output_format(output_format, keep_table=True)
|
|
4917
4947
|
client = get_client()
|
|
4918
4948
|
result = client.list_benchmarks()
|