tmp-nepher-cli 0.2.5__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.
- nepher_cli/__init__.py +10 -0
- nepher_cli/__main__.py +4 -0
- nepher_cli/cli.py +64 -0
- nepher_cli/commands/__init__.py +1 -0
- nepher_cli/commands/account.py +646 -0
- nepher_cli/commands/envhub.py +481 -0
- nepher_cli/commands/hackathon.py +760 -0
- nepher_cli/commands/highlight.py +307 -0
- nepher_cli/commands/simstore.py +49 -0
- nepher_cli/commands/tournament.py +656 -0
- nepher_cli/config.py +47 -0
- nepher_cli/core/__init__.py +1 -0
- nepher_cli/core/credentials.py +243 -0
- nepher_cli/core/http.py +90 -0
- nepher_cli/envhub/__init__.py +1 -0
- nepher_cli/envhub/cache.py +56 -0
- nepher_cli/envhub/config.py +180 -0
- nepher_cli/py.typed +0 -0
- nepher_cli/tournament/__init__.py +1 -0
- nepher_cli/tournament/agent_check.py +60 -0
- nepher_cli/tournament/api.py +100 -0
- nepher_cli/tournament/packer.py +50 -0
- nepher_cli/tournament/wallet.py +89 -0
- tmp_nepher_cli-0.2.5.dist-info/METADATA +200 -0
- tmp_nepher_cli-0.2.5.dist-info/RECORD +27 -0
- tmp_nepher_cli-0.2.5.dist-info/WHEEL +4 -0
- tmp_nepher_cli-0.2.5.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""tournament highlight subgroup — manage verified play videos.
|
|
2
|
+
|
|
3
|
+
Highlights are admin-uploaded videos bound to a submission (``--agent-id``) and an
|
|
4
|
+
eval phase. Placement is never supplied here: the platform reads rank and score
|
|
5
|
+
from the tournament leaderboard when the highlight is served.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json as jsonlib
|
|
11
|
+
import mimetypes
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
import httpx
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from nepher_cli.config import TOURNAMENT_BACKEND
|
|
21
|
+
from nepher_cli.core.http import (
|
|
22
|
+
authed_delete,
|
|
23
|
+
authed_get,
|
|
24
|
+
authed_patch,
|
|
25
|
+
authed_post,
|
|
26
|
+
parse_error_body,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
console = Console(stderr=True)
|
|
30
|
+
|
|
31
|
+
PHASES = ("public", "private")
|
|
32
|
+
STATUSES = ("draft", "published")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _admin_url(api_url: str | None, path: str = "") -> str:
|
|
36
|
+
return f"{(api_url or TOURNAMENT_BACKEND).rstrip('/')}/api/v1/admin/highlights{path}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _handle(response: httpx.Response) -> Any:
|
|
40
|
+
"""Raise a friendly SystemExit on failure, else return the decoded body."""
|
|
41
|
+
if response.status_code >= 400:
|
|
42
|
+
console.print(
|
|
43
|
+
f"[red]{parse_error_body(response.text) or response.text.strip() or f'HTTP {response.status_code}'}[/red]"
|
|
44
|
+
)
|
|
45
|
+
raise SystemExit(1)
|
|
46
|
+
if response.status_code == 204 or not response.content:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
return response.json()
|
|
50
|
+
except ValueError:
|
|
51
|
+
console.print("[red]Invalid JSON response.[/red]")
|
|
52
|
+
raise SystemExit(1)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _call(fn, *args, **kwargs) -> Any:
|
|
56
|
+
try:
|
|
57
|
+
return _handle(fn(*args, **kwargs))
|
|
58
|
+
except httpx.RequestError as e:
|
|
59
|
+
console.print(f"[red]Network error[/red]: {e}")
|
|
60
|
+
raise SystemExit(1) from e
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _print_highlight(data: dict[str, Any], heading: str) -> None:
|
|
64
|
+
console.print(f"[green]{heading}[/green]")
|
|
65
|
+
for label, key in [
|
|
66
|
+
("Highlight ID", "id"),
|
|
67
|
+
("Tournament", "tournament_name"),
|
|
68
|
+
("Submission", "agent_id"),
|
|
69
|
+
("Hotkey", "miner_hotkey"),
|
|
70
|
+
("Phase", "phase"),
|
|
71
|
+
("Status", "status"),
|
|
72
|
+
("Rank", "rank"),
|
|
73
|
+
]:
|
|
74
|
+
value = data.get(key)
|
|
75
|
+
if value is not None:
|
|
76
|
+
console.print(f" {label:<13}: [bold]{value}[/bold]")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@click.group("highlight")
|
|
80
|
+
def highlight() -> None:
|
|
81
|
+
"""Manage verified play videos (admin only).
|
|
82
|
+
|
|
83
|
+
\b
|
|
84
|
+
list List highlights, including drafts
|
|
85
|
+
upload Upload a play video for a submission
|
|
86
|
+
publish Make a highlight public
|
|
87
|
+
unpublish Return a highlight to draft
|
|
88
|
+
delete Remove a highlight and its video
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
_api_url_option = click.option(
|
|
93
|
+
"--api-url", default=None, help=f"Tournament API URL (default: {TOURNAMENT_BACKEND})."
|
|
94
|
+
)
|
|
95
|
+
_api_key_option = click.option(
|
|
96
|
+
"--api-key", "--apikey", "api_key",
|
|
97
|
+
default=None, envvar="NEPHER_API_KEY", metavar="KEY",
|
|
98
|
+
help="Nepher API key. Falls back to stored credentials.",
|
|
99
|
+
)
|
|
100
|
+
_json_option = click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@highlight.command("list")
|
|
104
|
+
@click.option("--tournament-id", default=None, help="Filter by tournament.")
|
|
105
|
+
@click.option("--phase", type=click.Choice(PHASES), default=None, help="Filter by eval phase.")
|
|
106
|
+
@click.option("--status", type=click.Choice(STATUSES), default=None, help="Filter by publish state.")
|
|
107
|
+
@click.option("--limit", type=int, default=24, show_default=True, help="Highlights per page.")
|
|
108
|
+
@_api_key_option
|
|
109
|
+
@_api_url_option
|
|
110
|
+
@_json_option
|
|
111
|
+
def highlight_list(
|
|
112
|
+
tournament_id: str | None,
|
|
113
|
+
phase: str | None,
|
|
114
|
+
status: str | None,
|
|
115
|
+
limit: int,
|
|
116
|
+
api_key: str | None,
|
|
117
|
+
api_url: str | None,
|
|
118
|
+
output_json: bool,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""List highlights with their live leaderboard placement."""
|
|
121
|
+
params = {
|
|
122
|
+
k: v
|
|
123
|
+
for k, v in {
|
|
124
|
+
"tournament_id": tournament_id,
|
|
125
|
+
"phase": phase,
|
|
126
|
+
"status": status,
|
|
127
|
+
"page_size": limit,
|
|
128
|
+
}.items()
|
|
129
|
+
if v is not None
|
|
130
|
+
}
|
|
131
|
+
data = _call(authed_get, _admin_url(api_url), api_key=api_key, params=params)
|
|
132
|
+
|
|
133
|
+
if output_json:
|
|
134
|
+
click.echo(jsonlib.dumps(data, indent=2))
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
items: list[dict[str, Any]] = data.get("items", [])
|
|
138
|
+
if not items:
|
|
139
|
+
console.print("[dim]No highlights found.[/dim]")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
table = Table(show_header=True, header_style="bold", box=None)
|
|
143
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
144
|
+
table.add_column("Rank", justify="right")
|
|
145
|
+
table.add_column("Phase", no_wrap=True)
|
|
146
|
+
table.add_column("Status", no_wrap=True)
|
|
147
|
+
table.add_column("Hotkey")
|
|
148
|
+
table.add_column("Tournament", style="dim")
|
|
149
|
+
|
|
150
|
+
for item in items:
|
|
151
|
+
rank = "winner" if item.get("is_winner") else (item.get("rank") or "—")
|
|
152
|
+
table.add_row(
|
|
153
|
+
str(item.get("id", "")),
|
|
154
|
+
str(rank),
|
|
155
|
+
str(item.get("phase", "—")),
|
|
156
|
+
str(item.get("status", "—")),
|
|
157
|
+
str(item.get("miner_hotkey", "—")),
|
|
158
|
+
str(item.get("tournament_name") or "—"),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
from rich import print as rprint
|
|
162
|
+
|
|
163
|
+
rprint(table)
|
|
164
|
+
console.print(f"\n[dim]{len(items)} of {data.get('total', len(items))} highlight(s).[/dim]")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@highlight.command("upload")
|
|
168
|
+
@click.option("--tournament-id", required=True, help="Tournament the run belongs to.")
|
|
169
|
+
@click.option("--phase", type=click.Choice(PHASES), required=True, help="Eval phase of the run.")
|
|
170
|
+
@click.option("--file", "video_path", type=click.Path(exists=True, dir_okay=False), required=True,
|
|
171
|
+
help="Play video (MP4 or WebM, max 100 MB).")
|
|
172
|
+
@click.option("--agent-id", default=None, help="Submission the video shows.")
|
|
173
|
+
@click.option("--rank", type=int, default=None,
|
|
174
|
+
help="Resolve the submission by its current rank in this phase instead of --agent-id.")
|
|
175
|
+
@click.option("--title", default=None, help="Display title.")
|
|
176
|
+
@click.option("--poster", "poster_path", type=click.Path(exists=True, dir_okay=False), default=None,
|
|
177
|
+
help="Optional poster image (JPG, PNG, WebP, max 10 MB).")
|
|
178
|
+
@click.option("--publish", is_flag=True, help="Publish immediately instead of saving a draft.")
|
|
179
|
+
@_api_key_option
|
|
180
|
+
@_api_url_option
|
|
181
|
+
@_json_option
|
|
182
|
+
def highlight_upload(
|
|
183
|
+
tournament_id: str,
|
|
184
|
+
phase: str,
|
|
185
|
+
video_path: str,
|
|
186
|
+
agent_id: str | None,
|
|
187
|
+
rank: int | None,
|
|
188
|
+
title: str | None,
|
|
189
|
+
poster_path: str | None,
|
|
190
|
+
publish: bool,
|
|
191
|
+
api_key: str | None,
|
|
192
|
+
api_url: str | None,
|
|
193
|
+
output_json: bool,
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Upload a verified play video and bind it to a submission.
|
|
196
|
+
|
|
197
|
+
Provide either --agent-id or --rank; the platform stores the resolved
|
|
198
|
+
submission so the card's placement always tracks the leaderboard.
|
|
199
|
+
"""
|
|
200
|
+
if not agent_id and rank is None:
|
|
201
|
+
console.print("[red]Provide --agent-id or --rank.[/red]")
|
|
202
|
+
raise SystemExit(1)
|
|
203
|
+
|
|
204
|
+
form: dict[str, str] = {
|
|
205
|
+
"tournament_id": tournament_id,
|
|
206
|
+
"phase": phase,
|
|
207
|
+
"publish": str(publish).lower(),
|
|
208
|
+
}
|
|
209
|
+
if agent_id:
|
|
210
|
+
form["agent_id"] = agent_id
|
|
211
|
+
if rank is not None:
|
|
212
|
+
form["rank"] = str(rank)
|
|
213
|
+
if title:
|
|
214
|
+
form["title"] = title
|
|
215
|
+
|
|
216
|
+
video = Path(video_path)
|
|
217
|
+
files: dict[str, Any] = {
|
|
218
|
+
"file": (video.name, video.read_bytes(), mimetypes.guess_type(video.name)[0] or "video/mp4"),
|
|
219
|
+
}
|
|
220
|
+
if poster_path:
|
|
221
|
+
poster = Path(poster_path)
|
|
222
|
+
files["poster"] = (
|
|
223
|
+
poster.name,
|
|
224
|
+
poster.read_bytes(),
|
|
225
|
+
mimetypes.guess_type(poster.name)[0] or "image/jpeg",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
console.print("Uploading play video...")
|
|
229
|
+
data = _call(
|
|
230
|
+
authed_post,
|
|
231
|
+
_admin_url(api_url, "/upload"),
|
|
232
|
+
api_key=api_key,
|
|
233
|
+
data=form,
|
|
234
|
+
files=files,
|
|
235
|
+
timeout=600.0,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if output_json:
|
|
239
|
+
click.echo(jsonlib.dumps(data, indent=2))
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
_print_highlight(data, "Highlight published." if publish else "Highlight saved as draft.")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _set_status(
|
|
246
|
+
highlight_id: str,
|
|
247
|
+
status: str,
|
|
248
|
+
api_key: str | None,
|
|
249
|
+
api_url: str | None,
|
|
250
|
+
output_json: bool,
|
|
251
|
+
) -> None:
|
|
252
|
+
data = _call(
|
|
253
|
+
authed_patch,
|
|
254
|
+
_admin_url(api_url, f"/{highlight_id}"),
|
|
255
|
+
api_key=api_key,
|
|
256
|
+
json_body={"status": status},
|
|
257
|
+
)
|
|
258
|
+
if output_json:
|
|
259
|
+
click.echo(jsonlib.dumps(data, indent=2))
|
|
260
|
+
return
|
|
261
|
+
_print_highlight(data, f"Highlight marked {status}.")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@highlight.command("publish")
|
|
265
|
+
@click.argument("highlight_id")
|
|
266
|
+
@_api_key_option
|
|
267
|
+
@_api_url_option
|
|
268
|
+
@_json_option
|
|
269
|
+
def highlight_publish(
|
|
270
|
+
highlight_id: str, api_key: str | None, api_url: str | None, output_json: bool
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Publish a draft highlight to the public feed."""
|
|
273
|
+
_set_status(highlight_id, "published", api_key, api_url, output_json)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@highlight.command("unpublish")
|
|
277
|
+
@click.argument("highlight_id")
|
|
278
|
+
@_api_key_option
|
|
279
|
+
@_api_url_option
|
|
280
|
+
@_json_option
|
|
281
|
+
def highlight_unpublish(
|
|
282
|
+
highlight_id: str, api_key: str | None, api_url: str | None, output_json: bool
|
|
283
|
+
) -> None:
|
|
284
|
+
"""Return a published highlight to draft."""
|
|
285
|
+
_set_status(highlight_id, "draft", api_key, api_url, output_json)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@highlight.command("delete")
|
|
289
|
+
@click.argument("highlight_id")
|
|
290
|
+
@click.option("--keep-media", is_flag=True, help="Keep the video in the Media Library.")
|
|
291
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
|
|
292
|
+
@_api_key_option
|
|
293
|
+
@_api_url_option
|
|
294
|
+
def highlight_delete(
|
|
295
|
+
highlight_id: str,
|
|
296
|
+
keep_media: bool,
|
|
297
|
+
yes: bool,
|
|
298
|
+
api_key: str | None,
|
|
299
|
+
api_url: str | None,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Delete a highlight and, unless --keep-media, its video."""
|
|
302
|
+
if not yes:
|
|
303
|
+
click.confirm(f"Delete highlight {highlight_id}?", abort=True)
|
|
304
|
+
|
|
305
|
+
query = "?keep_media=true" if keep_media else ""
|
|
306
|
+
_call(authed_delete, _admin_url(api_url, f"/{highlight_id}{query}"), api_key=api_key)
|
|
307
|
+
console.print("[green]Highlight deleted.[/green]")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""simstore command group — coming soon placeholder."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
console = Console(stderr=True)
|
|
9
|
+
|
|
10
|
+
_COMING_SOON = (
|
|
11
|
+
"[bold]SimStore[/bold] is coming soon.\n\n"
|
|
12
|
+
"SimStore will let you browse, purchase, and publish Isaac Lab simulation "
|
|
13
|
+
"assets directly from the command line.\n\n"
|
|
14
|
+
"Follow [link=https://nepher.ai]nepher.ai[/link] for updates."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group("simstore")
|
|
19
|
+
def simstore() -> None:
|
|
20
|
+
"""SimStore — buy and sell Isaac Lab simulation assets. (Coming soon)"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@simstore.command("status")
|
|
24
|
+
def simstore_status() -> None:
|
|
25
|
+
"""Show SimStore availability."""
|
|
26
|
+
console.print(_COMING_SOON)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@simstore.result_callback()
|
|
30
|
+
def simstore_fallback(result, **kwargs) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@simstore.command("browse", hidden=False)
|
|
35
|
+
def simstore_browse() -> None:
|
|
36
|
+
"""Browse available simulation assets. (Coming soon)"""
|
|
37
|
+
console.print(_COMING_SOON)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@simstore.command("publish", hidden=False)
|
|
41
|
+
def simstore_publish() -> None:
|
|
42
|
+
"""Publish a simulation asset to SimStore. (Coming soon)"""
|
|
43
|
+
console.print(_COMING_SOON)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@simstore.command("purchase", hidden=False)
|
|
47
|
+
def simstore_purchase() -> None:
|
|
48
|
+
"""Purchase a simulation asset. (Coming soon)"""
|
|
49
|
+
console.print(_COMING_SOON)
|