envloop-cli 1.23.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.
Files changed (64) hide show
  1. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/.gitignore +4 -0
  2. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/PKG-INFO +24 -18
  3. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/README.pypi.md +22 -13
  4. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/_optional.py +32 -14
  5. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/auth_cli.py +1 -1
  6. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/auth_login.py +2 -2
  7. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/cli.py +14 -7
  8. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/cli_errors.py +19 -4
  9. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/cli_output.py +21 -5
  10. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/client.py +2 -1
  11. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/http.py +18 -4
  12. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/lifecycle_cli.py +4 -4
  13. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/navigation_resources.py +49 -1
  14. envloop_cli-1.27.0/envloop/profile_secrets.py +133 -0
  15. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/resource_cli.py +43 -4
  16. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/resources.py +2 -1
  17. envloop_cli-1.27.0/packages/data/envloop_data/__init__.py +1 -0
  18. envloop_cli-1.27.0/packages/data/envloop_data/cli.py +174 -0
  19. envloop_cli-1.27.0/packages/data/envloop_data/cli_output.py +81 -0
  20. envloop_cli-1.27.0/packages/data/envloop_data/client.py +239 -0
  21. envloop_cli-1.27.0/packages/data/envloop_data/data.py +483 -0
  22. envloop_cli-1.27.0/packages/data/envloop_data/data_cli.py +233 -0
  23. envloop_cli-1.27.0/packages/data/envloop_data/data_publications.py +133 -0
  24. envloop_cli-1.27.0/packages/data/envloop_data/data_upload.py +280 -0
  25. envloop_cli-1.27.0/packages/data/envloop_data/http.py +150 -0
  26. envloop_cli-1.27.0/packages/data/envloop_data/navigation_resources.py +257 -0
  27. envloop_cli-1.27.0/packages/data/envloop_data/profile_updates.py +77 -0
  28. envloop_cli-1.27.0/packages/data/envloop_data/recipes.py +25 -0
  29. envloop_cli-1.27.0/packages/data/envloop_data/resource_cli.py +112 -0
  30. envloop_cli-1.27.0/packages/data/envloop_data/runner_packages.py +71 -0
  31. envloop_cli-1.27.0/packages/experiment/envloop_experiment/__init__.py +1 -0
  32. envloop_cli-1.27.0/packages/experiment/envloop_experiment/cli.py +467 -0
  33. envloop_cli-1.27.0/packages/experiment/envloop_experiment/cli_output.py +50 -0
  34. envloop_cli-1.27.0/packages/experiment/envloop_experiment/client.py +473 -0
  35. envloop_cli-1.27.0/packages/experiment/envloop_experiment/decision.py +20 -0
  36. envloop_cli-1.27.0/packages/experiment/envloop_experiment/http.py +147 -0
  37. envloop_cli-1.27.0/packages/experiment/envloop_experiment/inference.py +36 -0
  38. envloop_cli-1.27.0/packages/experiment/envloop_experiment/job_cost.py +41 -0
  39. envloop_cli-1.27.0/packages/experiment/envloop_experiment/job_logs.py +21 -0
  40. envloop_cli-1.27.0/packages/experiment/envloop_experiment/job_spec_cli.py +146 -0
  41. envloop_cli-1.27.0/packages/experiment/envloop_experiment/navigation_resources.py +43 -0
  42. envloop_cli-1.27.0/packages/experiment/envloop_experiment/resource_cli.py +38 -0
  43. envloop_cli-1.27.0/packages/experiment/envloop_experiment/resources.py +153 -0
  44. envloop_cli-1.27.0/packages/experiment/envloop_experiment/runtime_params.py +83 -0
  45. envloop_cli-1.27.0/packages/experiment/envloop_experiment/sandbox.py +28 -0
  46. envloop_cli-1.27.0/packages/experiment/envloop_experiment/train.py +96 -0
  47. envloop_cli-1.27.0/packages/experiment/envloop_experiment/train_log_cli.py +138 -0
  48. envloop_cli-1.27.0/packages/experiment/envloop_experiment/train_progress.py +52 -0
  49. envloop_cli-1.27.0/packages/experiment/envloop_experiment/train_runner_cli.py +66 -0
  50. envloop_cli-1.27.0/packages/experiment/envloop_experiment/training/__init__.py +4 -0
  51. envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/__init__.py +1 -0
  52. envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/agent_runs.py +254 -0
  53. envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/cli.py +33 -0
  54. envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/cli_output.py +73 -0
  55. envloop_cli-1.27.0/packages/rsi-loop/envloop_rsi_loop/rsi_cli.py +65 -0
  56. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/pyproject.toml +9 -15
  57. envloop_cli-1.23.0/envloop/profile_secrets.py +0 -28
  58. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/__init__.py +0 -0
  59. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/auth_workspace.py +0 -0
  60. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/config.py +0 -0
  61. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/downloads.py +0 -0
  62. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/terminal.py +0 -0
  63. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/utils/__init__.py +0 -0
  64. {envloop_cli-1.23.0 → envloop_cli-1.27.0}/envloop/utils/logging.py +0 -0
