envloop-cli 1.14.0__tar.gz → 1.23.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 (45) hide show
  1. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/.gitignore +1 -0
  2. envloop_cli-1.14.0/README.pypi.md → envloop_cli-1.23.0/PKG-INFO +97 -8
  3. envloop_cli-1.14.0/PKG-INFO → envloop_cli-1.23.0/README.pypi.md +77 -22
  4. envloop_cli-1.23.0/envloop/__init__.py +13 -0
  5. envloop_cli-1.23.0/envloop/_optional.py +185 -0
  6. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/auth_cli.py +19 -17
  7. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/auth_login.py +15 -9
  8. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/auth_workspace.py +17 -15
  9. envloop_cli-1.23.0/envloop/cli.py +322 -0
  10. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/cli_errors.py +99 -20
  11. envloop_cli-1.23.0/envloop/cli_output.py +105 -0
  12. envloop_cli-1.23.0/envloop/client.py +86 -0
  13. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/config.py +74 -40
  14. envloop_cli-1.23.0/envloop/downloads.py +71 -0
  15. envloop_cli-1.23.0/envloop/http.py +355 -0
  16. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/lifecycle_cli.py +5 -3
  17. envloop_cli-1.23.0/envloop/navigation_resources.py +141 -0
  18. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/profile_secrets.py +2 -1
  19. envloop_cli-1.23.0/envloop/resource_cli.py +180 -0
  20. envloop_cli-1.23.0/envloop/resources.py +71 -0
  21. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/terminal.py +6 -6
  22. envloop_cli-1.23.0/pyproject.toml +82 -0
  23. envloop_cli-1.14.0/envloop/__init__.py +0 -5
  24. envloop_cli-1.14.0/envloop/agent_runs.py +0 -248
  25. envloop_cli-1.14.0/envloop/cli.py +0 -667
  26. envloop_cli-1.14.0/envloop/cli_output.py +0 -238
  27. envloop_cli-1.14.0/envloop/client.py +0 -710
  28. envloop_cli-1.14.0/envloop/http.py +0 -388
  29. envloop_cli-1.14.0/envloop/inference.py +0 -35
  30. envloop_cli-1.14.0/envloop/job_logs.py +0 -20
  31. envloop_cli-1.14.0/envloop/job_spec_cli.py +0 -138
  32. envloop_cli-1.14.0/envloop/navigation_resources.py +0 -348
  33. envloop_cli-1.14.0/envloop/profile_updates.py +0 -76
  34. envloop_cli-1.14.0/envloop/resource_cli.py +0 -166
  35. envloop_cli-1.14.0/envloop/resources.py +0 -208
  36. envloop_cli-1.14.0/envloop/rsi_cli.py +0 -60
  37. envloop_cli-1.14.0/envloop/sandbox.py +0 -27
  38. envloop_cli-1.14.0/envloop/train.py +0 -96
  39. envloop_cli-1.14.0/envloop/train_log_cli.py +0 -128
  40. envloop_cli-1.14.0/envloop/train_progress.py +0 -51
  41. envloop_cli-1.14.0/envloop/train_runner_cli.py +0 -64
  42. envloop_cli-1.14.0/envloop/training/__init__.py +0 -4
  43. envloop_cli-1.14.0/pyproject.toml +0 -56
  44. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/utils/__init__.py +0 -0
  45. {envloop_cli-1.14.0 → envloop_cli-1.23.0}/envloop/utils/logging.py +0 -0
@@ -9,3 +9,4 @@ dist/
9
9
 
10
10
  # Local runtime output from offline platform API contract fixtures.
11
11
  .envplatform/
