envloop-cli 1.24.0__tar.gz → 1.27.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.
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/.gitignore +4 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/PKG-INFO +24 -18
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/README.pypi.md +22 -13
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/_optional.py +32 -14
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/auth_cli.py +1 -1
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/auth_login.py +2 -2
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/cli.py +14 -7
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/cli_errors.py +10 -3
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/cli_output.py +3 -3
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/http.py +11 -3
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/lifecycle_cli.py +4 -4
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/navigation_resources.py +41 -0
- envloop_cli-1.27.0/envloop/profile_secrets.py +133 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/resource_cli.py +43 -4
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/resources.py +2 -1
- envloop_cli-1.27.0/packages/data/envloop_data/__init__.py +1 -0
- envloop_cli-1.27.0/packages/data/envloop_data/cli.py +174 -0
- envloop_cli-1.27.0/packages/data/envloop_data/cli_output.py +81 -0
- envloop_cli-1.27.0/packages/data/envloop_data/client.py +239 -0
- envloop_cli-1.27.0/packages/data/envloop_data/data.py +483 -0
- envloop_cli-1.27.0/packages/data/envloop_data/data_cli.py +233 -0
- envloop_cli-1.27.0/packages/data/envloop_data/data_publications.py +133 -0
- envloop_cli-1.27.0/packages/data/envloop_data/data_upload.py +280 -0
- envloop_cli-1.27.0/packages/data/envloop_data/http.py +150 -0
- envloop_cli-1.27.0/packages/data/envloop_data/navigation_resources.py +257 -0
- envloop_cli-1.27.0/packages/data/envloop_data/profile_updates.py +77 -0
- envloop_cli-1.27.0/packages/data/envloop_data/recipes.py +25 -0
- envloop_cli-1.27.0/packages/data/envloop_data/resource_cli.py +112 -0
- envloop_cli-1.27.0/packages/data/envloop_data/runner_packages.py +71 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/__init__.py +1 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/cli.py +467 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/cli_output.py +50 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/client.py +473 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/decision.py +20 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/http.py +147 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/inference.py +36 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/job_cost.py +41 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/job_logs.py +21 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/job_spec_cli.py +146 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/navigation_resources.py +43 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/resource_cli.py +38 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/resources.py +153 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/runtime_params.py +83 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/sandbox.py +28 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/train.py +96 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/train_log_cli.py +138 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/train_progress.py +52 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/train_runner_cli.py +66 -0
- envloop_cli-1.27.0/packages/experiment/envloop_experiment/training/__init__.py +4 -0
- envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/__init__.py +1 -0
- envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/agent_runs.py +254 -0
- envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/cli.py +33 -0
- envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/cli_output.py +73 -0
- envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/rsi_cli.py +65 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/pyproject.toml +9 -15
- envloop_cli-1.24.0/envloop/profile_secrets.py +0 -28
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/__init__.py +0 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/auth_workspace.py +0 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/client.py +0 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/config.py +0 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/downloads.py +0 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/terminal.py +0 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/utils/__init__.py +0 -0
- {envloop_cli-1.24.0 → envloop_cli-1.27.0}/envloop/utils/logging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: envloop-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.27.0
|
|
4
4
|
Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
|
|
5
5
|
Project-URL: Homepage, https://envloop.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.envloop.ai
|
|
@@ -8,14 +8,11 @@ Project-URL: Repository, https://github.com/EnvLoop/envloop-cli
|
|
|
8
8
|
Project-URL: Issues, https://github.com/EnvLoop/envloop-cli/issues
|
|
9
9
|
Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
|
-
Requires-Dist: envloop-cli-experiment==1.24.0
|
|
12
11
|
Provides-Extra: data
|
|
13
|
-
Requires-Dist: envloop-
|
|
12
|
+
Requires-Dist: envloop-envhub==1.23.0; extra == 'data'
|
|
14
13
|
Provides-Extra: default
|
|
15
14
|
Provides-Extra: experiment
|
|
16
|
-
Requires-Dist: envloop-cli-experiment==1.24.0; extra == 'experiment'
|
|
17
15
|
Provides-Extra: rsi-loop
|
|
18
|
-
Requires-Dist: envloop-cli-rsi-loop==1.23.0; extra == 'rsi-loop'
|
|
19
16
|
Description-Content-Type: text/markdown
|
|
20
17
|
|
|
21
18
|
# envloop-cli: EnvLoop CLI and Python SDK
|
|
@@ -55,7 +52,7 @@ uv pip install --upgrade envloop-cli
|
|
|
55
52
|
python3.12 -m pip install --upgrade envloop-cli
|
|
56
53
|
```
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
Every installation includes Jobs, Trials, training, logs, inference calls, Data resources, checkpoints and RSI Loop controls.
|
|
59
56
|
Check that the SDK can be imported:
|
|
60
57
|
|
|
61
58
|
```bash
|
|
@@ -68,18 +65,26 @@ follow the [managed training guide](https://docs.envloop.ai/training/cli-hello-w
|
|
|
68
65
|
|
|
69
66
|
Install the client in your Python environment; the platform provides the execution resources.
|
|
70
67
|
|
|
71
|
-
## Optional features
|
|
68
|
+
## Optional features
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
One `envloop-cli` wheel and source archive contain all feature code. Extras add dependencies only:
|
|
74
71
|
|
|
75
72
|
```bash
|
|
76
|
-
python -m pip install
|
|
77
|
-
python -m pip install envloop-cli
|
|
78
|
-
python -m pip install "envloop-cli[experiment]"
|
|
79
|
-
python -m pip install "envloop-cli[rsi-loop]" # Existing job run paths; includes experiment
|
|
73
|
+
python -m pip install envloop-cli
|
|
74
|
+
python -m pip install "envloop-cli[data]"
|
|
75
|
+
python -m pip install "envloop-cli[data,experiment,rsi-loop]"
|
|
80
76
|
```
|
|
81
77
|
|
|
82
|
-
|
|
78
|
+
Every installation includes the existing Job, Trial, Data and RSI Loop commands. The bare installation and `default`, `experiment`, `rsi-loop` groups have no runtime dependencies. `data` adds `envloop-envhub==1.23.0` for ANS parsing and verified directory downloads. Other Data API operations use the bundled client implementation. Missing ANS dependencies produce the `envloop-cli[data]` installation instruction.
|
|
79
|
+
|
|
80
|
+
Feature modules load lazily. Default imports, help and experiment operations do not require EnvHub or certifi. Only the single `envloop-cli` distribution owns the `el` / `envloop` scripts. The examples below require an authorized matching platform; installation does not grant permissions.
|
|
81
|
+
|
|
82
|
+
Versions 1.23.0 and 1.24.0 on PyPI depend on feature packages that were never published: their `data` extra cannot be installed and the DATA command group is empty. They also install a separate `envloop-cli-experiment` package that shares files with this release. Remove both before installing; uninstalling `envloop-cli-experiment` after an upgrade deletes files the client needs and leads to `FEATURE_NOT_INSTALLED`, which the same commands repair:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m pip uninstall -y envloop-cli envloop-cli-experiment
|
|
86
|
+
python -m pip install "envloop-cli[data]"
|
|
87
|
+
```
|
|
83
88
|
|
|
84
89
|
## Connect
|
|
85
90
|
|
|
@@ -130,15 +135,14 @@ Error: PROFILE_NOT_FOUND
|
|
|
130
135
|
```bash
|
|
131
136
|
el job list --limit 5
|
|
132
137
|
el job list --type train --limit 5
|
|
133
|
-
el
|
|
134
|
-
el
|
|
138
|
+
el trial list --limit 5
|
|
139
|
+
el trial list --type train --job-id JOB_ID
|
|
135
140
|
el checkpoint list --limit 5
|
|
136
141
|
el job list --type inference
|
|
137
142
|
```
|
|
138
143
|
|
|
139
144
|
Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
|
|
140
|
-
Trial commands use
|
|
141
|
-
`el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
|
|
145
|
+
Trial commands use `el trial`, for example `el trial artifact list TRIAL_ID`. Use each command's `--help` for options.
|
|
142
146
|
|
|
143
147
|
For the Python SDK:
|
|
144
148
|
|
|
@@ -237,7 +241,7 @@ el job result JOB_ID
|
|
|
237
241
|
|
|
238
242
|
Replace `JOB_ID` with the returned value. The example has two independent Trials, each requesting four H100 GPUs: concurrent execution needs eight GPUs. Capacity and budget determine actual concurrency. Startup and execution are billable; check the Runner requirements, project `training.run` permission, and applicable prices and balance in your authorized Console account first. Reduce the Trial or GPU count when your program supports a smaller first run. The [usage guide](https://docs.envloop.ai/manage/usage) explains resource observations.
|
|
239
243
|
|
|
240
|
-
The request file does not depend on GPU shortcut flags. If `el job submit --help` lists both `--
|
|
244
|
+
The request file does not depend on GPU shortcut flags. If `el job submit --help` lists both `--trials` and `--gpu-type`, you can instead use the shortcuts in [GPU Runner training](https://docs.envloop.ai/training/runner). Choose one submission method to avoid duplicate runs. The SDK accepts the same dictionary through `client.training_jobs.submit(request)`.
|
|
241
245
|
|
|
242
246
|
The platform starts the published Revision’s `main.sh` / `main.py` using the Runner image; it does not upload local code. An optional command can only supply a declared `run_command` parameter. If this mode or GPU type is unavailable, contact your project administrator. Use `el job stop JOB_ID` to stop every Trial, then inspect each Trial’s `cleanup_confirmed` and download the uploaded artifacts you need.
|
|
243
247
|
|
|
@@ -289,6 +293,8 @@ el data download DATA_ID --file FILE_ID --output ./dataset.tgz --resume --json
|
|
|
289
293
|
|
|
290
294
|
Unified ANS CRUD supports Dataset, Runner, Recipe and HF model Checkpoint registrations through `el data` and `client.data`. Dataset/Runner/Recipe creation requires a native immutable `--ref`, `--manifest` and exact `--directory`. HF metadata registration uses `el data create --type checkpoint --model namespace/model --revision FULL_COMMIT`; SDK uses `create(kind="checkpoint", model=..., revision=...)`. HF weights are not downloaded, and artifact imports do not establish Model or Job checkpoint provenance.
|
|
291
295
|
|
|
296
|
+
`el runner register DIRECTORY --revision FULL_COMMIT [--source-ref main]` publishes a Runner package directory through the same Data API with the manifest built for you, then reads the catalog revision back. `el profile create|update|fork ... --secret NAME[=prompt|stdin|env:VAR|file:PATH]` stores write-only Secret parameters whose names follow the platform's credential rule or `secret_params`; values never come from the command line. `el setting model-access grant NAMESPACE/NAME --protocol anthropic [--revision FULL_COMMIT]` (project Admin) lets a published Runner receive the project's metered model key on Fly Trials; `list` and `revoke GRANT_ID` manage grants.
|
|
297
|
+
|
|
292
298
|
Success follows native storage and original catalog readback and returns an `ans_` identity. Update publishes an explicit new revision. Delete checks active and unknown dependencies; Runner deletion covers all revisions. Historical bytes and quota usage remain. Commands default to the current login base URL/workspace/project, with no `--env`. Both Data grants and the corresponding resource capabilities are required.
|
|
293
299
|
|
|
294
300
|
Read methods include `list/get/files/open/download`; `client.recipes.list/get` queries the original Recipe catalog. Downloads verify size/SHA-256 and refuse existing destinations, symlinks and storage redirects. Large payloads use multipart with stable idempotency keys. Staging `pub_` IDs and failure phases support retry; they are not native success. Matching client/server support is required. See the [Data reference](https://docs.envloop.ai/reference/cli#data-commands).
|
|
@@ -35,7 +35,7 @@ uv pip install --upgrade envloop-cli
|
|
|
35
35
|
python3.12 -m pip install --upgrade envloop-cli
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Every installation includes Jobs, Trials, training, logs, inference calls, Data resources, checkpoints and RSI Loop controls.
|
|
39
39
|
Check that the SDK can be imported:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
@@ -48,18 +48,26 @@ follow the [managed training guide](https://docs.envloop.ai/training/cli-hello-w
|
|
|
48
48
|
|
|
49
49
|
Install the client in your Python environment; the platform provides the execution resources.
|
|
50
50
|
|
|
51
|
-
## Optional features
|
|
51
|
+
## Optional features
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
One `envloop-cli` wheel and source archive contain all feature code. Extras add dependencies only:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
python -m pip install
|
|
57
|
-
python -m pip install envloop-cli
|
|
58
|
-
python -m pip install "envloop-cli[experiment]"
|
|
59
|
-
python -m pip install "envloop-cli[rsi-loop]" # Existing job run paths; includes experiment
|
|
56
|
+
python -m pip install envloop-cli
|
|
57
|
+
python -m pip install "envloop-cli[data]"
|
|
58
|
+
python -m pip install "envloop-cli[data,experiment,rsi-loop]"
|
|
60
59
|
```
|
|
61
60
|
|
|
62
|
-
|
|
61
|
+
Every installation includes the existing Job, Trial, Data and RSI Loop commands. The bare installation and `default`, `experiment`, `rsi-loop` groups have no runtime dependencies. `data` adds `envloop-envhub==1.23.0` for ANS parsing and verified directory downloads. Other Data API operations use the bundled client implementation. Missing ANS dependencies produce the `envloop-cli[data]` installation instruction.
|
|
62
|
+
|
|
63
|
+
Feature modules load lazily. Default imports, help and experiment operations do not require EnvHub or certifi. Only the single `envloop-cli` distribution owns the `el` / `envloop` scripts. The examples below require an authorized matching platform; installation does not grant permissions.
|
|
64
|
+
|
|
65
|
+
Versions 1.23.0 and 1.24.0 on PyPI depend on feature packages that were never published: their `data` extra cannot be installed and the DATA command group is empty. They also install a separate `envloop-cli-experiment` package that shares files with this release. Remove both before installing; uninstalling `envloop-cli-experiment` after an upgrade deletes files the client needs and leads to `FEATURE_NOT_INSTALLED`, which the same commands repair:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python -m pip uninstall -y envloop-cli envloop-cli-experiment
|
|
69
|
+
python -m pip install "envloop-cli[data]"
|
|
70
|
+
```
|
|
63
71
|
|
|
64
72
|
## Connect
|
|
65
73
|
|
|
@@ -110,15 +118,14 @@ Error: PROFILE_NOT_FOUND
|
|
|
110
118
|
```bash
|
|
111
119
|
el job list --limit 5
|
|
112
120
|
el job list --type train --limit 5
|
|
113
|
-
el
|
|
114
|
-
el
|
|
121
|
+
el trial list --limit 5
|
|
122
|
+
el trial list --type train --job-id JOB_ID
|
|
115
123
|
el checkpoint list --limit 5
|
|
116
124
|
el job list --type inference
|
|
117
125
|
```
|
|
118
126
|
|
|
119
127
|
Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
|
|
120
|
-
Trial commands use
|
|
121
|
-
`el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
|
|
128
|
+
Trial commands use `el trial`, for example `el trial artifact list TRIAL_ID`. Use each command's `--help` for options.
|
|
122
129
|
|
|
123
130
|
For the Python SDK:
|
|
124
131
|
|
|
@@ -217,7 +224,7 @@ el job result JOB_ID
|
|
|
217
224
|
|
|
218
225
|
Replace `JOB_ID` with the returned value. The example has two independent Trials, each requesting four H100 GPUs: concurrent execution needs eight GPUs. Capacity and budget determine actual concurrency. Startup and execution are billable; check the Runner requirements, project `training.run` permission, and applicable prices and balance in your authorized Console account first. Reduce the Trial or GPU count when your program supports a smaller first run. The [usage guide](https://docs.envloop.ai/manage/usage) explains resource observations.
|
|
219
226
|
|
|
220
|
-
The request file does not depend on GPU shortcut flags. If `el job submit --help` lists both `--
|
|
227
|
+
The request file does not depend on GPU shortcut flags. If `el job submit --help` lists both `--trials` and `--gpu-type`, you can instead use the shortcuts in [GPU Runner training](https://docs.envloop.ai/training/runner). Choose one submission method to avoid duplicate runs. The SDK accepts the same dictionary through `client.training_jobs.submit(request)`.
|
|
221
228
|
|
|
222
229
|
The platform starts the published Revision’s `main.sh` / `main.py` using the Runner image; it does not upload local code. An optional command can only supply a declared `run_command` parameter. If this mode or GPU type is unavailable, contact your project administrator. Use `el job stop JOB_ID` to stop every Trial, then inspect each Trial’s `cleanup_confirmed` and download the uploaded artifacts you need.
|
|
223
230
|
|
|
@@ -269,6 +276,8 @@ el data download DATA_ID --file FILE_ID --output ./dataset.tgz --resume --json
|
|
|
269
276
|
|
|
270
277
|
Unified ANS CRUD supports Dataset, Runner, Recipe and HF model Checkpoint registrations through `el data` and `client.data`. Dataset/Runner/Recipe creation requires a native immutable `--ref`, `--manifest` and exact `--directory`. HF metadata registration uses `el data create --type checkpoint --model namespace/model --revision FULL_COMMIT`; SDK uses `create(kind="checkpoint", model=..., revision=...)`. HF weights are not downloaded, and artifact imports do not establish Model or Job checkpoint provenance.
|
|
271
278
|
|
|
279
|
+
`el runner register DIRECTORY --revision FULL_COMMIT [--source-ref main]` publishes a Runner package directory through the same Data API with the manifest built for you, then reads the catalog revision back. `el profile create|update|fork ... --secret NAME[=prompt|stdin|env:VAR|file:PATH]` stores write-only Secret parameters whose names follow the platform's credential rule or `secret_params`; values never come from the command line. `el setting model-access grant NAMESPACE/NAME --protocol anthropic [--revision FULL_COMMIT]` (project Admin) lets a published Runner receive the project's metered model key on Fly Trials; `list` and `revoke GRANT_ID` manage grants.
|
|
280
|
+
|
|
272
281
|
Success follows native storage and original catalog readback and returns an `ans_` identity. Update publishes an explicit new revision. Delete checks active and unknown dependencies; Runner deletion covers all revisions. Historical bytes and quota usage remain. Commands default to the current login base URL/workspace/project, with no `--env`. Both Data grants and the corresponding resource capabilities are required.
|
|
273
282
|
|
|
274
283
|
Read methods include `list/get/files/open/download`; `client.recipes.list/get` queries the original Recipe catalog. Downloads verify size/SHA-256 and refuse existing destinations, symlinks and storage redirects. Large payloads use multipart with stable idempotency keys. Staging `pub_` IDs and failure phases support retry; they are not native success. Matching client/server support is required. See the [Data reference](https://docs.envloop.ai/reference/cli#data-commands).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Lazy, static adapters for
|
|
1
|
+
"""Lazy, static adapters for features bundled in the single client distribution.
|
|
2
2
|
|
|
3
3
|
No feature code is imported when the base client is imported or constructed.
|
|
4
4
|
Legacy module names resolve to their single feature owner on explicit import.
|
|
@@ -24,7 +24,7 @@ FEATURE_COMMANDS = {
|
|
|
24
24
|
},
|
|
25
25
|
'experiment': {
|
|
26
26
|
'job': 'Submit and manage Agent, Sandbox, Train and Inference Jobs',
|
|
27
|
-
'
|
|
27
|
+
'trial': 'Inspect and control existing Trials',
|
|
28
28
|
},
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -35,10 +35,10 @@ MODULES: dict[str, str] = {
|
|
|
35
35
|
}
|
|
36
36
|
CLIENTS = {
|
|
37
37
|
**{name: ('data', 'client') for name in ('ProfileService', 'RunnerService', 'Profiles', 'Runners')},
|
|
38
|
-
**{name: ('experiment', 'client') for name in ('
|
|
38
|
+
**{name: ('experiment', 'client') for name in ('TrialService', 'RpcService', 'load_task', 'validate_command', 'dataset_selector', 'Trials', 'runner_selector', 'job_budget', 'Jobs')},
|
|
39
39
|
**{name: ('base', 'downloads') for name in ('ArtifactError', 'artifact_path')},
|
|
40
40
|
}
|
|
41
|
-
DATA_ENDPOINTS = set('open_download resolve_dataset list_runners profile_path put_profile list_profiles get_profile resolve_profile set_default_profile archive_profile revoke_profile_secret export_profile import_profile open_checkpoint_archive'.split())
|
|
41
|
+
DATA_ENDPOINTS = set('open_download resolve_dataset list_runners get_runner_revision register_runner_revision runner_path profile_path put_profile list_profiles get_profile resolve_profile set_default_profile archive_profile revoke_profile_secret export_profile import_profile open_checkpoint_archive'.split())
|
|
42
42
|
EXPERIMENT_ENDPOINTS = set('submit_job get_job get_job_result get_job_logs list_jobs job_action job_events stop_job list_trials get_task_run get_trial_detail get_trial_training list_trial_metrics list_trial_checkpoints get_task_result get_task_snapshot stop_task list_trial_artifacts open_trial_artifact'.split())
|
|
43
43
|
|
|
44
44
|
|
|
@@ -48,7 +48,23 @@ class MissingFeatureError(ImportError):
|
|
|
48
48
|
|
|
49
49
|
def __init__(self, feature: str):
|
|
50
50
|
self.feature = feature
|
|
51
|
-
super().__init__(
|
|
51
|
+
super().__init__(self.instruction(feature))
|
|
52
|
+
|
|
53
|
+
def instruction(self, feature: str) -> str:
|
|
54
|
+
return f'Install optional functionality with: pip install "envloop-cli[{feature}]"'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class IncompleteInstallationError(MissingFeatureError):
|
|
58
|
+
"""Feature code ships inside envloop-cli, so a missing module means removed files.
|
|
59
|
+
|
|
60
|
+
Uninstalling the separate envloop-cli-experiment package of 1.23.0/1.24.0 after an
|
|
61
|
+
upgrade deletes files it shared with the single distribution; an extra cannot restore them.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
def instruction(self, feature: str) -> str:
|
|
66
|
+
return ('Client files are missing. Reinstall with: '
|
|
67
|
+
'pip uninstall -y envloop-cli envloop-cli-experiment && pip install envloop-cli')
|
|
52
68
|
|
|
53
69
|
|
|
54
70
|
class CommandExit(Exception):
|
|
@@ -60,9 +76,15 @@ def available(feature: str):
|
|
|
60
76
|
return importlib.util.find_spec(FEATURES[feature]) is not None
|
|
61
77
|
|
|
62
78
|
|
|
79
|
+
def require_data_dependencies():
|
|
80
|
+
"""Only ANS operations need the public protocol/download dependency."""
|
|
81
|
+
if importlib.util.find_spec('envhub') is None:
|
|
82
|
+
raise MissingFeatureError('data')
|
|
83
|
+
|
|
84
|
+
|
|
63
85
|
def feature_module(feature: str, module: str):
|
|
64
86
|
if not available(feature):
|
|
65
|
-
raise
|
|
87
|
+
raise IncompleteInstallationError(feature)
|
|
66
88
|
return importlib.import_module(f'{FEATURES[feature]}.{module}')
|
|
67
89
|
|
|
68
90
|
|
|
@@ -107,7 +129,7 @@ def client_resource(client: Any, name: str):
|
|
|
107
129
|
return getattr(importlib.import_module(f'envloop.{module}'), cls)(server)
|
|
108
130
|
if name == 'dashboard':
|
|
109
131
|
if available('experiment'):
|
|
110
|
-
return feature_attribute('experiment', 'navigation_resources', 'Dashboard')(client.project, client.jobs, client.
|
|
132
|
+
return feature_attribute('experiment', 'navigation_resources', 'Dashboard')(client.project, client.jobs, client.training_jobs)
|
|
111
133
|
from .navigation_resources import Dashboard
|
|
112
134
|
return Dashboard(client.project)
|
|
113
135
|
if name in {'profiles', 'runners'}:
|
|
@@ -121,17 +143,13 @@ def client_resource(client: Any, name: str):
|
|
|
121
143
|
if name == 'jobs':
|
|
122
144
|
return feature_attribute('experiment', 'client', 'Jobs')(server)
|
|
123
145
|
if name == 'trials':
|
|
124
|
-
return client
|
|
125
|
-
if name == 'tasks':
|
|
126
|
-
return feature_attribute('experiment', 'client', 'Tasks')(server)
|
|
146
|
+
return feature_attribute('experiment', 'client', 'Trials')(server)
|
|
127
147
|
if name == 'training_jobs':
|
|
128
|
-
return client.train
|
|
129
|
-
if name == 'train':
|
|
130
148
|
return feature_attribute('experiment', 'train', 'TrainJobs')(server)
|
|
131
149
|
if name == 'decision':
|
|
132
150
|
return feature_attribute('experiment', 'decision', 'DecisionClient')(server)
|
|
133
151
|
if name == 'agent_runs':
|
|
134
|
-
return feature_attribute('rsi-loop', 'agent_runs', 'AgentRuns')(client.
|
|
152
|
+
return feature_attribute('rsi-loop', 'agent_runs', 'AgentRuns')(client.training_jobs)
|
|
135
153
|
if name in {'endpoints', 'adapters'}:
|
|
136
154
|
return feature_attribute('experiment', 'resources', name.capitalize())(server)
|
|
137
155
|
if name == 'inference':
|
|
@@ -140,7 +158,7 @@ def client_resource(client: Any, name: str):
|
|
|
140
158
|
|
|
141
159
|
|
|
142
160
|
def execute_command(args: argparse.Namespace, client: Any, cli: Any):
|
|
143
|
-
if args.group in {'job', '
|
|
161
|
+
if args.group in {'job', 'trial'}:
|
|
144
162
|
return getattr(feature_module('experiment', 'cli'), 'execute_' + args.group)(args, client, cli)
|
|
145
163
|
if args.group == 'profile':
|
|
146
164
|
return feature_module('data', 'cli').execute_profile(args, client, cli)
|
|
@@ -16,7 +16,7 @@ def add_auth_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParse
|
|
|
16
16
|
login = commands.add_parser("login", help="Open the Clerk sign-in page and authorize CLI/SDK access")
|
|
17
17
|
login.add_argument("--base-url", help="API Base URL; defaults to the effective connection or official API")
|
|
18
18
|
login.add_argument("--no-browser", action="store_true", help="Print the login link without opening a browser")
|
|
19
|
-
login.add_argument("--timeout", type=int, choices=range(1, 601), metavar="SECONDS", default=300,
|
|
19
|
+
login.add_argument("--timeout-seconds", type=int, choices=range(1, 601), metavar="SECONDS", default=300,
|
|
20
20
|
help="Wait up to 1–600 seconds (default: 300)")
|
|
21
21
|
configure = commands.add_parser("config", help="Save Base URL and API key for CLI and SDK")
|
|
22
22
|
configure.add_argument("--no-check", action="store_true", help="Save without API verification (offline; key remains unverified)")
|
|
@@ -70,13 +70,13 @@ def login(args: argparse.Namespace, store: Any) -> int:
|
|
|
70
70
|
|
|
71
71
|
saved, credential = store.workspace_snapshot()
|
|
72
72
|
url = normalize_url(args.base_url or resolve_cli_connection().api_url or DEFAULT_BASE_URL)
|
|
73
|
-
deadline = time.monotonic() + args.
|
|
73
|
+
deadline = time.monotonic() + args.timeout_seconds
|
|
74
74
|
key, poll_token = 'epk_' + secrets.token_hex(32), secrets.token_urlsafe(32)
|
|
75
75
|
code = ''.join(secrets.choice('ABCDEFGHJKLMNPQRSTUVWXYZ23456789') for _ in range(8))
|
|
76
76
|
def digest(value: str) -> str:
|
|
77
77
|
return hashlib.sha256(value.encode()).hexdigest()
|
|
78
78
|
response = _request(url, 'start', {'key_hash': digest(key), 'poll_hash': digest(poll_token),
|
|
79
|
-
'code_hash': digest(code)}, min(5, args.
|
|
79
|
+
'code_hash': digest(code)}, min(5, args.timeout_seconds))
|
|
80
80
|
identifier, uri = _verification_url(url, response)
|
|
81
81
|
interval, expires = response.get('interval'), response.get('expires_in')
|
|
82
82
|
if (isinstance(interval, bool) or not isinstance(interval, (int, float)) or not 0 < interval <= 10 or
|
|
@@ -14,7 +14,7 @@ from .http import CLI_VERSION
|
|
|
14
14
|
|
|
15
15
|
SECTIONS = {
|
|
16
16
|
'OVERVIEW': ('dashboard',),
|
|
17
|
-
'EXPERIMENT': ('job', '
|
|
17
|
+
'EXPERIMENT': ('job', 'trial'),
|
|
18
18
|
'DATA': ('dataset', 'data', 'runner', 'checkpoint', 'profile'),
|
|
19
19
|
'MANAGE': ('billing', 'limit', 'member', 'setting'),
|
|
20
20
|
'TOOLS': ('auth', 'help'),
|
|
@@ -43,6 +43,8 @@ class HelpArgumentParser(argparse.ArgumentParser):
|
|
|
43
43
|
|
|
44
44
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
45
45
|
kwargs.setdefault('formatter_class', CommandHelpFormatter)
|
|
46
|
+
# Prefixes of an option would be unlisted aliases; only full option names are accepted.
|
|
47
|
+
kwargs.setdefault('allow_abbrev', False)
|
|
46
48
|
super().__init__(*args, **kwargs)
|
|
47
49
|
self.add_argument("--no-color", action="store_true", default=argparse.SUPPRESS,
|
|
48
50
|
help="Disable terminal highlighting")
|
|
@@ -140,7 +142,7 @@ def parser():
|
|
|
140
142
|
cli = HelpArgumentParser(
|
|
141
143
|
prog=Path(sys.argv[0]).name if Path(sys.argv[0]).name in {"envloop", "el"} else "el",
|
|
142
144
|
usage="%(prog)s [options] [command]", formatter_class=CommandHelpFormatter,
|
|
143
|
-
description="Manage Agent, Sandbox, Train and Inference Jobs and
|
|
145
|
+
description="Manage Agent, Sandbox, Train and Inference Jobs and their Trials with EnvLoop.\nConfigure your connection with 'auth config'.",
|
|
144
146
|
)
|
|
145
147
|
cli._optionals.title = "Options"
|
|
146
148
|
cli.set_defaults(api_url=None, workspace_id=None, no_color=False)
|
|
@@ -222,18 +224,18 @@ def _main(argv: list[str] | None = None):
|
|
|
222
224
|
disable_color()
|
|
223
225
|
if any(value == "--api-key" or value.startswith("--api-key=") for value in argv):
|
|
224
226
|
cli.error("API keys must be entered with auth config or supplied through ENVLOOP_API_KEY")
|
|
225
|
-
from ._optional import
|
|
227
|
+
from ._optional import IncompleteInstallationError, available
|
|
226
228
|
requested = next((value for value in argv if not value.startswith('-')), None)
|
|
227
|
-
feature = cast(dict[str | None, str], {'job': 'experiment', '
|
|
229
|
+
feature = cast(dict[str | None, str], {'job': 'experiment', 'trial': 'experiment', 'data': 'data', 'dataset': 'data',
|
|
228
230
|
'runner': 'data', 'checkpoint': 'data', 'profile': 'data'}).get(requested)
|
|
229
231
|
if feature is not None and not available(feature):
|
|
230
232
|
from .cli_errors import format_error
|
|
231
|
-
print(format_error(
|
|
233
|
+
print(format_error(IncompleteInstallationError(feature)), file=sys.stderr)
|
|
232
234
|
return 1
|
|
233
235
|
job_action = next((value for value in argv[argv.index('job') + 1:] if not value.startswith('-')), None) if requested == 'job' else None
|
|
234
236
|
if job_action == 'run' and not available('rsi-loop'):
|
|
235
237
|
from .cli_errors import format_error
|
|
236
|
-
print(format_error(
|
|
238
|
+
print(format_error(IncompleteInstallationError('rsi-loop')), file=sys.stderr)
|
|
237
239
|
return 1
|
|
238
240
|
args = cli.parse_args(argv)
|
|
239
241
|
if args.group == "help":
|
|
@@ -254,7 +256,7 @@ def _main(argv: list[str] | None = None):
|
|
|
254
256
|
|
|
255
257
|
print(format_error(exc), file=sys.stderr)
|
|
256
258
|
return 1
|
|
257
|
-
if args.group in {'job', '
|
|
259
|
+
if args.group in {'job', 'trial'}:
|
|
258
260
|
from envloop_experiment.cli import validate
|
|
259
261
|
status = validate(args, cli)
|
|
260
262
|
if status is not None:
|
|
@@ -308,6 +310,11 @@ def _main(argv: list[str] | None = None):
|
|
|
308
310
|
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
309
311
|
print(format_error(exc), file=sys.stderr)
|
|
310
312
|
return 1
|
|
313
|
+
from .http import WaitTimeoutError
|
|
314
|
+
if isinstance(exc, WaitTimeoutError) and isinstance(exc.last, dict):
|
|
315
|
+
# The work continues on the server; keep its ID so it can be queried or waited on again.
|
|
316
|
+
last = operation_output(exc.last) if 'operation_id' in exc.last else exc.last
|
|
317
|
+
print(json.dumps(last, ensure_ascii=False))
|
|
311
318
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
312
319
|
print(format_error(exc), file=sys.stderr)
|
|
313
320
|
return 1
|
|
@@ -45,6 +45,13 @@ _register("EDGE_REJECTED", "The network edge (Cloudflare) rejected the request b
|
|
|
45
45
|
_register("HUMAN_SESSION_REQUIRED WORKSPACE_LIST_REQUIRES_SESSION PROJECT_LIST_REQUIRES_SESSION", "This operation requires a browser session authorized by the server.")
|
|
46
46
|
_register("CONFLICT VERSION_CONFLICT RESOURCE_VERSION_CONFLICT PRECONDITION_FAILED IDEMPOTENCY_CONFLICT", "The request conflicts with the current resource version or an earlier request.")
|
|
47
47
|
_register("BAD_REQUEST VALIDATION_ERROR VALIDATION_FAILED INVALID_REQUEST INVALID_ARGUMENT INVALID_CURSOR INVALID_PROFILE INVALID_PROFILE_SECRETS", "The request or input is invalid.")
|
|
48
|
+
_register("PROFILE_SECRET_NAME_INVALID", "A Secret parameter name must look like a credential, as the platform decides: it ends with API_KEY or _TOKEN, or contains PASSWORD, SECRET, CREDENTIAL, AUTHORIZATION, ACCESS_TOKEN, AUTH_TOKEN, BEARER_TOKEN or PRIVATE_KEY. Otherwise list the name in the Profile's secret_params. Names use letters, digits and underscores.")
|
|
49
|
+
_register("PROFILE_SECRET_VALUE_INVALID", "A Secret parameter value is missing or unusable: it must be non-empty UTF-8 text of at most 64 KiB from a prompt, stdin, an environment variable or a file.")
|
|
50
|
+
_register("PROFILE_SECRET_CONFLICT", "Give each Secret parameter once, by --secret only (not also in the Profile file or patch), and read at most one value from stdin.")
|
|
51
|
+
_register("RUNNER_PACKAGE_INVALID", "The Runner package directory is invalid. It needs runner.toml whose [runner] name matches the Runner and has no version, the [execution] entrypoint main.py or main.sh, at most 100 regular files, no symlinks, and path parts of letters, digits, '_', '.' or '-' that do not start with '.'.")
|
|
52
|
+
_register("RUNNER_REVISION_INVALID", "Use the full lowercase 40- or 64-character source commit as --revision and a NAMESPACE/NAME Runner name.")
|
|
53
|
+
_register("RUNNER_SOURCE_REF_INVALID", "Use a branch or tag name such as main or v1 with --source-ref-kind branch, tag or commit; HEAD, latest, default, release, '..', spaces and ~^:?*[\\ are not allowed.")
|
|
54
|
+
_register("RUNNER_REVISION_CONFLICT", "The platform catalog holds different package or index hashes for this Runner revision. Publish a new revision instead of changing an existing one.")
|
|
48
55
|
_register("DATASET_REQUIRED", "Missing Dataset input (--dataset or --source) for this Agent Job.")
|
|
49
56
|
_register("DATASET_CONFIG_REQUIRED", "An explicit Dataset configuration (--config) is required.")
|
|
50
57
|
_register("RUNNER_REQUIRED", "Missing Runner input (--runner) for this Job.")
|
|
@@ -115,7 +122,7 @@ _register("INVALID_RSI_REQUEST INVALID_STEER INVALID_DIRECTIVE", "The RSI servic
|
|
|
115
122
|
_register("RSI_CONFLICT RSI_RUN_NOT_ACTIVE", "The run id exists, the run is not in a state that allows this action, or no loop machine is free.")
|
|
116
123
|
_register("RSI_CONTROL_UNCONFIGURED RSI_CONTROL_UNAVAILABLE RSI_CONTROL_TIMEOUT", "The RSI loop service is not available through this platform right now.")
|
|
117
124
|
|
|
118
|
-
_register("UNSUPPORTED_ADAPTER_SOURCE", 'Register a training checkpoint with {"serving_profile": "NAME", "source": {"type": "train_checkpoint", "
|
|
125
|
+
_register("UNSUPPORTED_ADAPTER_SOURCE", 'Register a training checkpoint with {"serving_profile": "NAME", "source": {"type": "train_checkpoint", "uri": "envloop://..."}}. '
|
|
119
126
|
"List serving profiles with `el job serving-profile list` and checkpoint paths with `el checkpoint list`.")
|
|
120
127
|
_register("JOB_ACTION_UNSUPPORTED", "This Job type does not support the requested action.")
|
|
121
128
|
_register("INFERENCE_ROUTE_RETIRED", "The global inference route has been retired.")
|
|
@@ -142,14 +149,14 @@ for _code, _resource in {
|
|
|
142
149
|
_KNOWN_REMOTE_CODES = set("""
|
|
143
150
|
ADAPTER_FILES_MISSING ADAPTER_FORMAT_UNSUPPORTED ADAPTER_RANK_EXCEEDS_PROFILE ADAPTER_TARGET_UNSUPPORTED
|
|
144
151
|
DATASET_SELECTION_UNSUPPORTED INVALID_ADAPTERS INVALID_ADAPTER_ALIAS INVALID_AMOUNT INVALID_ATTEMPTS
|
|
145
|
-
INVALID_BILLING_WINDOW INVALID_CHECKPOINT_PATH INVALID_CLEANUP_RESOURCES INVALID_COMMAND INVALID_DATASET_DELIVERY
|
|
152
|
+
INVALID_BILLING_WINDOW INVALID_CHECKPOINT_PATH INVALID_CHECKPOINT_URI INVALID_CLEANUP_RESOURCES INVALID_COMMAND INVALID_DATASET_DELIVERY
|
|
146
153
|
INVALID_DATASET_INPUT INVALID_DATASET_SELECTOR INVALID_ENDPOINT_NAME INVALID_ENFORCEMENT INVALID_ENTRY_ID
|
|
147
154
|
INVALID_ENTRY_KIND INVALID_ENVHUB_REQUEST INVALID_GROUP_BY INVALID_JOB INVALID_JOB_ID INVALID_JOB_MEMBER
|
|
148
155
|
INVALID_JOB_MODE INVALID_JOB_SIZE INVALID_JOB_STATUS INVALID_JOB_TYPE INVALID_LIMIT INVALID_LOG_CURSOR
|
|
149
156
|
INVALID_LOG_PROCESS INVALID_LOG_QUERY INVALID_LOG_SOURCE INVALID_LOG_STREAM INVALID_PRIORITY
|
|
150
157
|
INVALID_PROFILE_COMBINATION INVALID_PROJECT INVALID_QUOTE INVALID_REPOSITORY_COMMIT INVALID_REPOSITORY_ROOT
|
|
151
158
|
INVALID_REQUEST_ID INVALID_RESOLVED_JOB INVALID_RUNNER INVALID_RUNNER_ENVIRONMENT INVALID_SANDBOX_RUNNER
|
|
152
|
-
INVALID_SCALING INVALID_SCOPE_TTL INVALID_TAG
|
|
159
|
+
INVALID_SCALING INVALID_SCOPE_TTL INVALID_TAG INVALID_TRAINING_LINEAGE INVALID_TRIALS
|
|
153
160
|
INVALID_TRIAL_STATUS INVALID_WORKSPACE JOB_REQUIRED LEADERBOARD_PROVENANCE_REQUIRED LEADERBOARD_SCORE_REQUIRED
|
|
154
161
|
PROFILE_IDENTITY_MISMATCH QUEUE_NOT_FOUND REPOSITORY_BINDING_REQUIRED RUNNER_GPU_UNAVAILABLE RUNNER_HASH_MISMATCH
|
|
155
162
|
TOO_MANY_ADAPTERS UNSUPPORTED_ADAPTER_SOURCE UNSUPPORTED_CAPABILITY UNSUPPORTED_COMMAND UNSUPPORTED_HARDWARE
|
|
@@ -49,7 +49,7 @@ def next_page(args: argparse.Namespace, cursor: object, program: str) -> str:
|
|
|
49
49
|
if getattr(args, 'no_color', False):
|
|
50
50
|
command.append('--no-color')
|
|
51
51
|
from ._optional import feature_module
|
|
52
|
-
feature = 'data' if args.group in {'data', 'dataset', 'runner', 'checkpoint', 'profile'} else 'experiment' if args.group in {'job', '
|
|
52
|
+
feature = 'data' if args.group in {'data', 'dataset', 'runner', 'checkpoint', 'profile'} else 'experiment' if args.group in {'job', 'trial'} else None
|
|
53
53
|
if feature is not None:
|
|
54
54
|
feature_module(feature, 'cli_output').pagination(command, args, cursor)
|
|
55
55
|
else:
|
|
@@ -62,7 +62,7 @@ def next_page(args: argparse.Namespace, cursor: object, program: str) -> str:
|
|
|
62
62
|
# Empty-list text names the listed resource, not the command group (`job adapter list` lists adapters).
|
|
63
63
|
EMPTY_RESOURCES = {
|
|
64
64
|
('job', 'adapter'): 'adapters', ('job', 'serving-profile'): 'serving profiles',
|
|
65
|
-
('
|
|
65
|
+
('trial',): 'trials', ('trial', 'artifact'): 'artifacts',
|
|
66
66
|
('dataset',): 'datasets', ('dataset', 'revision'): 'dataset revisions', ('dataset', 'task'): 'dataset tasks',
|
|
67
67
|
('data',): 'resources', ('data', 'files'): 'files',
|
|
68
68
|
('billing', 'usage'): 'usage records', ('billing', 'ledger'): 'ledger entries', ('billing', 'price'): 'prices',
|
|
@@ -95,7 +95,7 @@ def list_output(result: Any, args: argparse.Namespace, program: str) -> str:
|
|
|
95
95
|
items = [{'name': key, 'value': value} for key, value in items.items()]
|
|
96
96
|
from ._optional import feature_module
|
|
97
97
|
feature = ('rsi-loop' if args.group == 'job' and args.action == 'run' else
|
|
98
|
-
'experiment' if args.group in {'job', '
|
|
98
|
+
'experiment' if args.group in {'job', 'trial'} else
|
|
99
99
|
'data' if args.group in {'data', 'dataset', 'runner', 'checkpoint', 'profile'} else None)
|
|
100
100
|
if feature is not None:
|
|
101
101
|
headers, render, empty = feature_module(feature, 'cli_output').list_layout(items, args)
|
|
@@ -27,6 +27,14 @@ def validate_public_execution(value: Any) -> None:
|
|
|
27
27
|
validate_public_execution(value.get(key))
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
class WaitTimeoutError(TimeoutError):
|
|
31
|
+
"""A wait ended before the terminal state; ``last`` is the last resource or Operation read."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, message: str, last: dict[str, Any] | None = None):
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.last = last
|
|
36
|
+
|
|
37
|
+
|
|
30
38
|
class RemoteError(RuntimeError):
|
|
31
39
|
"""Platform failure. Server-supplied text lives in bounded attributes, never in str()."""
|
|
32
40
|
|
|
@@ -239,9 +247,9 @@ class HttpService:
|
|
|
239
247
|
self.opener = build_client_opener()
|
|
240
248
|
|
|
241
249
|
def remote_error(self, error: HTTPError, body: Any = None, **options: Any) -> RemoteError:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
fields = error_fields(error, (self.secret, *
|
|
250
|
+
from .profile_secrets import submitted_secrets
|
|
251
|
+
|
|
252
|
+
fields = error_fields(error, (self.secret, *submitted_secrets(body)))
|
|
245
253
|
return RemoteError(fields.pop("code"), error.code, **fields, **options)
|
|
246
254
|
|
|
247
255
|
def request(self, method: str, path: str, body: Any = None, *, headers: dict[str, str] | None = None) -> Any:
|
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
def wait_options(parser: argparse.ArgumentParser):
|
|
9
9
|
parser.add_argument('--wait', action='store_true', help='Wait for the persisted cleanup operation')
|
|
10
|
-
parser.add_argument('--timeout-
|
|
10
|
+
parser.add_argument('--timeout-seconds', type=float, default=600)
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def add_management_commands(settings: "argparse._SubParsersAction[argparse.ArgumentParser]"):
|
|
@@ -16,7 +16,7 @@ def add_management_commands(settings: "argparse._SubParsersAction[argparse.Argum
|
|
|
16
16
|
command = operations.add_parser(action)
|
|
17
17
|
command.add_argument('identifier')
|
|
18
18
|
if action == 'wait':
|
|
19
|
-
command.add_argument('--timeout-
|
|
19
|
+
command.add_argument('--timeout-seconds', type=float, default=600)
|
|
20
20
|
cleanup = settings.add_parser('cleanup', help='Clean an explicit set of project resource IDs')
|
|
21
21
|
cleanup.add_argument('--file', type=Path, required=True, help='JSON array of resource kind and ID objects')
|
|
22
22
|
cleanup.add_argument('--request-id', required=True)
|
|
@@ -38,7 +38,7 @@ def run_management(client: Any, args: argparse.Namespace):
|
|
|
38
38
|
from .resources import _id
|
|
39
39
|
|
|
40
40
|
if args.action == 'operation':
|
|
41
|
-
return (client.operations.wait(args.identifier, timeout=args.
|
|
41
|
+
return (client.operations.wait(args.identifier, timeout=args.timeout_seconds) if args.operation_action == 'wait'
|
|
42
42
|
else getattr(client.operations, args.operation_action)(args.identifier))
|
|
43
43
|
if args.action == 'resource-scope':
|
|
44
44
|
if args.scope_action == 'create':
|
|
@@ -52,4 +52,4 @@ def run_management(client: Any, args: argparse.Namespace):
|
|
|
52
52
|
if args.scope_action == 'retain' else client._transport.request('DELETE' if args.scope_action == 'cleanup' else 'GET', path))
|
|
53
53
|
else:
|
|
54
54
|
result = client.operations.cleanup(json.loads(args.file.read_text()), request_id=args.request_id)
|
|
55
|
-
return client.operations.wait(result['operation_id'], timeout=args.
|
|
55
|
+
return client.operations.wait(result['operation_id'], timeout=args.timeout_seconds) if getattr(args, 'wait', False) else result
|
|
@@ -126,6 +126,30 @@ class Project:
|
|
|
126
126
|
raise ValueError('enabled must be a boolean')
|
|
127
127
|
return self._settings_write('/compose', {'enabled': enabled}, expected_version)
|
|
128
128
|
|
|
129
|
+
def model_access(self):
|
|
130
|
+
"""Model access grants of this project and whether the platform can deliver them now."""
|
|
131
|
+
return self.execution_settings()['model_access']
|
|
132
|
+
|
|
133
|
+
def grant_model_access(self, runner: str, protocols: list[str], *, revision: str | None = None,
|
|
134
|
+
expected_version: int | str | None = None):
|
|
135
|
+
"""Let one published Runner (optionally one full commit Revision) receive the project's metered model
|
|
136
|
+
key for ``protocols`` (``anthropic``, ``openai``). Returns the settings view with the new ``grant``."""
|
|
137
|
+
validate_model_access_grant(runner, protocols, revision)
|
|
138
|
+
return self._settings_send('POST', '/model-access', {'runner': runner, 'revision': revision,
|
|
139
|
+
'protocols': list(protocols)}, expected_version)
|
|
140
|
+
|
|
141
|
+
def revoke_model_access(self, grant_id: str, *, expected_version: int | str | None = None):
|
|
142
|
+
"""Revoke a grant; Trials that have not started no longer receive the key."""
|
|
143
|
+
if not isinstance(grant_id, str) or not grant_id:
|
|
144
|
+
raise ValueError('grant_id is required')
|
|
145
|
+
return self._settings_send('DELETE', '/model-access/' + quote(grant_id, safe=''), None, expected_version)
|
|
146
|
+
|
|
147
|
+
def _settings_send(self, method: str, suffix: str, body: dict[str, Any] | None, expected_version: int | str | None):
|
|
148
|
+
path = self.path() + '/execution-settings'
|
|
149
|
+
if expected_version is None:
|
|
150
|
+
expected_version = self.transport.request('GET', path)['version']
|
|
151
|
+
return self.transport.request(method, path + suffix, body, headers={'If-Match': f'"{expected_version}"'})
|
|
152
|
+
|
|
129
153
|
def set_max_concurrent_trials(self, value: int | None, *, expected_version: int | str | None = None):
|
|
130
154
|
"""Lower the project's concurrent Trial limit (None clears it); the operator ceiling still applies."""
|
|
131
155
|
if value is not None and (type(value) is not int or not 1 <= value <= 10000):
|
|
@@ -133,6 +157,23 @@ class Project:
|
|
|
133
157
|
return self._settings_write('/concurrency', {'max_concurrent_trials': value}, expected_version)
|
|
134
158
|
|
|
135
159
|
|
|
160
|
+
MODEL_ACCESS_PROTOCOLS = ('anthropic', 'openai')
|
|
161
|
+
# Copied from EnvPlatform env_platform/project_settings.py RUNNER / REVISION (keep in sync).
|
|
162
|
+
_RUNNER_NAME = re.compile(r'[A-Za-z0-9][A-Za-z0-9._-]{0,127}/[A-Za-z0-9][A-Za-z0-9._-]{0,127}')
|
|
163
|
+
_REVISION = re.compile(r'[0-9a-f]{40}|[0-9a-f]{64}')
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def validate_model_access_grant(runner: str, protocols: Any, revision: str | None) -> None:
|
|
167
|
+
"""The same shape the platform accepts (EnvPlatform project_settings): checked before any request."""
|
|
168
|
+
if not isinstance(runner, str) or not _RUNNER_NAME.fullmatch(runner):
|
|
169
|
+
raise ValueError('runner must be namespace/name')
|
|
170
|
+
if (not isinstance(protocols, (list, tuple)) or not protocols or len(set(protocols)) != len(protocols)
|
|
171
|
+
or any(protocol not in MODEL_ACCESS_PROTOCOLS for protocol in protocols)):
|
|
172
|
+
raise ValueError('protocols must be anthropic, openai or both')
|
|
173
|
+
if revision is not None and (not isinstance(revision, str) or not _REVISION.fullmatch(revision)):
|
|
174
|
+
raise ValueError('revision must be a full 40- or 64-character lowercase commit')
|
|
175
|
+
|
|
176
|
+
|
|
136
177
|
class Dashboard:
|
|
137
178
|
def __init__(self, project: Any):
|
|
138
179
|
self.project = project
|