envloop-cli 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl
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/agent_runs.py +8 -8
- envloop/auth_cli.py +4 -1
- envloop/cli.py +188 -88
- envloop/cli_errors.py +248 -0
- envloop/cli_output.py +53 -20
- envloop/client.py +119 -8
- envloop/http.py +67 -13
- envloop/job_logs.py +16 -0
- envloop/navigation_resources.py +105 -18
- envloop/profile_secrets.py +27 -0
- envloop/profile_updates.py +49 -0
- envloop/resource_cli.py +40 -38
- envloop/resources.py +4 -2
- envloop/sandbox.py +27 -0
- envloop/train.py +35 -18
- envloop/train_log_cli.py +97 -0
- envloop/training/__init__.py +58 -65
- envloop/training/service_client.py +78 -10
- {envloop_cli-0.2.0.dist-info → envloop_cli-0.3.0.dist-info}/METADATA +32 -11
- envloop_cli-0.3.0.dist-info/RECORD +30 -0
- envloop_cli-0.2.0.dist-info/RECORD +0 -24
- {envloop_cli-0.2.0.dist-info → envloop_cli-0.3.0.dist-info}/WHEEL +0 -0
- {envloop_cli-0.2.0.dist-info → envloop_cli-0.3.0.dist-info}/entry_points.txt +0 -0
envloop/agent_runs.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The platform stores no run object. Agents tag every Train Job they create with
|
|
4
4
|
``user_metadata`` (the RSI loop: ``rsi_run``, ``rsi_round``, ``rsi_checkpoint``...);
|
|
5
|
-
this module pages through ``GET /v1/
|
|
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
|
"""
|
|
8
8
|
|
|
@@ -19,8 +19,8 @@ KEYS = {
|
|
|
19
19
|
"checkpoint": ("agent_checkpoint", "rsi_checkpoint"),
|
|
20
20
|
}
|
|
21
21
|
RUN_KEYS = KEYS["run"] # first non-empty string wins
|
|
22
|
-
ACTIVE_STATUSES = frozenset({"LAUNCHING", "STARTING", "RUNNING", "RECOVERING"})
|
|
23
|
-
TERMINAL_STATUSES = frozenset({"SUCCEEDED", "FAILED", "CANCELLED"})
|
|
22
|
+
ACTIVE_STATUSES = frozenset({"LAUNCHING", "STARTING", "RUNNING", "RECOVERING", "queued", "pending", "starting", "running", "recovering", "cancelling"})
|
|
23
|
+
TERMINAL_STATUSES = frozenset({"SUCCEEDED", "FAILED", "CANCELLED", "completed", "failed", "cancelled"})
|
|
24
24
|
PAGE_LIMIT = 100 # the platform's maximum page size
|
|
25
25
|
|
|
26
26
|
|
|
@@ -70,7 +70,7 @@ def _round(job) -> int | None:
|
|
|
70
70
|
|
|
71
71
|
def _order(job):
|
|
72
72
|
# Creation order, as the platform pages jobs (newest first, reversed here).
|
|
73
|
-
return (str(job.get("created_at") or ""), str(job.get("train_job_id") or ""))
|
|
73
|
+
return (str(job.get("created_at") or ""), str(job.get("job_id") or job.get("train_job_id") or ""))
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def _duration_sec(job) -> int | None:
|
|
@@ -97,7 +97,7 @@ def _checkpoint(job) -> dict | None:
|
|
|
97
97
|
def _job_view(job) -> dict:
|
|
98
98
|
metadata = _metadata(job)
|
|
99
99
|
return {
|
|
100
|
-
"train_job_id": job.get("train_job_id"),
|
|
100
|
+
"train_job_id": job.get("job_id") or job.get("train_job_id"),
|
|
101
101
|
"status": job.get("status"),
|
|
102
102
|
"round": _round(job),
|
|
103
103
|
"purpose": _field(job, "purpose"),
|
|
@@ -121,11 +121,11 @@ def _summary(run: str, jobs: list) -> dict:
|
|
|
121
121
|
status = "running"
|
|
122
122
|
else:
|
|
123
123
|
status = str(latest.get("status") or "unknown").lower()
|
|
124
|
-
succeeded = [job for job in jobs if job.get("status")
|
|
124
|
+
succeeded = [job for job in jobs if job.get("status") in {"SUCCEEDED", "completed"}]
|
|
125
125
|
latest_saved = next((job for job in reversed(jobs) if _checkpoint(job) is not None), None)
|
|
126
126
|
latest_checkpoint = _checkpoint(latest_saved) if latest_saved is not None else None
|
|
127
127
|
if latest_checkpoint is not None:
|
|
128
|
-
latest_checkpoint["train_job_id"] = latest_saved.get("train_job_id")
|
|
128
|
+
latest_checkpoint["train_job_id"] = latest_saved.get("job_id") or latest_saved.get("train_job_id")
|
|
129
129
|
rounds = [number for number in map(_round, jobs) if number is not None]
|
|
130
130
|
activity = [stamp for job in jobs if isinstance(stamp := job.get("updated_at") or job.get("created_at"), str)]
|
|
131
131
|
return {
|
|
@@ -172,7 +172,7 @@ class AgentRuns:
|
|
|
172
172
|
if observed_at is None:
|
|
173
173
|
observed_at = page.get("observed_at")
|
|
174
174
|
for job in page.get("items") or []:
|
|
175
|
-
job_id = job.get("train_job_id") if isinstance(job, dict) else None
|
|
175
|
+
job_id = job.get("job_id") or job.get("train_job_id") if isinstance(job, dict) else None
|
|
176
176
|
if not isinstance(job, dict) or job_id in ids:
|
|
177
177
|
continue
|
|
178
178
|
if job_id is not None:
|
envloop/auth_cli.py
CHANGED
|
@@ -277,9 +277,12 @@ def run_auth(args, parser):
|
|
|
277
277
|
config=False, environment=False).list_jobs(limit=1)
|
|
278
278
|
report["check"] = "access_confirmed" if connection.api_key else "reachable_anonymous"
|
|
279
279
|
except (ConfigError, RemoteError) as error:
|
|
280
|
+
from .cli_errors import format_error, safe_error_code
|
|
281
|
+
|
|
280
282
|
report["check"] = {401: "authentication_rejected", 403: "authorization_denied"}.get(
|
|
281
|
-
getattr(error, "status_code", None), error
|
|
283
|
+
getattr(error, "status_code", None), safe_error_code(error))
|
|
282
284
|
_print(report, args.json)
|
|
285
|
+
print(format_error(error, args=args, program=parser.prog), file=sys.stderr)
|
|
283
286
|
return 1
|
|
284
287
|
_print(report, getattr(args, "json", False))
|
|
285
288
|
return 0
|
envloop/cli.py
CHANGED
|
@@ -19,8 +19,8 @@ except PackageNotFoundError:
|
|
|
19
19
|
|
|
20
20
|
SECTIONS = {
|
|
21
21
|
'OVERVIEW': ('dashboard',),
|
|
22
|
-
'EXPERIMENT': ('job', '
|
|
23
|
-
'DATA': ('dataset', 'checkpoint', 'profile'),
|
|
22
|
+
'EXPERIMENT': ('job', 'trail'),
|
|
23
|
+
'DATA': ('dataset', 'runner', 'checkpoint', 'profile'),
|
|
24
24
|
'MANAGE': ('billing', 'limit', 'member', 'setting'),
|
|
25
25
|
'TOOLS': ('auth', 'help'),
|
|
26
26
|
}
|
|
@@ -111,20 +111,32 @@ def nonnegative_seconds(value):
|
|
|
111
111
|
return seconds
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
def wait_seconds(value):
|
|
115
|
+
try:
|
|
116
|
+
seconds = float(value)
|
|
117
|
+
except ValueError:
|
|
118
|
+
raise argparse.ArgumentTypeError("seconds must be finite and non-negative") from None
|
|
119
|
+
if not math.isfinite(seconds) or seconds < 0:
|
|
120
|
+
raise argparse.ArgumentTypeError("seconds must be finite and non-negative")
|
|
121
|
+
return seconds
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def log_page_size(value):
|
|
125
|
+
try:
|
|
126
|
+
count = int(value)
|
|
127
|
+
except ValueError:
|
|
128
|
+
raise argparse.ArgumentTypeError("expected an integer from 1 to 1000") from None
|
|
129
|
+
if not 1 <= count <= 1000:
|
|
130
|
+
raise argparse.ArgumentTypeError("expected an integer from 1 to 1000")
|
|
131
|
+
return count
|
|
117
132
|
|
|
118
|
-
try:
|
|
119
|
-
return runner_selector(value)
|
|
120
|
-
except ValueError as exc:
|
|
121
|
-
raise argparse.ArgumentTypeError(str(exc)) from None
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
def runner_arguments(submit):
|
|
135
|
+
submit.add_argument("--runner", metavar="NAME[@REVISION]",
|
|
136
|
+
help="Agent: override the Dataset runner with NAME@REVISION; Sandbox: registered Runner name (for example base)")
|
|
125
137
|
submit.add_argument("--job-id", help="caller-selected Job ID")
|
|
126
138
|
submit.add_argument("--tag", help="run label")
|
|
127
|
-
submit.add_argument("--priority", type=int,
|
|
139
|
+
submit.add_argument("--priority", type=int, help="waiting priority; larger values run first (default: 0)")
|
|
128
140
|
submit.add_argument("--command", help="shell script override executed in the runner directory; preserves multiline text")
|
|
129
141
|
|
|
130
142
|
|
|
@@ -132,7 +144,7 @@ def parser():
|
|
|
132
144
|
cli = HelpArgumentParser(
|
|
133
145
|
prog=Path(sys.argv[0]).name if Path(sys.argv[0]).name in {"envloop", "el"} else "el",
|
|
134
146
|
usage="%(prog)s [options] [command]", formatter_class=CommandHelpFormatter,
|
|
135
|
-
description="Manage
|
|
147
|
+
description="Manage Agent, Sandbox, Train and Inference Jobs and worker trials with EnvLoop.\nConfigure your connection with 'auth config'.",
|
|
136
148
|
)
|
|
137
149
|
cli._optionals.title = "Options"
|
|
138
150
|
cli.set_defaults(api_url=None, workspace_id=None)
|
|
@@ -140,8 +152,11 @@ def parser():
|
|
|
140
152
|
groups = cli.add_subparsers(dest="group", title="Commands", prog=cli.prog, required=True)
|
|
141
153
|
from .auth_cli import add_auth_commands
|
|
142
154
|
|
|
143
|
-
jobs = groups.add_parser("job", help="Submit and manage Agent
|
|
144
|
-
job_submit = jobs.add_parser("submit")
|
|
155
|
+
jobs = groups.add_parser("job", help="Submit and manage Agent, Sandbox, Train and Inference Jobs").add_subparsers(dest="action", required=True)
|
|
156
|
+
job_submit = jobs.add_parser("submit", help="Submit an Agent, Sandbox, Train or Inference Job")
|
|
157
|
+
job_submit.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), default="agent", help="Job type (default: agent)")
|
|
158
|
+
job_submit.add_argument("--image", metavar="IMAGE", help="Sandbox Docker image override (server default: python:3.11)")
|
|
159
|
+
job_submit.add_argument("--file", type=Path, help="Train or Inference Job request JSON (spec or Train trials array)")
|
|
145
160
|
job_submit.add_argument("--source", type=Path, help="two-file directory for a single-task Job")
|
|
146
161
|
job_submit.add_argument("--task-id", help="select one task within the dataset")
|
|
147
162
|
job_submit.add_argument(
|
|
@@ -154,42 +169,49 @@ def parser():
|
|
|
154
169
|
job_submit.add_argument("--split", help="one complete split name; omitted selects all splits")
|
|
155
170
|
job_submit.add_argument("--revision", help="Git branch/tag/commit; must match the platform's pinned snapshot")
|
|
156
171
|
runner_arguments(job_submit)
|
|
157
|
-
job_submit.add_argument("--attempts", type=int
|
|
172
|
+
job_submit.add_argument("--attempts", type=int)
|
|
158
173
|
job_submit.add_argument("--request-id")
|
|
159
174
|
job_submit.add_argument("--profile", type=profile_reference, metavar="NAME[@VERSION]",
|
|
160
175
|
help="named runtime Profile; omitted version requires an explicit default")
|
|
161
176
|
job_submit.add_argument("--seed", type=int)
|
|
162
|
-
job_submit.add_argument("--timeout-sec", type=
|
|
177
|
+
job_submit.add_argument("--timeout-sec", type=wait_seconds,
|
|
178
|
+
help="Agent/Sandbox execution timeout (whole seconds); Train wait timeout (default: 600)")
|
|
163
179
|
job_submit.add_argument("--delete-delay", type=nonnegative_seconds, metavar="SECONDS",
|
|
164
180
|
help="keep each sandbox (E2B or Fly) after completion for SECONDS (default: 0); cancellation/timeout delete immediately")
|
|
165
181
|
job_submit.add_argument("--wait", action="store_true")
|
|
166
|
-
for action in ("get", "stop", "result"):
|
|
167
|
-
jobs.add_parser(action)
|
|
182
|
+
for action in ("get", "stop", "result", "pause", "resume", "wake", "event"):
|
|
183
|
+
command = jobs.add_parser(action)
|
|
184
|
+
command.add_argument("job_id")
|
|
185
|
+
if action == 'event':
|
|
186
|
+
command.add_argument('--json', action='store_true')
|
|
187
|
+
command.set_defaults(_list_view=True)
|
|
188
|
+
from .resource_cli import add_job_resources
|
|
189
|
+
add_job_resources(jobs)
|
|
168
190
|
listing = jobs.add_parser("list")
|
|
169
|
-
listing.add_argument('--type', choices=['agent', 'train'], help='Select Agent or
|
|
191
|
+
listing.add_argument('--type', choices=['agent', 'sandbox', 'train', 'inference'], help='Select Agent, Sandbox, Train or Inference Jobs')
|
|
192
|
+
listing.add_argument('--status', help='Filter by Job status')
|
|
170
193
|
listing.add_argument("--cursor")
|
|
171
194
|
listing.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
172
195
|
listing.add_argument("--limit", type=int, default=25)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
train_list.add_argument("--limit", type=int, default=25)
|
|
184
|
-
train_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
185
|
-
runs = trains.add_parser("run", help="Inspect runs grouped from their Train Jobs").add_subparsers(dest="run_action", required=True)
|
|
196
|
+
train_log = jobs.add_parser("log", help="Read or follow Agent, Sandbox or Train Job logs")
|
|
197
|
+
train_log.add_argument("job_id")
|
|
198
|
+
train_log.add_argument("--source", choices=("all", "system", "runtime"), default="all")
|
|
199
|
+
position = train_log.add_mutually_exclusive_group()
|
|
200
|
+
position.add_argument("--tail", type=log_page_size, help="Start with the last N records (default: 200)")
|
|
201
|
+
position.add_argument("--cursor", help="Resume from a previously returned log cursor")
|
|
202
|
+
train_log.add_argument("--limit", type=log_page_size, default=200, help="Maximum records per page (default: 200)")
|
|
203
|
+
train_log.add_argument("--follow", action="store_true", help="Drain pages and poll until collection ends; Ctrl-C exits without stopping the Job")
|
|
204
|
+
train_log.add_argument("--json", action="store_true", help="Output the complete page as JSON; --follow emits one page per line (NDJSON)")
|
|
205
|
+
runs = jobs.add_parser("run", help="Inspect runs grouped from their Train Jobs").add_subparsers(dest="run_action", required=True)
|
|
186
206
|
runs_list = runs.add_parser("list", help="List agent runs, most recently updated first")
|
|
187
207
|
runs_list.add_argument("--json", action="store_true", help="Output the complete response as JSON (default: table)")
|
|
188
208
|
runs_get = runs.add_parser("get", help="Show a run's rounds, Train Jobs and checkpoints")
|
|
189
209
|
runs_get.add_argument("run", metavar="RUN", help="run label: user_metadata.rsi_run or agent_run")
|
|
190
210
|
runs_get.add_argument("--json", action="store_true", help="Output the complete run as JSON (default: tables)")
|
|
191
211
|
tasks = groups.add_parser("trail", help="Inspect and control existing trials").add_subparsers(dest="action", required=True)
|
|
192
|
-
listing = tasks.add_parser("list")
|
|
212
|
+
listing = tasks.add_parser("list", help="List Agent, Sandbox, Train and Inference worker Trials")
|
|
213
|
+
listing.add_argument("--type", choices=("agent", "sandbox", "train", "inference"), help="Filter by Trial type")
|
|
214
|
+
listing.add_argument("--job-id", help="Filter by parent Job ID")
|
|
193
215
|
listing.add_argument("--cursor")
|
|
194
216
|
listing.add_argument("--limit", type=int, default=25)
|
|
195
217
|
snapshot = tasks.add_parser("snapshot")
|
|
@@ -212,6 +234,17 @@ def parser():
|
|
|
212
234
|
put.add_argument("name")
|
|
213
235
|
put.add_argument("version")
|
|
214
236
|
put.add_argument("--file", type=Path, required=True, help="ProfileSpec JSON file")
|
|
237
|
+
secret_input = put.add_mutually_exclusive_group()
|
|
238
|
+
secret_input.add_argument("--prompt-model-api-key", action="store_true", help="Read the model API key from a non-echoing prompt")
|
|
239
|
+
secret_input.add_argument("--model-api-key-stdin", action="store_true", help="Read the model API key from one line of standard input")
|
|
240
|
+
update = profiles.add_parser("update", help="Edit an existing Profile by publishing a new immutable version")
|
|
241
|
+
update.add_argument("name")
|
|
242
|
+
update.add_argument("version", help="New version; existing versions cannot be overwritten")
|
|
243
|
+
update.add_argument("--from-version", help="Source version; omit to use the explicit default")
|
|
244
|
+
update.add_argument("--file", type=Path, required=True, help="JSON patch: merge literal params keys, replace supplied metadata; preserve omitted fields")
|
|
245
|
+
update_secret = update.add_mutually_exclusive_group()
|
|
246
|
+
update_secret.add_argument("--prompt-model-api-key", action="store_true", help="Read an encrypted model connection key from a non-echoing prompt")
|
|
247
|
+
update_secret.add_argument("--model-api-key-stdin", action="store_true", help="Read an encrypted model connection key from one line of standard input")
|
|
215
248
|
get = profiles.add_parser("get")
|
|
216
249
|
get.add_argument("name")
|
|
217
250
|
get.add_argument("--version")
|
|
@@ -245,11 +278,11 @@ def parser():
|
|
|
245
278
|
|
|
246
279
|
def submission_options(args):
|
|
247
280
|
options = {}
|
|
248
|
-
for key in ("runner", "job_id", "tag", "command"):
|
|
281
|
+
for key in ("runner", "job_id", "tag", "command", "image"):
|
|
249
282
|
value = getattr(args, key, None)
|
|
250
283
|
if value is not None:
|
|
251
284
|
options[key] = value
|
|
252
|
-
options["priority"] = args.priority
|
|
285
|
+
options["priority"] = args.priority if args.priority is not None else 0
|
|
253
286
|
if args.profile:
|
|
254
287
|
if args.runner is not None:
|
|
255
288
|
options["profile_id"] = args.profile
|
|
@@ -322,58 +355,96 @@ def main(argv=None):
|
|
|
322
355
|
|
|
323
356
|
return run_auth(args, cli)
|
|
324
357
|
except (EOFError, KeyboardInterrupt):
|
|
325
|
-
|
|
358
|
+
from .cli_errors import format_error
|
|
359
|
+
from .config import ConfigError
|
|
360
|
+
|
|
361
|
+
print(format_error(ConfigError('AUTH_INPUT_CANCELLED'), args=args, program=cli.prog), file=sys.stderr)
|
|
326
362
|
return 1
|
|
327
363
|
except Exception as exc:
|
|
328
|
-
|
|
364
|
+
from .cli_errors import format_error
|
|
365
|
+
|
|
366
|
+
print(format_error(exc, args=args, program=cli.prog), file=sys.stderr)
|
|
329
367
|
return 1
|
|
330
368
|
if args.group == "job" and args.action == "submit":
|
|
331
|
-
if
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
369
|
+
if args.type in {"train", "inference"}:
|
|
370
|
+
if args.file is None:
|
|
371
|
+
cli.error(f"--type {args.type} requires --file REQUEST.json")
|
|
372
|
+
if args.type == 'inference' and (args.wait or args.timeout_sec is not None):
|
|
373
|
+
cli.error('Inference is a continuous service; --wait and --timeout-sec do not apply')
|
|
374
|
+
agent_fields = ("dataset", "dataset_option", "source", "task_id", "config", "split", "revision",
|
|
375
|
+
"runner", "job_id", "tag", "priority", "command", "attempts", "request_id",
|
|
376
|
+
"profile", "seed", "delete_delay", "image")
|
|
377
|
+
if any(getattr(args, key) is not None for key in agent_fields):
|
|
378
|
+
cli.error(f"--type {args.type} accepts its request through --file; Agent submission options cannot be combined")
|
|
379
|
+
elif args.type == 'sandbox':
|
|
380
|
+
from .sandbox import sandbox_runner_selector, validate_image
|
|
381
|
+
|
|
382
|
+
if any(getattr(args, key) is not None for key in (
|
|
383
|
+
'file', 'dataset', 'dataset_option', 'source', 'task_id', 'config', 'split', 'revision', 'seed')):
|
|
384
|
+
cli.error('--type sandbox accepts --runner without Dataset, Task, Train file or seed options')
|
|
385
|
+
try:
|
|
386
|
+
args.runner = sandbox_runner_selector(args.runner)
|
|
387
|
+
if args.image is not None:
|
|
388
|
+
args.image = validate_image(args.image)
|
|
389
|
+
except ValueError as exc:
|
|
390
|
+
cli.error(str(exc))
|
|
391
|
+
if args.timeout_sec is not None:
|
|
392
|
+
if not args.timeout_sec.is_integer():
|
|
393
|
+
cli.error('Sandbox --timeout-sec must be whole seconds')
|
|
394
|
+
args.timeout_sec = int(args.timeout_sec)
|
|
395
|
+
else:
|
|
396
|
+
if args.file is not None:
|
|
397
|
+
cli.error("--file requires --type train or inference")
|
|
398
|
+
if args.image is not None:
|
|
399
|
+
cli.error('--image requires --type sandbox')
|
|
400
|
+
if args.runner is not None:
|
|
401
|
+
from .client import runner_selector
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
args.runner = runner_selector(args.runner)
|
|
405
|
+
except ValueError as exc:
|
|
406
|
+
cli.error(str(exc))
|
|
407
|
+
if sum(value is not None for value in (args.dataset, args.dataset_option, args.source)) != 1:
|
|
408
|
+
cli.error("provide exactly one positional dataset, --dataset or --source")
|
|
409
|
+
args.dataset = args.dataset if args.dataset is not None else args.dataset_option
|
|
410
|
+
if args.source is not None and any(getattr(args, key) is not None for key in ("runner", "task_id", "config", "split", "revision")):
|
|
411
|
+
cli.error("dataset selection options require a dataset")
|
|
412
|
+
if args.timeout_sec is not None:
|
|
413
|
+
if not args.timeout_sec.is_integer():
|
|
414
|
+
cli.error("Agent --timeout-sec must be whole seconds")
|
|
415
|
+
args.timeout_sec = int(args.timeout_sec)
|
|
336
416
|
args._command_path = args._help_parser.prog.split()[1:]
|
|
337
417
|
if args.group == 'setting' and args.action == 'update' and not any(getattr(args, key) is not None for key in ('name', 'slug', 'region')):
|
|
338
418
|
cli.error('provide at least one of --name, --slug or --region')
|
|
339
419
|
try:
|
|
340
420
|
resolved = resolve_cli_connection(args.api_url, args.workspace_id)
|
|
341
421
|
args.api_url = resolved.api_url
|
|
342
|
-
if args.group == "train" and args.action != "run":
|
|
343
|
-
if args.action == "submit" and args.wait and (not math.isfinite(args.timeout_sec) or args.timeout_sec < 0):
|
|
344
|
-
print("INVALID_TRAIN_WAIT_TIMEOUT", file=sys.stderr)
|
|
345
|
-
return 1
|
|
346
|
-
if not args.api_url:
|
|
347
|
-
print("TRAIN_API_URL_REQUIRED", file=sys.stderr)
|
|
348
|
-
return 1
|
|
349
|
-
from .http import HttpService
|
|
350
|
-
from .train import TrainJobs
|
|
351
|
-
|
|
352
|
-
train = TrainJobs(HttpService(args.api_url, api_key=resolved.api_key, workspace_id=resolved.workspace_id,
|
|
353
|
-
project_id=resolved.project_id, config=False, environment=False))
|
|
354
|
-
if args.action == "submit":
|
|
355
|
-
result = train.submit(json.loads(args.file.read_text(encoding="utf-8")))
|
|
356
|
-
if args.wait:
|
|
357
|
-
result = train.wait(result["train_job_id"], timeout=args.timeout_sec)
|
|
358
|
-
elif args.action == "list":
|
|
359
|
-
result = train.list(status=args.status, cursor=args.cursor, limit=args.limit)
|
|
360
|
-
else:
|
|
361
|
-
result = getattr(train, 'status' if args.action == 'get' else args.action)(args.train_job_id)
|
|
362
|
-
if args.action == "list" and not args.json:
|
|
363
|
-
from .cli_output import list_output
|
|
364
|
-
|
|
365
|
-
print(list_output(result, args, cli.prog))
|
|
366
|
-
else:
|
|
367
|
-
print(json.dumps(result, ensure_ascii=False))
|
|
368
|
-
return 1 if result.get("status") in {"FAILED", "CANCELLED"} else 0
|
|
369
422
|
connection = Client.remote(args.api_url, api_key=resolved.api_key, workspace_id=resolved.workspace_id,
|
|
370
423
|
project_id=resolved.project_id, config=False)
|
|
371
424
|
with connection as client:
|
|
372
425
|
if args.group == "profile":
|
|
373
|
-
if args.action
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
426
|
+
if args.action in {"create", "update"}:
|
|
427
|
+
secrets = None
|
|
428
|
+
if args.prompt_model_api_key:
|
|
429
|
+
import getpass
|
|
430
|
+
import warnings
|
|
431
|
+
|
|
432
|
+
# Fail closed if the terminal cannot disable echo.
|
|
433
|
+
with warnings.catch_warnings():
|
|
434
|
+
warnings.simplefilter("error", getpass.GetPassWarning)
|
|
435
|
+
key = getpass.getpass("Model API key: ")
|
|
436
|
+
secrets = {"model_api_key": key}
|
|
437
|
+
elif args.model_api_key_stdin:
|
|
438
|
+
secrets = {"model_api_key": sys.stdin.readline(4098).rstrip("\r\n")}
|
|
439
|
+
if args.action == "update":
|
|
440
|
+
from .profile_updates import read_profile_patch
|
|
441
|
+
|
|
442
|
+
result = client.profiles.update(args.name, args.version,
|
|
443
|
+
patch=read_profile_patch(args.file.read_bytes()), from_version=args.from_version, secrets=secrets)
|
|
444
|
+
else:
|
|
445
|
+
result = client.profiles.import_spec(
|
|
446
|
+
args.file.read_bytes(), name=args.name, version=args.version, secrets=secrets
|
|
447
|
+
)
|
|
377
448
|
elif args.action == "get":
|
|
378
449
|
result = client.profiles.get(args.name, args.version)
|
|
379
450
|
elif args.action == "list":
|
|
@@ -396,20 +467,37 @@ def main(argv=None):
|
|
|
396
467
|
return 0
|
|
397
468
|
elif args.group == "job":
|
|
398
469
|
if args.action == "submit":
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
470
|
+
if args.type == 'inference':
|
|
471
|
+
result = client.jobs.submit_inference(json.loads(args.file.read_text(encoding='utf-8')))
|
|
472
|
+
elif args.type == "train":
|
|
473
|
+
result = client.training_jobs.submit(json.loads(args.file.read_text(encoding="utf-8")))
|
|
474
|
+
else:
|
|
475
|
+
result = client.jobs.submit(args.dataset, type=args.type, source=args.source, task_id=args.task_id,
|
|
476
|
+
attempts=args.attempts if args.attempts is not None else 1,
|
|
477
|
+
request_id=args.request_id, config=args.config, split=args.split,
|
|
478
|
+
revision=args.revision, **submission_options(args))
|
|
402
479
|
if args.wait:
|
|
403
|
-
|
|
480
|
+
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.timeout_sec is not None else {}
|
|
481
|
+
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
482
|
+
elif args.action == 'event':
|
|
483
|
+
result = client.jobs.events(args.job_id)
|
|
484
|
+
elif args.action in {'serving-profile', 'adapter'}:
|
|
485
|
+
from .resource_cli import run_job_resource
|
|
486
|
+
result = run_job_resource(client, args)
|
|
487
|
+
elif args.action == "log":
|
|
488
|
+
from .train_log_cli import read_logs
|
|
489
|
+
|
|
490
|
+
return read_logs(client.jobs, args)
|
|
491
|
+
elif args.action == "run":
|
|
492
|
+
result = client.agent_runs.list() if args.run_action == "list" else client.agent_runs.get(args.run)
|
|
404
493
|
elif args.action == "list":
|
|
405
494
|
options = {'cursor': args.cursor, 'limit': args.limit}
|
|
406
|
-
|
|
407
|
-
|
|
495
|
+
for field in ('type', 'status'):
|
|
496
|
+
if getattr(args, field) is not None:
|
|
497
|
+
options[field] = getattr(args, field)
|
|
408
498
|
result = client.jobs.list(**options)
|
|
409
499
|
else:
|
|
410
500
|
result = getattr(client.jobs, 'status' if args.action == 'get' else args.action)(args.job_id)
|
|
411
|
-
elif args.group == "train":
|
|
412
|
-
result = client.agent_runs.list() if args.run_action == "list" else client.agent_runs.get(args.run)
|
|
413
501
|
elif args.group == "trail":
|
|
414
502
|
if args.action == "artifact":
|
|
415
503
|
if args.artifact_action == "download":
|
|
@@ -417,19 +505,30 @@ def main(argv=None):
|
|
|
417
505
|
else:
|
|
418
506
|
result = client.trials.artifacts(args.trial_id, cursor=args.cursor, limit=args.limit)
|
|
419
507
|
elif args.action == "list":
|
|
420
|
-
|
|
508
|
+
options = {'cursor': args.cursor, 'limit': args.limit}
|
|
509
|
+
for field in ('type', 'job_id'):
|
|
510
|
+
if getattr(args, field) is not None:
|
|
511
|
+
options[field] = getattr(args, field)
|
|
512
|
+
result = client.trials.list(**options)
|
|
421
513
|
else:
|
|
422
514
|
result = getattr(client.trials, 'status' if args.action == 'get' else args.action)(args.trial_id)
|
|
423
515
|
else:
|
|
424
516
|
from .resource_cli import execute
|
|
425
517
|
|
|
426
518
|
result = execute(args, client)
|
|
427
|
-
if
|
|
519
|
+
if isinstance(result, dict) and 'operation_id' in result:
|
|
520
|
+
from .cli_errors import operation_output
|
|
521
|
+
from .resources import CleanupError
|
|
522
|
+
|
|
523
|
+
if result.get('status') in {'FAILED', 'CANCELLED', 'RETRYABLE_FAILED', 'BLOCKED'}:
|
|
524
|
+
raise CleanupError(result)
|
|
525
|
+
result = operation_output(result)
|
|
526
|
+
if args.group == 'job' and args.action == 'run' and args.run_action == 'get' and not args.json:
|
|
428
527
|
from .cli_output import run_output
|
|
429
528
|
|
|
430
529
|
print(run_output(result))
|
|
431
530
|
elif (args.action == 'list' or getattr(args, '_list_view', False)
|
|
432
|
-
or (args.group == '
|
|
531
|
+
or (args.group == 'job' and args.action == 'run' and args.run_action == 'list')) and not args.json:
|
|
433
532
|
from .cli_output import list_output
|
|
434
533
|
|
|
435
534
|
print(list_output(result, args, cli.prog))
|
|
@@ -437,14 +536,15 @@ def main(argv=None):
|
|
|
437
536
|
print(json.dumps(result, ensure_ascii=False))
|
|
438
537
|
return 1 if isinstance(result, dict) and (result.get('status') in {'failed', 'FAILED', 'CANCELLED', 'RETRYABLE_FAILED', 'BLOCKED'} or result.get('skipped')) else 0
|
|
439
538
|
except Exception as exc:
|
|
539
|
+
from .cli_errors import format_error, operation_output
|
|
440
540
|
from .resources import CleanupError
|
|
441
541
|
|
|
442
542
|
if isinstance(exc, CleanupError):
|
|
443
|
-
print(json.dumps(exc.operation, ensure_ascii=False))
|
|
444
|
-
print(
|
|
543
|
+
print(json.dumps(operation_output(exc.operation), ensure_ascii=False))
|
|
544
|
+
print(format_error(exc, args=args, program=cli.prog), file=sys.stderr)
|
|
445
545
|
return 1
|
|
446
546
|
# Do not emit arbitrary exception messages, file content or credentials.
|
|
447
|
-
print(
|
|
547
|
+
print(format_error(exc, args=args, program=cli.prog), file=sys.stderr)
|
|
448
548
|
return 1
|
|
449
549
|
|
|
450
550
|
|