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.
@@ -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)