ap-client 0.7.0.dev11__tar.gz → 0.7.1.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.7.0.dev11 → ap_client-0.7.1.dev1}/PKG-INFO +1 -1
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/api.py +111 -1
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/cli.py +44 -35
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/dataset_commands.py +33 -12
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/dataset_register.py +1 -2
- ap_client-0.7.1.dev1/ap_client/profile_cascade_all.py +149 -0
- ap_client-0.7.1.dev1/ap_client/profile_cascade_cli.py +356 -0
- ap_client-0.7.1.dev1/ap_client/profile_cascade_display.py +279 -0
- ap_client-0.7.1.dev1/ap_client/profile_cascade_state.py +207 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/profile_commands.py +491 -48
- ap_client-0.7.1.dev1/ap_client/profile_diagnostics.py +117 -0
- ap_client-0.7.1.dev1/ap_client/profile_display.py +172 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/pyproject.toml +1 -1
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/__init__.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/akpool_commands.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/config.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/dataset_name.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/dfs_commands.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/exporter.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/fs_commands.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/fs_storage.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/fs_transfer.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/instance_commands.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/instance_options.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/irepo_sdk.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/managed_ak.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/manifest_view.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/ossutil_hash.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/ossutil_io.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/repo_read_guard.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/share_api.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/split_publish.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/tbb.py +0 -0
- {ap_client-0.7.0.dev11 → ap_client-0.7.1.dev1}/ap_client/waiter.py +0 -0
|
@@ -2963,6 +2963,8 @@ class APIClient:
|
|
|
2963
2963
|
account_pool: Optional[str] = None,
|
|
2964
2964
|
metadata: Optional[dict] = None,
|
|
2965
2965
|
tags: Optional[list[str]] = None,
|
|
2966
|
+
parents: Optional[list[str | dict]] = None,
|
|
2967
|
+
auto_follow_parents: Optional[list[str]] = None,
|
|
2966
2968
|
) -> dict:
|
|
2967
2969
|
"""Create a new submission profile."""
|
|
2968
2970
|
body: dict = {"name": name, "spec": spec}
|
|
@@ -2974,6 +2976,10 @@ class APIClient:
|
|
|
2974
2976
|
body["metadata"] = metadata
|
|
2975
2977
|
if tags is not None:
|
|
2976
2978
|
body["tags"] = tags
|
|
2979
|
+
if parents is not None:
|
|
2980
|
+
body["parents"] = parents
|
|
2981
|
+
if auto_follow_parents is not None:
|
|
2982
|
+
body["auto_follow_parents"] = auto_follow_parents
|
|
2977
2983
|
return self._post(self._PROFILES_BASE, body)
|
|
2978
2984
|
|
|
2979
2985
|
def list_profiles(
|
|
@@ -3115,6 +3121,9 @@ class APIClient:
|
|
|
3115
3121
|
account_pool: Optional[str] = None,
|
|
3116
3122
|
metadata: Optional[dict] = None,
|
|
3117
3123
|
reason: Optional[str] = None,
|
|
3124
|
+
version: Optional[int] = None,
|
|
3125
|
+
parents: Optional[list[str | dict]] = None,
|
|
3126
|
+
auto_follow_parents: Optional[list[str]] = None,
|
|
3118
3127
|
) -> dict:
|
|
3119
3128
|
"""Update a submission profile, preserving an omitted default Pool.
|
|
3120
3129
|
|
|
@@ -3130,7 +3139,108 @@ class APIClient:
|
|
|
3130
3139
|
body["metadata"] = metadata
|
|
3131
3140
|
if reason is not None:
|
|
3132
3141
|
body["reason"] = reason
|
|
3133
|
-
|
|
3142
|
+
if version is not None:
|
|
3143
|
+
if type(version) is not int or version < 1:
|
|
3144
|
+
raise ValueError("version must be a positive integer")
|
|
3145
|
+
body["version"] = version
|
|
3146
|
+
if parents is not None:
|
|
3147
|
+
body["parents"] = parents
|
|
3148
|
+
if auto_follow_parents is not None:
|
|
3149
|
+
body["auto_follow_parents"] = auto_follow_parents
|
|
3150
|
+
return self._put(f"{self._PROFILES_BASE}/{quote(profile_id, safe='')}", body)
|
|
3151
|
+
|
|
3152
|
+
def patch_profile(self, profile_id: str, fields: dict) -> dict:
|
|
3153
|
+
"""Patch own definition; callers should supply their original version."""
|
|
3154
|
+
return self._request(
|
|
3155
|
+
"PATCH", f"{self._PROFILES_BASE}/{quote(profile_id, safe='')}", json_body=fields
|
|
3156
|
+
)
|
|
3157
|
+
|
|
3158
|
+
def resolve_profile(self, identifier: str, *, version: Optional[int] = None) -> dict:
|
|
3159
|
+
return self._get(
|
|
3160
|
+
f"{self._PROFILES_BASE}/{quote(identifier, safe='')}/resolved",
|
|
3161
|
+
params={"version": version} if version is not None else None,
|
|
3162
|
+
)
|
|
3163
|
+
|
|
3164
|
+
def preview_profile(self, draft: dict) -> dict:
|
|
3165
|
+
return self._post(f"{self._PROFILES_BASE}/resolve", draft)
|
|
3166
|
+
|
|
3167
|
+
def profile_children(
|
|
3168
|
+
self,
|
|
3169
|
+
identifier: str,
|
|
3170
|
+
*,
|
|
3171
|
+
page: int = 1,
|
|
3172
|
+
page_size: int = 20,
|
|
3173
|
+
target_version: Optional[int] = None,
|
|
3174
|
+
eligible_only: bool = False,
|
|
3175
|
+
current_only: bool = False,
|
|
3176
|
+
) -> dict:
|
|
3177
|
+
params = {"page": page, "page_size": page_size}
|
|
3178
|
+
if target_version is not None:
|
|
3179
|
+
params["target_version"] = target_version
|
|
3180
|
+
if eligible_only:
|
|
3181
|
+
params["eligible_only"] = "true"
|
|
3182
|
+
if current_only:
|
|
3183
|
+
params["current_only"] = "true"
|
|
3184
|
+
return self._get(
|
|
3185
|
+
f"{self._PROFILES_BASE}/{quote(identifier, safe='')}/children",
|
|
3186
|
+
params=params,
|
|
3187
|
+
)
|
|
3188
|
+
|
|
3189
|
+
def profile_diff(
|
|
3190
|
+
self,
|
|
3191
|
+
identifier: str,
|
|
3192
|
+
from_version: Optional[int] = None,
|
|
3193
|
+
to_version: Optional[int] = None,
|
|
3194
|
+
view: str = "own",
|
|
3195
|
+
*,
|
|
3196
|
+
compare_to: Optional[str] = None,
|
|
3197
|
+
) -> dict:
|
|
3198
|
+
"""Compare versions of one Profile, or two Profiles (omitted versions use current)."""
|
|
3199
|
+
params = {"from": from_version, "to": to_version, "view": view}
|
|
3200
|
+
if compare_to is not None:
|
|
3201
|
+
params["compare_to"] = compare_to
|
|
3202
|
+
result = self._get(
|
|
3203
|
+
f"{self._PROFILES_BASE}/{quote(identifier, safe='')}/diff",
|
|
3204
|
+
params=params,
|
|
3205
|
+
)
|
|
3206
|
+
if compare_to is not None:
|
|
3207
|
+
# Older servers ignore unknown query fields. Do not present their
|
|
3208
|
+
# single-Profile response as a successful cross-Profile comparison.
|
|
3209
|
+
for field in ("from_profile", "to_profile"):
|
|
3210
|
+
ref = result.get(field) if isinstance(result, dict) else None
|
|
3211
|
+
if (
|
|
3212
|
+
not isinstance(ref, dict)
|
|
3213
|
+
or not isinstance(ref.get("profile_id"), str)
|
|
3214
|
+
or not ref["profile_id"].startswith("sp-")
|
|
3215
|
+
or type(ref.get("version")) is not int
|
|
3216
|
+
or ref["version"] < 1
|
|
3217
|
+
):
|
|
3218
|
+
raise APIError(
|
|
3219
|
+
200,
|
|
3220
|
+
"Server did not confirm cross-Profile diff support; upgrade the server before comparing different Profiles",
|
|
3221
|
+
None,
|
|
3222
|
+
None,
|
|
3223
|
+
)
|
|
3224
|
+
return result
|
|
3225
|
+
|
|
3226
|
+
def cascade_profiles(self, identifier: str, request: dict) -> dict:
|
|
3227
|
+
return self._post(f"{self._PROFILES_BASE}/{quote(identifier, safe='')}/cascades", request)
|
|
3228
|
+
|
|
3229
|
+
def get_profile_cascade(self, operation_id: str) -> dict:
|
|
3230
|
+
return self._get(f"/apis/v1/profile-cascades/{quote(operation_id, safe='')}")
|
|
3231
|
+
|
|
3232
|
+
def profile_cascade_history(
|
|
3233
|
+
self, profile_id: str, *, page: int = 1, page_size: int = 20
|
|
3234
|
+
) -> dict:
|
|
3235
|
+
return self._get(
|
|
3236
|
+
f"{self._PROFILES_BASE}/{quote(profile_id, safe='')}/cascades",
|
|
3237
|
+
params={"page": page, "page_size": page_size},
|
|
3238
|
+
)
|
|
3239
|
+
|
|
3240
|
+
def retry_profile_cascade(self, operation_id: str, items: list[dict]) -> dict:
|
|
3241
|
+
return self._post(
|
|
3242
|
+
f"/apis/v1/profile-cascades/{quote(operation_id, safe='')}/retry", {"items": items}
|
|
3243
|
+
)
|
|
3134
3244
|
|
|
3135
3245
|
def delete_profile(self, profile_id: str) -> dict:
|
|
3136
3246
|
"""Delete a submission profile."""
|
|
@@ -3003,22 +3003,22 @@ def job_create(
|
|
|
3003
3003
|
meta_model: Optional[str] = typer.Option(
|
|
3004
3004
|
None,
|
|
3005
3005
|
"--meta-model",
|
|
3006
|
-
help="
|
|
3006
|
+
help="Meta model tag; omit to leave unset",
|
|
3007
3007
|
),
|
|
3008
3008
|
meta_job_type: Optional[str] = typer.Option(
|
|
3009
3009
|
None,
|
|
3010
3010
|
"--meta-job-type",
|
|
3011
|
-
help="
|
|
3011
|
+
help="Meta job type tag: Eval-Eval (default) or Others",
|
|
3012
3012
|
),
|
|
3013
3013
|
meta_teacher_direction: Optional[str] = typer.Option(
|
|
3014
3014
|
None,
|
|
3015
3015
|
"--meta-teacher-direction",
|
|
3016
|
-
help="
|
|
3016
|
+
help="Meta teacher direction tag; omit to derive from dataset_series; options: ap meta-teacher-direction list",
|
|
3017
3017
|
),
|
|
3018
3018
|
meta_train_stage: Optional[str] = typer.Option(
|
|
3019
3019
|
None,
|
|
3020
3020
|
"--meta-train-stage",
|
|
3021
|
-
help="
|
|
3021
|
+
help="Meta train stage tag; omit to derive from dataset run_type; options: ap meta-train-stage list",
|
|
3022
3022
|
),
|
|
3023
3023
|
overrides: Optional[str] = typer.Option(None, "--overrides", help="Override config (JSON)"),
|
|
3024
3024
|
concurrency: Optional[int] = typer.Option(
|
|
@@ -3190,7 +3190,7 @@ def job_create(
|
|
|
3190
3190
|
benchmark: Optional[str] = typer.Option(
|
|
3191
3191
|
None,
|
|
3192
3192
|
"--benchmark",
|
|
3193
|
-
help="
|
|
3193
|
+
help="Benchmark name from the AP registry (see 'ap benchmark list')",
|
|
3194
3194
|
),
|
|
3195
3195
|
checkpoint_id: Optional[str] = typer.Option(
|
|
3196
3196
|
None,
|
|
@@ -3236,41 +3236,46 @@ def job_create(
|
|
|
3236
3236
|
):
|
|
3237
3237
|
"""Submit a job.
|
|
3238
3238
|
|
|
3239
|
-
For agents:
|
|
3240
|
-
\b
|
|
3241
|
-
Before submitting, you MUST ensure these five parameters are explicitly specified:
|
|
3242
|
-
benchmark, meta-model, meta-job-type, meta-train-stage, meta-teacher-direction.
|
|
3243
|
-
Keep specified values. Fill missing parameters using available Job, Group,
|
|
3244
|
-
Profile, Dataset, Template, or other relevant information.
|
|
3245
|
-
If a missing value cannot be inferred, use default for benchmark
|
|
3246
|
-
or Others for the corresponding meta parameter.
|
|
3247
|
-
Do not ask the user for these values.
|
|
3248
|
-
|
|
3249
3239
|
Model configuration:
|
|
3250
3240
|
Supply template parameters through --params/-p; select a managed credential with --ak-id.
|
|
3251
3241
|
|
|
3252
3242
|
Examples:
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
#
|
|
3257
|
-
ap job create
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
--meta-train-stage "$META_TRAIN_STAGE" \\
|
|
3262
|
-
--meta-teacher-direction "$META_TEACHER_DIRECTION"
|
|
3263
|
-
|
|
3264
|
-
\b
|
|
3265
|
-
# Batch jobs from a Dataset (first 3 instances)
|
|
3266
|
-
ap job create claw-eval-task --dataset "$DATASET_PATH" --range 0,3 \\
|
|
3267
|
-
-p '{"model":"qwen-max"}' \\
|
|
3268
|
-
--benchmark "$BENCHMARK_ID" \\
|
|
3269
|
-
--meta-model "$META_MODEL" \\
|
|
3270
|
-
--meta-job-type "$META_JOB_TYPE" \\
|
|
3271
|
-
--meta-train-stage "$META_TRAIN_STAGE" \\
|
|
3272
|
-
--meta-teacher-direction "$META_TEACHER_DIRECTION"
|
|
3243
|
+
# Single job - full parameters
|
|
3244
|
+
ap job create claw-eval-task -p '{"task_id":"T02_email_triage","model":"qwen-max"}'
|
|
3245
|
+
|
|
3246
|
+
# Single job - instance_id + shared params
|
|
3247
|
+
ap job create claw-eval-task -i T02_email_triage -p '{"model":"qwen-max"}'
|
|
3248
|
+
|
|
3249
|
+
# Batch jobs - multiple instance_ids + shared params
|
|
3250
|
+
ap job create claw-eval-task -i T02_email_triage,T03zh_calendar_scheduling,T04_calendar_scheduling -p '{"model":"qwen-max"}'
|
|
3273
3251
|
|
|
3252
|
+
# Batch jobs - dataset + shared params
|
|
3253
|
+
ap job create claw-eval-task --dataset claw-eval/claw-eval/v1@0 -p '{"model":"qwen-max"}'
|
|
3254
|
+
|
|
3255
|
+
# Batch jobs - dataset subset with exclusions
|
|
3256
|
+
ap job create claw-eval-task --dataset claw-eval/claw-eval/v1@0 --range 0,10,2 --exclude-instance-ids T03zh_calendar_scheduling
|
|
3257
|
+
|
|
3258
|
+
# Batch jobs from a params list(inline JSON) with shared base params (-p merged into each item)
|
|
3259
|
+
ap job create claw-eval-task --params-list '[{"task_id":"T02_email_triage"},{"task_id":"T03zh_calendar_scheduling"}]' -p '{"model":"qwen-max"}'
|
|
3260
|
+
|
|
3261
|
+
# Batch jobs from a params list file (also supports -p for shared base params)
|
|
3262
|
+
ap job create claw-eval-task --params-list params.json -p '{"model":"qwen-max"}'
|
|
3263
|
+
|
|
3264
|
+
# Debug mode - keep pod alive for interactive debugging (Pod Login)
|
|
3265
|
+
ap job create stress-test -p '{"duration":60}' --debug
|
|
3266
|
+
|
|
3267
|
+
# Debug mode with custom keep-alive duration
|
|
3268
|
+
ap job create stress-test -p '{"duration":60}' --debug --debug-keep-alive-seconds 7200
|
|
3269
|
+
|
|
3270
|
+
# Apply per-instance resource settings from a Resource Profile
|
|
3271
|
+
ap job create claw-eval-task --dataset claw-eval/v1 --resource-profile rp-123
|
|
3272
|
+
|
|
3273
|
+
Note: benchmark+ckpt metadata (--benchmark/--checkpoint-*/--experiment) on a
|
|
3274
|
+
standalone single job needs no client-side group handling — the server
|
|
3275
|
+
auto-creates a Group (auto_created_benchmark_group) to persist the
|
|
3276
|
+
Group-level fields. Generating a group_id client-side would break
|
|
3277
|
+
idempotent retries: the server rejects a same idempotency_key with a
|
|
3278
|
+
different requested group_id (409 idempotency_conflict).
|
|
3274
3279
|
"""
|
|
3275
3280
|
if transparency is not None and transparency not in {"whitebox", "blackbox"}:
|
|
3276
3281
|
raise typer.BadParameter("Choose whitebox or blackbox", param_hint="--transparency")
|
|
@@ -6675,6 +6680,10 @@ def main() -> None:
|
|
|
6675
6680
|
|
|
6676
6681
|
|
|
6677
6682
|
def _format_api_error(exc: APIError) -> str:
|
|
6683
|
+
from ap_client.profile_diagnostics import format_profile_error
|
|
6684
|
+
|
|
6685
|
+
if profile_error := format_profile_error(exc):
|
|
6686
|
+
return profile_error
|
|
6678
6687
|
if isinstance(exc.detail, dict) and exc.detail.get("code") == "blackbox_content_restricted":
|
|
6679
6688
|
return f"error: Blackbox content is hidden; Group Reader access is required. (request_id={exc.request_id})"
|
|
6680
6689
|
if str(exc.detail) != _NON_JSON_RESPONSE_DETAIL:
|
|
@@ -695,20 +695,31 @@ def _unversioned_split(
|
|
|
695
695
|
|
|
696
696
|
|
|
697
697
|
def _versioned_split(
|
|
698
|
-
dataset: str,
|
|
698
|
+
dataset: str,
|
|
699
|
+
version: str,
|
|
700
|
+
split: str,
|
|
701
|
+
*,
|
|
702
|
+
environment: Optional[str] = None,
|
|
703
|
+
oss_fallback: bool = True,
|
|
699
704
|
) -> Optional[dict]:
|
|
700
705
|
"""取指定 version 里的单个 split 记录;不存在返回 ``None``(不当作错误)。
|
|
701
706
|
|
|
702
707
|
与 ``_unversioned_split`` 同源,走 recordsvc 的 ``splits.get``(SDK 自带
|
|
703
|
-
version_first 内嵌投影的 legacy 回退)
|
|
708
|
+
version_first 内嵌投影的 legacy 回退)。``split get`` 保持默认 OSS 发现回退;
|
|
709
|
+
``split create`` 的幂等检查须传 ``oss_fallback=False``:dataset 刚创建或
|
|
710
|
+
尚未上传时 OSS prefix 不存在,``_discover_split`` 会把"split 不存在"误报为
|
|
711
|
+
查询失败,404 → legacy 回退 → None 即可判定"不存在 → 走 create"。
|
|
704
712
|
"""
|
|
705
713
|
from .irepo_sdk import _reason
|
|
706
714
|
|
|
707
715
|
env = _environment(environment)
|
|
708
716
|
repo = _dataset_repo()
|
|
717
|
+
read_kwargs = _read_env_kwargs(repo, repo.splits, "get", env)
|
|
718
|
+
if not oss_fallback:
|
|
719
|
+
read_kwargs["fallback_to_oss"] = False
|
|
709
720
|
try:
|
|
710
721
|
record = repo.splits.get(
|
|
711
|
-
dataset, split, version=version, **
|
|
722
|
+
dataset, split, version=version, **read_kwargs
|
|
712
723
|
)
|
|
713
724
|
except Exception as exc: # noqa: BLE001
|
|
714
725
|
raise _fail(
|
|
@@ -716,7 +727,9 @@ def _versioned_split(
|
|
|
716
727
|
f"Check the dataset name and permission: ap dataset get {dataset}",
|
|
717
728
|
) from exc
|
|
718
729
|
detail = _model_dict(record) if record is not None else None
|
|
719
|
-
return
|
|
730
|
+
return (
|
|
731
|
+
detail if detail is not None and _version_value(detail.get("version")) == version else None
|
|
732
|
+
)
|
|
720
733
|
|
|
721
734
|
|
|
722
735
|
def _versioned_splits(
|
|
@@ -1286,8 +1299,9 @@ def dataset_get(
|
|
|
1286
1299
|
|
|
1287
1300
|
def dataset_create(
|
|
1288
1301
|
dataset: str = typer.Argument(
|
|
1289
|
-
...,
|
|
1290
|
-
"
|
|
1302
|
+
...,
|
|
1303
|
+
help="Dataset name: L1, L1/L2, or a deeper path "
|
|
1304
|
+
"(e.g. my-bench, alibaba/my-bench, or gdpval/synthetic/my-bench)",
|
|
1291
1305
|
),
|
|
1292
1306
|
benchmark: str = typer.Option(..., "--benchmark", help="Benchmark name or source_id"),
|
|
1293
1307
|
description: Optional[str] = typer.Option(None, "--description", help="Dataset description"),
|
|
@@ -1946,9 +1960,7 @@ def dataset_split_get(
|
|
|
1946
1960
|
resolved_version = _version_value(version)
|
|
1947
1961
|
|
|
1948
1962
|
if resolved_version:
|
|
1949
|
-
record = _versioned_split(
|
|
1950
|
-
dataset, resolved_version, split, environment=environment
|
|
1951
|
-
)
|
|
1963
|
+
record = _versioned_split(dataset, resolved_version, split, environment=environment)
|
|
1952
1964
|
if record is not None:
|
|
1953
1965
|
payload = dict(record)
|
|
1954
1966
|
payload.setdefault("dataset", dataset)
|
|
@@ -2026,7 +2038,12 @@ def dataset_split_create(
|
|
|
2026
2038
|
# 契约对齐——旧 Version PATCH(splits 全量替换 / split_run_types)已被服务端
|
|
2027
2039
|
# 拒绝("split-first Version group updates are not supported")。
|
|
2028
2040
|
env = _environment(environment)
|
|
2029
|
-
|
|
2041
|
+
# 幂等预检禁用 OSS 发现回退:dataset 刚创建或尚未上传时 OSS prefix 不存在,
|
|
2042
|
+
# _discover_split 会把"split 不存在"误报为查询失败("not found in OSS");
|
|
2043
|
+
# recordsvc 404 → legacy 回退 → None 即判定"不存在 → 走 create"。
|
|
2044
|
+
existing = _versioned_split(
|
|
2045
|
+
dataset, resolved_version, split, environment=environment, oss_fallback=False
|
|
2046
|
+
)
|
|
2030
2047
|
if existing is not None:
|
|
2031
2048
|
# 幂等:已存在同名 split(设计 §4.3)。
|
|
2032
2049
|
payload = dict(existing)
|
|
@@ -2151,7 +2168,9 @@ def dataset_split_update(
|
|
|
2151
2168
|
", ".join(
|
|
2152
2169
|
_split_names_of(
|
|
2153
2170
|
_versioned_splits(
|
|
2154
|
-
dataset,
|
|
2171
|
+
dataset,
|
|
2172
|
+
resolved_version,
|
|
2173
|
+
stage="split update precheck",
|
|
2155
2174
|
environment=environment,
|
|
2156
2175
|
)
|
|
2157
2176
|
)
|
|
@@ -2171,7 +2190,9 @@ def dataset_split_update(
|
|
|
2171
2190
|
repo = _dataset_repo()
|
|
2172
2191
|
update_kwargs = _sdk_read_kwargs(repo.splits, "update", environment=env, metadata_model="")
|
|
2173
2192
|
try:
|
|
2174
|
-
repo.splits.update(
|
|
2193
|
+
repo.splits.update(
|
|
2194
|
+
dataset, split, version=resolved_version, run_type=resolved_run_type, **update_kwargs
|
|
2195
|
+
)
|
|
2175
2196
|
except Exception as exc: # noqa: BLE001 - 统一走设计 §5.1 的映射
|
|
2176
2197
|
raise _fail(
|
|
2177
2198
|
f"Failed to update split {split} of dataset {dataset} version {resolved_version}: {_reason(exc)}",
|
|
@@ -121,8 +121,7 @@ def _create_owned_dataset(repo: Any, dataset: str, benchmark_id: str) -> dict:
|
|
|
121
121
|
raise datasets._fail(
|
|
122
122
|
f"Failed to create dataset {dataset}: the name must be one or more "
|
|
123
123
|
"canonical path segments",
|
|
124
|
-
"Use a dataset name such as my-bench, your-team/my-bench, "
|
|
125
|
-
"or gdpval/synthetic/my-bench",
|
|
124
|
+
"Use a dataset name such as my-bench, your-team/my-bench, or gdpval/synthetic/my-bench",
|
|
126
125
|
exit_code=2,
|
|
127
126
|
)
|
|
128
127
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Bounded all-Child discovery and replayable client-side cascade batches."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
|
|
8
|
+
from ap_client.api import APIClient
|
|
9
|
+
|
|
10
|
+
BATCH_SIZE = 100
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def preview_all(client: APIClient, identifier: str, target_version: int) -> dict:
|
|
14
|
+
batches: list[dict] = []
|
|
15
|
+
after = end = ""
|
|
16
|
+
base_id = None
|
|
17
|
+
seen: set[str] = set()
|
|
18
|
+
while True:
|
|
19
|
+
body = {"dry_run": True, "all": True, "target_version": target_version}
|
|
20
|
+
if after:
|
|
21
|
+
body.update(after=after, selection_end=end)
|
|
22
|
+
value = client.cascade_profiles(base_id or identifier, body)
|
|
23
|
+
if (
|
|
24
|
+
value.get("all") is not True
|
|
25
|
+
or value.get("dry_run") is not True
|
|
26
|
+
or not value.get("base_profile_id")
|
|
27
|
+
or value.get("target_version") != target_version
|
|
28
|
+
or (base_id and value["base_profile_id"] != base_id)
|
|
29
|
+
):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"Invalid all-Children preview response; update the server and preview again"
|
|
32
|
+
)
|
|
33
|
+
base_id = value["base_profile_id"]
|
|
34
|
+
next_end = value.get("selection_end", "")
|
|
35
|
+
if after and next_end != end:
|
|
36
|
+
raise ValueError("The all-Children selection boundary changed; preview again")
|
|
37
|
+
end = next_end
|
|
38
|
+
items = value.get("items")
|
|
39
|
+
if not isinstance(items, list) or len(items) > BATCH_SIZE:
|
|
40
|
+
raise ValueError("Invalid preview batch size")
|
|
41
|
+
ids = [item.get("child_id") for item in items]
|
|
42
|
+
if (
|
|
43
|
+
any(not isinstance(i, str) or not i or i <= after or (end and i > end) for i in ids)
|
|
44
|
+
or ids != sorted(ids)
|
|
45
|
+
or len(set(ids)) != len(ids)
|
|
46
|
+
or seen.intersection(ids)
|
|
47
|
+
):
|
|
48
|
+
raise ValueError("Invalid preview cursor or duplicate Child")
|
|
49
|
+
seen.update(ids)
|
|
50
|
+
if items:
|
|
51
|
+
batches.append(value)
|
|
52
|
+
cursor = value.get("next_cursor", "")
|
|
53
|
+
if not cursor:
|
|
54
|
+
break
|
|
55
|
+
if not ids or cursor != ids[-1] or cursor <= after or cursor >= end:
|
|
56
|
+
raise ValueError("Invalid preview cursor")
|
|
57
|
+
after = cursor
|
|
58
|
+
return {
|
|
59
|
+
"dry_run": True,
|
|
60
|
+
"all": True,
|
|
61
|
+
"base_profile_id": base_id,
|
|
62
|
+
"target_version": target_version,
|
|
63
|
+
"selection_end": end,
|
|
64
|
+
"batches": batches,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def execution_targets(
|
|
69
|
+
items: list[dict], base_id: str, target: int, seen: set[str]
|
|
70
|
+
) -> tuple[list[dict], int]:
|
|
71
|
+
"""Use the same selection rules for manual and all-Child execution."""
|
|
72
|
+
if not isinstance(items, list) or len(items) > BATCH_SIZE:
|
|
73
|
+
raise ValueError("Each preview batch must have at most 100 Children")
|
|
74
|
+
targets, excluded = [], 0
|
|
75
|
+
for item in items:
|
|
76
|
+
if not isinstance(item, dict):
|
|
77
|
+
raise ValueError("Invalid Child in saved preview")
|
|
78
|
+
child_id = item.get("child_id")
|
|
79
|
+
if not isinstance(child_id, str) or not child_id.startswith("sp-") or child_id in seen:
|
|
80
|
+
raise ValueError("Invalid or duplicate Child in saved preview")
|
|
81
|
+
seen.add(child_id)
|
|
82
|
+
if item.get("valid") is not True:
|
|
83
|
+
excluded += 1
|
|
84
|
+
continue
|
|
85
|
+
baseline = item.get("target")
|
|
86
|
+
if not isinstance(baseline, dict) or set(baseline) != {
|
|
87
|
+
"child_id",
|
|
88
|
+
"expected_child_version",
|
|
89
|
+
"expected_parent",
|
|
90
|
+
}:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"Every valid Child requires a complete target; upgrade the server and preview again"
|
|
93
|
+
)
|
|
94
|
+
parent, version = baseline.get("expected_parent"), baseline.get("expected_child_version")
|
|
95
|
+
if (
|
|
96
|
+
baseline.get("child_id") != child_id
|
|
97
|
+
or type(version) is not int
|
|
98
|
+
or version < 1
|
|
99
|
+
or not isinstance(parent, dict)
|
|
100
|
+
or set(parent) != {"profile_id", "version"}
|
|
101
|
+
or parent.get("profile_id") != base_id
|
|
102
|
+
or type(parent.get("version")) is not int
|
|
103
|
+
or parent["version"] < 1
|
|
104
|
+
):
|
|
105
|
+
raise ValueError("Every valid Child requires a complete preview baseline")
|
|
106
|
+
if parent["version"] >= target:
|
|
107
|
+
excluded += 1
|
|
108
|
+
continue
|
|
109
|
+
# Send the server's target unchanged, independent of display filters.
|
|
110
|
+
targets.append(deepcopy(baseline))
|
|
111
|
+
return targets, excluded
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def validate_preview_identity(preview: dict) -> tuple[str, int]:
|
|
115
|
+
if not isinstance(preview, dict) or preview.get("dry_run") is not True:
|
|
116
|
+
raise ValueError("Expected a saved dry-run preview")
|
|
117
|
+
base_id, target = preview.get("base_profile_id"), preview.get("target_version")
|
|
118
|
+
if (
|
|
119
|
+
not isinstance(base_id, str)
|
|
120
|
+
or not base_id.startswith("sp-")
|
|
121
|
+
or type(target) is not int
|
|
122
|
+
or target < 1
|
|
123
|
+
):
|
|
124
|
+
raise ValueError("Invalid saved preview identity")
|
|
125
|
+
return base_id, target
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def execution_batches(preview: dict, root_key: str) -> tuple[list[dict], int]:
|
|
129
|
+
"""Validate the whole saved plan before any writes; each batch gets a stable UUID."""
|
|
130
|
+
root = uuid.UUID(root_key)
|
|
131
|
+
base_id, target = validate_preview_identity(preview)
|
|
132
|
+
if preview.get("all") is not True or not isinstance(preview.get("batches"), list):
|
|
133
|
+
raise ValueError("Expected an --all preview file")
|
|
134
|
+
requests, seen, excluded = [], set(), 0
|
|
135
|
+
for index, batch in enumerate(preview["batches"]):
|
|
136
|
+
if not isinstance(batch, dict) or validate_preview_identity(batch) != (base_id, target):
|
|
137
|
+
raise ValueError("Preview batches must use the same fixed Base and target")
|
|
138
|
+
targets, skipped = execution_targets(batch.get("items"), base_id, target, seen)
|
|
139
|
+
excluded += skipped
|
|
140
|
+
if targets:
|
|
141
|
+
requests.append(
|
|
142
|
+
{
|
|
143
|
+
"dry_run": False,
|
|
144
|
+
"request_key": str(uuid.uuid5(root, f"profile-cascade-batch:{index}")),
|
|
145
|
+
"target_version": target,
|
|
146
|
+
"targets": targets,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
return requests, excluded
|