12
+ .superpowers/
@@ -1,3 +1,23 @@
1
+ Metadata-Version: 2.5
2
+ Name: envloop-cli
3
+ Version: 1.23.0
4
+ Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
5
+ Project-URL: Homepage, https://envloop.ai
6
+ Project-URL: Documentation, https://docs.envloop.ai
7
+ Project-URL: Repository, https://github.com/EnvLoop/envloop-cli
8
+ Project-URL: Issues, https://github.com/EnvLoop/envloop-cli/issues
9
+ Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: envloop-cli-experiment==1.23.0
12
+ Provides-Extra: data
13
+ Requires-Dist: envloop-cli-data==1.23.0; extra == 'data'
14
+ Provides-Extra: default
15
+ Provides-Extra: experiment
16
+ Requires-Dist: envloop-cli-experiment==1.23.0; extra == 'experiment'
17
+ Provides-Extra: rsi-loop
18
+ Requires-Dist: envloop-cli-rsi-loop==1.23.0; extra == 'rsi-loop'
19
+ Description-Content-Type: text/markdown
20
+
1
21
  # envloop-cli: EnvLoop CLI and Python SDK
2
22
 
3
23
  Install the public client to run agent Jobs, inspect Trials, manage training and
@@ -17,14 +37,25 @@ installation alone does not establish those prerequisites. See the
17
37
 
18
38
  ## Install
19
39
 