@@ -10,3 +10,7 @@ dist/
10
10
  # Local runtime output from offline platform API contract fixtures.
11
11
  .envplatform/
12
12
  .superpowers/
13
+
14
+ # Isolated build and integration-test fixtures
15
+ .ci-deps/
16
+ .local-runs/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: envloop-cli
3
- Version: 1.23.0
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.23.0
12
11
  Provides-Extra: data
13
- Requires-Dist: envloop-cli-data==1.23.0; extra == 'data'
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.23.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
- In this source candidate the bare installation and `default` include experiment: Jobs, Trials, training, logs and inference calls. Data resources require `data`; RSI Loop controls require `rsi-loop`. This source change is not a claim that the modular packages have been published.
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 (source candidate)
68
+ ## Optional features
72
69
 
73
- The current source candidate uses independent feature wheels at version 1.23.0. Matching modular distributions and public ANS core must be released before using this contract from a public index:
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 "envloop-cli[data]" # Dataset/Data/Runner/Checkpoint/Profile/Recipe
77
- python -m pip install envloop-cli # Includes Job/Trial/logs/inference
78
- python -m pip install "envloop-cli[experiment]" # Explicit spelling of the default installation
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
- `envloop-cli[default]` equals the bare installation: base plus `envloop-cli-experiment==1.23.0`, delivered as separate wheels. Only base owns the `el`/`envloop` scripts. Feature resources load on access; absent optional features report installation guidance. The default installation uses standard-library verified TLS and system CA configuration, without EnvHub or certifi. The examples below require their corresponding feature packages and an authorized matching platform.
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 trail list --limit 5
134
- el trail list --type train --job-id JOB_ID
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 the spelling `trail`, for example
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 `--trails` and `--gput-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)`.
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
- In this source candidate the bare installation and `default` include experiment: Jobs, Trials, training, logs and inference calls. Data resources require `data`; RSI Loop controls require `rsi-loop`. This source change is not a claim that the modular packages have been published.
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 (source candidate)
51
+ ## Optional features
52
52
 
53
- The current source candidate uses independent feature wheels at version 1.23.0. Matching modular distributions and public ANS core must be released before using this contract from a public index:
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 "envloop-cli[data]" # Dataset/Data/Runner/Checkpoint/Profile/Recipe
57
- python -m pip install envloop-cli # Includes Job/Trial/logs/inference
58
- python -m pip install "envloop-cli[experiment]" # Explicit spelling of the default installation
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
- `envloop-cli[default]` equals the bare installation: base plus `envloop-cli-experiment==1.23.0`, delivered as separate wheels. Only base owns the `el`/`envloop` scripts. Feature resources load on access; absent optional features report installation guidance. The default installation uses standard-library verified TLS and system CA configuration, without EnvHub or certifi. The examples below require their corresponding feature packages and an authorized matching platform.
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 trail list --limit 5
114
- el trail list --type train --job-id JOB_ID
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 the spelling `trail`, for example
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 `--trails` and `--gput-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)`.
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 independently installed client features.
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
- 'trail': 'Inspect and control existing trials',
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 ('TaskService', 'RpcService', 'load_task', 'validate_command', 'dataset_selector', 'Tasks', 'runner_selector', 'job_budget', 'Jobs')},
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__(f'Install optional functionality with: pip install "envloop-cli[{feature}]"')
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 MissingFeatureError(feature)
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.train)
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.tasks
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.train)
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', 'trail'}:
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.timeout
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.timeout))
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', 'trail'),
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 worker trials with EnvLoop.\nConfigure your connection with 'auth config'.",
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 MissingFeatureError, available
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', 'trail': 'experiment', 'data': 'data', 'dataset': 'data',
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(MissingFeatureError(feature)), file=sys.stderr)
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(MissingFeatureError('rsi-loop')), file=sys.stderr)
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', 'trail'}:
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
@@ -17,7 +17,7 @@ from collections.abc import Iterable
17
17
  from datetime import datetime
18
18
  from typing import Any, cast
