valkyrie-cli 2026.11.9__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.
- valkyrie/cli/agent/__init__.py +27 -0
- valkyrie/cli/agent/lifecycle.py +130 -0
- valkyrie/cli/benchmark/__init__.py +29 -0
- valkyrie/cli/benchmark/definitions.py +73 -0
- valkyrie/cli/benchmark/services.py +147 -0
- valkyrie/cli/benchmark/tasks.py +65 -0
- valkyrie/cli/client.py +50 -0
- valkyrie/cli/dataset.py +182 -0
- valkyrie/cli/display.py +158 -0
- valkyrie/cli/entry.py +12 -0
- valkyrie/cli/lazy.py +38 -0
- valkyrie/cli/logging.py +20 -0
- valkyrie/cli/login.py +115 -0
- valkyrie/cli/main.py +62 -0
- valkyrie/cli/org.py +121 -0
- valkyrie/cli/project.py +339 -0
- valkyrie/cli/provider.py +68 -0
- valkyrie/cli/queue.py +105 -0
- valkyrie/cli/resource.py +65 -0
- valkyrie/cli/run/__init__.py +50 -0
- valkyrie/cli/run/analyze.py +81 -0
- valkyrie/cli/run/errors.py +113 -0
- valkyrie/cli/run/fetch.py +46 -0
- valkyrie/cli/run/filter_options.py +18 -0
- valkyrie/cli/run/list_runs.py +217 -0
- valkyrie/cli/run/logs.py +109 -0
- valkyrie/cli/run/outputs.py +111 -0
- valkyrie/cli/run/progress.py +264 -0
- valkyrie/cli/run/results.py +127 -0
- valkyrie/cli/run/resume.py +168 -0
- valkyrie/cli/run/retag.py +28 -0
- valkyrie/cli/run/shed.py +48 -0
- valkyrie/cli/run/snapshot.py +142 -0
- valkyrie/cli/run/start.py +386 -0
- valkyrie/cli/run/status.py +88 -0
- valkyrie/cli/run/stop.py +59 -0
- valkyrie/cli/run/task_ids.py +51 -0
- valkyrie/cli/run/tasks.py +98 -0
- valkyrie/cli/run/update.py +25 -0
- valkyrie/cli/secret.py +65 -0
- valkyrie/cli/ui.py +20 -0
- valkyrie/display.py +36 -0
- valkyrie/tui/__init__.py +1 -0
- valkyrie/tui/app.py +97 -0
- valkyrie/tui/browse.py +167 -0
- valkyrie/tui/datasets.py +219 -0
- valkyrie/tui/errors.py +128 -0
- valkyrie/tui/fake.py +452 -0
- valkyrie/tui/fixtures/runs.json +128 -0
- valkyrie/tui/run.py +692 -0
- valkyrie/tui/runs.py +310 -0
- valkyrie/tui/source.py +137 -0
- valkyrie/tui/theme.py +126 -0
- valkyrie/tui/valkyrie.tcss +185 -0
- valkyrie/tui/widgets.py +214 -0
- valkyrie_cli-2026.11.9.dist-info/METADATA +105 -0
- valkyrie_cli-2026.11.9.dist-info/RECORD +60 -0
- valkyrie_cli-2026.11.9.dist-info/WHEEL +4 -0
- valkyrie_cli-2026.11.9.dist-info/entry_points.txt +3 -0
- valkyrie_cli-2026.11.9.dist-info/licenses/LICENSE +661 -0
valkyrie/cli/dataset.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from valkyrie.cli.client import call
|
|
6
|
+
from valkyrie.cli.display import format_option, format_table, local_time, one_line
|
|
7
|
+
from valkyrie.cli.resource import (
|
|
8
|
+
INCLUDE_ARCHIVED,
|
|
9
|
+
RESOURCE_NAME,
|
|
10
|
+
TASK_ID,
|
|
11
|
+
list_resources,
|
|
12
|
+
show_resource,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.group(help="Create, list, show, update and remove the datasets that belong to your benchmarks.")
|
|
17
|
+
def dataset() -> None:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataset.command(
|
|
22
|
+
name="create",
|
|
23
|
+
help=(
|
|
24
|
+
"Create a dataset in a benchmark.\n\n"
|
|
25
|
+
"BENCHMARK is a benchmark you created with `valkyrie benchmark create`. NAME is lowercase letters and "
|
|
26
|
+
"digits with single '.', '_' or '-' separators, such as verified.\n\n"
|
|
27
|
+
"Example:\n"
|
|
28
|
+
' valkyrie dataset create swe-bench verified --description "Human-verified issues"'
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
32
|
+
@click.argument("name", metavar="NAME", type=RESOURCE_NAME)
|
|
33
|
+
@click.option("--description", default="", help="What the dataset holds.")
|
|
34
|
+
def create_dataset(benchmark: str, name: str, description: str) -> None:
|
|
35
|
+
created = call(lambda client: client.datasets.create(benchmark, name, description=description))
|
|
36
|
+
click.echo(
|
|
37
|
+
click.style(f"✓ Dataset created: {one_line(created.benchmark)}/{one_line(created.name)}", fg="green", bold=True)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataset.command(name="list", help="List the datasets in a benchmark.")
|
|
42
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
43
|
+
@INCLUDE_ARCHIVED
|
|
44
|
+
@format_option()
|
|
45
|
+
def list_datasets(benchmark: str, include_archived: bool, output_format: str) -> None:
|
|
46
|
+
response = call(lambda client: client.datasets.list(benchmark, include_archived=include_archived))
|
|
47
|
+
list_resources(response, response.datasets, output_format, "dataset")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataset.command(name="show", help="Show one dataset, archived or not.")
|
|
51
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
52
|
+
@click.argument("name", metavar="NAME", type=RESOURCE_NAME)
|
|
53
|
+
@format_option()
|
|
54
|
+
def show_dataset(benchmark: str, name: str, output_format: str) -> None:
|
|
55
|
+
found = call(lambda client: client.datasets.get(benchmark, name))
|
|
56
|
+
show_resource(found, output_format, f"Benchmark: {one_line(found.benchmark)}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataset.command(
|
|
60
|
+
name="update",
|
|
61
|
+
help=(
|
|
62
|
+
"Change a dataset's description. Its name never changes.\n\n"
|
|
63
|
+
"Example:\n"
|
|
64
|
+
' valkyrie dataset update swe-bench verified --description "SWE-bench Verified"'
|
|
65
|
+
),
|
|
66
|
+
)
|
|
67
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
68
|
+
@click.argument("name", metavar="NAME", type=RESOURCE_NAME)
|
|
69
|
+
@click.option("--description", required=True, help="The description the dataset now has.")
|
|
70
|
+
def update_dataset(benchmark: str, name: str, description: str) -> None:
|
|
71
|
+
updated = call(lambda client: client.datasets.update(benchmark, name, description=description))
|
|
72
|
+
click.echo(
|
|
73
|
+
click.style(f"✓ Dataset updated: {one_line(updated.benchmark)}/{one_line(updated.name)}", fg="green", bold=True)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataset.command(
|
|
78
|
+
name="remove",
|
|
79
|
+
help="Archive a dataset. It stays readable, keeps its name, and can be restored.",
|
|
80
|
+
)
|
|
81
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
82
|
+
@click.argument("name", metavar="NAME", type=RESOURCE_NAME)
|
|
83
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
|
|
84
|
+
def remove_dataset(benchmark: str, name: str, yes: bool) -> None:
|
|
85
|
+
if not yes and not click.confirm(f"Archive dataset '{benchmark}/{name}'?"):
|
|
86
|
+
click.echo("Cancelled.")
|
|
87
|
+
return
|
|
88
|
+
call(lambda client: client.datasets.remove(benchmark, name))
|
|
89
|
+
click.echo(click.style(f"✓ Dataset '{benchmark}/{name}' archived.", fg="green", bold=True))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataset.command(name="restore", help="Restore an archived dataset so its tasks can change again.")
|
|
93
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
94
|
+
@click.argument("name", metavar="NAME", type=RESOURCE_NAME)
|
|
95
|
+
def restore_dataset(benchmark: str, name: str) -> None:
|
|
96
|
+
restored = call(lambda client: client.datasets.restore(benchmark, name))
|
|
97
|
+
click.echo(
|
|
98
|
+
click.style(
|
|
99
|
+
f"✓ Dataset restored: {one_line(restored.benchmark)}/{one_line(restored.name)}", fg="green", bold=True
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataset.group(name="task", help="Store, list, download and remove the task archives of a dataset.")
|
|
105
|
+
def task() -> None:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@task.command(
|
|
110
|
+
name="put",
|
|
111
|
+
help=(
|
|
112
|
+
"Store a task's .zip archive in a dataset, replacing the task if it exists.\n\n"
|
|
113
|
+
"TASK_ID starts with a letter or digit and uses letters, digits, '.', '_' and '-'.\n\n"
|
|
114
|
+
"Example:\n"
|
|
115
|
+
" valkyrie dataset task put swe-bench verified django__django-11099 ./django__django-11099.zip"
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
119
|
+
@click.argument("dataset_name", metavar="DATASET", type=RESOURCE_NAME)
|
|
120
|
+
@click.argument("task_id", metavar="TASK_ID", type=TASK_ID)
|
|
121
|
+
@click.argument("archive", metavar="ARCHIVE", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
122
|
+
def put_task(benchmark: str, dataset_name: str, task_id: str, archive: Path) -> None:
|
|
123
|
+
stored = call(lambda client: client.datasets.tasks.put(benchmark, dataset_name, task_id, archive))
|
|
124
|
+
click.echo(
|
|
125
|
+
click.style(
|
|
126
|
+
f"✓ Task stored: {benchmark}/{dataset_name}/{one_line(stored.task_id)} ({stored.size:,} bytes)",
|
|
127
|
+
fg="green",
|
|
128
|
+
bold=True,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@task.command(name="list", help="List the tasks stored in a dataset.")
|
|
134
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
135
|
+
@click.argument("dataset_name", metavar="DATASET", type=RESOURCE_NAME)
|
|
136
|
+
@format_option()
|
|
137
|
+
def list_tasks(benchmark: str, dataset_name: str, output_format: str) -> None:
|
|
138
|
+
response = call(lambda client: client.datasets.tasks.list_all(benchmark, dataset_name))
|
|
139
|
+
if output_format == "json":
|
|
140
|
+
click.echo(response.model_dump_json())
|
|
141
|
+
return
|
|
142
|
+
format_table(
|
|
143
|
+
[
|
|
144
|
+
{
|
|
145
|
+
"Task": one_line(entry.task_id),
|
|
146
|
+
"Bytes": f"{entry.size:,}",
|
|
147
|
+
"Updated": local_time(entry.last_modified) if entry.last_modified else "",
|
|
148
|
+
}
|
|
149
|
+
for entry in response.tasks
|
|
150
|
+
],
|
|
151
|
+
["Task", "Bytes", "Updated"],
|
|
152
|
+
item_name="task",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@task.command(name="download", help="Download a task's archive. It is saved as TASK_ID.zip unless you pass --output.")
|
|
157
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
158
|
+
@click.argument("dataset_name", metavar="DATASET", type=RESOURCE_NAME)
|
|
159
|
+
@click.argument("task_id", metavar="TASK_ID", type=TASK_ID)
|
|
160
|
+
@click.option("--output", "-o", type=click.Path(dir_okay=False, path_type=Path), help="Where to save the archive.")
|
|
161
|
+
@click.option("--overwrite", is_flag=True, help="Replace the file if it exists.")
|
|
162
|
+
def download_task(benchmark: str, dataset_name: str, task_id: str, output: Path | None, overwrite: bool) -> None:
|
|
163
|
+
destination = output or Path(f"{task_id}.zip")
|
|
164
|
+
saved = call(
|
|
165
|
+
lambda client: client.datasets.tasks.download(
|
|
166
|
+
benchmark, dataset_name, task_id, destination, overwrite=overwrite
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
click.echo(click.style(f"✓ Task downloaded: {saved}", fg="green", bold=True))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@task.command(name="remove", help="Delete a task's archive from a dataset.")
|
|
173
|
+
@click.argument("benchmark", metavar="BENCHMARK", type=RESOURCE_NAME)
|
|
174
|
+
@click.argument("dataset_name", metavar="DATASET", type=RESOURCE_NAME)
|
|
175
|
+
@click.argument("task_id", metavar="TASK_ID", type=TASK_ID)
|
|
176
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
|
|
177
|
+
def remove_task(benchmark: str, dataset_name: str, task_id: str, yes: bool) -> None:
|
|
178
|
+
if not yes and not click.confirm(f"Delete task '{benchmark}/{dataset_name}/{task_id}'?"):
|
|
179
|
+
click.echo("Cancelled.")
|
|
180
|
+
return
|
|
181
|
+
call(lambda client: client.datasets.tasks.remove(benchmark, dataset_name, task_id))
|
|
182
|
+
click.echo(click.style(f"✓ Task '{benchmark}/{dataset_name}/{task_id}' removed.", fg="green", bold=True))
|
valkyrie/cli/display.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from click.decorators import FC
|
|
11
|
+
|
|
12
|
+
from valkyrie.display import terminal_safe
|
|
13
|
+
|
|
14
|
+
Page = TypeVar("Page")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_option(help: str = "Output format.", choices: Sequence[str] = ("text", "json")) -> Callable[[FC], FC]:
|
|
18
|
+
return click.option(
|
|
19
|
+
"--format",
|
|
20
|
+
"output_format",
|
|
21
|
+
type=click.Choice(list(choices), case_sensitive=False),
|
|
22
|
+
default="text",
|
|
23
|
+
show_default=True,
|
|
24
|
+
help=help,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def one_line(value: str) -> str:
|
|
29
|
+
return terminal_safe(value, preserve_newlines=False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def status_label(value: str) -> str:
|
|
33
|
+
return value.replace("_", " ").title()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compact_json(payload: Mapping[str, object]) -> str:
|
|
37
|
+
return json.dumps(payload, allow_nan=False, sort_keys=True, separators=(",", ":"))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def utc_isoformat(value: datetime | None = None) -> str:
|
|
41
|
+
value = value or datetime.now(timezone.utc)
|
|
42
|
+
if value.tzinfo is None:
|
|
43
|
+
value = value.replace(tzinfo=timezone.utc)
|
|
44
|
+
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def progress_percent(finished_tasks: int, total_tasks: int) -> float:
|
|
48
|
+
return finished_tasks / total_tasks * 100 if total_tasks > 0 else 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def local_time(dt: datetime) -> str:
|
|
52
|
+
return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def short_local_time(dt: datetime, include_date: bool = True) -> str:
|
|
56
|
+
fmt = "%m/%d %H:%M" if include_date else "%H:%M"
|
|
57
|
+
return dt.astimezone().strftime(fmt)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_table(
|
|
61
|
+
rows: list[dict[str, str]],
|
|
62
|
+
headers: list[str],
|
|
63
|
+
current_page: int = 1,
|
|
64
|
+
total_pages: int = 1,
|
|
65
|
+
total_count: int | None = None,
|
|
66
|
+
item_name: str = "item",
|
|
67
|
+
) -> None:
|
|
68
|
+
if not rows:
|
|
69
|
+
click.echo(click.style(f"No {item_name}s found.", fg="yellow"))
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if total_count is None:
|
|
73
|
+
total_count = len(rows)
|
|
74
|
+
|
|
75
|
+
column_widths = {}
|
|
76
|
+
for header in headers:
|
|
77
|
+
max_width = len(header)
|
|
78
|
+
for row in rows:
|
|
79
|
+
value = str(row.get(header, ""))
|
|
80
|
+
clean_value = click.unstyle(value)
|
|
81
|
+
max_width = max(max_width, len(clean_value))
|
|
82
|
+
column_widths[header] = max_width
|
|
83
|
+
|
|
84
|
+
header_line = " ".join(f"{header:<{column_widths[header]}}" for header in headers)
|
|
85
|
+
separator = "─" * len(header_line)
|
|
86
|
+
|
|
87
|
+
click.echo()
|
|
88
|
+
click.echo(click.style(header_line, bold=True))
|
|
89
|
+
click.echo(separator)
|
|
90
|
+
|
|
91
|
+
for row in rows:
|
|
92
|
+
cells: list[str] = []
|
|
93
|
+
for header in headers:
|
|
94
|
+
value = str(row.get(header, ""))
|
|
95
|
+
clean_value = click.unstyle(value)
|
|
96
|
+
padding = column_widths[header] - len(clean_value)
|
|
97
|
+
cells.append(f"{value}{' ' * padding}")
|
|
98
|
+
click.echo(" ".join(cells))
|
|
99
|
+
|
|
100
|
+
click.echo(separator)
|
|
101
|
+
|
|
102
|
+
total_text = f"Total: {total_count} {item_name}(s)"
|
|
103
|
+
if total_pages > 1 and not _interactive_terminal():
|
|
104
|
+
click.echo(f"{total_text}, page {current_page}/{total_pages} (paging needs a terminal)")
|
|
105
|
+
elif total_pages > 1:
|
|
106
|
+
nav_text_raw = f"[h] ← prev {current_page}/{total_pages} next → [l] [q] quit"
|
|
107
|
+
nav_text = click.style(nav_text_raw, fg="bright_black")
|
|
108
|
+
padding = len(separator) - len(total_text) - len(nav_text_raw)
|
|
109
|
+
click.echo(f"{total_text}{' ' * padding}{nav_text}")
|
|
110
|
+
else:
|
|
111
|
+
click.echo(total_text)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _interactive_terminal() -> bool:
|
|
115
|
+
return all(stream is not None and stream.isatty() for stream in (sys.stdin, sys.stdout))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _clear_pager() -> None:
|
|
119
|
+
click.echo("\033[2J\033[3J\033[1;1H", nl=False, color=True)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def paginate_cli_pages(
|
|
123
|
+
load_page: Callable[[int, int], tuple[int, Page]],
|
|
124
|
+
render_page: Callable[[Page, int, int, int], None],
|
|
125
|
+
*,
|
|
126
|
+
limit: int,
|
|
127
|
+
render_empty: Callable[[], None],
|
|
128
|
+
) -> None:
|
|
129
|
+
current_page = 1
|
|
130
|
+
interactive = _interactive_terminal()
|
|
131
|
+
|
|
132
|
+
while True:
|
|
133
|
+
total_count, page = load_page((current_page - 1) * limit, limit)
|
|
134
|
+
total_pages = max(1, (total_count + limit - 1) // limit)
|
|
135
|
+
if current_page > total_pages:
|
|
136
|
+
current_page = total_pages
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
if interactive:
|
|
140
|
+
_clear_pager()
|
|
141
|
+
|
|
142
|
+
if total_count == 0:
|
|
143
|
+
render_empty()
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
render_page(page, current_page, total_pages, total_count)
|
|
147
|
+
|
|
148
|
+
if total_pages <= 1 or not interactive:
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
char = click.getchar()
|
|
152
|
+
|
|
153
|
+
if char == "l" and current_page < total_pages:
|
|
154
|
+
current_page += 1
|
|
155
|
+
elif char == "h" and current_page > 1:
|
|
156
|
+
current_page -= 1
|
|
157
|
+
elif char == "q" or char == "\x03":
|
|
158
|
+
break
|
valkyrie/cli/entry.py
ADDED
valkyrie/cli/lazy.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click.utils import make_default_short_help
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LazyGroup(click.Group):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
*args: Any,
|
|
13
|
+
lazy_commands: Mapping[str, tuple[str, str]],
|
|
14
|
+
hidden_commands: Mapping[str, str] | None = None,
|
|
15
|
+
**kwargs: Any,
|
|
16
|
+
) -> None:
|
|
17
|
+
super().__init__(*args, **kwargs)
|
|
18
|
+
self.lazy_commands = dict(lazy_commands)
|
|
19
|
+
self.hidden_commands = dict(hidden_commands or {})
|
|
20
|
+
|
|
21
|
+
def list_commands(self, ctx: click.Context) -> list[str]:
|
|
22
|
+
return sorted(self.lazy_commands)
|
|
23
|
+
|
|
24
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
|
|
25
|
+
if cmd_name in self.lazy_commands:
|
|
26
|
+
target = self.lazy_commands[cmd_name][0]
|
|
27
|
+
elif cmd_name in self.hidden_commands:
|
|
28
|
+
target = self.hidden_commands[cmd_name]
|
|
29
|
+
else:
|
|
30
|
+
return None
|
|
31
|
+
module, attribute = target.split(":")
|
|
32
|
+
return getattr(import_module(module), attribute)
|
|
33
|
+
|
|
34
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
35
|
+
names = self.list_commands(ctx)
|
|
36
|
+
limit = formatter.width - 6 - max(map(len, names))
|
|
37
|
+
with formatter.section("Commands"):
|
|
38
|
+
formatter.write_dl([(name, make_default_short_help(self.lazy_commands[name][1], limit)) for name in names])
|
valkyrie/cli/logging.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
CLI_LOGS_ENV_VAR = "VALKYRIE_CLI_LOGS"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def configure_cli_logging() -> None:
|
|
10
|
+
if os.environ.get(CLI_LOGS_ENV_VAR) == "true":
|
|
11
|
+
logging.disable(logging.NOTSET)
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
logging.disable(logging.CRITICAL)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_cli_environment() -> None:
|
|
18
|
+
configure_cli_logging()
|
|
19
|
+
load_dotenv()
|
|
20
|
+
configure_cli_logging()
|
valkyrie/cli/login.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from valkyrie.cli.client import call
|
|
6
|
+
from valkyrie.cli.display import utc_isoformat
|
|
7
|
+
from valkyrie.sdk import (
|
|
8
|
+
IdentityResponse,
|
|
9
|
+
LoginSession,
|
|
10
|
+
OrganizationsResponse,
|
|
11
|
+
SessionAuth,
|
|
12
|
+
ValkyrieAPIError,
|
|
13
|
+
ValkyrieClient,
|
|
14
|
+
ValkyrieConfig,
|
|
15
|
+
ValkyrieTransportError,
|
|
16
|
+
device_login,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def show_organizations(listing: OrganizationsResponse) -> None:
|
|
21
|
+
if not listing.organizations and not listing.invitations:
|
|
22
|
+
click.echo(
|
|
23
|
+
"You're not in an organization yet. Ask Vals AI to add you, "
|
|
24
|
+
"or ask an organization admin to run `valkyrie org invite EMAIL`."
|
|
25
|
+
)
|
|
26
|
+
for entry in listing.organizations:
|
|
27
|
+
click.echo(f"{'*' if entry.current else ' '} {entry.id}{' (admin)' if entry.admin else ''}")
|
|
28
|
+
for organization in listing.invitations:
|
|
29
|
+
click.echo(f" {organization} (invited; run `valkyrie org accept {organization}`)")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _organizations_or_identity(client: ValkyrieClient) -> OrganizationsResponse | IdentityResponse:
|
|
33
|
+
try:
|
|
34
|
+
return await client.account.organizations()
|
|
35
|
+
except ValkyrieAPIError as error:
|
|
36
|
+
if error.status_code != 404:
|
|
37
|
+
raise
|
|
38
|
+
return await client.account.identity()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.command(help="Sign in with your browser, save the login, and use this Tracker by default.")
|
|
42
|
+
@click.option("--no-browser", is_flag=True, help="Print the sign-in link without opening a browser.")
|
|
43
|
+
def login(no_browser: bool) -> None:
|
|
44
|
+
tracker_url = ValkyrieConfig().url
|
|
45
|
+
|
|
46
|
+
def announce(link: str, user_code: str) -> None:
|
|
47
|
+
click.echo(f"Open {link} and confirm the code {user_code}.", err=True)
|
|
48
|
+
if not no_browser:
|
|
49
|
+
click.launch(link)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
session = device_login(tracker_url, announce)
|
|
53
|
+
except ValkyrieTransportError as error:
|
|
54
|
+
raise click.ClickException(str(error)) from error
|
|
55
|
+
session.save()
|
|
56
|
+
session.remember()
|
|
57
|
+
if ValkyrieConfig().api_key:
|
|
58
|
+
click.echo("VALKYRIE_API_KEY is set and takes precedence over this login; unset it to use the login.", err=True)
|
|
59
|
+
listing = call(_organizations_or_identity, auth=SessionAuth(session))
|
|
60
|
+
if isinstance(listing, IdentityResponse):
|
|
61
|
+
who = listing.email or listing.name or "your account"
|
|
62
|
+
click.echo(f"Logged in to {session.tracker_url} as {who} for {listing.organization}.")
|
|
63
|
+
return
|
|
64
|
+
click.echo(f"Logged in to {session.tracker_url} as {listing.email or 'your account'}.")
|
|
65
|
+
show_organizations(listing)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.command(help="Forget the saved login for this Tracker, and stop using it by default.")
|
|
69
|
+
def logout() -> None:
|
|
70
|
+
tracker_url = ValkyrieConfig().url
|
|
71
|
+
if LoginSession.forget(tracker_url):
|
|
72
|
+
click.echo(f"Logged out of {tracker_url}.")
|
|
73
|
+
else:
|
|
74
|
+
click.echo(f"You were not logged in to {tracker_url}.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@click.command(help="Show which Tracker, organization and account your requests use.")
|
|
78
|
+
def whoami() -> None:
|
|
79
|
+
identity = call(lambda client: client.account.identity())
|
|
80
|
+
credential = {"login": "a login", "access_key": "an access key"}.get(identity.credential, "no credential")
|
|
81
|
+
who = identity.email or identity.name or "unknown user"
|
|
82
|
+
click.echo(f"{who} in {identity.organization} on {ValkyrieConfig().url} (using {credential})")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@click.group(help="Create and revoke access keys for CI and other headless use.")
|
|
86
|
+
def token() -> None:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@token.command(
|
|
91
|
+
"create", help="Create an access key for your organization. Set it as VALKYRIE_API_KEY where you need it."
|
|
92
|
+
)
|
|
93
|
+
@click.argument("name")
|
|
94
|
+
@click.option(
|
|
95
|
+
"--expires-in-days",
|
|
96
|
+
type=click.IntRange(1, 90),
|
|
97
|
+
default=30,
|
|
98
|
+
show_default=True,
|
|
99
|
+
help="Days until the key stops working.",
|
|
100
|
+
)
|
|
101
|
+
def token_create(name: str, expires_in_days: int) -> None:
|
|
102
|
+
created = call(lambda client: client.account.create_access_key(name, expires_in_days=expires_in_days))
|
|
103
|
+
click.echo(f"Created access key {created.key_id}, expiring {utc_isoformat(created.expires_at)}.", err=True)
|
|
104
|
+
click.echo("It is shown only once:", err=True)
|
|
105
|
+
click.echo(created.key)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@token.command(
|
|
109
|
+
"revoke",
|
|
110
|
+
help="Delete an access key you created. The Tracker may accept it for up to one more minute, and tokens exchanged from it until they expire.",
|
|
111
|
+
)
|
|
112
|
+
@click.argument("key_id")
|
|
113
|
+
def token_revoke(key_id: str) -> None:
|
|
114
|
+
call(lambda client: client.account.revoke_access_key(key_id))
|
|
115
|
+
click.echo(f"Revoked access key {key_id}.")
|
valkyrie/cli/main.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from valkyrie.cli.entry import main
|
|
7
|
+
from valkyrie.cli.lazy import LazyGroup
|
|
8
|
+
from valkyrie.cli.logging import load_cli_environment
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _ValkyrieCLI(LazyGroup):
|
|
12
|
+
def invoke(self, ctx: click.Context) -> Any:
|
|
13
|
+
try:
|
|
14
|
+
return super().invoke(ctx)
|
|
15
|
+
except Exception as error:
|
|
16
|
+
errors = sys.modules.get("valkyrie.sdk.errors")
|
|
17
|
+
if errors is not None and isinstance(error, errors.ValkyrieConfigError):
|
|
18
|
+
raise click.ClickException(str(error)) from error
|
|
19
|
+
raise
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group(
|
|
23
|
+
cls=_ValkyrieCLI,
|
|
24
|
+
lazy_commands={
|
|
25
|
+
"run": ("valkyrie.cli.run:run", "Start, monitor, manage, and retrieve runs."),
|
|
26
|
+
"agent": ("valkyrie.cli.agent:agent", "Install, upload, list, download, and remove agents."),
|
|
27
|
+
"benchmark": (
|
|
28
|
+
"valkyrie.cli.benchmark:benchmark",
|
|
29
|
+
"Export the task IDs in a benchmark dataset, and save benchmark services.",
|
|
30
|
+
),
|
|
31
|
+
"queue": ("valkyrie.cli.queue:queue", "Inspect your organization's sandbox scheduler."),
|
|
32
|
+
"project": ("valkyrie.cli.project:project", "Manage the projects runs are started in."),
|
|
33
|
+
"secret": ("valkyrie.cli.secret:secret", "Store secrets that your runs reference."),
|
|
34
|
+
"provider": (
|
|
35
|
+
"valkyrie.cli.provider:provider",
|
|
36
|
+
"Choose the secret that configures each sandbox provider, and your default provider.",
|
|
37
|
+
),
|
|
38
|
+
"login": (
|
|
39
|
+
"valkyrie.cli.login:login",
|
|
40
|
+
"Sign in with your browser, save the login, and use this Tracker by default.",
|
|
41
|
+
),
|
|
42
|
+
"logout": (
|
|
43
|
+
"valkyrie.cli.login:logout",
|
|
44
|
+
"Forget the saved login for this Tracker, and stop using it by default.",
|
|
45
|
+
),
|
|
46
|
+
"whoami": ("valkyrie.cli.login:whoami", "Show which Tracker, organization and account your requests use."),
|
|
47
|
+
"token": ("valkyrie.cli.login:token", "Create and revoke access keys for CI and other headless use."),
|
|
48
|
+
"org": ("valkyrie.cli.org:org", "Create, join and switch organizations, and manage their members."),
|
|
49
|
+
"ui": ("valkyrie.cli.ui:ui", "Browse runs, tasks and logs in a terminal UI."),
|
|
50
|
+
},
|
|
51
|
+
hidden_commands={"dataset": "valkyrie.cli.dataset:dataset"},
|
|
52
|
+
help="Run agents against benchmarks and manage their runs.",
|
|
53
|
+
)
|
|
54
|
+
def cli():
|
|
55
|
+
load_cli_environment()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = ["cli"]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
main()
|