ap-client 0.6.0.dev2__tar.gz → 0.7.0.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.6.0.dev2 → ap_client-0.7.0.dev0}/PKG-INFO +4 -4
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/api.py +71 -4
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/cli.py +132 -218
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/exporter.py +33 -2
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/managed_ak.py +44 -14
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/profile_commands.py +109 -1
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/pyproject.toml +4 -4
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/__init__.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/akpool_commands.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/config.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/dataset_commands.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/dataset_register.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/fs_commands.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/instance_commands.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/irepo_sdk.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/manifest_view.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/repo_read_guard.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/share_api.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/split_publish.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/tbb.py +0 -0
- {ap_client-0.6.0.dev2 → ap_client-0.7.0.dev0}/ap_client/waiter.py +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0.dev0
|
|
4
4
|
Summary: Agent Platform API Client & CLI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: irepo-artifact==0.2.
|
|
6
|
+
Requires-Dist: irepo-artifact==0.2.2
|
|
7
7
|
Requires-Dist: pyyaml>=6.0
|
|
8
8
|
Requires-Dist: requests>=2.28.0
|
|
9
9
|
Requires-Dist: rich>=13.0.0
|
|
10
10
|
Requires-Dist: typer>=0.9.0
|
|
11
11
|
Requires-Dist: websockets>=13.0
|
|
12
12
|
Provides-Extra: all
|
|
13
|
-
Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.
|
|
13
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.5; extra == 'all'
|
|
14
14
|
Provides-Extra: dataset
|
|
15
|
-
Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.
|
|
15
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.3.5; extra == 'dataset'
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
18
18
|
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.
|
|
@@ -1127,6 +1127,8 @@ class APIClient:
|
|
|
1127
1127
|
ak_ids: Optional[list[str]] = None,
|
|
1128
1128
|
priority: Optional[str] = None,
|
|
1129
1129
|
priority_weight: Optional[int] = None,
|
|
1130
|
+
transparency: Optional[Literal["whitebox", "blackbox"]] = None,
|
|
1131
|
+
idempotency_key: Optional[str] = None,
|
|
1130
1132
|
) -> dict:
|
|
1131
1133
|
"""Create a group with optional scheduling defaults for its Jobs.
|
|
1132
1134
|
|
|
@@ -1134,6 +1136,10 @@ class APIClient:
|
|
|
1134
1136
|
"""
|
|
1135
1137
|
if priority is not None and priority not in {"high", "medium"}:
|
|
1136
1138
|
raise ValueError("priority must be one of high/medium")
|
|
1139
|
+
if transparency is not None and transparency not in {"whitebox", "blackbox"}:
|
|
1140
|
+
raise ValueError("transparency must be whitebox or blackbox")
|
|
1141
|
+
if transparency == "blackbox" and not idempotency_key:
|
|
1142
|
+
raise ValueError("blackbox Group creation requires a reusable idempotency_key (UUID4)")
|
|
1137
1143
|
if ak_ids is not None and not isinstance(ak_ids, list):
|
|
1138
1144
|
raise ValueError("ak_ids must be a list")
|
|
1139
1145
|
references = list(ak_ids or [])
|
|
@@ -1142,6 +1148,10 @@ class APIClient:
|
|
|
1142
1148
|
raise ValueError("ak_ids entries must be nonempty strings")
|
|
1143
1149
|
references = list(dict.fromkeys(references))
|
|
1144
1150
|
body: dict = {}
|
|
1151
|
+
if transparency is not None:
|
|
1152
|
+
body["transparency"] = transparency
|
|
1153
|
+
if idempotency_key is not None:
|
|
1154
|
+
body["idempotency_key"] = idempotency_key
|
|
1145
1155
|
if references:
|
|
1146
1156
|
body["ak_ids"] = references
|
|
1147
1157
|
if name:
|
|
@@ -1263,9 +1273,11 @@ class APIClient:
|
|
|
1263
1273
|
ak_mode: str = "auto",
|
|
1264
1274
|
instance_range: Optional[str] = None,
|
|
1265
1275
|
profile: Optional[str] = None,
|
|
1276
|
+
transparency: Optional[Literal["whitebox", "blackbox"]] = None,
|
|
1266
1277
|
) -> dict:
|
|
1267
1278
|
"""Create a job."""
|
|
1268
1279
|
body = self.build_create_job_body(
|
|
1280
|
+
transparency=transparency,
|
|
1269
1281
|
template=template,
|
|
1270
1282
|
params=params,
|
|
1271
1283
|
params_list=params_list,
|
|
@@ -1377,8 +1389,11 @@ class APIClient:
|
|
|
1377
1389
|
ak_mode: str = "auto",
|
|
1378
1390
|
instance_range: Optional[str] = None,
|
|
1379
1391
|
profile: Optional[str] = None,
|
|
1392
|
+
transparency: Optional[Literal["whitebox", "blackbox"]] = None,
|
|
1380
1393
|
) -> dict:
|
|
1381
1394
|
"""Build the /jobs request body for job creation."""
|
|
1395
|
+
if transparency is not None and transparency not in {"whitebox", "blackbox"}:
|
|
1396
|
+
raise ValueError("transparency must be whitebox or blackbox")
|
|
1382
1397
|
if profile is not None and (profile_id is not None or profile_version is not None):
|
|
1383
1398
|
raise ValueError("profile cannot be combined with profile_id or profile_version")
|
|
1384
1399
|
_validate_credential_source_selection(
|
|
@@ -1503,8 +1518,23 @@ class APIClient:
|
|
|
1503
1518
|
if experiment is not None:
|
|
1504
1519
|
body["experiment"] = experiment
|
|
1505
1520
|
|
|
1521
|
+
if transparency is not None:
|
|
1522
|
+
body["transparency"] = transparency
|
|
1506
1523
|
return body
|
|
1507
1524
|
|
|
1525
|
+
def get_job_access_contact(self, job_id: str) -> dict:
|
|
1526
|
+
"""Discover the owner and grant contact without reading protected details."""
|
|
1527
|
+
result = self._get(f"/jobs/{quote(job_id, safe='')}", params={"access_contact": True})
|
|
1528
|
+
if (
|
|
1529
|
+
not isinstance(result, dict)
|
|
1530
|
+
or result.get("resource_type") != "job"
|
|
1531
|
+
or "authorization_contact" not in result
|
|
1532
|
+
):
|
|
1533
|
+
raise ConfigurationError(
|
|
1534
|
+
"Server does not support access contact queries; upgrade Agent-Platform first"
|
|
1535
|
+
)
|
|
1536
|
+
return result
|
|
1537
|
+
|
|
1508
1538
|
def get_job(
|
|
1509
1539
|
self,
|
|
1510
1540
|
job_id: str,
|
|
@@ -1630,6 +1660,19 @@ class APIClient:
|
|
|
1630
1660
|
|
|
1631
1661
|
# ==================== Group operations ====================
|
|
1632
1662
|
|
|
1663
|
+
def get_group_access_contact(self, group_id: str) -> dict:
|
|
1664
|
+
"""Discover the owner and grant contact without reading protected details."""
|
|
1665
|
+
result = self._get(f"/groups/{quote(group_id, safe='')}", params={"access_contact": True})
|
|
1666
|
+
if (
|
|
1667
|
+
not isinstance(result, dict)
|
|
1668
|
+
or result.get("resource_type") != "group"
|
|
1669
|
+
or "authorization_contact" not in result
|
|
1670
|
+
):
|
|
1671
|
+
raise ConfigurationError(
|
|
1672
|
+
"Server does not support access contact queries; upgrade Agent-Platform first"
|
|
1673
|
+
)
|
|
1674
|
+
return result
|
|
1675
|
+
|
|
1633
1676
|
def get_group(self, group_id: str, timeout: TimeoutType = None) -> dict:
|
|
1634
1677
|
"""Get group details."""
|
|
1635
1678
|
return self._get(f"/groups/{quote(group_id)}", timeout=timeout)
|
|
@@ -3046,6 +3089,23 @@ class APIClient:
|
|
|
3046
3089
|
return self._get(f"{self._PROFILES_BASE}/{quote(id_or_name)}/versions/{version}")
|
|
3047
3090
|
return self._get(f"{self._PROFILES_BASE}/{quote(id_or_name)}")
|
|
3048
3091
|
|
|
3092
|
+
def update_profile_job_meta(self, profile_id: str, patch: dict) -> dict:
|
|
3093
|
+
"""Patch live Job defaults without changing the Profile version.
|
|
3094
|
+
|
|
3095
|
+
Omitted fields stay unchanged. Set benchmark to None to clear it.
|
|
3096
|
+
Tags use {"append": [...], "remove": [...]} and are applied atomically.
|
|
3097
|
+
"""
|
|
3098
|
+
return self._request_central(
|
|
3099
|
+
"PATCH", f"/profiles/{quote(profile_id, safe='')}/job-meta", json_body=patch
|
|
3100
|
+
)
|
|
3101
|
+
|
|
3102
|
+
def replace_profile_job_meta(self, profile_id: str, job_meta: dict) -> dict:
|
|
3103
|
+
"""Replace all live Job defaults; {} clears benchmark and tags.
|
|
3104
|
+
|
|
3105
|
+
This does not change spec, Profile tags, version, verification or release.
|
|
3106
|
+
"""
|
|
3107
|
+
return self._put_central(f"/profiles/{quote(profile_id, safe='')}/job-meta", job_meta)
|
|
3108
|
+
|
|
3049
3109
|
def update_profile(
|
|
3050
3110
|
self,
|
|
3051
3111
|
profile_id: str,
|
|
@@ -3054,8 +3114,13 @@ class APIClient:
|
|
|
3054
3114
|
description: str = "",
|
|
3055
3115
|
account_pool: Optional[str] = None,
|
|
3056
3116
|
metadata: Optional[dict] = None,
|
|
3117
|
+
reason: Optional[str] = None,
|
|
3057
3118
|
) -> dict:
|
|
3058
|
-
"""Update a submission profile, preserving an omitted default Pool.
|
|
3119
|
+
"""Update a submission profile, preserving an omitted default Pool.
|
|
3120
|
+
|
|
3121
|
+
reason describes this update; omission never reuses a previous reason.
|
|
3122
|
+
The server validates whether it is required and its 500-character limit.
|
|
3123
|
+
"""
|
|
3059
3124
|
body: dict = {"name": name, "spec": spec}
|
|
3060
3125
|
if description:
|
|
3061
3126
|
body["description"] = description
|
|
@@ -3063,6 +3128,8 @@ class APIClient:
|
|
|
3063
3128
|
body["account_pool"] = account_pool
|
|
3064
3129
|
if metadata is not None:
|
|
3065
3130
|
body["metadata"] = metadata
|
|
3131
|
+
if reason is not None:
|
|
3132
|
+
body["reason"] = reason
|
|
3066
3133
|
return self._put(f"{self._PROFILES_BASE}/{quote(profile_id)}", body)
|
|
3067
3134
|
|
|
3068
3135
|
def delete_profile(self, profile_id: str) -> dict:
|
|
@@ -3085,8 +3152,8 @@ class APIClient:
|
|
|
3085
3152
|
) -> dict:
|
|
3086
3153
|
"""把某个 job/group 实例的访问权授予某 user/user_group(scope=resource 绑定)。
|
|
3087
3154
|
|
|
3088
|
-
workspace_id = 该实例所属 queue_id;
|
|
3089
|
-
|
|
3155
|
+
workspace_id = 该实例所属 queue_id;Job/Group Owner、Group Admin 或所属
|
|
3156
|
+
WorkSpace 的管理员/创建者及平台管理员可授权。鉴权走当前 API Key 解析出的调用者身份。
|
|
3090
3157
|
"""
|
|
3091
3158
|
body = {
|
|
3092
3159
|
"scope": "resource",
|
|
@@ -3352,7 +3419,7 @@ def _response_error_detail(resp: requests.Response) -> Any:
|
|
|
3352
3419
|
except Exception:
|
|
3353
3420
|
return resp.text
|
|
3354
3421
|
if isinstance(err_data, dict):
|
|
3355
|
-
return err_data.get("detail", resp.text)
|
|
3422
|
+
return err_data.get("detail", err_data.get("message", resp.text))
|
|
3356
3423
|
return err_data
|
|
3357
3424
|
|
|
3358
3425
|
|
|
@@ -17,9 +17,7 @@ from ap_client.akpool_commands import register as _register_akpool_commands
|
|
|
17
17
|
from ap_client.api import (
|
|
18
18
|
APIError,
|
|
19
19
|
PaginationProtocolError,
|
|
20
|
-
_is_sensitive_key,
|
|
21
20
|
_merge_meta_tags,
|
|
22
|
-
_redact_sensitive_data,
|
|
23
21
|
set_cluster_override,
|
|
24
22
|
set_verbose_override,
|
|
25
23
|
)
|
|
@@ -207,146 +205,8 @@ def _job_create_progress():
|
|
|
207
205
|
)
|
|
208
206
|
|
|
209
207
|
|
|
210
|
-
def
|
|
211
|
-
return "
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
def _retry_argument_contains_secret(raw: str) -> bool:
|
|
215
|
-
"""Inspect display arguments without changing the submission payload."""
|
|
216
|
-
try:
|
|
217
|
-
value = json.loads(raw)
|
|
218
|
-
except (json.JSONDecodeError, TypeError):
|
|
219
|
-
value = raw
|
|
220
|
-
|
|
221
|
-
def contains_secret(item: Any) -> bool:
|
|
222
|
-
if isinstance(item, dict):
|
|
223
|
-
return any(
|
|
224
|
-
_is_sensitive_key(str(key)) or contains_secret(field) for key, field in item.items()
|
|
225
|
-
)
|
|
226
|
-
if isinstance(item, list):
|
|
227
|
-
return any(contains_secret(field) for field in item)
|
|
228
|
-
return _redact_sensitive_data(item) != item
|
|
229
|
-
|
|
230
|
-
return contains_secret(value)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _build_job_create_retry_command(
|
|
234
|
-
*,
|
|
235
|
-
template: Optional[str],
|
|
236
|
-
agenthub_ref: Optional[str],
|
|
237
|
-
instance_id: Optional[str],
|
|
238
|
-
dataset: Optional[str],
|
|
239
|
-
instance_range: Optional[str],
|
|
240
|
-
exclude_instance_ids: Optional[str],
|
|
241
|
-
params: Optional[str],
|
|
242
|
-
params_list_input: Optional[str],
|
|
243
|
-
suite_name: Optional[str],
|
|
244
|
-
group_id: Optional[str],
|
|
245
|
-
tags: Optional[str],
|
|
246
|
-
overrides: Optional[str],
|
|
247
|
-
concurrency: Optional[int],
|
|
248
|
-
batch_size: int,
|
|
249
|
-
trials: Optional[int] = None,
|
|
250
|
-
max_failure_retries: int = 0,
|
|
251
|
-
wait: bool,
|
|
252
|
-
wait_interval: float,
|
|
253
|
-
wait_timeout: Optional[float],
|
|
254
|
-
output_format: str,
|
|
255
|
-
enable_otel_tracing: Optional[bool],
|
|
256
|
-
runner: Optional[str],
|
|
257
|
-
runner_options: Optional[str],
|
|
258
|
-
model_base_url_collection: Optional[str],
|
|
259
|
-
model_base_url_file: Optional[str],
|
|
260
|
-
datasources: Optional[str],
|
|
261
|
-
account_pool: Optional[str] = None,
|
|
262
|
-
idempotency_key: str,
|
|
263
|
-
enable_post_process: bool = False,
|
|
264
|
-
group_post_process: Optional[str] = None,
|
|
265
|
-
profile: Optional[str] = None,
|
|
266
|
-
resource_profile_id: Optional[str] = None,
|
|
267
|
-
timeout: Optional[float] = None,
|
|
268
|
-
ak_id: Optional[str] = None,
|
|
269
|
-
ak_mode: str = "auto",
|
|
270
|
-
) -> str:
|
|
271
|
-
args = ["ap", "job", "create"]
|
|
272
|
-
if template is not None:
|
|
273
|
-
args.append(template)
|
|
274
|
-
if ak_id is not None:
|
|
275
|
-
args.extend(["--ak-id", ak_id])
|
|
276
|
-
if ak_mode != "auto":
|
|
277
|
-
args.extend(["--ak-mode", ak_mode])
|
|
278
|
-
if profile is not None:
|
|
279
|
-
args.extend(["--profile", profile])
|
|
280
|
-
if resource_profile_id:
|
|
281
|
-
args.extend(["--resource-profile", resource_profile_id])
|
|
282
|
-
if agenthub_ref:
|
|
283
|
-
args.extend(["--agenthub-ref", agenthub_ref])
|
|
284
|
-
if instance_id:
|
|
285
|
-
args.extend(["-i", instance_id])
|
|
286
|
-
if dataset:
|
|
287
|
-
args.extend(["--dataset", dataset])
|
|
288
|
-
if instance_range:
|
|
289
|
-
args.extend(["--range", instance_range])
|
|
290
|
-
if exclude_instance_ids:
|
|
291
|
-
args.extend(["--exclude-instance-ids", exclude_instance_ids])
|
|
292
|
-
if params:
|
|
293
|
-
args.extend(["-p", params])
|
|
294
|
-
if params_list_input:
|
|
295
|
-
args.extend(["--params-list", params_list_input])
|
|
296
|
-
if suite_name:
|
|
297
|
-
args.extend(["--suite-name", suite_name])
|
|
298
|
-
if group_id:
|
|
299
|
-
args.extend(["--group-id", group_id])
|
|
300
|
-
if tags:
|
|
301
|
-
args.extend(["--tags", tags])
|
|
302
|
-
if overrides:
|
|
303
|
-
args.extend(["--overrides", overrides])
|
|
304
|
-
if concurrency is not None:
|
|
305
|
-
args.extend(["--concurrency", str(concurrency)])
|
|
306
|
-
if batch_size != 200:
|
|
307
|
-
args.extend(["--batch-size", str(batch_size)])
|
|
308
|
-
if trials is not None:
|
|
309
|
-
args.extend(["--trials", str(trials)])
|
|
310
|
-
if max_failure_retries:
|
|
311
|
-
args.extend(["--max-failure-retries", str(max_failure_retries)])
|
|
312
|
-
if wait:
|
|
313
|
-
args.append("--wait")
|
|
314
|
-
if wait_interval != 5.0:
|
|
315
|
-
args.extend(["--wait-interval", f"{wait_interval:g}"])
|
|
316
|
-
if wait_timeout is not None:
|
|
317
|
-
args.extend(["--wait-timeout", f"{wait_timeout:g}"])
|
|
318
|
-
if timeout is not None:
|
|
319
|
-
args.extend(["--timeout", f"{timeout:g}"])
|
|
320
|
-
if output_format != "plain":
|
|
321
|
-
args.extend(["--format", output_format])
|
|
322
|
-
if enable_otel_tracing is not None:
|
|
323
|
-
args.extend(["--enable-otel-tracing", str(enable_otel_tracing).lower()])
|
|
324
|
-
if runner:
|
|
325
|
-
args.extend(["--runner", runner])
|
|
326
|
-
if runner_options:
|
|
327
|
-
args.extend(["--runner-options", runner_options])
|
|
328
|
-
if model_base_url_collection:
|
|
329
|
-
args.extend(["--model-base-url-collection", model_base_url_collection])
|
|
330
|
-
if model_base_url_file:
|
|
331
|
-
args.extend(["--model-base-url-file", model_base_url_file])
|
|
332
|
-
if datasources:
|
|
333
|
-
args.extend(["--datasources", datasources])
|
|
334
|
-
if account_pool:
|
|
335
|
-
args.extend(["--account-pool", account_pool])
|
|
336
|
-
if group_post_process:
|
|
337
|
-
args.extend(["--group-post-process", group_post_process])
|
|
338
|
-
elif enable_post_process:
|
|
339
|
-
args.append("--enable-post-process")
|
|
340
|
-
if any(_retry_argument_contains_secret(str(arg)) for arg in args):
|
|
341
|
-
# A redacted command would silently retry with different credentials.
|
|
342
|
-
# Keep the original invocation private and print only the retry key.
|
|
343
|
-
return (
|
|
344
|
-
"Rerun your original command with "
|
|
345
|
-
f"--idempotency-key {shlex.quote(idempotency_key)}. "
|
|
346
|
-
"The command is omitted because it contains inline credentials."
|
|
347
|
-
)
|
|
348
|
-
args.extend(["--idempotency-key", idempotency_key])
|
|
349
|
-
return _shell_command(args)
|
|
208
|
+
def _build_job_create_retry_command(*, idempotency_key: str) -> str:
|
|
209
|
+
return f"Rerun your original command with --idempotency-key {shlex.quote(idempotency_key)}"
|
|
350
210
|
|
|
351
211
|
|
|
352
212
|
def _emit_idempotency_retry_hint(command: str) -> None:
|
|
@@ -1059,8 +919,26 @@ def _print_params_plain(params: Any, masked_params: list) -> None:
|
|
|
1059
919
|
_print_key_values(rows, skip_empty=False)
|
|
1060
920
|
|
|
1061
921
|
|
|
922
|
+
def _print_content_notice(rows: list[dict]) -> None:
|
|
923
|
+
if any(
|
|
924
|
+
row.get("transparency") == "blackbox" and row.get("can_read_content") is False
|
|
925
|
+
for row in rows
|
|
926
|
+
):
|
|
927
|
+
typer.echo("Blackbox content is hidden; Group Reader access is required.")
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _content_columns(rows: list[dict], columns: list[str]) -> list[str]:
|
|
931
|
+
restricted = {field for row in rows for field in (row.get("restricted_fields") or [])}
|
|
932
|
+
return [
|
|
933
|
+
field
|
|
934
|
+
for field in columns
|
|
935
|
+
if field not in restricted or any(row.get(field) is not None for row in rows)
|
|
936
|
+
]
|
|
937
|
+
|
|
938
|
+
|
|
1062
939
|
def _print_job_get_plain(job: dict) -> None:
|
|
1063
940
|
typer.echo("Job")
|
|
941
|
+
_print_content_notice([job])
|
|
1064
942
|
_print_key_values(
|
|
1065
943
|
[
|
|
1066
944
|
("job_id", job.get("job_id")),
|
|
@@ -1333,6 +1211,8 @@ _DEFAULT_JOB_LIST_COLUMNS = [
|
|
|
1333
1211
|
|
|
1334
1212
|
_JOB_LIST_AVAILABLE_COLUMNS = [
|
|
1335
1213
|
*_DEFAULT_JOB_LIST_COLUMNS,
|
|
1214
|
+
"transparency",
|
|
1215
|
+
"can_read_content",
|
|
1336
1216
|
"job_name",
|
|
1337
1217
|
"group_id",
|
|
1338
1218
|
"quota_id",
|
|
@@ -1494,6 +1374,8 @@ def _print_job_list_plain(
|
|
|
1494
1374
|
)
|
|
1495
1375
|
else:
|
|
1496
1376
|
selected_columns = default_columns
|
|
1377
|
+
_print_content_notice(rows)
|
|
1378
|
+
selected_columns = _content_columns(rows, selected_columns)
|
|
1497
1379
|
render = _print_rich_table if rich else _print_plain_table
|
|
1498
1380
|
render(
|
|
1499
1381
|
rows,
|
|
@@ -1549,6 +1431,8 @@ _GROUP_LIST_TABLE_MIN_COLUMNS = 3
|
|
|
1549
1431
|
|
|
1550
1432
|
_GROUP_LIST_AVAILABLE_COLUMNS = [
|
|
1551
1433
|
*_DEFAULT_GROUP_LIST_COLUMNS,
|
|
1434
|
+
"transparency",
|
|
1435
|
+
"can_read_content",
|
|
1552
1436
|
"template",
|
|
1553
1437
|
"scheduling_status",
|
|
1554
1438
|
"profile",
|
|
@@ -1637,7 +1521,8 @@ def _prepare_group_list_rows(groups: list[dict]) -> list[dict]:
|
|
|
1637
1521
|
"upstream_platform",
|
|
1638
1522
|
"upstream_job_id",
|
|
1639
1523
|
):
|
|
1640
|
-
|
|
1524
|
+
if key not in row.get("restricted_fields", []):
|
|
1525
|
+
row[key] = row.get(key) or "-"
|
|
1641
1526
|
rows.append(row)
|
|
1642
1527
|
return rows
|
|
1643
1528
|
|
|
@@ -1654,6 +1539,8 @@ def _print_group_list_plain(
|
|
|
1654
1539
|
)
|
|
1655
1540
|
else:
|
|
1656
1541
|
selected_columns = _DEFAULT_GROUP_LIST_COLUMNS
|
|
1542
|
+
_print_content_notice(rows)
|
|
1543
|
+
selected_columns = _content_columns(rows, selected_columns)
|
|
1657
1544
|
render = _print_rich_table if rich else _print_plain_table
|
|
1658
1545
|
render(
|
|
1659
1546
|
rows,
|
|
@@ -1688,6 +1575,7 @@ def _pick_available_columns(
|
|
|
1688
1575
|
|
|
1689
1576
|
|
|
1690
1577
|
def _print_group_eval_plain(result: dict) -> None:
|
|
1578
|
+
_print_content_notice([result])
|
|
1691
1579
|
header_parts: list[str] = []
|
|
1692
1580
|
for key in ("group_id", "name"):
|
|
1693
1581
|
if key in result:
|
|
@@ -1761,6 +1649,7 @@ def _print_group_eval_plain(result: dict) -> None:
|
|
|
1761
1649
|
rows,
|
|
1762
1650
|
[
|
|
1763
1651
|
("task_id", "task_id"),
|
|
1652
|
+
("job_ids", "job_ids"),
|
|
1764
1653
|
("trials", "trials"),
|
|
1765
1654
|
("scored", "scored"),
|
|
1766
1655
|
("avg_score", "avg_score"),
|
|
@@ -3016,6 +2905,11 @@ def job_list(
|
|
|
3016
2905
|
|
|
3017
2906
|
@job_app.command("create")
|
|
3018
2907
|
def job_create(
|
|
2908
|
+
transparency: Optional[str] = typer.Option(
|
|
2909
|
+
None,
|
|
2910
|
+
"--transparency",
|
|
2911
|
+
help="whitebox or blackbox; omit to inherit the Group/Profile default",
|
|
2912
|
+
),
|
|
3019
2913
|
template: Optional[str] = typer.Argument(None, help="Template name; optional with --profile"),
|
|
3020
2914
|
ak_id: Optional[str] = typer.Option(
|
|
3021
2915
|
None, "--ak-id", help="AP-managed AK ID; requires server support"
|
|
@@ -3362,6 +3256,8 @@ def job_create(
|
|
|
3362
3256
|
idempotent retries: the server rejects a same idempotency_key with a
|
|
3363
3257
|
different requested group_id (409 idempotency_conflict).
|
|
3364
3258
|
"""
|
|
3259
|
+
if transparency is not None and transparency not in {"whitebox", "blackbox"}:
|
|
3260
|
+
raise typer.BadParameter("Choose whitebox or blackbox", param_hint="--transparency")
|
|
3365
3261
|
output_format = _normalize_output_format(output_format)
|
|
3366
3262
|
if not template and not profile:
|
|
3367
3263
|
raise typer.BadParameter("TEMPLATE is required unless --profile is provided")
|
|
@@ -3384,6 +3280,7 @@ def job_create(
|
|
|
3384
3280
|
)
|
|
3385
3281
|
idempotency_enabled = (
|
|
3386
3282
|
enable_idempotency
|
|
3283
|
+
or transparency == "blackbox"
|
|
3387
3284
|
or idempotency_key is not None
|
|
3388
3285
|
or profile is not None
|
|
3389
3286
|
or implicit_resource_profile_idempotency
|
|
@@ -3535,6 +3432,7 @@ def job_create(
|
|
|
3535
3432
|
|
|
3536
3433
|
# Build request body via create_job (reuse existing method with profile fields)
|
|
3537
3434
|
body = client.build_create_job_body(
|
|
3435
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
3538
3436
|
template=template,
|
|
3539
3437
|
params=params_dict or None,
|
|
3540
3438
|
suite_name=suite_name,
|
|
@@ -3603,43 +3501,7 @@ def job_create(
|
|
|
3603
3501
|
|
|
3604
3502
|
# Build retry command before submitting so it's available on failure
|
|
3605
3503
|
profile_retry_command = (
|
|
3606
|
-
_build_job_create_retry_command(
|
|
3607
|
-
template=template,
|
|
3608
|
-
agenthub_ref=agenthub_ref,
|
|
3609
|
-
instance_id=instance_id,
|
|
3610
|
-
dataset=dataset,
|
|
3611
|
-
instance_range=instance_range,
|
|
3612
|
-
exclude_instance_ids=None,
|
|
3613
|
-
params=params,
|
|
3614
|
-
params_list_input=None,
|
|
3615
|
-
suite_name=suite_name,
|
|
3616
|
-
group_id=group_id,
|
|
3617
|
-
tags=tags,
|
|
3618
|
-
overrides=overrides,
|
|
3619
|
-
concurrency=concurrency,
|
|
3620
|
-
batch_size=batch_size,
|
|
3621
|
-
trials=trials,
|
|
3622
|
-
max_failure_retries=max_failure_retries,
|
|
3623
|
-
wait=wait,
|
|
3624
|
-
wait_interval=wait_interval,
|
|
3625
|
-
wait_timeout=wait_timeout,
|
|
3626
|
-
output_format=output_format,
|
|
3627
|
-
enable_otel_tracing=enable_otel_tracing,
|
|
3628
|
-
runner=runner,
|
|
3629
|
-
runner_options=runner_options,
|
|
3630
|
-
model_base_url_collection=model_base_url_collection,
|
|
3631
|
-
model_base_url_file=model_base_url_file,
|
|
3632
|
-
datasources=datasources,
|
|
3633
|
-
account_pool=account_pool,
|
|
3634
|
-
enable_post_process=enable_post_process,
|
|
3635
|
-
group_post_process=group_post_process,
|
|
3636
|
-
idempotency_key=submission_idempotency_key,
|
|
3637
|
-
profile=profile,
|
|
3638
|
-
resource_profile_id=resource_profile_id,
|
|
3639
|
-
**({"ak_id": ak_id} if ak_id is not None else {}),
|
|
3640
|
-
**({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
|
|
3641
|
-
timeout=timeout,
|
|
3642
|
-
)
|
|
3504
|
+
_build_job_create_retry_command(idempotency_key=submission_idempotency_key)
|
|
3643
3505
|
if idempotency_enabled and submission_idempotency_key is not None
|
|
3644
3506
|
else None
|
|
3645
3507
|
)
|
|
@@ -3655,6 +3517,7 @@ def job_create(
|
|
|
3655
3517
|
try:
|
|
3656
3518
|
with console.status("[bold green]Submitting by profile..."):
|
|
3657
3519
|
result = client.create_job(
|
|
3520
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
3658
3521
|
template=template,
|
|
3659
3522
|
params=params_dict or None,
|
|
3660
3523
|
suite_name=suite_name,
|
|
@@ -3866,41 +3729,7 @@ def job_create(
|
|
|
3866
3729
|
model_base_url_collection_list = None
|
|
3867
3730
|
|
|
3868
3731
|
retry_command = (
|
|
3869
|
-
_build_job_create_retry_command(
|
|
3870
|
-
template=template,
|
|
3871
|
-
agenthub_ref=agenthub_ref,
|
|
3872
|
-
instance_id=instance_id,
|
|
3873
|
-
dataset=dataset,
|
|
3874
|
-
instance_range=instance_range,
|
|
3875
|
-
exclude_instance_ids=exclude_instance_ids,
|
|
3876
|
-
params=params,
|
|
3877
|
-
params_list_input=params_list_input,
|
|
3878
|
-
suite_name=suite_name,
|
|
3879
|
-
group_id=group_id,
|
|
3880
|
-
tags=tags,
|
|
3881
|
-
overrides=overrides,
|
|
3882
|
-
concurrency=concurrency,
|
|
3883
|
-
batch_size=batch_size,
|
|
3884
|
-
max_failure_retries=max_failure_retries,
|
|
3885
|
-
wait=wait,
|
|
3886
|
-
wait_interval=wait_interval,
|
|
3887
|
-
wait_timeout=wait_timeout,
|
|
3888
|
-
output_format=output_format,
|
|
3889
|
-
enable_otel_tracing=enable_otel_tracing,
|
|
3890
|
-
runner=runner,
|
|
3891
|
-
runner_options=runner_options,
|
|
3892
|
-
model_base_url_collection=model_base_url_collection,
|
|
3893
|
-
model_base_url_file=model_base_url_file,
|
|
3894
|
-
datasources=datasources,
|
|
3895
|
-
account_pool=account_pool,
|
|
3896
|
-
enable_post_process=enable_post_process,
|
|
3897
|
-
group_post_process=group_post_process,
|
|
3898
|
-
idempotency_key=submission_idempotency_key,
|
|
3899
|
-
resource_profile_id=resource_profile_id,
|
|
3900
|
-
**({"ak_id": ak_id} if ak_id is not None else {}),
|
|
3901
|
-
**({"ak_mode": ak_mode} if ak_mode != "auto" else {}),
|
|
3902
|
-
timeout=timeout,
|
|
3903
|
-
)
|
|
3732
|
+
_build_job_create_retry_command(idempotency_key=submission_idempotency_key)
|
|
3904
3733
|
if not dry_run
|
|
3905
3734
|
and not job_spec_flag
|
|
3906
3735
|
and idempotency_enabled
|
|
@@ -4003,6 +3832,7 @@ def job_create(
|
|
|
4003
3832
|
|
|
4004
3833
|
if job_spec_flag:
|
|
4005
3834
|
payload = client.build_create_job_body(
|
|
3835
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
4006
3836
|
template=template,
|
|
4007
3837
|
params_list=[dict(p) for p in params_list],
|
|
4008
3838
|
dataset=dataset,
|
|
@@ -4046,6 +3876,7 @@ def job_create(
|
|
|
4046
3876
|
batch = build_batch(start)
|
|
4047
3877
|
payloads.append(
|
|
4048
3878
|
client.build_create_job_body(
|
|
3879
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
4049
3880
|
template=template,
|
|
4050
3881
|
params_list=batch,
|
|
4051
3882
|
dataset=dataset,
|
|
@@ -4105,6 +3936,7 @@ def job_create(
|
|
|
4105
3936
|
current_stage=f"batch {batch_index + 1}: {start + 1}-{batch_end}/{total_count}",
|
|
4106
3937
|
)
|
|
4107
3938
|
batch_result = client.create_job(
|
|
3939
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
4108
3940
|
template=template,
|
|
4109
3941
|
params_list=batch,
|
|
4110
3942
|
dataset=dataset,
|
|
@@ -4214,6 +4046,7 @@ def job_create(
|
|
|
4214
4046
|
else:
|
|
4215
4047
|
if job_spec_flag:
|
|
4216
4048
|
payload = client.build_create_job_body(
|
|
4049
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
4217
4050
|
template=template,
|
|
4218
4051
|
params=params_dict,
|
|
4219
4052
|
dataset=dataset,
|
|
@@ -4251,6 +4084,7 @@ def job_create(
|
|
|
4251
4084
|
|
|
4252
4085
|
if dry_run:
|
|
4253
4086
|
payload = client.build_create_job_body(
|
|
4087
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
4254
4088
|
template=template,
|
|
4255
4089
|
params=params_dict,
|
|
4256
4090
|
dataset=dataset,
|
|
@@ -4300,6 +4134,7 @@ def job_create(
|
|
|
4300
4134
|
current_stage="submitting",
|
|
4301
4135
|
)
|
|
4302
4136
|
result = client.create_job(
|
|
4137
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
4303
4138
|
template=template,
|
|
4304
4139
|
params=params_dict,
|
|
4305
4140
|
dataset=dataset,
|
|
@@ -4477,6 +4312,11 @@ def job_get(
|
|
|
4477
4312
|
"--field",
|
|
4478
4313
|
help="Print one field path, e.g. status, lifecycle.run_duration, params.model",
|
|
4479
4314
|
),
|
|
4315
|
+
owner: bool = typer.Option(
|
|
4316
|
+
False,
|
|
4317
|
+
"--owner",
|
|
4318
|
+
help="Show only the owner and authorization contact, without reading protected details",
|
|
4319
|
+
),
|
|
4480
4320
|
output_format: str = typer.Option(
|
|
4481
4321
|
None,
|
|
4482
4322
|
"--format",
|
|
@@ -4493,7 +4333,20 @@ def job_get(
|
|
|
4493
4333
|
include_spec = include_spec or _field_path_requires_spec(field)
|
|
4494
4334
|
if field == "job_spec_data.spec_yaml":
|
|
4495
4335
|
spec_format = "yaml"
|
|
4336
|
+
if owner and share_token is not None:
|
|
4337
|
+
raise typer.BadParameter("--owner cannot be combined with --share-token")
|
|
4496
4338
|
client = _read_client(share_token)
|
|
4339
|
+
if owner and (include_params or include_spec or field):
|
|
4340
|
+
raise typer.BadParameter(
|
|
4341
|
+
"--owner cannot be combined with --include-params, --include-spec or --field"
|
|
4342
|
+
)
|
|
4343
|
+
if owner:
|
|
4344
|
+
result = client.get_job_access_contact(job_id)
|
|
4345
|
+
if output_format in {"plain", "table"}:
|
|
4346
|
+
_print_detail(result)
|
|
4347
|
+
else:
|
|
4348
|
+
_print_formatted(result, output_format)
|
|
4349
|
+
return
|
|
4497
4350
|
result = client.get_job(
|
|
4498
4351
|
job_id,
|
|
4499
4352
|
include_params=include_params,
|
|
@@ -4501,6 +4354,18 @@ def job_get(
|
|
|
4501
4354
|
spec_format=spec_format,
|
|
4502
4355
|
)
|
|
4503
4356
|
if field:
|
|
4357
|
+
safe_job_name = (
|
|
4358
|
+
field == "job_name"
|
|
4359
|
+
and isinstance(result.get("job_id"), str)
|
|
4360
|
+
and bool(result["job_id"])
|
|
4361
|
+
and result.get("job_name") == result["job_id"]
|
|
4362
|
+
)
|
|
4363
|
+
if not safe_job_name and any(
|
|
4364
|
+
field == path or field.startswith(path + ".") or field.startswith(path + "[")
|
|
4365
|
+
for path in (result.get("restricted_fields") or [])
|
|
4366
|
+
):
|
|
4367
|
+
_emit_error(f"field hidden: {field}; Group Reader access is required")
|
|
4368
|
+
raise typer.Exit(1)
|
|
4504
4369
|
try:
|
|
4505
4370
|
value = _extract_field(result, field)
|
|
4506
4371
|
except ValueError as exc:
|
|
@@ -4729,15 +4594,21 @@ _RESOURCE_ROLE_IDS = {
|
|
|
4729
4594
|
("job", "reader"): "built-in:job-reader",
|
|
4730
4595
|
("job", "writer"): "built-in:job-writer",
|
|
4731
4596
|
("group", "reader"): "built-in:group-reader",
|
|
4597
|
+
("group", "blackbox-reader"): "built-in:blackbox-reader",
|
|
4732
4598
|
("group", "writer"): "built-in:group-writer",
|
|
4733
4599
|
("group", "cloner"): "built-in:group-cloner",
|
|
4600
|
+
("group", "admin"): "built-in:group-admin",
|
|
4734
4601
|
}
|
|
4735
4602
|
|
|
4736
4603
|
|
|
4737
4604
|
def _resource_role_id(resource: str, role: str) -> str:
|
|
4738
4605
|
key = (resource, (role or "").strip().lower())
|
|
4739
4606
|
if key not in _RESOURCE_ROLE_IDS:
|
|
4740
|
-
choices =
|
|
4607
|
+
choices = (
|
|
4608
|
+
"reader, writer, cloner, blackbox-reader, admin"
|
|
4609
|
+
if resource == "group"
|
|
4610
|
+
else "reader, writer"
|
|
4611
|
+
)
|
|
4741
4612
|
raise typer.BadParameter(f"role must be one of: {choices}")
|
|
4742
4613
|
return _RESOURCE_ROLE_IDS[key]
|
|
4743
4614
|
|
|
@@ -5439,6 +5310,14 @@ def checkpoint_get(
|
|
|
5439
5310
|
|
|
5440
5311
|
@group_app.command("create")
|
|
5441
5312
|
def group_create(
|
|
5313
|
+
transparency: Optional[str] = typer.Option(
|
|
5314
|
+
None,
|
|
5315
|
+
"--transparency",
|
|
5316
|
+
help="whitebox or blackbox; omit to inherit the Group/Profile default",
|
|
5317
|
+
),
|
|
5318
|
+
idempotency_key: Optional[str] = typer.Option(
|
|
5319
|
+
None, "--idempotency-key", help="Reuse this key when retrying blackbox Group creation"
|
|
5320
|
+
),
|
|
5442
5321
|
ak_id: Optional[list[str]] = typer.Option(
|
|
5443
5322
|
None, "--ak-id", help="Managed AK reference (repeatable) to associate with the Group"
|
|
5444
5323
|
),
|
|
@@ -5591,6 +5470,8 @@ def group_create(
|
|
|
5591
5470
|
),
|
|
5592
5471
|
):
|
|
5593
5472
|
"""Create a Group."""
|
|
5473
|
+
if transparency is not None and transparency not in {"whitebox", "blackbox"}:
|
|
5474
|
+
raise typer.BadParameter("Choose whitebox or blackbox", param_hint="--transparency")
|
|
5594
5475
|
output_format = _normalize_output_format(output_format)
|
|
5595
5476
|
if priority is not None and priority not in {"high", "medium"}:
|
|
5596
5477
|
raise typer.BadParameter("--priority must be one of high/medium")
|
|
@@ -5599,6 +5480,8 @@ def group_create(
|
|
|
5599
5480
|
validate_ak_id(reference)
|
|
5600
5481
|
except ValueError as exc:
|
|
5601
5482
|
raise typer.BadParameter(str(exc), param_hint="--ak-id") from None
|
|
5483
|
+
if transparency == "blackbox" and idempotency_key is None:
|
|
5484
|
+
idempotency_key = str(uuid.uuid4())
|
|
5602
5485
|
client = get_client()
|
|
5603
5486
|
eval_config_dict = json.loads(eval_config) if eval_config else None
|
|
5604
5487
|
group_post_process_dict = _parse_json_object(group_post_process, "--group-post-process")
|
|
@@ -5656,6 +5539,8 @@ def group_create(
|
|
|
5656
5539
|
|
|
5657
5540
|
try:
|
|
5658
5541
|
result = client.create_group(
|
|
5542
|
+
**({"transparency": transparency} if transparency is not None else {}),
|
|
5543
|
+
**({"idempotency_key": idempotency_key} if idempotency_key is not None else {}),
|
|
5659
5544
|
ak_ids=ak_id,
|
|
5660
5545
|
name=name,
|
|
5661
5546
|
tags=group_tags,
|
|
@@ -5678,7 +5563,17 @@ def group_create(
|
|
|
5678
5563
|
**({"priority": priority} if priority is not None else {}),
|
|
5679
5564
|
**({"priority_weight": priority_weight} if priority_weight is not None else {}),
|
|
5680
5565
|
)
|
|
5566
|
+
except APIError:
|
|
5567
|
+
if idempotency_key is not None:
|
|
5568
|
+
_emit_idempotency_retry_hint(
|
|
5569
|
+
f"Rerun your original command with --idempotency-key {shlex.quote(idempotency_key)}"
|
|
5570
|
+
)
|
|
5571
|
+
raise
|
|
5681
5572
|
except ManagedAKProtocolError as exc:
|
|
5573
|
+
if idempotency_key is not None:
|
|
5574
|
+
_emit_idempotency_retry_hint(
|
|
5575
|
+
_build_job_create_retry_command(idempotency_key=idempotency_key)
|
|
5576
|
+
)
|
|
5682
5577
|
_emit_error(str(exc))
|
|
5683
5578
|
raise typer.Exit(1) from None
|
|
5684
5579
|
if output_format == "plain":
|
|
@@ -5815,6 +5710,11 @@ def group_get(
|
|
|
5815
5710
|
share_token: Optional[str] = typer.Option(
|
|
5816
5711
|
None, "--share-token", help="Read with a share token"
|
|
5817
5712
|
),
|
|
5713
|
+
owner: bool = typer.Option(
|
|
5714
|
+
False,
|
|
5715
|
+
"--owner",
|
|
5716
|
+
help="Show only the owner and authorization contact, without reading protected details",
|
|
5717
|
+
),
|
|
5818
5718
|
output_format: str = typer.Option(
|
|
5819
5719
|
None,
|
|
5820
5720
|
"--format",
|
|
@@ -5823,7 +5723,16 @@ def group_get(
|
|
|
5823
5723
|
):
|
|
5824
5724
|
"""Show Group details."""
|
|
5825
5725
|
output_format = _normalize_output_format(output_format, keep_table=True)
|
|
5726
|
+
if owner and share_token is not None:
|
|
5727
|
+
raise typer.BadParameter("--owner cannot be combined with --share-token")
|
|
5826
5728
|
client = _read_client(share_token, group_id if share_token else None)
|
|
5729
|
+
if owner:
|
|
5730
|
+
result = client.get_group_access_contact(group_id)
|
|
5731
|
+
if output_format in {"plain", "table"}:
|
|
5732
|
+
_print_detail(result)
|
|
5733
|
+
else:
|
|
5734
|
+
_print_formatted(result, output_format)
|
|
5735
|
+
return
|
|
5827
5736
|
result = client.get_group(group_id)
|
|
5828
5737
|
if output_format == "table":
|
|
5829
5738
|
_print_detail(result)
|
|
@@ -6029,6 +5938,7 @@ def group_clone(
|
|
|
6029
5938
|
("source_group_id", preview.get("source_group_id")),
|
|
6030
5939
|
("target_cluster", preview.get("target_cluster")),
|
|
6031
5940
|
("target_name", (preview.get("target_group") or {}).get("name")),
|
|
5941
|
+
("transparency", (preview.get("target_group") or {}).get("transparency")),
|
|
6032
5942
|
("target_queue", (preview.get("target_group") or {}).get("queue_id")),
|
|
6033
5943
|
("jobs", preview.get("source_job_count")),
|
|
6034
5944
|
("target_jobs", preview.get("target_job_count")),
|
|
@@ -6553,7 +6463,7 @@ def group_grant(
|
|
|
6553
6463
|
role: str = typer.Option(
|
|
6554
6464
|
"reader",
|
|
6555
6465
|
"--role",
|
|
6556
|
-
help="reader (view), cloner (view+clone),
|
|
6466
|
+
help="reader (view), blackbox-reader (protected Group content), cloner (view+clone), writer (view+cancel/update), or admin (view+clone+update+grant)",
|
|
6557
6467
|
),
|
|
6558
6468
|
):
|
|
6559
6469
|
"""Grant a user/user-group access to a specific group (RBAC v2 instance delegation)."""
|
|
@@ -6567,7 +6477,9 @@ def group_revoke(
|
|
|
6567
6477
|
user_group: Optional[str] = typer.Option(
|
|
6568
6478
|
None, "--user-group", help="Revoke from this user_group_id"
|
|
6569
6479
|
),
|
|
6570
|
-
role: str = typer.Option(
|
|
6480
|
+
role: str = typer.Option(
|
|
6481
|
+
"reader", "--role", help="reader, blackbox-reader, cloner, writer, or admin"
|
|
6482
|
+
),
|
|
6571
6483
|
):
|
|
6572
6484
|
"""Revoke a user/user-group's instance access to a specific group."""
|
|
6573
6485
|
_revoke_instance_access("group", group_id, role, user, user_group)
|
|
@@ -6747,6 +6659,8 @@ def main() -> None:
|
|
|
6747
6659
|
|
|
6748
6660
|
|
|
6749
6661
|
def _format_api_error(exc: APIError) -> str:
|
|
6662
|
+
if isinstance(exc.detail, dict) and exc.detail.get("code") == "blackbox_content_restricted":
|
|
6663
|
+
return f"error: Blackbox content is hidden; Group Reader access is required. (request_id={exc.request_id})"
|
|
6750
6664
|
if str(exc.detail) != _NON_JSON_RESPONSE_DETAIL:
|
|
6751
6665
|
return f"error: {exc}"
|
|
6752
6666
|
|
|
@@ -23,7 +23,7 @@ import requests
|
|
|
23
23
|
if TYPE_CHECKING:
|
|
24
24
|
from irepo_artifact import ArtifactClient, DownloadResult
|
|
25
25
|
|
|
26
|
-
from .api import APIClient
|
|
26
|
+
from .api import APIClient, APIError
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
@@ -429,8 +429,9 @@ def _export_job_into_dir(
|
|
|
429
429
|
extract_artifacts=extract_artifacts,
|
|
430
430
|
skip_no_artifact=skip_no_artifact,
|
|
431
431
|
)
|
|
432
|
-
from irepo_artifact import ArtifactError, ArtifactNotFound
|
|
432
|
+
from irepo_artifact import AccessDenied, ArtifactError, ArtifactNotFound
|
|
433
433
|
|
|
434
|
+
job_data = job or {}
|
|
434
435
|
current_stage = "job metadata"
|
|
435
436
|
selective = bool(include_files or exclude_files)
|
|
436
437
|
|
|
@@ -480,6 +481,11 @@ def _export_job_into_dir(
|
|
|
480
481
|
except ArtifactError as exc:
|
|
481
482
|
outcome = error = exc
|
|
482
483
|
|
|
484
|
+
# A denied refresh can retain old metadata. Never publish or delete
|
|
485
|
+
# existing export files on an authorization failure.
|
|
486
|
+
if isinstance(error, AccessDenied):
|
|
487
|
+
raise error
|
|
488
|
+
|
|
483
489
|
artifact = outcome.artifacts_response
|
|
484
490
|
manifest = outcome.manifest_response
|
|
485
491
|
missing_manifest = (
|
|
@@ -580,6 +586,31 @@ def _export_job_into_dir(
|
|
|
580
586
|
except Exception as exc:
|
|
581
587
|
if result_callback is not None:
|
|
582
588
|
result_callback(job_id, "failed", None)
|
|
589
|
+
|
|
590
|
+
if isinstance(exc, AccessDenied):
|
|
591
|
+
if (
|
|
592
|
+
job_data.get("transparency") == "blackbox"
|
|
593
|
+
and job_data.get("can_read_content") is False
|
|
594
|
+
and getattr(exc, "_request_status", None) == 403
|
|
595
|
+
):
|
|
596
|
+
raise APIError(
|
|
597
|
+
403,
|
|
598
|
+
{
|
|
599
|
+
"code": "blackbox_content_restricted",
|
|
600
|
+
"message": "Group Reader access is required",
|
|
601
|
+
},
|
|
602
|
+
None,
|
|
603
|
+
None,
|
|
604
|
+
) from exc
|
|
605
|
+
raise RuntimeError(
|
|
606
|
+
f"Access denied while exporting job {job_id}; check your login/API key and resource permissions."
|
|
607
|
+
) from exc
|
|
608
|
+
if (
|
|
609
|
+
isinstance(exc, APIError)
|
|
610
|
+
and isinstance(exc.detail, dict)
|
|
611
|
+
and exc.detail.get("code") == "blackbox_content_restricted"
|
|
612
|
+
):
|
|
613
|
+
raise
|
|
583
614
|
raise RuntimeError(
|
|
584
615
|
f"Failed to export job {job_id} at stage '{current_stage}': {exc}"
|
|
585
616
|
) from exc
|
|
@@ -21,7 +21,6 @@ def validate_ak_selection(ak_id: Any, params: Any, params_list: Any, overrides:
|
|
|
21
21
|
if isinstance(params_list, list):
|
|
22
22
|
candidates.extend(params_list)
|
|
23
23
|
for value in candidates:
|
|
24
|
-
parameter_ak_ids(value)
|
|
25
24
|
if (
|
|
26
25
|
isinstance(value, dict)
|
|
27
26
|
and {"ak_id", "ak_selection_source", "user_ak_id", "key_id"} & value.keys()
|
|
@@ -31,13 +30,8 @@ def validate_ak_selection(ak_id: Any, params: Any, params_list: Any, overrides:
|
|
|
31
30
|
)
|
|
32
31
|
|
|
33
32
|
|
|
34
|
-
def parameter_ak_ids(value: Any) -> set[str]:
|
|
35
|
-
"""Find explicit parameter references without resolving credential plaintext."""
|
|
36
|
-
return parameter_ak_usage(value)[0]
|
|
37
|
-
|
|
38
|
-
|
|
39
33
|
def parameter_ak_usage(value: Any) -> tuple[set[str], set[tuple[str, str]]]:
|
|
40
|
-
"""
|
|
34
|
+
"""Collect references for receipt checks; the server validates their syntax."""
|
|
41
35
|
if isinstance(value, dict):
|
|
42
36
|
usages = [parameter_ak_usage(v) for v in value.values()]
|
|
43
37
|
return set().union(*(ids for ids, _ in usages)), set().union(
|
|
@@ -49,6 +43,8 @@ def parameter_ak_usage(value: Any) -> tuple[set[str], set[tuple[str, str]]]:
|
|
|
49
43
|
*(selected for _, selected in usages)
|
|
50
44
|
)
|
|
51
45
|
if isinstance(value, str) and "$akpool" in value:
|
|
46
|
+
# Normalize only for receipt recognition; the request keeps the original value.
|
|
47
|
+
value = re.sub(r"^\$akpool:[ \t]+", "$akpool:", value)
|
|
52
48
|
match = re.fullmatch(r"\$akpool:([A-Za-z0-9][A-Za-z0-9_.-]{0,127})", value)
|
|
53
49
|
if match:
|
|
54
50
|
return {match[1]}, set()
|
|
@@ -64,8 +60,14 @@ def parameter_ak_usage(value: Any) -> tuple[set[str], set[tuple[str, str]]]:
|
|
|
64
60
|
c in ",@#.$" or c.isspace() or unicodedata.category(c) == "Cc" for c in tag
|
|
65
61
|
)
|
|
66
62
|
):
|
|
67
|
-
return set(), {
|
|
68
|
-
|
|
63
|
+
return set(), {
|
|
64
|
+
(
|
|
65
|
+
"personal_default" if match[2] == "me" else "queue_default",
|
|
66
|
+
"" if match[2] == "me" and tag == "default" else tag,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
# Keep the capability check for future syntax without echoing arbitrary values.
|
|
70
|
+
return {"$akpool"}, set()
|
|
69
71
|
return set(), set()
|
|
70
72
|
|
|
71
73
|
|
|
@@ -90,6 +92,20 @@ def _version_one(value: Any) -> bool:
|
|
|
90
92
|
return type(value) in (int, float) and value == 1
|
|
91
93
|
|
|
92
94
|
|
|
95
|
+
def _personal_default_alias(selectors: set[tuple[str, str]]) -> set[tuple[str, str]]:
|
|
96
|
+
"""Accept the pre-alias spelling of the personal default on reused Jobs.
|
|
97
|
+
|
|
98
|
+
``$akpool:#default@me`` is now the untagged personal default, but Jobs frozen by an
|
|
99
|
+
older version stored ``selection_tag="default"``. An idempotent resubmission returns
|
|
100
|
+
that frozen binding verbatim, so the alias must acknowledge the legacy tag as well.
|
|
101
|
+
"""
|
|
102
|
+
expanded = set(selectors)
|
|
103
|
+
for source, tag in selectors:
|
|
104
|
+
if source == "personal_default" and tag in {"", "default"}:
|
|
105
|
+
expanded.add((source, "default" if tag == "" else ""))
|
|
106
|
+
return expanded
|
|
107
|
+
|
|
108
|
+
|
|
93
109
|
def require_parameter_ak_ack(
|
|
94
110
|
response: Any, ak_ids: set[str], selectors: set[tuple[str, str]] | None = None
|
|
95
111
|
) -> None:
|
|
@@ -102,6 +118,7 @@ def require_parameter_ak_ack(
|
|
|
102
118
|
if selectors:
|
|
103
119
|
if not _version_one(response.get("ak_selection_version")):
|
|
104
120
|
raise ManagedAKProtocolError("$akpool selector", response)
|
|
121
|
+
reused_bindings: set[tuple[str, str]] = set()
|
|
105
122
|
if response.get("dry_run") is True:
|
|
106
123
|
preview = response.get("preview_job")
|
|
107
124
|
if not response_ak_ids(preview):
|
|
@@ -113,18 +130,31 @@ def require_parameter_ak_ack(
|
|
|
113
130
|
jobs = response.get("jobs")
|
|
114
131
|
if not isinstance(jobs, list):
|
|
115
132
|
raise ManagedAKProtocolError("$akpool selector", response)
|
|
133
|
+
job_list = [job for job in jobs if isinstance(job, dict)] if jobs else []
|
|
134
|
+
aggregate = _confirmed_selectors(response.get("ak_bindings"))
|
|
116
135
|
confirmed = (
|
|
136
|
+
set().union(*(_confirmed_selectors(job.get("ak_bindings")) for job in job_list))
|
|
137
|
+
if job_list
|
|
138
|
+
else aggregate
|
|
139
|
+
)
|
|
140
|
+
# Only a reused Job may answer with the legacy tag: a fresh submission must
|
|
141
|
+
# bind the alias as the untagged personal default. Large Profile receipts omit
|
|
142
|
+
# per-Job details and sample the persisted bindings at the top level, so those
|
|
143
|
+
# aggregated bindings are frozen evidence too: a fresh alias binding is always
|
|
144
|
+
# untagged, which leaves "default" only on Jobs frozen before the alias existed.
|
|
145
|
+
reused_bindings = (
|
|
117
146
|
set().union(
|
|
118
147
|
*(
|
|
119
148
|
_confirmed_selectors(job.get("ak_bindings"))
|
|
120
|
-
for job in
|
|
121
|
-
if
|
|
149
|
+
for job in job_list
|
|
150
|
+
if job.get("reused") is True
|
|
122
151
|
)
|
|
123
152
|
)
|
|
124
|
-
if
|
|
125
|
-
else
|
|
153
|
+
if job_list
|
|
154
|
+
else aggregate
|
|
126
155
|
)
|
|
127
|
-
|
|
156
|
+
accepted = confirmed | _personal_default_alias(reused_bindings)
|
|
157
|
+
if not selectors <= accepted:
|
|
128
158
|
raise ManagedAKProtocolError("$akpool selector", response)
|
|
129
159
|
|
|
130
160
|
|
|
@@ -15,6 +15,7 @@ from ap_client.api import APIError # noqa: F401
|
|
|
15
15
|
from ap_client.config import normalize_output_format
|
|
16
16
|
from rich import print
|
|
17
17
|
from rich.table import Table
|
|
18
|
+
from rich.text import Text
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _import_yaml():
|
|
@@ -153,6 +154,23 @@ def _fmt_dt(value: Optional[str]) -> str:
|
|
|
153
154
|
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
|
154
155
|
|
|
155
156
|
|
|
157
|
+
def _print_job_meta_plain(job_meta: dict) -> None:
|
|
158
|
+
benchmark = job_meta.get("benchmark") or "(inherit from spec)"
|
|
159
|
+
tags = ", ".join(job_meta.get("tags") or []) or "(none)"
|
|
160
|
+
typer.echo(f" Job benchmark default: {benchmark}")
|
|
161
|
+
typer.echo(f" Job tag defaults: {tags}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _print_job_meta_result(result: dict, output_format: str) -> None:
|
|
165
|
+
if output_format != "plain":
|
|
166
|
+
_print_formatted(result, output_format)
|
|
167
|
+
return
|
|
168
|
+
typer.echo("Profile Job defaults saved. Profile version is unchanged.")
|
|
169
|
+
payload = result.get("data", result)
|
|
170
|
+
if isinstance(payload, dict) and isinstance(payload.get("job_meta"), dict):
|
|
171
|
+
_print_job_meta_plain(payload["job_meta"])
|
|
172
|
+
|
|
173
|
+
|
|
156
174
|
def _print_profile_plain(profile: dict) -> None:
|
|
157
175
|
"""Pretty-print a single profile in plain format."""
|
|
158
176
|
pid = profile.get("profile_id") or profile.get("id", "-")
|
|
@@ -185,6 +203,10 @@ def _print_profile_plain(profile: dict) -> None:
|
|
|
185
203
|
)
|
|
186
204
|
print(f" [bold]Created:[/] {_fmt_dt(profile.get('created_at'))}")
|
|
187
205
|
print(f" [bold]Updated:[/] {_fmt_dt(profile.get('updated_at'))}")
|
|
206
|
+
if profile.get("update_reason"):
|
|
207
|
+
typer.echo(f" Update reason: {profile['update_reason']}")
|
|
208
|
+
if isinstance(profile.get("job_meta"), dict):
|
|
209
|
+
_print_job_meta_plain(profile["job_meta"])
|
|
188
210
|
|
|
189
211
|
spec = profile.get("spec")
|
|
190
212
|
if not isinstance(spec, dict):
|
|
@@ -277,6 +299,7 @@ def _print_versions_plain(data: dict, profile_id: str) -> None:
|
|
|
277
299
|
table.add_column("VERIFIED")
|
|
278
300
|
table.add_column("RELEASED")
|
|
279
301
|
table.add_column("GROUP")
|
|
302
|
+
table.add_column("REASON")
|
|
280
303
|
|
|
281
304
|
for v in items:
|
|
282
305
|
ver = v.get("verification") or {}
|
|
@@ -288,6 +311,7 @@ def _print_versions_plain(data: dict, profile_id: str) -> None:
|
|
|
288
311
|
"true" if ver else "-",
|
|
289
312
|
"true" if v.get("release") else "-",
|
|
290
313
|
ver.get("group_id") or "-",
|
|
314
|
+
Text(v.get("update_reason") or "-"),
|
|
291
315
|
)
|
|
292
316
|
print(table)
|
|
293
317
|
|
|
@@ -383,6 +407,80 @@ def profile_set_tags(
|
|
|
383
407
|
_print_formatted(result, output_format)
|
|
384
408
|
|
|
385
409
|
|
|
410
|
+
@profile_app.command("patch-job-meta")
|
|
411
|
+
def profile_patch_job_meta(
|
|
412
|
+
identifier: str = typer.Argument(..., help="Profile ID or name"),
|
|
413
|
+
benchmark: Optional[str] = typer.Option(
|
|
414
|
+
None, "--benchmark", help="Set the Job benchmark default; an empty value clears it"
|
|
415
|
+
),
|
|
416
|
+
clear_benchmark: bool = typer.Option(
|
|
417
|
+
False, "--clear-benchmark", help="Clear the live benchmark default and inherit from spec"
|
|
418
|
+
),
|
|
419
|
+
append_tags: Optional[list[str]] = typer.Option(
|
|
420
|
+
None, "--append-tag", help="Append a Job default tag (repeatable)"
|
|
421
|
+
),
|
|
422
|
+
remove_tags: Optional[list[str]] = typer.Option(
|
|
423
|
+
None, "--remove-tag", help="Remove an exact Job default tag (repeatable)"
|
|
424
|
+
),
|
|
425
|
+
output_format: str = typer.Option(
|
|
426
|
+
None, "--format", help="Output format: plain/table/json/yaml (default: AP_FORMAT or plain)"
|
|
427
|
+
),
|
|
428
|
+
):
|
|
429
|
+
"""Patch Job defaults without changing the Profile version.
|
|
430
|
+
|
|
431
|
+
Omitted fields stay unchanged. Remove and append tags in one request to
|
|
432
|
+
replace a keyed tag. Profile tags and spec are not changed.
|
|
433
|
+
"""
|
|
434
|
+
output_format = _normalize_output_format(output_format)
|
|
435
|
+
if benchmark is not None and clear_benchmark:
|
|
436
|
+
raise typer.BadParameter("--benchmark and --clear-benchmark are mutually exclusive")
|
|
437
|
+
patch: dict = {}
|
|
438
|
+
if clear_benchmark or benchmark is not None:
|
|
439
|
+
patch["benchmark"] = None if clear_benchmark else benchmark
|
|
440
|
+
delta: dict = {}
|
|
441
|
+
if append_tags:
|
|
442
|
+
delta["append"] = append_tags
|
|
443
|
+
if remove_tags:
|
|
444
|
+
delta["remove"] = remove_tags
|
|
445
|
+
if delta:
|
|
446
|
+
patch["tags"] = delta
|
|
447
|
+
if not patch:
|
|
448
|
+
raise typer.BadParameter(
|
|
449
|
+
"provide --benchmark, --clear-benchmark, --append-tag or --remove-tag"
|
|
450
|
+
)
|
|
451
|
+
result = get_client().update_profile_job_meta(identifier, patch)
|
|
452
|
+
_print_job_meta_result(result, output_format)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@profile_app.command("update-job-meta")
|
|
456
|
+
def profile_update_job_meta(
|
|
457
|
+
identifier: str = typer.Argument(..., help="Profile ID or name"),
|
|
458
|
+
job_meta_json: str = typer.Option(
|
|
459
|
+
..., "--json", help="Complete job_meta JSON object; '{}' clears all live defaults"
|
|
460
|
+
),
|
|
461
|
+
output_format: str = typer.Option(
|
|
462
|
+
None, "--format", help="Output format: plain/table/json/yaml (default: AP_FORMAT or plain)"
|
|
463
|
+
),
|
|
464
|
+
):
|
|
465
|
+
"""Replace all Job defaults without changing the Profile version.
|
|
466
|
+
|
|
467
|
+
Supply the full job_meta object using --json. Omitted fields are cleared;
|
|
468
|
+
'{}' clears all live defaults. Use patch-job-meta to change selected fields.
|
|
469
|
+
Profile tags and spec are not changed, and --reason is not required.
|
|
470
|
+
"""
|
|
471
|
+
output_format = _normalize_output_format(output_format)
|
|
472
|
+
try:
|
|
473
|
+
job_meta = json.loads(job_meta_json)
|
|
474
|
+
# Reject non-JSON constants such as NaN before constructing a request.
|
|
475
|
+
json.dumps(job_meta, allow_nan=False)
|
|
476
|
+
except ValueError as exc:
|
|
477
|
+
raise typer.BadParameter(f"Invalid JSON: {exc}", param_hint="--json") from exc
|
|
478
|
+
if not isinstance(job_meta, dict):
|
|
479
|
+
raise typer.BadParameter("must be a JSON object", param_hint="--json")
|
|
480
|
+
result = get_client().replace_profile_job_meta(identifier, job_meta)
|
|
481
|
+
_print_job_meta_result(result, output_format)
|
|
482
|
+
|
|
483
|
+
|
|
386
484
|
@profile_app.command("list")
|
|
387
485
|
def profile_list(
|
|
388
486
|
keyword: Optional[str] = typer.Option(None, "--keyword", "-k", help="Filter by name keyword"),
|
|
@@ -468,6 +566,9 @@ def profile_update(
|
|
|
468
566
|
None, "--spec", "-s", help="New inline spec as JSON string"
|
|
469
567
|
),
|
|
470
568
|
description: Optional[str] = typer.Option(None, "--description", "-d", help="New description"),
|
|
569
|
+
reason: str = typer.Option(
|
|
570
|
+
..., "--reason", help="Required reason for this update (1-500 characters after trimming)"
|
|
571
|
+
),
|
|
471
572
|
account_pool: Optional[str] = typer.Option(
|
|
472
573
|
None,
|
|
473
574
|
"--account-pool",
|
|
@@ -488,9 +589,15 @@ def profile_update(
|
|
|
488
589
|
|
|
489
590
|
Renaming a profile is intentionally not supported via the CLI — the
|
|
490
591
|
profile name is treated as a stable handle. Provide either --file or
|
|
491
|
-
--spec to replace the spec
|
|
592
|
+
--spec to replace the spec. --reason is required and must contain 1-500
|
|
593
|
+
characters after trimming. A previous version's reason is never reused.
|
|
492
594
|
"""
|
|
493
595
|
output_format = _normalize_output_format(output_format)
|
|
596
|
+
reason = reason.strip()
|
|
597
|
+
if not reason:
|
|
598
|
+
raise typer.BadParameter("must not be empty or whitespace", param_hint="--reason")
|
|
599
|
+
if len(reason) > 500:
|
|
600
|
+
raise typer.BadParameter("must be at most 500 characters", param_hint="--reason")
|
|
494
601
|
spec = _load_spec(file, spec_inline)
|
|
495
602
|
metadata = _load_metadata(metadata_inline)
|
|
496
603
|
|
|
@@ -511,6 +618,7 @@ def profile_update(
|
|
|
511
618
|
description=final_description,
|
|
512
619
|
account_pool=final_account_pool,
|
|
513
620
|
metadata=final_metadata,
|
|
621
|
+
reason=reason,
|
|
514
622
|
)
|
|
515
623
|
if output_format == "plain":
|
|
516
624
|
print("[green]Profile updated:[/]")
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ap-client"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.7.0.dev0"
|
|
8
8
|
description = "Agent Platform API Client & CLI"
|
|
9
9
|
readme = { text = "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.", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,15 +14,15 @@ dependencies = [
|
|
|
14
14
|
"rich>=13.0.0",
|
|
15
15
|
"PyYAML>=6.0",
|
|
16
16
|
"websockets>=13.0",
|
|
17
|
-
"irepo-artifact==0.2.
|
|
17
|
+
"irepo-artifact==0.2.2",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
[project.optional-dependencies]
|
|
21
21
|
dataset = [
|
|
22
|
-
"instance-repo[oss]>=1.3.
|
|
22
|
+
"instance-repo[oss]>=1.3.5,<2.0.0",
|
|
23
23
|
]
|
|
24
24
|
all = [
|
|
25
|
-
"instance-repo[oss]>=1.3.
|
|
25
|
+
"instance-repo[oss]>=1.3.5,<2.0.0",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[project.scripts]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|