corethink 1.0.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.
- corethink/__init__.py +17 -0
- corethink/__main__.py +3 -0
- corethink/_version.py +3 -0
- corethink/account.py +470 -0
- corethink/cli.py +1058 -0
- corethink/client.py +253 -0
- corethink/commands/__init__.py +0 -0
- corethink/commands/admin.py +703 -0
- corethink/commands/api.py +59 -0
- corethink/commands/archive.py +433 -0
- corethink/commands/auth.py +375 -0
- corethink/commands/billing.py +314 -0
- corethink/commands/capture.py +564 -0
- corethink/commands/connection.py +1266 -0
- corethink/commands/endpoint.py +1124 -0
- corethink/commands/env.py +1966 -0
- corethink/commands/hub.py +269 -0
- corethink/commands/library.py +347 -0
- corethink/commands/models.py +258 -0
- corethink/commands/train.py +2655 -0
- corethink/commands/workspace.py +622 -0
- corethink/config.py +181 -0
- corethink/context.py +46 -0
- corethink/env_template.py +117 -0
- corethink/environment_http_store.py +96 -0
- corethink/experiment_cli.py +2187 -0
- corethink/hosted_profiles.py +44 -0
- corethink/hosted_rl.py +688 -0
- corethink/hosted_rl_archive.py +74 -0
- corethink/hosted_rl_attribution.py +44 -0
- corethink/hosted_rl_billing.py +103 -0
- corethink/hosted_rl_state.py +182 -0
- corethink/jobs_cli.py +839 -0
- corethink/main.py +204 -0
- corethink/output.py +201 -0
- corethink/py.typed +0 -0
- corethink/rl_cli.py +997 -0
- corethink/rl_inspect.py +521 -0
- corethink/rl_playground.py +129 -0
- corethink/rl_reassessment.py +214 -0
- corethink/rl_settings.py +43 -0
- corethink/sdk.py +331 -0
- corethink/settings.py +162 -0
- corethink-1.0.0.dist-info/METADATA +120 -0
- corethink-1.0.0.dist-info/RECORD +48 -0
- corethink-1.0.0.dist-info/WHEEL +5 -0
- corethink-1.0.0.dist-info/entry_points.txt +2 -0
- corethink-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""`corethink api`: any Workbench API call, signed in. Everything the console can do is reachable through it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from corethink import output as ui
|
|
12
|
+
from corethink.context import client
|
|
13
|
+
|
|
14
|
+
METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def api(
|
|
18
|
+
method: str = typer.Argument(..., help="GET, POST, PUT, PATCH or DELETE."),
|
|
19
|
+
path: str = typer.Argument(..., help="The API path, e.g. /api/v1/harnesses/usecases"),
|
|
20
|
+
data: str = typer.Option(None, "--data", "-d", help="JSON body; @file reads it from a file, @- from stdin."),
|
|
21
|
+
param: list[str] = typer.Option(None, "--param", "-p", help="Query parameter key=value (repeatable)."),
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Call any Workbench API route directly and print its JSON answer.
|
|
24
|
+
|
|
25
|
+
Example: corethink api GET /api/v1/harnesses/training-jobs -p qa=1
|
|
26
|
+
"""
|
|
27
|
+
verb = method.upper()
|
|
28
|
+
if verb not in METHODS:
|
|
29
|
+
ui.error(f"Use one of {', '.join(sorted(METHODS))}.")
|
|
30
|
+
raise typer.Exit(2)
|
|
31
|
+
if not path.startswith("/api/"):
|
|
32
|
+
path = "/api/v1/" + path.lstrip("/")
|
|
33
|
+
body = None
|
|
34
|
+
if data is not None:
|
|
35
|
+
text = (
|
|
36
|
+
sys.stdin.read()
|
|
37
|
+
if data == "@-"
|
|
38
|
+
else Path(data[1:]).read_text(encoding="utf-8")
|
|
39
|
+
if data.startswith("@")
|
|
40
|
+
else data
|
|
41
|
+
)
|
|
42
|
+
try:
|
|
43
|
+
body = json.loads(text) if text.strip() else None
|
|
44
|
+
except ValueError as error:
|
|
45
|
+
ui.error(f"--data is not JSON: {error}")
|
|
46
|
+
raise typer.Exit(2) from None
|
|
47
|
+
params = {}
|
|
48
|
+
for item in param or []:
|
|
49
|
+
key, sep, value = item.partition("=")
|
|
50
|
+
if not sep:
|
|
51
|
+
ui.error(f"--param {item}: use key=value.")
|
|
52
|
+
raise typer.Exit(2)
|
|
53
|
+
params[key] = value
|
|
54
|
+
answer = client().request(verb, path, json=body, params=params or None)
|
|
55
|
+
ui.print_json(answer)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register(root: typer.Typer) -> None:
|
|
59
|
+
root.command(rich_help_panel="Advanced")(api)
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"""`corethink archive`: archived runs, tests and environments; restore them, or (admins) delete them for good.
|
|
2
|
+
|
|
3
|
+
As the console's Archive page: restoring a run or test is for members, an environment for admins; deleting is for admins,
|
|
4
|
+
after the server's own preview of what would go. "Delete all" deletes a few at a time and says what did not go, and why.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
|
|
14
|
+
from corethink import output as ui
|
|
15
|
+
from corethink.client import ApiError
|
|
16
|
+
from corethink.commands.train import (
|
|
17
|
+
ARCHIVE,
|
|
18
|
+
JOBS,
|
|
19
|
+
USECASES,
|
|
20
|
+
_test_result,
|
|
21
|
+
base_vs_trained,
|
|
22
|
+
fail,
|
|
23
|
+
pick_run,
|
|
24
|
+
plural,
|
|
25
|
+
run_environment,
|
|
26
|
+
run_name,
|
|
27
|
+
)
|
|
28
|
+
from corethink.context import client
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(
|
|
31
|
+
help="Archived runs, tests and environments: restore them, or delete them for good.",
|
|
32
|
+
no_args_is_help=True,
|
|
33
|
+
rich_markup_mode="rich",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# how many ids one bulk delete request carries (the console's BULK_CHUNK; the server takes at most 25)
|
|
37
|
+
BULK_CHUNK = 10
|
|
38
|
+
DELETE_ALL_WORD = "delete all"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_archive() -> dict[str, Any]:
|
|
42
|
+
answer = client().get(ARCHIVE) or {}
|
|
43
|
+
return {
|
|
44
|
+
"runs": list(answer.get("runs") or []),
|
|
45
|
+
"environments": list(answer.get("environments") or []),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def runs_and_tests(runs: int, tests: int) -> str:
|
|
50
|
+
return " and ".join(p for p in (plural(runs, "run") if runs else "", plural(tests, "test") if tests else "") if p)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _result(run: dict[str, Any]) -> str | None:
|
|
54
|
+
if run.get("kind") == "test":
|
|
55
|
+
return _test_result(run)
|
|
56
|
+
if run.get("state") != "SUCCEEDED":
|
|
57
|
+
return str(run.get("state") or "").lower() or None
|
|
58
|
+
scores = base_vs_trained(run.get("gate"))
|
|
59
|
+
if scores["base"] is None or scores["trained"] is None:
|
|
60
|
+
return None
|
|
61
|
+
return f"{ui.score(scores['base'])} → {ui.score(scores['trained'])}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _archived(at: Any, by: Any) -> str:
|
|
65
|
+
who = f" by {by}" if by and not str(by).startswith("local:") else ""
|
|
66
|
+
return f"{ui.when(at)}{who}" if at else (f"archived{who}" if who else "—")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command("list")
|
|
70
|
+
def list_archive() -> None:
|
|
71
|
+
"""Archived environments, runs and tests in this workspace.
|
|
72
|
+
|
|
73
|
+
Example: corethink archive list
|
|
74
|
+
"""
|
|
75
|
+
archive = load_archive()
|
|
76
|
+
|
|
77
|
+
def human() -> None:
|
|
78
|
+
environments, runs = archive["environments"], archive["runs"]
|
|
79
|
+
if not environments and not runs:
|
|
80
|
+
ui.info(
|
|
81
|
+
"Nothing is archived. Archive a run with `corethink train archive RUN`: it waits here, with everything "
|
|
82
|
+
"it had, until it is restored or deleted for good."
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
if environments:
|
|
86
|
+
ui.table(
|
|
87
|
+
environments,
|
|
88
|
+
[
|
|
89
|
+
("ID", "id"),
|
|
90
|
+
("Name", "name"),
|
|
91
|
+
("Runs", lambda e: str(e.get("runs", 0))),
|
|
92
|
+
("Tests", lambda e: str(e.get("tests", 0))),
|
|
93
|
+
("Archived", lambda e: _archived(e.get("archived_at"), e.get("archived_by"))),
|
|
94
|
+
],
|
|
95
|
+
title=f"Environments · {len(environments)}",
|
|
96
|
+
)
|
|
97
|
+
ui.out.print()
|
|
98
|
+
if runs:
|
|
99
|
+
archived_envs = {e.get("id") for e in environments}
|
|
100
|
+
names = {e.get("id"): e.get("name") for e in environments}
|
|
101
|
+
ui.table(
|
|
102
|
+
runs,
|
|
103
|
+
[
|
|
104
|
+
("ID", lambda r: ui.short_id(r.get("job_id"))),
|
|
105
|
+
("Name", run_name),
|
|
106
|
+
(
|
|
107
|
+
"Kind",
|
|
108
|
+
lambda r: "Base-model test" if r.get("kind") == "test" else "Training run",
|
|
109
|
+
),
|
|
110
|
+
("Environment", lambda r: run_environment(r, names)),
|
|
111
|
+
("Result", _result),
|
|
112
|
+
(
|
|
113
|
+
"Archived",
|
|
114
|
+
lambda r: (
|
|
115
|
+
_archived(r.get("archived_at"), r.get("archived_by"))
|
|
116
|
+
+ (" with its environment" if r.get("archived_with") else "")
|
|
117
|
+
+ (
|
|
118
|
+
" (environment archived)"
|
|
119
|
+
if r.get("usecase") in archived_envs and not r.get("archived_with")
|
|
120
|
+
else ""
|
|
121
|
+
)
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
],
|
|
125
|
+
title=f"Runs and tests · {len(runs)}",
|
|
126
|
+
)
|
|
127
|
+
ui.info("An archived run's or environment's endpoint keeps serving what it served when it was archived.")
|
|
128
|
+
|
|
129
|
+
ui.emit(archive, human)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _which(ref: str, archive: dict[str, Any], kind: str | None) -> tuple[str, dict[str, Any]]:
|
|
133
|
+
"""("environment" | "run", the archived thing) that `ref` names."""
|
|
134
|
+
wanted = ref.strip()
|
|
135
|
+
if kind != "run":
|
|
136
|
+
envs = archive["environments"]
|
|
137
|
+
exact = [e for e in envs if e.get("id") == wanted]
|
|
138
|
+
found = (
|
|
139
|
+
exact
|
|
140
|
+
or [e for e in envs if str(e.get("id", "")).startswith(wanted)]
|
|
141
|
+
or [e for e in envs if str(e.get("name", "")).lower() == wanted.lower()]
|
|
142
|
+
)
|
|
143
|
+
if len(found) == 1:
|
|
144
|
+
return "environment", found[0]
|
|
145
|
+
if len(found) > 1:
|
|
146
|
+
raise fail(
|
|
147
|
+
f"{wanted!r} matches {len(found)} archived environments: {', '.join(e['id'] for e in found[:6])}."
|
|
148
|
+
)
|
|
149
|
+
if kind == "environment":
|
|
150
|
+
raise fail(f"No archived environment matches {wanted!r}. See `corethink archive list`.", 5)
|
|
151
|
+
return "run", pick_run(wanted, archive["runs"], where=" in the Archive")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def restore_run(ref: str) -> None:
|
|
155
|
+
"""Restore an archived run or test (members): `corethink train restore` and `corethink archive restore`."""
|
|
156
|
+
archive = load_archive()
|
|
157
|
+
_restore("run", pick_run(ref, archive["runs"], where=" in the Archive"), archive)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _restore(kind: str, thing: dict[str, Any], archive: dict[str, Any]) -> None:
|
|
161
|
+
if kind == "environment":
|
|
162
|
+
summary = client().post(f"{USECASES}/{thing['id']}:restore")
|
|
163
|
+
moved = (summary or {}).get("moved") or {}
|
|
164
|
+
|
|
165
|
+
def human() -> None:
|
|
166
|
+
ui.success(
|
|
167
|
+
f"Restored {thing.get('name') or thing['id']}, with {plural(moved.get('runs', 0), 'run')} and "
|
|
168
|
+
f"{plural(moved.get('tests', 0), 'test')}."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
ui.emit(summary, human)
|
|
172
|
+
return
|
|
173
|
+
name = run_name(thing)
|
|
174
|
+
noun = "test" if thing.get("kind") == "test" else "run"
|
|
175
|
+
if thing.get("usecase") in {e.get("id") for e in archive["environments"]}:
|
|
176
|
+
if thing.get("archived_with"):
|
|
177
|
+
raise fail(
|
|
178
|
+
f"{noun.capitalize()} {name} was archived with its environment: restoring the environment brings it "
|
|
179
|
+
f"back (`corethink archive restore {thing['usecase']}`).",
|
|
180
|
+
6,
|
|
181
|
+
)
|
|
182
|
+
raise fail(
|
|
183
|
+
f"Its environment is archived: restore the environment first (`corethink archive restore {thing['usecase']}`).",
|
|
184
|
+
6,
|
|
185
|
+
)
|
|
186
|
+
summary = client().post(f"{JOBS}/{thing['job_id']}:restore")
|
|
187
|
+
ui.emit(
|
|
188
|
+
summary,
|
|
189
|
+
lambda: ui.success(f"Restored {noun} {name}. See it with `corethink train show {str(thing['job_id'])[:8]}`."),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@app.command("restore")
|
|
194
|
+
def restore(
|
|
195
|
+
ref: str = typer.Argument(
|
|
196
|
+
..., help="An archived run's id (or the start of it, or its name), or an environment's id."
|
|
197
|
+
),
|
|
198
|
+
environment: bool = typer.Option(False, "--env", help="It is an environment."),
|
|
199
|
+
run: bool = typer.Option(False, "--run", help="It is a run or test."),
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Bring an archived run, test or environment back (an environment comes back with the runs archived with it).
|
|
202
|
+
|
|
203
|
+
Examples:
|
|
204
|
+
corethink archive restore 3f2a
|
|
205
|
+
corethink archive restore payroll-0ecb50 --env
|
|
206
|
+
"""
|
|
207
|
+
if environment and run:
|
|
208
|
+
raise fail("Choose --env or --run, not both.")
|
|
209
|
+
archive = load_archive()
|
|
210
|
+
kind, thing = _which(ref, archive, "environment" if environment else "run" if run else None)
|
|
211
|
+
_restore(kind, thing, archive)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def deletion_lines(summary: dict[str, Any]) -> list[str]:
|
|
215
|
+
"""What a delete removes, line by line, from the server's own count (DeleteDialog.tsx deletionLines)."""
|
|
216
|
+
d = summary.get("deleted") or {}
|
|
217
|
+
target = (summary.get("target") or {}).get("kind")
|
|
218
|
+
lines: list[str] = []
|
|
219
|
+
if target == "environment":
|
|
220
|
+
if d.get("runs") or d.get("tests"):
|
|
221
|
+
what = " and ".join(
|
|
222
|
+
p
|
|
223
|
+
for p in (
|
|
224
|
+
plural(d.get("runs", 0), "training run") if d.get("runs") else "",
|
|
225
|
+
plural(d.get("tests", 0), "base-model test") if d.get("tests") else "",
|
|
226
|
+
)
|
|
227
|
+
if p
|
|
228
|
+
)
|
|
229
|
+
lines.append(
|
|
230
|
+
f"{what}, with their logs, charts, final checks and files ({plural(d.get('run_files', 0), 'file')})"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
lines.append("No runs or tests")
|
|
234
|
+
else:
|
|
235
|
+
lines.append(
|
|
236
|
+
f"The {'test' if target == 'test' else 'run'} and its record: logs, charts, final check and files "
|
|
237
|
+
f"({plural(d.get('run_files', 0), 'file')})"
|
|
238
|
+
)
|
|
239
|
+
if d.get("packages"):
|
|
240
|
+
lines.append(
|
|
241
|
+
("The learned program's package" if d["packages"] == 1 else f"{d['packages']} learned-program packages")
|
|
242
|
+
+ f" and {'its' if d.get('gate_results') == 1 else 'their'} final-check result"
|
|
243
|
+
)
|
|
244
|
+
aliases = summary.get("cleared_aliases") or []
|
|
245
|
+
for alias in aliases:
|
|
246
|
+
if alias.endswith(" canary"):
|
|
247
|
+
lines.append(f"The canary on {alias[: -len(' canary')]}")
|
|
248
|
+
else:
|
|
249
|
+
lines.append(
|
|
250
|
+
f"What {alias} serves: the endpoint has nothing on {alias.split('@')[1] if '@' in alias else 'it'} afterwards"
|
|
251
|
+
)
|
|
252
|
+
if d.get("canary_plans") and not any(a.endswith(" canary") for a in aliases):
|
|
253
|
+
lines.append(f"Its endpoint's canary records ({d['canary_plans']})")
|
|
254
|
+
if d.get("endpoint_keys"):
|
|
255
|
+
lines.append(
|
|
256
|
+
f"{plural(d['endpoint_keys'], 'endpoint key')}: callers using {'it' if d['endpoint_keys'] == 1 else 'them'} stop working"
|
|
257
|
+
)
|
|
258
|
+
if target == "environment":
|
|
259
|
+
lines.append(
|
|
260
|
+
"The environment's own files: tasks, tools, traces, captured conversations and labels "
|
|
261
|
+
f"({plural(d.get('environment_files', 0), 'file')})"
|
|
262
|
+
)
|
|
263
|
+
for kept in summary.get("kept") or []:
|
|
264
|
+
lines.append(f"Kept: {kept}")
|
|
265
|
+
return lines
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _typed(expected: str, yes: bool, question: str) -> None:
|
|
269
|
+
"""Ask for `expected` to be typed (skipped with --yes; refused without a terminal)."""
|
|
270
|
+
if yes:
|
|
271
|
+
return
|
|
272
|
+
if not sys.stdin.isatty():
|
|
273
|
+
ui.error(f"{question} Re-run with --yes to confirm without a prompt.")
|
|
274
|
+
raise typer.Exit(2)
|
|
275
|
+
typed = typer.prompt(f"Type {expected} to confirm", default="", show_default=False)
|
|
276
|
+
if typed.strip().lower() != expected.lower():
|
|
277
|
+
ui.error("That does not match; nothing was deleted.")
|
|
278
|
+
raise typer.Exit(1)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@app.command("delete")
|
|
282
|
+
def delete(
|
|
283
|
+
ref: str = typer.Argument(
|
|
284
|
+
..., help="An archived run's id (or the start of it, or its name), or an environment's id."
|
|
285
|
+
),
|
|
286
|
+
environment: bool = typer.Option(False, "--env", help="It is an environment."),
|
|
287
|
+
run: bool = typer.Option(False, "--run", help="It is a run or test."),
|
|
288
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Delete without asking."),
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Delete an archived run, test or environment for good (admins). Shows what would go first.
|
|
291
|
+
|
|
292
|
+
An environment takes its archived runs and tests, its endpoint (staging, keys) and its files with it.
|
|
293
|
+
This cannot be undone.
|
|
294
|
+
|
|
295
|
+
Example: corethink archive delete 3f2a
|
|
296
|
+
"""
|
|
297
|
+
if environment and run:
|
|
298
|
+
raise fail("Choose --env or --run, not both.")
|
|
299
|
+
archive = load_archive()
|
|
300
|
+
kind, thing = _which(ref, archive, "environment" if environment else "run" if run else None)
|
|
301
|
+
if kind == "environment":
|
|
302
|
+
path, name, typed = (
|
|
303
|
+
f"{ARCHIVE}/usecases/{thing['id']}",
|
|
304
|
+
thing.get("name") or thing["id"],
|
|
305
|
+
thing["id"],
|
|
306
|
+
)
|
|
307
|
+
noun = "environment"
|
|
308
|
+
else:
|
|
309
|
+
path, name, typed = (
|
|
310
|
+
f"{ARCHIVE}/training-jobs/{thing['job_id']}",
|
|
311
|
+
run_name(thing),
|
|
312
|
+
str(thing["job_id"])[:8],
|
|
313
|
+
)
|
|
314
|
+
noun = "test" if thing.get("kind") == "test" else "run"
|
|
315
|
+
preview = client().get(f"{path}/deletion") or {}
|
|
316
|
+
if not preview.get("deletable", False):
|
|
317
|
+
raise fail(
|
|
318
|
+
f"{noun.capitalize()} {name} cannot be deleted now: {preview.get('reason') or 'the Workbench refused'}.",
|
|
319
|
+
6,
|
|
320
|
+
)
|
|
321
|
+
summary = preview.get("summary") or {}
|
|
322
|
+
ui.err.print(f"Deleting {noun} {name} for good removes:")
|
|
323
|
+
for line in deletion_lines(summary):
|
|
324
|
+
ui.err.print(f" • {line}")
|
|
325
|
+
ui.err.print("This cannot be undone.", style="bold red")
|
|
326
|
+
_typed(typed, yes, f"Delete {noun} {name} for good?")
|
|
327
|
+
done = client().delete(path)
|
|
328
|
+
|
|
329
|
+
def human() -> None:
|
|
330
|
+
deleted = (done or {}).get("deleted") or {}
|
|
331
|
+
if kind == "environment":
|
|
332
|
+
ui.success(
|
|
333
|
+
f"Deleted {name}, with {plural(deleted.get('runs', 0), 'run')} and {plural(deleted.get('tests', 0), 'test')}."
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
cleared = (done or {}).get("cleared_aliases") or []
|
|
337
|
+
ui.success(f"Deleted {noun} {name}" + (f"; {', '.join(cleared)} is empty now" if cleared else "") + ".")
|
|
338
|
+
|
|
339
|
+
ui.emit(done, human)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def delete_in_chunks(ids: list[str], path: str) -> dict[str, Any]:
|
|
343
|
+
"""Delete `ids` for good, BULK_CHUNK at a time; a request that fails as a whole stops the rest."""
|
|
344
|
+
result: dict[str, Any] = {"asked": len(ids), "deleted": [], "failed": [], "stopped": None}
|
|
345
|
+
for at in range(0, len(ids), BULK_CHUNK):
|
|
346
|
+
chunk = ids[at : at + BULK_CHUNK]
|
|
347
|
+
try:
|
|
348
|
+
answer = client().post(path, {"ids": chunk}) or {}
|
|
349
|
+
except ApiError as problem:
|
|
350
|
+
result["stopped"] = problem.message
|
|
351
|
+
break
|
|
352
|
+
result["deleted"].extend(answer.get("deleted") or [])
|
|
353
|
+
result["failed"].extend(answer.get("failed") or [])
|
|
354
|
+
ui.info(f"Deleting… {min(len(ids), at + len(chunk))} of {len(ids)}")
|
|
355
|
+
return result
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@app.command("delete-all")
|
|
359
|
+
def delete_all(
|
|
360
|
+
what: str = typer.Argument(..., help="runs (archived runs and tests) or environments."),
|
|
361
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Delete without asking."),
|
|
362
|
+
) -> None:
|
|
363
|
+
"""Delete every archived run and test, or every archived environment, for good (admins).
|
|
364
|
+
|
|
365
|
+
Any that cannot be deleted are skipped and listed. Spent credit stays in the ledger. This cannot be undone.
|
|
366
|
+
|
|
367
|
+
Example: corethink archive delete-all runs
|
|
368
|
+
"""
|
|
369
|
+
if what not in ("runs", "environments"):
|
|
370
|
+
raise fail("Say what to delete: runs or environments.")
|
|
371
|
+
archive = load_archive()
|
|
372
|
+
if what == "runs":
|
|
373
|
+
items = archive["runs"]
|
|
374
|
+
ids = [str(r["job_id"]) for r in items]
|
|
375
|
+
label = runs_and_tests(sum(r.get("kind") != "test" for r in items), sum(r.get("kind") == "test" for r in items))
|
|
376
|
+
lines = [
|
|
377
|
+
"Each one's record, logs, charts, final check and files",
|
|
378
|
+
"Its learned program's package, unless another run or endpoint still uses it",
|
|
379
|
+
"What its endpoint's staging serves, when that is its package",
|
|
380
|
+
]
|
|
381
|
+
unit, path = "runs and tests", f"{ARCHIVE}/training-jobs:delete"
|
|
382
|
+
name_of = {str(r["job_id"]): run_name(r) for r in items}
|
|
383
|
+
else:
|
|
384
|
+
items = archive["environments"]
|
|
385
|
+
ids = [str(e["id"]) for e in items]
|
|
386
|
+
label = plural(len(items), "environment")
|
|
387
|
+
runs = sum(int(e.get("runs") or 0) for e in items)
|
|
388
|
+
tests = sum(int(e.get("tests") or 0) for e in items)
|
|
389
|
+
lines = [
|
|
390
|
+
f"Their archived runs and tests{f' ({runs_and_tests(runs, tests)})' if runs or tests else ''}, with their logs, charts and files",
|
|
391
|
+
"Their endpoints: staging, canaries and keys. Callers using those keys stop working",
|
|
392
|
+
"Their files: tasks, tools, traces, captured conversations and labels",
|
|
393
|
+
]
|
|
394
|
+
unit, path = "environments", f"{ARCHIVE}/usecases:delete"
|
|
395
|
+
name_of = {str(e["id"]): e.get("name") or e["id"] for e in items}
|
|
396
|
+
if not ids:
|
|
397
|
+
ui.info(f"No archived {what}.")
|
|
398
|
+
ui.emit({"asked": 0, "deleted": [], "failed": [], "stopped": None}, lambda: None)
|
|
399
|
+
return
|
|
400
|
+
ui.err.print(f"Delete {label} for good? This removes:")
|
|
401
|
+
for line in lines:
|
|
402
|
+
ui.err.print(f" • {line}")
|
|
403
|
+
ui.err.print(
|
|
404
|
+
"Any that cannot be deleted are skipped and listed. Spent credit stays in the ledger.",
|
|
405
|
+
style="dim",
|
|
406
|
+
)
|
|
407
|
+
ui.err.print("This cannot be undone.", style="bold red")
|
|
408
|
+
if what == "environments":
|
|
409
|
+
_typed(DELETE_ALL_WORD, yes, f"Delete {label} for good?")
|
|
410
|
+
else:
|
|
411
|
+
ui.confirm(f"Delete {label} for good?", yes=yes)
|
|
412
|
+
result = delete_in_chunks(ids, path)
|
|
413
|
+
|
|
414
|
+
def human() -> None:
|
|
415
|
+
deleted = len(result["deleted"])
|
|
416
|
+
part = f"{deleted} of {result['asked']} {unit}"
|
|
417
|
+
if result["stopped"]:
|
|
418
|
+
ui.error(f"Deleted {part}, then stopped: {result['stopped']}")
|
|
419
|
+
elif deleted == result["asked"]:
|
|
420
|
+
ui.success(f"Deleted {label}.")
|
|
421
|
+
else:
|
|
422
|
+
failed = len(result["failed"])
|
|
423
|
+
ui.warn(f"Deleted {part}. {failed} {'was' if failed == 1 else 'were'} not deleted:")
|
|
424
|
+
for failure in result["failed"]:
|
|
425
|
+
ui.err.print(f" • {name_of.get(str(failure.get('id')), failure.get('id'))}: {failure.get('reason')}")
|
|
426
|
+
|
|
427
|
+
ui.emit(result, human)
|
|
428
|
+
if result["stopped"] or result["failed"]:
|
|
429
|
+
raise typer.Exit(1)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def register(root: typer.Typer) -> None:
|
|
433
|
+
root.add_typer(app, name="archive", rich_help_panel="Manage")
|