20
- Use a Python virtual environment:
40
+ envloop-cli requires Python 3.12 or later; an existing compatible interpreter can
41
+ be reused. The following [uv](https://docs.astral.sh/uv/) example uses Python 3.12:
21
42
 
22
43
  ```bash
23
- python -m pip install --upgrade envloop-cli
44
+ uv tool install --python 3.12 envloop-cli
45
+ el --version # upgrade later with: uv tool upgrade --python 3.12 envloop-cli
24
46
  el --help
25
47
  ```
26
48
 
27
- The standard installation includes the client dependencies for managed training.
49
+ To use the Python SDK, install the package into a Python 3.12+ virtual environment:
50
+
51
+ ```bash
52
+ uv venv --python 3.12
53
+ uv pip install --upgrade envloop-cli
54
+ # or, with an existing Python 3.12 interpreter:
55
+ python3.12 -m pip install --upgrade envloop-cli
56
+ ```
57
+
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.
28
59
  Check that the SDK can be imported:
29
60
 
30
61
  ```bash
@@ -37,6 +68,19 @@ follow the [managed training guide](https://docs.envloop.ai/training/cli-hello-w
37
68
 
38
69
  Install the client in your Python environment; the platform provides the execution resources.
39
70
 
71
+ ## Optional features (source candidate)
72
+
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:
74
+
75
+ ```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
80
+ ```
81
+
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.
83
+
40
84
  ## Connect
41
85
 
42
86
  Use an authorized EnvLoop account and project. Browser authorization:
@@ -112,7 +156,7 @@ cleaning up temporary resources.
112
156
 
113
157
  ## Sandbox jobs
114
158
 
115
- With a matching platform and a published `envhub/base` ANS Runner, submit a
159
+ With a matching platform and a published `envloop/base` ANS Runner, submit a
116
160
  Sandbox Job without a Dataset:
117
161
 
118
162
  ```bash
@@ -122,7 +166,7 @@ el job list --type sandbox
122
166
  el job log JOB_ID --follow
123
167
  ```
124
168
 
125
- The platform resolves `base` to the registered `envhub/base@main` source ref and
169
+ The platform resolves `base` to the registered `envloop/base@main` source ref and
126
170
  freezes its immutable Revision. The Sandbox image defaults to `python:3.11`;
127
171
  `--image` accepts an image name, tag or digest. The execution provider is configured
128
172
  by the server. Job logs retain the actual captured output and collection metadata.
@@ -178,8 +222,8 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
178
222
  {
179
223
  "request_id": "gpu-run-001",
180
224
  "trials": [
181
- {"name": "first", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
182
- {"name": "second", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
225
+ {"name": "first", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
226
+ {"name": "second", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
183
227
  ]
184
228
  }
185
229
  ```
@@ -191,7 +235,7 @@ el job log JOB_ID --follow
191
235
  el job result JOB_ID
192
236
  ```
193
237
 
194
- 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 [training prices](https://docs.envloop.ai/pricing) first. Reduce the Trial or GPU count when your program supports a smaller first run.
238
+ 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.
195
239
 
196
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)`.
197
241
 
@@ -203,3 +247,48 @@ Use `el --no-color job list` or append `--no-color` to a command to disable high
203
247
  `NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
204
248
  List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
205
249
  `el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
250
+
251
+ ## Job-bound decision plugins
252
+
253
+ When the platform and published Runner support Job Profiles, include the authorized
254
+ `profile` snapshot in the Train or Inference request's `spec`. The platform freezes
255
+ its inputs, recipe and approved plugin pins. This does not select an executor or provider.
256
+
257
+ For an Inference Job with a bound decision plugin, save the plugin payload as JSON:
258
+
259
+ ```bash
260
+ el job decision JOB_ID --model WORKSPACE/MODEL --spec payload.json
261
+ ```
262
+
263
+ ```python
264
+ with Client.remote() as client:
265
+ result = client.decision.evaluate(
266
+ "JOB_ID", model="WORKSPACE/MODEL", payload=payload
267
+ )
268
+ ```
269
+
270
+ Use the exact model returned by the Job. The SDK forwards the payload to the
271
+ Job's `/Inference/decision` API; the approved plugin validates its business fields.
272
+ Availability requires matching platform support and approved content in your project.
273
+
274
+ ## ANS data and verified downloads
275
+
276
+ `client.data.download_directory(uri, destination)` and `el data download URI --output ./content`
277
+ download a complete registered ANS object directory through EnvHub. Datasets require
278
+ `split="lite"` / `--split lite`; the Dataset version and filenames are preserved. After API
279
+ authorization the SDK uses GET URLs valid for up to five minutes, verifies file size/SHA-256,
280
+ and installs the directory atomically. Matching platform support is required.
281
+
282
+ A compatible server and an explicitly granted project Key with `envloop:data:read` are required; writes additionally need `envloop:data:write`. Default and older Keys do not gain these permissions. The current login selects the base URL, workspace and project; there is no `--env` flag. After configuring the Key using `el auth config`:
283
+
284
+ ```bash
285
+ el data list --type dataset --json
286
+ el data get DATA_ID --json
287
+ el data download DATA_ID --file FILE_ID --output ./dataset.tgz --resume --json
288
+ ```
289
+
290
+ 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
+
292
+ 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
+
294
+ 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,17 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: envloop-cli
3
- Version: 1.14.0
4
- Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
5
- Project-URL: Homepage, https://envloop.ai
6
- Project-URL: Documentation, https://docs.envloop.ai
7
- Project-URL: Repository, https://github.com/EnvLoop/envloop-cli
8
- Project-URL: Issues, https://github.com/EnvLoop/envloop-cli/issues
9
- Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
10
- Requires-Python: >=3.11
11
- Requires-Dist: certifi>=2026.10.19
12
- Requires-Dist: tinker==0.34.0
13
- Description-Content-Type: text/markdown
14
-
15
1
  # envloop-cli: EnvLoop CLI and Python SDK
16
2
 
17
3
  Install the public client to run agent Jobs, inspect Trials, manage training and
@@ -31,14 +17,25 @@ installation alone does not establish those prerequisites. See the
31
17
 
32
18
  ## Install
33
19
 
34
- Use a Python virtual environment:
20
+ envloop-cli requires Python 3.12 or later; an existing compatible interpreter can
21
+ be reused. The following [uv](https://docs.astral.sh/uv/) example uses Python 3.12:
35
22
 
36
23
  ```bash
37
- python -m pip install --upgrade envloop-cli
24
+ uv tool install --python 3.12 envloop-cli
25
+ el --version # upgrade later with: uv tool upgrade --python 3.12 envloop-cli
38
26
  el --help
39
27
  ```
40
28
 
41
- The standard installation includes the client dependencies for managed training.
29
+ To use the Python SDK, install the package into a Python 3.12+ virtual environment:
30
+
31
+ ```bash
32
+ uv venv --python 3.12
33
+ uv pip install --upgrade envloop-cli
34
+ # or, with an existing Python 3.12 interpreter:
35
+ python3.12 -m pip install --upgrade envloop-cli
36
+ ```
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.
42
39
  Check that the SDK can be imported:
43
40
 
44
41
  ```bash
@@ -51,6 +48,19 @@ follow the [managed training guide](https://docs.envloop.ai/training/cli-hello-w
51
48
 
52
49
  Install the client in your Python environment; the platform provides the execution resources.
53
50
 
51
+ ## Optional features (source candidate)
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:
54
+
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
60
+ ```
61
+
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.
63
+
54
64
  ## Connect
55
65
 
56
66
  Use an authorized EnvLoop account and project. Browser authorization:
@@ -126,7 +136,7 @@ cleaning up temporary resources.
126
136
 
127
137
  ## Sandbox jobs
128
138
 
129
- With a matching platform and a published `envhub/base` ANS Runner, submit a
139
+ With a matching platform and a published `envloop/base` ANS Runner, submit a
130
140
  Sandbox Job without a Dataset:
131
141
 
132
142
  ```bash
@@ -136,7 +146,7 @@ el job list --type sandbox
136
146
  el job log JOB_ID --follow
137
147
  ```
138
148
 
139
- The platform resolves `base` to the registered `envhub/base@main` source ref and
149
+ The platform resolves `base` to the registered `envloop/base@main` source ref and
140
150
  freezes its immutable Revision. The Sandbox image defaults to `python:3.11`;
141
151
  `--image` accepts an image name, tag or digest. The execution provider is configured
142
152
  by the server. Job logs retain the actual captured output and collection metadata.
@@ -192,8 +202,8 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
192
202
  {
193
203
  "request_id": "gpu-run-001",
194
204
  "trials": [
195
- {"name": "first", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
196
- {"name": "second", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
205
+ {"name": "first", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
206
+ {"name": "second", "spec": {"runner": "envloop/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
197
207
  ]
198
208
  }
199
209
  ```
@@ -205,7 +215,7 @@ el job log JOB_ID --follow
205
215
  el job result JOB_ID
206
216
  ```
207
217
 
208
- 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 [training prices](https://docs.envloop.ai/pricing) first. Reduce the Trial or GPU count when your program supports a smaller first run.
218
+ 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.
209
219
 
210
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)`.
211
221
 
@@ -217,3 +227,48 @@ Use `el --no-color job list` or append `--no-color` to a command to disable high
217
227
  `NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
218
228
  List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
219
229
  `el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
230
+
231
+ ## Job-bound decision plugins
232
+
233
+ When the platform and published Runner support Job Profiles, include the authorized
234
+ `profile` snapshot in the Train or Inference request's `spec`. The platform freezes
235
+ its inputs, recipe and approved plugin pins. This does not select an executor or provider.
236
+
237
+ For an Inference Job with a bound decision plugin, save the plugin payload as JSON:
238
+
239
+ ```bash
240
+ el job decision JOB_ID --model WORKSPACE/MODEL --spec payload.json
241
+ ```
242
+
243
+ ```python
244
+ with Client.remote() as client:
245
+ result = client.decision.evaluate(
246
+ "JOB_ID", model="WORKSPACE/MODEL", payload=payload
247
+ )
248
+ ```
249
+
250
+ Use the exact model returned by the Job. The SDK forwards the payload to the
251
+ Job's `/Inference/decision` API; the approved plugin validates its business fields.
252
+ Availability requires matching platform support and approved content in your project.
253
+
254
+ ## ANS data and verified downloads
255
+
256
+ `client.data.download_directory(uri, destination)` and `el data download URI --output ./content`
257
+ download a complete registered ANS object directory through EnvHub. Datasets require
258
+ `split="lite"` / `--split lite`; the Dataset version and filenames are preserved. After API
259
+ authorization the SDK uses GET URLs valid for up to five minutes, verifies file size/SHA-256,
260
+ and installs the directory atomically. Matching platform support is required.
261
+
262
+ A compatible server and an explicitly granted project Key with `envloop:data:read` are required; writes additionally need `envloop:data:write`. Default and older Keys do not gain these permissions. The current login selects the base URL, workspace and project; there is no `--env` flag. After configuring the Key using `el auth config`:
263
+
264
+ ```bash
265
+ el data list --type dataset --json
266
+ el data get DATA_ID --json
267
+ el data download DATA_ID --file FILE_ID --output ./dataset.tgz --resume --json
268
+ ```
269
+
270
+ 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
+
272
+ 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
+
274
+ 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).
@@ -0,0 +1,13 @@
1
+ """EnvLoop Python client, with optional functionality loaded on demand."""
2
+ from ._optional import install_legacy_imports
3
+ from .client import Client
4
+
5
+ install_legacy_imports()
6
+ __all__ = ['Client', 'DecisionClient']
7
+
8
+
9
+ def __getattr__(name: str):
10
+ if name == 'DecisionClient':
11
+ from ._optional import feature_attribute
12
+ return feature_attribute('experiment', 'decision', name)
13
+ raise AttributeError(name)
@@ -0,0 +1,185 @@
1
+ """Lazy, static adapters for independently installed client features.
2
+
3
+ No feature code is imported when the base client is imported or constructed.
4
+ Legacy module names resolve to their single feature owner on explicit import.
5
+ """
6
+ import argparse
7
+ import importlib
8
+ import importlib.abc
9
+ import importlib.util
10
+ import sys
11
+ from collections.abc import Sequence
12
+ from importlib.machinery import ModuleSpec
13
+ from types import ModuleType
14
+ from typing import Any, override
15
+
16
+ FEATURES = {'data': 'envloop_data', 'experiment': 'envloop_experiment', 'rsi-loop': 'envloop_rsi_loop'}
17
+ FEATURE_COMMANDS = {
18
+ 'data': {
19
+ 'dataset': 'Discover datasets, revisions and tasks',
20
+ 'data': 'Manage versioned Data resources and verified file downloads',
21
+ 'runner': 'Discover available Runner revisions',
22
+ 'checkpoint': 'List and download saved training checkpoints',
23
+ 'profile': 'Manage platform runtime profiles',
24
+ },
25
+ 'experiment': {
26
+ 'job': 'Submit and manage Agent, Sandbox, Train and Inference Jobs',
27
+ 'trail': 'Inspect and control existing trials',
28
+ },
29
+ }
30
+
31
+ MODULES: dict[str, str] = {
32
+ **dict.fromkeys('data data_publications data_upload data_cli profile_updates recipes'.split(), 'data'),
33
+ **dict.fromkeys('train train_progress train_runner_cli train_log_cli job_logs job_cost inference decision sandbox runtime_params job_spec_cli training'.split(), 'experiment'),
34
+ **dict.fromkeys('agent_runs rsi_cli'.split(), 'rsi-loop'),
35
+ }
36
+ CLIENTS = {
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')},
39
+ **{name: ('base', 'downloads') for name in ('ArtifactError', 'artifact_path')},
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())
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
+
44
+
45
+ class MissingFeatureError(ImportError):
46
+ """Static installation instructions, without reflecting user input."""
47
+ code = 'FEATURE_NOT_INSTALLED'
48
+
49
+ def __init__(self, feature: str):
50
+ self.feature = feature
51
+ super().__init__(f'Install optional functionality with: pip install "envloop-cli[{feature}]"')
52
+
53
+
54
+ class CommandExit(Exception):
55
+ def __init__(self, status: int):
56
+ self.status = status
57
+
58
+
59
+ def available(feature: str):
60
+ return importlib.util.find_spec(FEATURES[feature]) is not None
61
+
62
+
63
+ def feature_module(feature: str, module: str):
64
+ if not available(feature):
65
+ raise MissingFeatureError(feature)
66
+ return importlib.import_module(f'{FEATURES[feature]}.{module}')
67
+
68
+
69
+ def feature_attribute(feature: str, module: str, name: str):
70
+ if name.startswith('__'):
71
+ raise AttributeError(name)
72
+ return getattr(feature_module(feature, module), name)
73
+
74
+
75
+ def client_attribute(name: str):
76
+ if name not in CLIENTS:
77
+ raise AttributeError(name)
78
+ feature, module = CLIENTS[name]
79
+ if feature == 'base':
80
+ return getattr(importlib.import_module(f'envloop.{module}'), name)
81
+ return feature_attribute(feature, module, name)
82
+
83
+
84
+ def endpoint(name: str):
85
+ if name in DATA_ENDPOINTS:
86
+ feature = 'data'
87
+ elif name in EXPERIMENT_ENDPOINTS:
88
+ feature = 'experiment'
89
+ else:
90
+ raise AttributeError(name)
91
+ # Inspect the class dictionary: static methods retain their descriptor contract.
92
+ return feature_module(feature, 'http').Endpoints.__dict__[name]
93
+
94
+
95
+ def navigation(name: str):
96
+ if name in {'Datasets', 'Checkpoints', '_checkpoint_matches', '_checkpoint_target', '_verified_checkpoint_file'}:
97
+ return feature_attribute('data', 'navigation_resources', name)
98
+ if name in {'Inference', 'InferenceResource'}:
99
+ return feature_attribute('experiment', 'navigation_resources', name)
100
+ raise AttributeError(name)
101
+
102
+
103
+ def client_resource(client: Any, name: str):
104
+ server = client._transport
105
+ if name in {'billing', 'project', 'operations'}:
106
+ module, cls = ('resources', 'Operations') if name == 'operations' else ('navigation_resources', name.capitalize())
107
+ return getattr(importlib.import_module(f'envloop.{module}'), cls)(server)
108
+ if name == 'dashboard':
109
+ if available('experiment'):
110
+ return feature_attribute('experiment', 'navigation_resources', 'Dashboard')(client.project, client.jobs, client.train)
111
+ from .navigation_resources import Dashboard
112
+ return Dashboard(client.project)
113
+ if name in {'profiles', 'runners'}:
114
+ return feature_attribute('data', 'client', name.capitalize())(server)
115
+ if name in {'data', 'recipes'}:
116
+ return feature_attribute('data', name, name.capitalize())(server)
117
+ if name == 'datasets':
118
+ return navigation('Datasets')(server)
119
+ if name == 'checkpoints':
120
+ return feature_attribute('data', 'navigation_resources', 'Checkpoints')(server)
121
+ if name == 'jobs':
122
+ return feature_attribute('experiment', 'client', 'Jobs')(server)
123
+ if name == 'trials':
124
+ return client.tasks
125
+ if name == 'tasks':
126
+ return feature_attribute('experiment', 'client', 'Tasks')(server)
127
+ if name == 'training_jobs':
128
+ return client.train
129
+ if name == 'train':
130
+ return feature_attribute('experiment', 'train', 'TrainJobs')(server)
131
+ if name == 'decision':
132
+ return feature_attribute('experiment', 'decision', 'DecisionClient')(server)
133
+ if name == 'agent_runs':
134
+ return feature_attribute('rsi-loop', 'agent_runs', 'AgentRuns')(client.train)
135
+ if name in {'endpoints', 'adapters'}:
136
+ return feature_attribute('experiment', 'resources', name.capitalize())(server)
137
+ if name == 'inference':
138
+ return navigation('Inference')(server)
139
+ raise AttributeError(name)
140
+
141
+
142
+ def execute_command(args: argparse.Namespace, client: Any, cli: Any):
143
+ if args.group in {'job', 'trail'}:
144
+ return getattr(feature_module('experiment', 'cli'), 'execute_' + args.group)(args, client, cli)
145
+ if args.group == 'profile':
146
+ return feature_module('data', 'cli').execute_profile(args, client, cli)
147
+ if args.group in {'data', 'dataset', 'runner', 'checkpoint'}:
148
+ return feature_module('data', 'resource_cli').execute(args, client)
149
+ from .resource_cli import execute
150
+ return execute(args, client)
151
+
152
+
153
+ class _LegacyFeatureLoader(importlib.abc.Loader):
154
+ def __init__(self, feature: str, module: str):
155
+ self.feature, self.module = feature, module
156
+
157
+ @override
158
+ def create_module(self, spec: ModuleSpec):
159
+ module = feature_module(self.feature, self.module)
160
+ self.spec = module.__spec__
161
+ return module
162
+
163
+ @override
164
+ def exec_module(self, module: ModuleType):
165
+ # importlib rebinds __spec__ to the legacy alias; restore the feature module's own
166
+ # spec so its relative imports keep resolving inside the feature package.
167
+ module.__spec__ = self.spec
168
+
169
+
170
+ class _LegacyFeatureFinder(importlib.abc.MetaPathFinder):
171
+ @override
172
+ def find_spec(self, fullname: str, path: Sequence[str] | None = None, target: ModuleType | None = None):
173
+ if not fullname.startswith('envloop.'):
174
+ return None
175
+ module = fullname.removeprefix('envloop.')
176
+ feature = MODULES.get(module)
177
+ if feature is None:
178
+ return None
179
+ loader = _LegacyFeatureLoader(feature, module)
180
+ return importlib.util.spec_from_loader(fullname, loader, is_package=module == 'training')
181
+
182
+
183
+ def install_legacy_imports():
184
+ if not any(isinstance(finder, _LegacyFeatureFinder) for finder in sys.meta_path):
185
+ sys.meta_path.append(_LegacyFeatureFinder())
@@ -3,13 +3,14 @@ import argparse
3
3
  import json
4
4
  import os
5
5
  import sys
6
+ from typing import Any, cast
6
7
 
7
8
  from .config import ConfigError, ConfigStore, normalize_url, validate_key
8
9
 
9
10
  DEFAULT_BASE_URL = "https://api.envloop.ai"
10
11
 
11
12
 
12
- def add_auth_commands(groups):
13
+ def add_auth_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
13
14
  auth = groups.add_parser("auth", help="Configure credentials, inspect status, or remove a saved key")
14
15
  commands = auth.add_subparsers(dest="auth_command", required=True)
15
16
  login = commands.add_parser("login", help="Open the Clerk sign-in page and authorize CLI/SDK access")
@@ -47,7 +48,7 @@ def add_auth_commands(groups):
47
48
  workspace.add_argument("--stdin", action="store_true", help="Read the selected workspace's API key from stdin")
48
49
 
49
50
 
50
- def _key_from_input(stdin=False):
51
+ def _key_from_input(stdin: bool = False) -> str:
51
52
  if stdin:
52
53
  value = sys.stdin.read(515)
53
54
  if value.endswith("\r\n"):
@@ -72,7 +73,7 @@ def _masked_key():
72
73
  masked[3] &= ~(termios.ECHO | termios.ICANON | termios.ISIG | termios.IEXTEN)
73
74
  masked[6][termios.VMIN] = 1
74
75
  masked[6][termios.VTIME] = 0
75
- characters = []
76
+ characters: list[str] = []
76
77
  try:
77
78
  termios.tcsetattr(descriptor, termios.TCSAFLUSH, masked)
78
79
  sys.stderr.write("API key: ")
@@ -102,7 +103,7 @@ def _masked_key():
102
103
  sys.stderr.flush()
103
104
 
104
105
 
105
- def _report(args, store):
106
+ def _report(args: argparse.Namespace, store: ConfigStore):
106
107
  saved, configured = store.inspect()
107
108
  from .cli import resolve_cli_connection
108
109
 
@@ -116,7 +117,7 @@ def _report(args, store):
116
117
  }, connection
117
118
 
118
119
 
119
- def _verify_key(url, key, workspace_id=None, project_id=None):
120
+ def _verify_key(url: str, key: str, workspace_id: str | None = None, project_id: str | None = None):
120
121
  from .http import HttpService, RemoteError
121
122
 
122
123
  validate_key(key)
@@ -130,10 +131,11 @@ def _verify_key(url, key, workspace_id=None, project_id=None):
130
131
  response = HttpService(url, timeout=5, api_key=key, project_id=project_id, config=False,
131
132
  environment=False).request("GET", "/v1/me")
132
133
  except RemoteError as error:
133
- code = {401: "AUTHENTICATION_REJECTED", 403: "AUTHORIZATION_DENIED"}.get(error.status_code)
134
+ code = "EDGE_REJECTED" if error.code == "EDGE_REJECTED" else {
135
+ 401: "AUTHENTICATION_REJECTED", 403: "AUTHORIZATION_DENIED"}.get(cast(int, error.status_code))
134
136
  if code is None:
135
137
  code = error.code if error.status_code is None and error.code in {
136
- "NETWORK_ERROR", "INVALID_RESPONSE", "RESPONSE_TOO_LARGE",
138
+ "NETWORK_ERROR", "REQUEST_TIMEOUT", "RESPONSE_INTERRUPTED", "INVALID_RESPONSE", "RESPONSE_TOO_LARGE",
137
139
  } else "HTTP_ERROR"
138
140
  raise ConfigError(code) from None
139
141
  if not isinstance(response, dict) or response.get("auth_method") != "api_key":
@@ -144,7 +146,7 @@ def _verify_key(url, key, workspace_id=None, project_id=None):
144
146
  return _scope(response, project=project_id)
145
147
 
146
148
 
147
- def _print(report, as_json=False):
149
+ def _print(report: dict[str, Any], as_json: bool = False) -> None:
148
150
  if as_json:
149
151
  print(json.dumps(report, ensure_ascii=False))
150
152
  return
@@ -159,7 +161,7 @@ def _print(report, as_json=False):
159
161
  print(f"Check: {report['check']}")
160
162
 
161
163
 
162
- def run_auth(args, parser):
164
+ def run_auth(args: argparse.Namespace, parser: argparse.ArgumentParser):
163
165
  store = ConfigStore()
164
166
  command = args.auth_command
165
167
  if command == "login":
@@ -197,20 +199,19 @@ def run_auth(args, parser):
197
199
  raise ConfigError("API_URL_REQUIRED")
198
200
  if not no_check:
199
201
  scope = _verify_key(url, value, saved.get("workspace_id"), saved.get("project_id"))
202
+ if scope is not None:
203
+ store.configure(url, value, expected_saved=saved, expected_credential=credential, scope=scope)
204
+ else:
205
+ store.update(args.field, value, expected_url=url, expected_saved=saved, expected_credential=credential)
200
206
  elif args.field == "api-url":
201
207
  if args.stdin:
202
208
  parser.error("this field does not support --stdin")
203
209
  value = args.value if args.value is not None else DEFAULT_BASE_URL
210
+ store.update(args.field, value)
204
211
  else:
205
212
  if args.value is None or args.stdin:
206
213
  parser.error("this field requires a value and does not support --stdin")
207
214
  value = args.value
208
- if args.field == "api-key":
209
- if scope is not None:
210
- store.configure(url, value, expected_saved=saved, expected_credential=credential, scope=scope)
211
- else:
212
- store.update(args.field, value, expected_url=url, expected_saved=saved, expected_credential=credential)
213
- else:
214
215
  store.update(args.field, value)
215
216
  if args.field == "api-key":
216
217
  print("Saved. API key check: " + ("not_checked (offline)" if no_check else "authentication_confirmed"))
@@ -279,8 +280,9 @@ def run_auth(args, parser):
279
280
  except (ConfigError, RemoteError) as error:
280
281
  from .cli_errors import format_error, safe_error_code
281
282
 
282
- report["check"] = {401: "authentication_rejected", 403: "authorization_denied"}.get(
283
- getattr(error, "status_code", None), safe_error_code(error))
283
+ report["check"] = "edge_rejected" if getattr(error, "code", None) == "EDGE_REJECTED" else {
284
+ 401: "authentication_rejected", 403: "authorization_denied"}.get(
285
+ cast(int, getattr(error, "status_code", None)), safe_error_code(error))
284
286
  _print(report, args.json)
285
287
  print(format_error(error), file=sys.stderr)
286
288
  return 1