19
19
 
20
- from .http import RemoteError
20
+ from .http import GATEWAY_TIMEOUT_STATUSES, RemoteError
21
21
 
22
22
  _MESSAGES: dict[str, str] = {}
23
23
 
@@ -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.")
@@ -54,6 +61,7 @@ _register("RATE_LIMITED RATE_LIMIT_EXCEEDED TOO_MANY_REQUESTS", "The API rate li
54
61
  _register("SERVICE_UNAVAILABLE INTERNAL_SERVER_ERROR EXECUTOR_UNAVAILABLE PROVIDER_UNAVAILABLE LOG_STORAGE_UNAVAILABLE", "The service is unavailable or encountered an internal error.")
55
62
  _register("INTERNAL_ERROR", "The command encountered an unexpected error.")
56
63
  _register("NETWORK_ERROR", "A connection to the API or download service could not be completed.")
64
+ _register("SOURCE_UNAVAILABLE", "The API gateway did not get a valid answer from the service, for example because it stopped waiting. A write may still have been processed: read the Job or resource before retrying it.")
57
65
  _register("REQUEST_TIMEOUT", "The API did not answer before the client timeout. The request may still have been processed: read the Job or resource before retrying a write.")
58
66
  _register("RESPONSE_INTERRUPTED", "The connection closed before the API finished answering; the service may have failed or restarted. The request may have been processed: read the Job or resource before retrying a write, and report the time if it repeats.")
59
67
  _register("INVALID_RESPONSE RESPONSE_TOO_LARGE INVALID_LOG_PAGE INVALID_PAGINATION TRAIN_JOB_CURSOR_REPEATED", "The service returned an invalid or unsupported response.")
@@ -114,6 +122,8 @@ _register("INVALID_RSI_REQUEST INVALID_STEER INVALID_DIRECTIVE", "The RSI servic
114
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.")
115
123
  _register("RSI_CONTROL_UNCONFIGURED RSI_CONTROL_UNAVAILABLE RSI_CONTROL_TIMEOUT", "The RSI loop service is not available through this platform right now.")
116
124
 
125
+ _register("UNSUPPORTED_ADAPTER_SOURCE", 'Register a training checkpoint with {"serving_profile": "NAME", "source": {"type": "train_checkpoint", "uri": "envloop://..."}}. '
126
+ "List serving profiles with `el job serving-profile list` and checkpoint paths with `el checkpoint list`.")
117
127
  _register("JOB_ACTION_UNSUPPORTED", "This Job type does not support the requested action.")
118
128
  _register("INFERENCE_ROUTE_RETIRED", "The global inference route has been retired.")
119
129
 
@@ -139,14 +149,14 @@ for _code, _resource in {
139
149
  _KNOWN_REMOTE_CODES = set("""
140
150
  ADAPTER_FILES_MISSING ADAPTER_FORMAT_UNSUPPORTED ADAPTER_RANK_EXCEEDS_PROFILE ADAPTER_TARGET_UNSUPPORTED
141
151
  DATASET_SELECTION_UNSUPPORTED INVALID_ADAPTERS INVALID_ADAPTER_ALIAS INVALID_AMOUNT INVALID_ATTEMPTS
142
- 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
143
153
  INVALID_DATASET_INPUT INVALID_DATASET_SELECTOR INVALID_ENDPOINT_NAME INVALID_ENFORCEMENT INVALID_ENTRY_ID
144
154
  INVALID_ENTRY_KIND INVALID_ENVHUB_REQUEST INVALID_GROUP_BY INVALID_JOB INVALID_JOB_ID INVALID_JOB_MEMBER
145
155
  INVALID_JOB_MODE INVALID_JOB_SIZE INVALID_JOB_STATUS INVALID_JOB_TYPE INVALID_LIMIT INVALID_LOG_CURSOR
146
156
  INVALID_LOG_PROCESS INVALID_LOG_QUERY INVALID_LOG_SOURCE INVALID_LOG_STREAM INVALID_PRIORITY
147
157
  INVALID_PROFILE_COMBINATION INVALID_PROJECT INVALID_QUOTE INVALID_REPOSITORY_COMMIT INVALID_REPOSITORY_ROOT
148
158
  INVALID_REQUEST_ID INVALID_RESOLVED_JOB INVALID_RUNNER INVALID_RUNNER_ENVIRONMENT INVALID_SANDBOX_RUNNER
149
- INVALID_SCALING INVALID_SCOPE_TTL INVALID_TAG INVALID_TINKER_PATH INVALID_TRAINING_LINEAGE INVALID_TRIALS
159
+ INVALID_SCALING INVALID_SCOPE_TTL INVALID_TAG INVALID_TRAINING_LINEAGE INVALID_TRIALS
150
160
  INVALID_TRIAL_STATUS INVALID_WORKSPACE JOB_REQUIRED LEADERBOARD_PROVENANCE_REQUIRED LEADERBOARD_SCORE_REQUIRED
151
161
  PROFILE_IDENTITY_MISMATCH QUEUE_NOT_FOUND REPOSITORY_BINDING_REQUIRED RUNNER_GPU_UNAVAILABLE RUNNER_HASH_MISMATCH
152
162
  TOO_MANY_ADAPTERS UNSUPPORTED_ADAPTER_SOURCE UNSUPPORTED_CAPABILITY UNSUPPORTED_COMMAND UNSUPPORTED_HARDWARE
@@ -209,6 +219,9 @@ def _http_message(status: int | None) -> str:
209
219
  return _MESSAGES["INVALID_REQUEST"]
210
220
  if status == 429:
211
221
  return _MESSAGES["RATE_LIMITED"]
222
+ if status in GATEWAY_TIMEOUT_STATUSES:
223
+ return ("The API gateway stopped waiting for the service. The request may still have been processed: "
224
+ "read the Job or resource before retrying a write.")
212
225
  if status is not None and status >= 500:
213
226
  return _MESSAGES["SERVICE_UNAVAILABLE"]
214
227
  return _MESSAGES["HTTP_ERROR"]
@@ -294,7 +307,9 @@ def format_error(error: BaseException) -> str:
294
307
  extra: dict[str, Any] = {"hint": remote.hint, "details": remote.details, "request_id": remote.request_id} if remote else {}
295
308
  if remote and remote.message:
296
309
  # Local text only adds guidance for codes the CLI documents.
297
- guidance = local if code in _MESSAGES and code != "HTTP_ERROR" and local != remote.message else None
310
+ # A gateway timeout always adds that the request may have been processed.
311
+ documented = (code in _MESSAGES and code != "HTTP_ERROR") or status in GATEWAY_TIMEOUT_STATUSES
312
+ guidance = local if documented and local != remote.message else None
298
313
  rendered = error_block(code, remote.message, status=status, guidance=guidance, **extra)
299
314
  else:
300
315
  rendered = error_block(code, local, status=status, **extra)
@@ -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', 'trail'} else None
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:
@@ -59,15 +59,31 @@ def next_page(args: argparse.Namespace, cursor: object, program: str) -> str:
59
59
  return "Next page: " + shlex.join(command)
60
60
 
61
61
 
62
+ # Empty-list text names the listed resource, not the command group (`job adapter list` lists adapters).
63
+ EMPTY_RESOURCES = {
64
+ ('job', 'adapter'): 'adapters', ('job', 'serving-profile'): 'serving profiles',
65
+ ('trial',): 'trials', ('trial', 'artifact'): 'artifacts',
66
+ ('dataset',): 'datasets', ('dataset', 'revision'): 'dataset revisions', ('dataset', 'task'): 'dataset tasks',
67
+ ('data',): 'resources', ('data', 'files'): 'files',
68
+ ('billing', 'usage'): 'usage records', ('billing', 'ledger'): 'ledger entries', ('billing', 'price'): 'prices',
69
+ ('member',): 'members', ('member', 'candidate'): 'member candidates',
70
+ ('setting', 'registry'): 'registry credentials',
71
+ }
72
+
73
+
74
+ def empty_message(args: argparse.Namespace) -> str:
75
+ path = getattr(args, '_command_path', [args.group, 'list'])
76
+ resource = EMPTY_RESOURCES.get(tuple(path[:-1] if path[-1:] == ['list'] else path))
77
+ return f'No {resource or args.group} found.'
78
+
79
+
62
80
  def generic_layout(items: list[dict[str, Any]], args: argparse.Namespace, layouts: dict[str, tuple[str, ...]]) -> tuple[tuple[str, ...], Callable[[dict[str, Any]], tuple[object, ...]], str]:
63
81
  fields = tuple(field for field in layouts.get(args.group, ()) if any(field in item for item in items))
64
82
  fields = fields or tuple(dict.fromkeys(key for item in items for key in item))[:6]
65
83
  headers = tuple(field.replace('_', ' ').upper() for field in fields)
66
84
  render: Callable[[dict[str, Any]], tuple[object, ...]] = lambda item: tuple(timestamp(item.get(field)) if field.endswith('_at') or field == 'at'
67
85
  else item.get(field) for field in fields)
68
- empty = ('No registry credentials found.' if args.group == 'setting'
69
- else f'No {getattr(args, "resource", args.group)} found.')
70
- return headers, render, empty
86
+ return headers, render, empty_message(args)
71
87
 
72
88
 
73
89
  def list_output(result: Any, args: argparse.Namespace, program: str) -> str:
@@ -79,7 +95,7 @@ def list_output(result: Any, args: argparse.Namespace, program: str) -> str:
79
95
  items = [{'name': key, 'value': value} for key, value in items.items()]
80
96
  from ._optional import feature_module
81
97
  feature = ('rsi-loop' if args.group == 'job' and args.action == 'run' else
82
- 'experiment' if args.group in {'job', 'trail'} else
98
+ 'experiment' if args.group in {'job', 'trial'} else
83
99
  'data' if args.group in {'data', 'dataset', 'runner', 'checkpoint', 'profile'} else None)
84
100
  if feature is not None:
85
101
  headers, render, empty = feature_module(feature, 'cli_output').list_layout(items, args)
@@ -33,9 +33,10 @@ class Client:
33
33
  raise RuntimeError(INTERACTIVE_TRAINING_REMOVED)
34
34
 
35
35
  @classmethod
36
- def remote(cls, api_url: str | None = None, *, timeout: float = 30, api_key: str | None = None, workspace_id: str | None = None, project_id: str | None = None, config: bool = True):
36
+ def remote(cls, api_url: str | None = None, *, timeout: float = 90, api_key: str | None = None, workspace_id: str | None = None, project_id: str | None = None, config: bool = True):
37
37
  from .http import HttpService
38
38
 
39
+ # The default matches http.DEFAULT_TIMEOUT_SEC (longer than the hosted proxy's 60 s wait).
39
40
  return cls(HttpService(api_url, timeout, api_key=api_key, workspace_id=workspace_id, project_id=project_id, config=config))
40
41
 
41
42
  @classmethod
@@ -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
 
@@ -63,6 +71,12 @@ def retry_after_seconds(value: str | None) -> float | None:
63
71
  return None
64
72
 
65
73
 
74
+ # The hosted API proxy waits up to 60 s for the platform. Wait longer by default so a slow answer, or the
75
+ # proxy's own gateway timeout, reaches the caller instead of a client-side timeout. Explicit values are kept.
76
+ DEFAULT_TIMEOUT_SEC = 90.0
77
+ # The proxy or edge stopped waiting while the platform was still working: the request may have been processed.
78
+ GATEWAY_TIMEOUT_STATUSES = frozenset({504, 524})
79
+
66
80
  # Transport failures that are not an HTTP answer. A request that was sent and then timed out or lost its
67
81
  # response may still have been processed, unlike one that never connected.
68
82
  TRANSIENT_CODES = frozenset({"NETWORK_ERROR", "REQUEST_TIMEOUT", "RESPONSE_INTERRUPTED"})
@@ -213,7 +227,7 @@ def encode_json(body: Any) -> bytes:
213
227
 
214
228
 
215
229
  class HttpService:
216
- def __init__(self, base_url: str | None = None, timeout: float = 30, *, api_key: str | None = None, workspace_id: str | None = None, project_id: str | None = None, config: bool = True, environment: bool = True):
230
+ def __init__(self, base_url: str | None = None, timeout: float = DEFAULT_TIMEOUT_SEC, *, api_key: str | None = None, workspace_id: str | None = None, project_id: str | None = None, config: bool = True, environment: bool = True):
217
231
  from .config import require_remote, resolve_connection
218
232
 
219
233
  connection = resolve_connection(base_url, api_key, workspace_id, project_id, config=config, environment=environment)
@@ -233,9 +247,9 @@ class HttpService:
233
247
  self.opener = build_client_opener()
234
248
 
235
249
  def remote_error(self, error: HTTPError, body: Any = None, **options: Any) -> RemoteError:
236
- submitted = body.get("secrets") if isinstance(body, dict) else None
237
- submitted = [value for value in submitted.values() if isinstance(value, str)] if isinstance(submitted, dict) else []
238
- fields = error_fields(error, (self.secret, *submitted))
250
+ from .profile_secrets import submitted_secrets
251
+
252
+ fields = error_fields(error, (self.secret, *submitted_secrets(body)))
239
253
  return RemoteError(fields.pop("code"), error.code, **fields, **options)
240
254
 
241
255
  def request(self, method: str, path: str, body: Any = None, *, headers: dict[str, str] | None = None) -> Any: