envloop-cli 1.7.0__tar.gz → 1.10.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.7.0 → envloop_cli-1.10.0}/PKG-INFO +26 -3
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/README.pypi.md +25 -2
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/agent_runs.py +7 -3
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/auth_cli.py +1 -1
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/cli.py +78 -32
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/cli_errors.py +152 -104
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/cli_output.py +15 -16
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/client.py +9 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/http.py +78 -25
- envloop_cli-1.10.0/envloop/job_spec_cli.py +138 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/rsi_cli.py +16 -5
- envloop_cli-1.10.0/envloop/terminal.py +77 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/train.py +2 -1
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/train_log_cli.py +33 -29
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/pyproject.toml +1 -1
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/.gitignore +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/__init__.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/auth_login.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/auth_workspace.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/config.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/inference.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/job_logs.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/lifecycle_cli.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/navigation_resources.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/profile_secrets.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/profile_updates.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/resource_cli.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/resources.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/sandbox.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/train_runner_cli.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/training/__init__.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.0}/envloop/utils/__init__.py +0 -0
- {envloop_cli-1.7.0 → envloop_cli-1.10.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.10.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
|
|
@@ -81,6 +81,19 @@ chmod 700 "${ENVLOOP_HOME:-$HOME/.envloop}"
|
|
|
81
81
|
|
|
82
82
|
An insecure directory causes `el` to fail with `CONFIG_INSECURE_PATH`.
|
|
83
83
|
|
|
84
|
+
## Errors
|
|
85
|
+
|
|
86
|
+
Errors go to stderr as an error code, a short explanation and the applicable
|
|
87
|
+
HTTP status, without a Hint. Interactive terminals use a red heading and a dim
|
|
88
|
+
HTTP status. Redirected output, pipes, `TERM=dumb`, or a nonempty `NO_COLOR`
|
|
89
|
+
disable color. `--json` stdout remains JSON.
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
Error: PROFILE_NOT_FOUND
|
|
93
|
+
The Profile was not found or is not readable in the current scope.
|
|
94
|
+
HTTP 404
|
|
95
|
+
```
|
|
96
|
+
|
|
84
97
|
## Explore your project
|
|
85
98
|
|
|
86
99
|
```bash
|
|
@@ -153,13 +166,16 @@ Prepare a recipe-supported `REQUEST.json` using the [SFT quickstart](https://doc
|
|
|
153
166
|
Training uses billable resources. Check your project permissions, balance and data format, then submit once:
|
|
154
167
|
|
|
155
168
|
```bash
|
|
156
|
-
el job submit --type train --
|
|
169
|
+
el job submit --type train --spec REQUEST.json
|
|
157
170
|
el job get JOB_ID
|
|
158
171
|
el job log JOB_ID --follow
|
|
159
172
|
el job result JOB_ID
|
|
160
173
|
el checkpoint list --job-id JOB_ID
|
|
161
174
|
```
|
|
162
175
|
|
|
176
|
+
`job submit --spec` reads the complete Job specification JSON file for Train or Inference.
|
|
177
|
+
The former `job submit --file` option is removed; the JSON payload is unchanged.
|
|
178
|
+
|
|
163
179
|
Replace `JOB_ID` with the receipt's value. Read the training status and logs, then find the registered checkpoint after success.
|
|
164
180
|
Ctrl-C ends log reading; to stop training, use `el job stop JOB_ID` and check its state and resource cleanup.
|
|
165
181
|
See [Checkpoints](https://docs.envloop.ai/training/checkpoints) to download or delete saved weights.
|
|
@@ -182,7 +198,7 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
|
|
|
182
198
|
```
|
|
183
199
|
|
|
184
200
|
```bash
|
|
185
|
-
el job submit --type train --
|
|
201
|
+
el job submit --type train --spec gpu-run.json
|
|
186
202
|
el job get JOB_ID
|
|
187
203
|
el job log JOB_ID --follow
|
|
188
204
|
el job result JOB_ID
|
|
@@ -193,3 +209,10 @@ Replace `JOB_ID` with the returned value. The example has two independent Trials
|
|
|
193
209
|
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)`.
|
|
194
210
|
|
|
195
211
|
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.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
CLI output uses compact timestamps and highlights statuses and log levels by default on terminals.
|
|
215
|
+
Use `el --no-color job list` or append `--no-color` to a command to disable highlighting, including errors.
|
|
216
|
+
`NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
|
|
217
|
+
List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
|
|
218
|
+
`el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
|
|
@@ -68,6 +68,19 @@ chmod 700 "${ENVLOOP_HOME:-$HOME/.envloop}"
|
|
|
68
68
|
|
|
69
69
|
An insecure directory causes `el` to fail with `CONFIG_INSECURE_PATH`.
|
|
70
70
|
|
|
71
|
+
## Errors
|
|
72
|
+
|
|
73
|
+
Errors go to stderr as an error code, a short explanation and the applicable
|
|
74
|
+
HTTP status, without a Hint. Interactive terminals use a red heading and a dim
|
|
75
|
+
HTTP status. Redirected output, pipes, `TERM=dumb`, or a nonempty `NO_COLOR`
|
|
76
|
+
disable color. `--json` stdout remains JSON.
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
Error: PROFILE_NOT_FOUND
|
|
80
|
+
The Profile was not found or is not readable in the current scope.
|
|
81
|
+
HTTP 404
|
|
82
|
+
```
|
|
83
|
+
|
|
71
84
|
## Explore your project
|
|
72
85
|
|
|
73
86
|
```bash
|
|
@@ -140,13 +153,16 @@ Prepare a recipe-supported `REQUEST.json` using the [SFT quickstart](https://doc
|
|
|
140
153
|
Training uses billable resources. Check your project permissions, balance and data format, then submit once:
|
|
141
154
|
|
|
142
155
|
```bash
|
|
143
|
-
el job submit --type train --
|
|
156
|
+
el job submit --type train --spec REQUEST.json
|
|
144
157
|
el job get JOB_ID
|
|
145
158
|
el job log JOB_ID --follow
|
|
146
159
|
el job result JOB_ID
|
|
147
160
|
el checkpoint list --job-id JOB_ID
|
|
148
161
|
```
|
|
149
162
|
|
|
163
|
+
`job submit --spec` reads the complete Job specification JSON file for Train or Inference.
|
|
164
|
+
The former `job submit --file` option is removed; the JSON payload is unchanged.
|
|
165
|
+
|
|
150
166
|
Replace `JOB_ID` with the receipt's value. Read the training status and logs, then find the registered checkpoint after success.
|
|
151
167
|
Ctrl-C ends log reading; to stop training, use `el job stop JOB_ID` and check its state and resource cleanup.
|
|
152
168
|
See [Checkpoints](https://docs.envloop.ai/training/checkpoints) to download or delete saved weights.
|
|
@@ -169,7 +185,7 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
|
|
|
169
185
|
```
|
|
170
186
|
|
|
171
187
|
```bash
|
|
172
|
-
el job submit --type train --
|
|
188
|
+
el job submit --type train --spec gpu-run.json
|
|
173
189
|
el job get JOB_ID
|
|
174
190
|
el job log JOB_ID --follow
|
|
175
191
|
el job result JOB_ID
|
|
@@ -180,3 +196,10 @@ Replace `JOB_ID` with the returned value. The example has two independent Trials
|
|
|
180
196
|
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)`.
|
|
181
197
|
|
|
182
198
|
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.
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
CLI output uses compact timestamps and highlights statuses and log levels by default on terminals.
|
|
202
|
+
Use `el --no-color job list` or append `--no-color` to a command to disable highlighting, including errors.
|
|
203
|
+
`NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
|
|
204
|
+
List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
|
|
205
|
+
`el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Agent runs: a read-only view of Train Jobs grouped by their agent's run label.
|
|
2
2
|
|
|
3
3
|
The platform stores no run object. Agents tag every Train Job they create with
|
|
4
|
-
``
|
|
4
|
+
``tags`` (the RSI loop: ``rsi_run``, ``rsi_round``, ``rsi_checkpoint``...);
|
|
5
5
|
this module pages through ``GET /v1/job`` and derives runs from those tags,
|
|
6
6
|
using the same rules as the Console's agent runs page.
|
|
7
7
|
"""
|
|
@@ -34,7 +34,11 @@ class AgentRunError(RuntimeError):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _metadata(job) -> dict:
|
|
37
|
-
metadata = job.get("
|
|
37
|
+
metadata = job.get("tags")
|
|
38
|
+
if not isinstance(metadata, dict):
|
|
39
|
+
metadata = job.get("labels")
|
|
40
|
+
if not isinstance(metadata, dict):
|
|
41
|
+
metadata = job.get("user_metadata")
|
|
38
42
|
return metadata if isinstance(metadata, dict) else {}
|
|
39
43
|
|
|
40
44
|
|
|
@@ -111,7 +115,7 @@ def _job_view(job) -> dict:
|
|
|
111
115
|
"lora_rank": job.get("lora_rank"),
|
|
112
116
|
"model_id": job.get("model_id"),
|
|
113
117
|
"checkpoint": _checkpoint(job),
|
|
114
|
-
"
|
|
118
|
+
"tags": metadata,
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
|
|
@@ -282,7 +282,7 @@ def run_auth(args, parser):
|
|
|
282
282
|
report["check"] = {401: "authentication_rejected", 403: "authorization_denied"}.get(
|
|
283
283
|
getattr(error, "status_code", None), safe_error_code(error))
|
|
284
284
|
_print(report, args.json)
|
|
285
|
-
print(format_error(error
|
|
285
|
+
print(format_error(error), file=sys.stderr)
|
|
286
286
|
return 1
|
|
287
287
|
_print(report, getattr(args, "json", False))
|
|
288
288
|
return 0
|
|
@@ -39,6 +39,8 @@ class HelpArgumentParser(argparse.ArgumentParser):
|
|
|
39
39
|
def __init__(self, *args, **kwargs):
|
|
40
40
|
kwargs.setdefault('formatter_class', CommandHelpFormatter)
|
|
41
41
|
super().__init__(*args, **kwargs)
|
|
42
|
+
self.add_argument("--no-color", action="store_true", default=argparse.SUPPRESS,
|
|
43
|
+
help="Disable terminal highlighting")
|
|
42
44
|
self.set_defaults(_help_parser=self)
|
|
43
45
|
self._help_parser = self
|
|
44
46
|
|
|
@@ -61,9 +63,21 @@ class HelpArgumentParser(argparse.ArgumentParser):
|
|
|
61
63
|
def error(self, message):
|
|
62
64
|
if message.startswith("unrecognized arguments:"):
|
|
63
65
|
message = "unrecognized arguments"
|
|
66
|
+
# argparse includes raw argument values in these diagnostics. Keep the
|
|
67
|
+
# option/type description while excluding values (which can be secrets).
|
|
68
|
+
if "invalid choice:" in message:
|
|
69
|
+
message = message.split("invalid choice:", 1)[0] + "invalid choice"
|
|
70
|
+
elif " value:" in message:
|
|
71
|
+
message = message.split(" value:", 1)[0] + " value"
|
|
72
|
+
elif message.startswith("ambiguous option:"):
|
|
73
|
+
message = "ambiguous option"
|
|
74
|
+
from .cli_errors import error_block
|
|
75
|
+
|
|
64
76
|
command = self._help_parser
|
|
77
|
+
print(error_block("INVALID_ARGUMENT", message), file=sys.stderr)
|
|
78
|
+
print(file=sys.stderr)
|
|
65
79
|
command.print_help(sys.stderr)
|
|
66
|
-
command.exit(2
|
|
80
|
+
command.exit(2)
|
|
67
81
|
|
|
68
82
|
|
|
69
83
|
class CommandHelpFormatter(argparse.HelpFormatter):
|
|
@@ -133,10 +147,10 @@ def log_page_size(value):
|
|
|
133
147
|
|
|
134
148
|
def runner_arguments(submit):
|
|
135
149
|
submit.add_argument("--runner", metavar="NAME[@REVISION]",
|
|
136
|
-
help="Agent/Train: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
|
|
137
|
-
submit.add_argument("--job-id", help="caller-selected Job ID")
|
|
138
|
-
submit.add_argument("--tag", help="run label")
|
|
139
|
-
submit.add_argument("--priority", type=int, help="waiting priority;
|
|
150
|
+
help="Agent/Train/Inference: namespace/name@REVISION; Sandbox: registered Runner name (for example base)")
|
|
151
|
+
submit.add_argument("--job-id", help="Agent/Sandbox: caller-selected Job ID")
|
|
152
|
+
submit.add_argument("--tag", help="Agent/Sandbox: run label")
|
|
153
|
+
submit.add_argument("--priority", type=int, help="Agent/Sandbox/Train recipe: waiting priority; explicit value overrides spec (server default: 0)")
|
|
140
154
|
submit.add_argument("--command", help="Agent/Sandbox: shell override; Train: declared run_command parameter passed to the published Runner entrypoint; preserves multiline text")
|
|
141
155
|
|
|
142
156
|
|
|
@@ -147,14 +161,14 @@ def parser():
|
|
|
147
161
|
description="Manage Agent, Sandbox, Train and Inference Jobs and worker trials with EnvLoop.\nConfigure your connection with 'auth config'.",
|
|
148
162
|
)
|
|
149
163
|
cli._optionals.title = "Options"
|
|
150
|
-
cli.set_defaults(api_url=None, workspace_id=None)
|
|
164
|
+
cli.set_defaults(api_url=None, workspace_id=None, no_color=False)
|
|
151
165
|
cli.add_argument("-V", "--version", action="version", version=f"%(prog)s {CLI_VERSION}", help="Display CLI version")
|
|
152
166
|
groups = cli.add_subparsers(dest="group", title="Commands", prog=cli.prog, required=True)
|
|
153
167
|
from .auth_cli import add_auth_commands
|
|
154
168
|
|
|
155
169
|
jobs = groups.add_parser("job", help="Submit and manage Agent, Sandbox, Train and Inference Jobs").add_subparsers(dest="action", required=True)
|
|
156
170
|
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job", allow_abbrev=False)
|
|
157
|
-
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=
|
|
171
|
+
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default=None, help="Job type (default: spec type, otherwise agent)")
|
|
158
172
|
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox image override (server default: python:3.11); Train Runner: public spelling of --docker")
|
|
159
173
|
from .train_runner_cli import memory_mebibytes, positive_integer
|
|
160
174
|
|
|
@@ -166,7 +180,7 @@ def parser():
|
|
|
166
180
|
for option in ('memory', 'disk', 'shm-size'):
|
|
167
181
|
job_submit.add_argument('--' + option, type=memory_mebibytes, metavar="SIZE",
|
|
168
182
|
help="Train Runner: per-Trial size in Mi/Gi/Ti (for example 64Gi)")
|
|
169
|
-
job_submit.add_argument("--
|
|
183
|
+
job_submit.add_argument("--spec", type=Path, metavar="REQUEST.json", help="Job specification JSON; explicit submission options override file values (except --trails)")
|
|
170
184
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
171
185
|
job_submit.add_argument("--task-id", help="select one task within the dataset")
|
|
172
186
|
job_submit.add_argument(
|
|
@@ -175,17 +189,17 @@ def parser():
|
|
|
175
189
|
)
|
|
176
190
|
job_submit.add_argument("--dataset", dest="dataset_option", type=dataset_reference, metavar="NAME[@VERSION]",
|
|
177
191
|
help="dataset name; default selection uses the platform's pinned snapshot")
|
|
178
|
-
job_submit.add_argument("--config", help="dataset subset;
|
|
192
|
+
job_submit.add_argument("--config", help="Agent: dataset subset; Train recipe: named preset; omitted preserves spec or server default")
|
|
179
193
|
job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
|
|
180
194
|
job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
|
|
181
195
|
runner_arguments(job_submit)
|
|
182
196
|
job_submit.add_argument("--attempts", type=int)
|
|
183
|
-
job_submit.add_argument("--request-id")
|
|
197
|
+
job_submit.add_argument("--request-id", help="idempotency key; overrides request_id in --spec")
|
|
184
198
|
job_submit.add_argument("--profile", type=profile_reference, metavar="NAME[@VERSION]",
|
|
185
|
-
help="named runtime Profile; omitted version
|
|
199
|
+
help="named runtime Profile; omitted version prefers default, then newest unarchived creation")
|
|
186
200
|
job_submit.add_argument("--seed", type=int)
|
|
187
201
|
job_submit.add_argument("--timeout-sec", type=wait_seconds,
|
|
188
|
-
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train --
|
|
202
|
+
help="Agent/Sandbox or Train Runner execution timeout (whole seconds); Train recipe --wait timeout (default: 600)")
|
|
189
203
|
job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
|
|
190
204
|
help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
191
205
|
job_submit.add_argument("--wait", action="store_true")
|
|
@@ -217,7 +231,7 @@ def parser():
|
|
|
217
231
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
218
232
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
219
233
|
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and checkpoints")
|
|
220
|
-
runs_get.add_argument("run", metavar="RUN", help="run
|
|
234
|
+
runs_get.add_argument("run", metavar="RUN", help="run tag: tags.rsi_run or agent_run")
|
|
221
235
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
222
236
|
runs_start = runs.add_parser("start", help="Start a new RSI loop run (paid; needs training.run)")
|
|
223
237
|
runs_start.add_argument("run", metavar="RUN", help="new run id: lower-case letters, digits and dashes")
|
|
@@ -233,6 +247,7 @@ def parser():
|
|
|
233
247
|
runs_start.add_argument("--student-model", help="gateway student model of a dry run")
|
|
234
248
|
runs_live = runs.add_parser("live", help="Show what a run does now; --follow streams new events")
|
|
235
249
|
runs_live.add_argument("run", metavar="RUN")
|
|
250
|
+
runs_live.add_argument("--json", action="store_true", help="Output JSON; --follow emits complete frames as NDJSON")
|
|
236
251
|
runs_live.add_argument("--since", type=int, help="only events after this cursor (the previous next_cursor)")
|
|
237
252
|
runs_live.add_argument("--follow", action="store_true", help="poll and print new events until the run ends; Ctrl-C exits without stopping it")
|
|
238
253
|
runs_steer = runs.add_parser("steer", help="Message the running operator now (needs training.run)")
|
|
@@ -273,7 +288,7 @@ def parser():
|
|
|
273
288
|
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
274
289
|
update.add_argument("name")
|
|
275
290
|
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
276
|
-
update.add_argument("--from-version", help="Source version; omit to use
|
|
291
|
+
update.add_argument("--from-version", help="Source version; omit to use default, then newest unarchived creation")
|
|
277
292
|
update.add_argument("--file", type=Path, required=True, help="TOML or JSON patch: merge params keys, replace supplied metadata; preserve omitted fields")
|
|
278
293
|
update.add_argument("--runner", metavar="RUNNER", help="Bind a Runner revision; v1 requires a full commit; omit to preserve the source binding")
|
|
279
294
|
update_secret = update.add_mutually_exclusive_group()
|
|
@@ -378,11 +393,24 @@ def resolve_cli_connection(api_url=None, workspace_id=None, project_id=None):
|
|
|
378
393
|
|
|
379
394
|
|
|
380
395
|
def main(argv=None):
|
|
396
|
+
from .terminal import output_options
|
|
397
|
+
|
|
398
|
+
with output_options():
|
|
399
|
+
return _main(argv)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _main(argv=None):
|
|
381
403
|
cli = parser()
|
|
382
404
|
argv = sys.argv[1:] if argv is None else argv
|
|
383
405
|
if not argv:
|
|
384
406
|
cli.print_help()
|
|
385
407
|
return 0
|
|
408
|
+
# Apply display options before validation, even if an earlier argument is invalid.
|
|
409
|
+
options = argv[:argv.index("--")] if "--" in argv else argv
|
|
410
|
+
if "--no-color" in options:
|
|
411
|
+
from .terminal import disable_color
|
|
412
|
+
|
|
413
|
+
disable_color()
|
|
386
414
|
if any(value == "--api-key" or value.startswith("--api-key=") for value in argv):
|
|
387
415
|
cli.error("API keys must be entered with auth config or supplied through ENVLOOP_API_KEY")
|
|
388
416
|
args = cli.parse_args(argv)
|
|
@@ -397,22 +425,38 @@ def main(argv=None):
|
|
|
397
425
|
from .cli_errors import format_error
|
|
398
426
|
from .config import ConfigError
|
|
399
427
|
|
|
400
|
-
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')
|
|
428
|
+
print(format_error(ConfigError('AUTH_INPUT_CANCELLED')), file=sys.stderr)
|
|
401
429
|
return 1
|
|
402
430
|
except Exception as exc:
|
|
403
431
|
from .cli_errors import format_error
|
|
404
432
|
|
|
405
|
-
print(format_error(exc
|
|
433
|
+
print(format_error(exc), file=sys.stderr)
|
|
406
434
|
return 1
|
|
407
435
|
if args.group == "job" and args.action == "submit":
|
|
408
436
|
from .train_runner_cli import SHORTCUT_FIELDS, runner_request
|
|
409
437
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
438
|
+
if args.spec is not None:
|
|
439
|
+
from .job_spec_cli import SpecFileError, spec_request
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
args.job_request = spec_request(args)
|
|
443
|
+
except (OSError, UnicodeError, json.JSONDecodeError, SpecFileError) as exc:
|
|
444
|
+
from .cli_errors import format_error
|
|
445
|
+
|
|
446
|
+
print(format_error(exc), file=sys.stderr)
|
|
447
|
+
return 1
|
|
448
|
+
except (ValueError, TypeError) as exc:
|
|
449
|
+
cli.error(str(exc) if isinstance(exc, ValueError) else 'Unable to merge Job spec')
|
|
450
|
+
else:
|
|
451
|
+
args.type = args.type or 'agent'
|
|
452
|
+
runner_mode = args.type == 'train' and args.spec is None
|
|
453
|
+
if args.spec is None and not runner_mode and any(getattr(args, key) is not None for key in SHORTCUT_FIELDS):
|
|
454
|
+
cli.error('Train Runner shortcuts require --type train --runner and cannot be combined with --spec')
|
|
455
|
+
if args.spec is not None:
|
|
456
|
+
pass # The merged request was validated above; the platform owns final admission.
|
|
457
|
+
elif args.type in {"train", "inference"}:
|
|
458
|
+
if args.spec is None and not runner_mode:
|
|
459
|
+
cli.error(f"--type {args.type} requires --spec REQUEST.json")
|
|
416
460
|
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
417
461
|
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
418
462
|
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
@@ -420,7 +464,7 @@ def main(argv=None):
|
|
|
420
464
|
if not runner_mode:
|
|
421
465
|
agent_fields += ('runner', 'command', 'request_id', 'image')
|
|
422
466
|
if any(getattr(args, key) is not None for key in agent_fields):
|
|
423
|
-
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --
|
|
467
|
+
cli.error(f"--type {args.type} cannot be combined with Agent submission options or mixed --spec/Runner input")
|
|
424
468
|
if runner_mode:
|
|
425
469
|
try:
|
|
426
470
|
args.train_request = runner_request(args)
|
|
@@ -430,8 +474,8 @@ def main(argv=None):
|
|
|
430
474
|
from .sandbox import sandbox_runner_selector, validate_image
|
|
431
475
|
|
|
432
476
|
if any(getattr(args, key) is not None for key in (
|
|
433
|
-
'
|
|
434
|
-
cli.error('--type sandbox accepts --runner without Dataset, Task,
|
|
477
|
+
'spec', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
|
|
478
|
+
cli.error('--type sandbox accepts --runner without Dataset, Task, Job spec or seed options')
|
|
435
479
|
try:
|
|
436
480
|
args.runner = sandbox_runner_selector(args.runner)
|
|
437
481
|
if args.image is not None:
|
|
@@ -443,8 +487,8 @@ def main(argv=None):
|
|
|
443
487
|
cli.error('Sandbox --timeout-sec must be whole seconds')
|
|
444
488
|
args.timeout_sec = int(args.timeout_sec)
|
|
445
489
|
else:
|
|
446
|
-
if args.
|
|
447
|
-
cli.error("--
|
|
490
|
+
if args.spec is not None:
|
|
491
|
+
cli.error("--spec requires --type train or inference")
|
|
448
492
|
if args.image is not None:
|
|
449
493
|
cli.error('--image requires --type sandbox')
|
|
450
494
|
if args.runner is not None:
|
|
@@ -526,17 +570,19 @@ def main(argv=None):
|
|
|
526
570
|
elif args.group == "job":
|
|
527
571
|
if args.action == "submit":
|
|
528
572
|
if args.type == 'inference':
|
|
529
|
-
result = client.jobs.submit_inference(
|
|
573
|
+
result = client.jobs.submit_inference(args.job_request)
|
|
530
574
|
elif args.type == "train":
|
|
531
|
-
request = args.train_request if args.
|
|
575
|
+
request = args.train_request if args.spec is None else args.job_request
|
|
532
576
|
result = client.training_jobs.submit(request)
|
|
577
|
+
elif args.spec is not None:
|
|
578
|
+
result = client.jobs.submit_spec(args.job_request)
|
|
533
579
|
else:
|
|
534
580
|
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
535
581
|
attempts=args.attempts if args.attempts is not None else 1,
|
|
536
582
|
request_id=args.request_id, config=args.config, split=args.split,
|
|
537
583
|
revision=args.revision, **submission_options(args))
|
|
538
584
|
if args.wait:
|
|
539
|
-
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.
|
|
585
|
+
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.spec is not None and args.timeout_sec is not None else {}
|
|
540
586
|
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
541
587
|
elif args.action == 'event':
|
|
542
588
|
result = client.jobs.events(args.job_id)
|
|
@@ -606,10 +652,10 @@ def main(argv=None):
|
|
|
606
652
|
|
|
607
653
|
if isinstance(exc, CleanupError):
|
|
608
654
|
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
609
|
-
print(format_error(exc
|
|
655
|
+
print(format_error(exc), file=sys.stderr)
|
|
610
656
|
return 1
|
|
611
657
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
612
|
-
print(format_error(exc
|
|
658
|
+
print(format_error(exc), file=sys.stderr)
|
|
613
659
|
return 1
|
|
614
660
|
|
|
615
661
|
|