nepher-cli 0.2.2__tar.gz → 0.2.4__tar.gz
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-0.2.2 → nepher_cli-0.2.4}/PKG-INFO +10 -7
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/README.md +8 -5
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/pyproject.toml +5 -3
- nepher_cli-0.2.4/src/nepher_cli/__init__.py +10 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/cli.py +6 -1
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/commands/account.py +109 -8
- nepher_cli-0.2.4/src/nepher_cli/commands/highlight.py +307 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/commands/tournament.py +7 -2
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/core/http.py +14 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/tournament/wallet.py +3 -3
- nepher_cli-0.2.2/src/nepher_cli/__init__.py +0 -3
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/.gitignore +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/__main__.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/commands/__init__.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/commands/envhub.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/commands/hackathon.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/commands/simstore.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/config.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/core/__init__.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/core/credentials.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/envhub/__init__.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/envhub/cache.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/envhub/config.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/py.typed +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/tournament/__init__.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/tournament/agent_check.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/tournament/api.py +0 -0
- {nepher_cli-0.2.2 → nepher_cli-0.2.4}/src/nepher_cli/tournament/packer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nepher-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore
|
|
5
5
|
Project-URL: Homepage, https://github.com/nepher-ai/nepher-cli
|
|
6
6
|
Author: Nepher
|
|
@@ -20,7 +20,7 @@ Requires-Dist: keyring>=25.0.0
|
|
|
20
20
|
Requires-Dist: platformdirs>=4.0.0
|
|
21
21
|
Requires-Dist: rich>=13.7.0
|
|
22
22
|
Provides-Extra: bittensor
|
|
23
|
-
Requires-Dist: bittensor; extra == 'bittensor'
|
|
23
|
+
Requires-Dist: bittensor-wallet>=4.1.0; extra == 'bittensor'
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
26
26
|
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
@@ -87,10 +87,10 @@ npcli tournament check --path ./my-agent
|
|
|
87
87
|
npcli tournament check --path ./my-agent --verbose # also show recommended-file warnings
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
Submit an agent (requires `bittensor` for wallet signing):
|
|
90
|
+
Submit an agent (requires `bittensor-wallet` for wallet signing):
|
|
91
91
|
|
|
92
92
|
```bash
|
|
93
|
-
pip install bittensor
|
|
93
|
+
pip install bittensor-wallet
|
|
94
94
|
# or: pip install "nepher-cli[bittensor]"
|
|
95
95
|
```
|
|
96
96
|
|
|
@@ -154,13 +154,15 @@ npcli account api-keys revoke <key_id>
|
|
|
154
154
|
|
|
155
155
|
### Coldkey Registration
|
|
156
156
|
|
|
157
|
-
Bind a Bittensor coldkey to your account (requires `bittensor
|
|
157
|
+
Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is optional fallback):
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
|
-
pip install bittensor
|
|
160
|
+
pip install bittensor-wallet
|
|
161
161
|
npcli account register-coldkey --wallet <wallet_name>
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
Signing uses the local coldkey via `bittensor-wallet` and does not import `btcli`. If you still have a mixed Bittensor venv (`scalecodec` + `cyscale`), `register-coldkey` keeps working as long as `bittensor-wallet` is installed.
|
|
165
|
+
|
|
164
166
|
## API Key Requirements
|
|
165
167
|
|
|
166
168
|
- Must start with `nepher_`
|
|
@@ -177,8 +179,9 @@ npcli account register-coldkey --wallet <wallet_name>
|
|
|
177
179
|
| `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
|
|
178
180
|
| `api key expired` | Run `npcli account api-keys create` |
|
|
179
181
|
| `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
|
|
180
|
-
| `bittensor not installed` | `pip install bittensor` (
|
|
182
|
+
| `bittensor-wallet not installed` | `pip install bittensor-wallet` (needed for `tournament submit` and `account register-coldkey`) |
|
|
181
183
|
| `Not logged in` | `npcli login --api-key nepher_...` |
|
|
184
|
+
| `Conflict detected: 'scalecodec' ... conflicts with 'cyscale'` | Leftover `scalecodec` after a Bittensor upgrade. Prefer `pip install bittensor-wallet` so npcli never needs `btcli`. To repair `btcli` itself: `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
|
|
182
185
|
|
|
183
186
|
Run any command with `--help` for full flag details:
|
|
184
187
|
|
|
@@ -59,10 +59,10 @@ npcli tournament check --path ./my-agent
|
|
|
59
59
|
npcli tournament check --path ./my-agent --verbose # also show recommended-file warnings
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Submit an agent (requires `bittensor` for wallet signing):
|
|
62
|
+
Submit an agent (requires `bittensor-wallet` for wallet signing):
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
pip install bittensor
|
|
65
|
+
pip install bittensor-wallet
|
|
66
66
|
# or: pip install "nepher-cli[bittensor]"
|
|
67
67
|
```
|
|
68
68
|
|
|
@@ -126,13 +126,15 @@ npcli account api-keys revoke <key_id>
|
|
|
126
126
|
|
|
127
127
|
### Coldkey Registration
|
|
128
128
|
|
|
129
|
-
Bind a Bittensor coldkey to your account (requires `bittensor
|
|
129
|
+
Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is optional fallback):
|
|
130
130
|
|
|
131
131
|
```bash
|
|
132
|
-
pip install bittensor
|
|
132
|
+
pip install bittensor-wallet
|
|
133
133
|
npcli account register-coldkey --wallet <wallet_name>
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
Signing uses the local coldkey via `bittensor-wallet` and does not import `btcli`. If you still have a mixed Bittensor venv (`scalecodec` + `cyscale`), `register-coldkey` keeps working as long as `bittensor-wallet` is installed.
|
|
137
|
+
|
|
136
138
|
## API Key Requirements
|
|
137
139
|
|
|
138
140
|
- Must start with `nepher_`
|
|
@@ -149,8 +151,9 @@ npcli account register-coldkey --wallet <wallet_name>
|
|
|
149
151
|
| `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
|
|
150
152
|
| `api key expired` | Run `npcli account api-keys create` |
|
|
151
153
|
| `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
|
|
152
|
-
| `bittensor not installed` | `pip install bittensor` (
|
|
154
|
+
| `bittensor-wallet not installed` | `pip install bittensor-wallet` (needed for `tournament submit` and `account register-coldkey`) |
|
|
153
155
|
| `Not logged in` | `npcli login --api-key nepher_...` |
|
|
156
|
+
| `Conflict detected: 'scalecodec' ... conflicts with 'cyscale'` | Leftover `scalecodec` after a Bittensor upgrade. Prefer `pip install bittensor-wallet` so npcli never needs `btcli`. To repair `btcli` itself: `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
|
|
154
157
|
|
|
155
158
|
Run any command with `--help` for full flag details:
|
|
156
159
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["hatchling"]
|
|
2
|
+
requires = ["hatchling>=1.27,<1.32"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nepher-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -29,7 +29,7 @@ dependencies = [
|
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
|
-
bittensor = ["bittensor"]
|
|
32
|
+
bittensor = ["bittensor-wallet>=4.1.0"]
|
|
33
33
|
dev = [
|
|
34
34
|
"pytest>=8.0.0",
|
|
35
35
|
"ruff>=0.4.0",
|
|
@@ -44,9 +44,11 @@ Homepage = "https://github.com/nepher-ai/nepher-cli"
|
|
|
44
44
|
|
|
45
45
|
[tool.hatch.build.targets.wheel]
|
|
46
46
|
packages = ["src/nepher_cli"]
|
|
47
|
+
core-metadata-version = "2.4"
|
|
47
48
|
|
|
48
49
|
[tool.hatch.build.targets.sdist]
|
|
49
50
|
include = ["src/nepher_cli", "README.md", "pyproject.toml"]
|
|
51
|
+
core-metadata-version = "2.4"
|
|
50
52
|
|
|
51
53
|
[tool.ruff]
|
|
52
54
|
line-length = 100
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""npcli — unified Nepher command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("nepher-cli")
|
|
9
|
+
except PackageNotFoundError: # pragma: no cover
|
|
10
|
+
__version__ = "0.0.0"
|
|
@@ -15,7 +15,7 @@ from nepher_cli.commands.tournament import tournament
|
|
|
15
15
|
@click.group(
|
|
16
16
|
context_settings={"help_option_names": ["-h", "--help"], "max_content_width": 100},
|
|
17
17
|
)
|
|
18
|
-
@click.version_option(version=__version__, prog_name="npcli")
|
|
18
|
+
@click.version_option(version=__version__, package_name="nepher-cli", prog_name="npcli")
|
|
19
19
|
def main() -> None:
|
|
20
20
|
"""npcli — the unified Nepher command-line interface.
|
|
21
21
|
|
|
@@ -39,6 +39,11 @@ def main() -> None:
|
|
|
39
39
|
npcli envhub list
|
|
40
40
|
npcli tournament list
|
|
41
41
|
|
|
42
|
+
\b
|
|
43
|
+
Tournament submit requires bittensor-wallet for wallet signing:
|
|
44
|
+
pip install bittensor-wallet
|
|
45
|
+
# or: pip install "nepher-cli[bittensor]"
|
|
46
|
+
|
|
42
47
|
Run any sub-command with --help for details and examples.
|
|
43
48
|
|
|
44
49
|
API keys are created at https://account.nepher.ai (Account > API Keys).
|
|
@@ -35,6 +35,25 @@ console = Console(stderr=True)
|
|
|
35
35
|
|
|
36
36
|
BTCLI_SIGN_TIMEOUT_SECONDS = 120
|
|
37
37
|
|
|
38
|
+
_WALLET_INSTALL_HINT = (
|
|
39
|
+
"Install the wallet library:\n"
|
|
40
|
+
" [bold]pip install bittensor-wallet[/bold]\n"
|
|
41
|
+
" [bold]pip install \"nepher-cli[bittensor]\"[/bold]"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_SCALECODEC_CONFLICT_HINT = (
|
|
45
|
+
"[red]btcli cannot start[/red] — [bold]scalecodec[/bold] (py-scale-codec) and "
|
|
46
|
+
"[bold]cyscale[/bold] are both installed and share the same Python namespace.\n\n"
|
|
47
|
+
"This is a known Bittensor CLI conflict. Fix it in this venv:\n\n"
|
|
48
|
+
" [bold]pip uninstall scalecodec cyscale bt-decode -y[/bold]\n"
|
|
49
|
+
" [bold]pip install -U cyscale --force-reinstall[/bold]\n\n"
|
|
50
|
+
"If [bold]btcli --version[/bold] still fails:\n\n"
|
|
51
|
+
" [bold]pip uninstall scalecodec cyscale bt-decode bittensor bittensor-cli -y[/bold]\n"
|
|
52
|
+
" [bold]pip install -U \"bittensor[cli]\" --force-reinstall[/bold]\n\n"
|
|
53
|
+
"npcli itself only needs [bold]bittensor-wallet[/bold] to sign. Prefer:\n"
|
|
54
|
+
" [bold]pip install bittensor-wallet[/bold]"
|
|
55
|
+
)
|
|
56
|
+
|
|
38
57
|
# ---------------------------------------------------------------------------
|
|
39
58
|
# Internal helpers
|
|
40
59
|
# ---------------------------------------------------------------------------
|
|
@@ -146,16 +165,99 @@ def _extract_btcli_payload(stdout_text: str, original_message: str) -> dict[str,
|
|
|
146
165
|
return data
|
|
147
166
|
|
|
148
167
|
|
|
168
|
+
def _installed_distribution_names() -> set[str]:
|
|
169
|
+
try:
|
|
170
|
+
from importlib.metadata import distributions
|
|
171
|
+
except ImportError: # pragma: no cover
|
|
172
|
+
return set()
|
|
173
|
+
names: set[str] = set()
|
|
174
|
+
for dist in distributions():
|
|
175
|
+
raw = dist.metadata.get("Name") or dist.metadata.get("name") or ""
|
|
176
|
+
if raw:
|
|
177
|
+
names.add(raw.lower().replace("_", "-"))
|
|
178
|
+
return names
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def has_scalecodec_cyscale_conflict(names: set[str] | None = None) -> bool:
|
|
182
|
+
"""True when both SCALE codec packages occupy the ``scalecodec`` namespace."""
|
|
183
|
+
installed = names if names is not None else _installed_distribution_names()
|
|
184
|
+
has_legacy = "scalecodec" in installed or "py-scale-codec" in installed
|
|
185
|
+
return has_legacy and "cyscale" in installed
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _looks_like_scalecodec_conflict(text: str) -> bool:
|
|
189
|
+
low = text.lower()
|
|
190
|
+
return "conflict detected" in low and "scalecodec" in low and "cyscale" in low
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _signature_to_hex(sig: Any) -> str:
|
|
194
|
+
if isinstance(sig, (bytes, bytearray)):
|
|
195
|
+
return bytes(sig).hex()
|
|
196
|
+
text = str(sig).strip()
|
|
197
|
+
if text.startswith(("0x", "0X")):
|
|
198
|
+
return text[2:]
|
|
199
|
+
return text
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _sign_with_bittensor_wallet(wallet_name: str, message: str) -> dict[str, Any] | None:
|
|
203
|
+
"""Sign with ``bittensor_wallet`` (no btcli / ASI import). None if not installed."""
|
|
204
|
+
try:
|
|
205
|
+
from bittensor_wallet import Wallet # type: ignore[import-untyped]
|
|
206
|
+
except ImportError:
|
|
207
|
+
return None
|
|
208
|
+
except RuntimeError as e:
|
|
209
|
+
if _looks_like_scalecodec_conflict(str(e)):
|
|
210
|
+
console.print(_SCALECODEC_CONFLICT_HINT)
|
|
211
|
+
raise SystemExit(1) from e
|
|
212
|
+
raise
|
|
213
|
+
|
|
214
|
+
wallet = Wallet(name=wallet_name)
|
|
215
|
+
if not wallet.coldkey_file.exists_on_device():
|
|
216
|
+
console.print(
|
|
217
|
+
f"[red]Coldkey not found[/red] for wallet [bold]{wallet_name}[/bold]. "
|
|
218
|
+
"Check the name with [bold]btcli wallet list[/bold]."
|
|
219
|
+
)
|
|
220
|
+
raise SystemExit(1)
|
|
221
|
+
|
|
222
|
+
console.print("[dim]Enter the coldkey password if prompted.[/dim]")
|
|
223
|
+
try:
|
|
224
|
+
keypair = wallet.coldkey
|
|
225
|
+
except Exception as e:
|
|
226
|
+
console.print(
|
|
227
|
+
f"[red]Could not unlock coldkey[/red] for wallet [bold]{wallet_name}[/bold]: {e}"
|
|
228
|
+
)
|
|
229
|
+
raise SystemExit(1) from e
|
|
230
|
+
|
|
231
|
+
sig_hex = _signature_to_hex(keypair.sign(message.encode("utf-8")))
|
|
232
|
+
return {
|
|
233
|
+
"message": message,
|
|
234
|
+
"address": keypair.ss58_address,
|
|
235
|
+
"signature": sig_hex,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def sign_coldkey_challenge(wallet_name: str, message: str) -> dict[str, Any]:
|
|
240
|
+
"""Sign a coldkey challenge with bittensor_wallet, falling back to btcli."""
|
|
241
|
+
signed = _sign_with_bittensor_wallet(wallet_name, message)
|
|
242
|
+
if signed is not None:
|
|
243
|
+
return signed
|
|
244
|
+
return run_btcli_sign(wallet_name, message)
|
|
245
|
+
|
|
246
|
+
|
|
149
247
|
def run_btcli_sign(wallet_name: str, message: str) -> dict[str, Any]:
|
|
150
248
|
"""Run btcli wallet sign; inherit stdin/stderr so password prompts work."""
|
|
151
249
|
btcli = shutil.which("btcli")
|
|
152
250
|
if not btcli:
|
|
153
251
|
console.print(
|
|
154
|
-
"[red]
|
|
155
|
-
"
|
|
252
|
+
"[red]bittensor-wallet not installed[/red] and [bold]btcli[/bold] is not on PATH.\n\n"
|
|
253
|
+
f"{_WALLET_INSTALL_HINT}"
|
|
156
254
|
)
|
|
157
255
|
raise SystemExit(1)
|
|
158
256
|
|
|
257
|
+
if has_scalecodec_cyscale_conflict():
|
|
258
|
+
console.print(_SCALECODEC_CONFLICT_HINT)
|
|
259
|
+
raise SystemExit(1)
|
|
260
|
+
|
|
159
261
|
cmd = [btcli, "wallet", "sign", "--wallet-name", wallet_name, "--message", message, "--json-output"]
|
|
160
262
|
try:
|
|
161
263
|
proc = subprocess.Popen(cmd, stdin=sys.stdin, stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=False)
|
|
@@ -200,7 +302,10 @@ def run_btcli_sign(wallet_name: str, message: str) -> dict[str, Any]:
|
|
|
200
302
|
t_err.join(timeout=2)
|
|
201
303
|
out = b"".join(stdout_chunks).decode("utf-8", errors="replace")
|
|
202
304
|
|
|
305
|
+
err = b"".join(stderr_chunks).decode("utf-8", errors="replace")
|
|
203
306
|
if proc.returncode != 0:
|
|
307
|
+
if _looks_like_scalecodec_conflict(out + err):
|
|
308
|
+
console.print(_SCALECODEC_CONFLICT_HINT)
|
|
204
309
|
raise SystemExit(1)
|
|
205
310
|
|
|
206
311
|
data = _extract_btcli_payload(out, message)
|
|
@@ -242,12 +347,8 @@ def register_coldkey(wallet: str, api_key: str, base_url: str) -> int:
|
|
|
242
347
|
return 1
|
|
243
348
|
|
|
244
349
|
console.print(f"Signing with wallet [bold]{wallet}[/bold]...")
|
|
245
|
-
console.print(
|
|
246
|
-
"[dim]Passing through btcli output and prompts below. "
|
|
247
|
-
"Respond directly in this terminal when btcli asks for input.[/dim]"
|
|
248
|
-
)
|
|
249
350
|
try:
|
|
250
|
-
signed =
|
|
351
|
+
signed = sign_coldkey_challenge(wallet, msg)
|
|
251
352
|
except KeyboardInterrupt:
|
|
252
353
|
console.print("\n[yellow]Interrupted — coldkey registration was not completed.[/yellow]")
|
|
253
354
|
return 130
|
|
@@ -518,7 +619,7 @@ def api_keys_revoke(key_id: str, api_key: str | None) -> None:
|
|
|
518
619
|
def cmd_register_coldkey(wallet: str, api_key: str | None) -> None:
|
|
519
620
|
"""Bind or replace the Bittensor coldkey on your Nepher account.
|
|
520
621
|
|
|
521
|
-
Requires
|
|
622
|
+
Requires bittensor-wallet (or btcli on PATH). Run the same command
|
|
522
623
|
with a different --wallet to replace an existing coldkey.
|
|
523
624
|
"""
|
|
524
625
|
resolved_key = api_key or get_stored_api_key()
|
|
@@ -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]")
|
|
@@ -17,6 +17,7 @@ from rich.console import Console
|
|
|
17
17
|
from rich.panel import Panel
|
|
18
18
|
from rich.table import Table
|
|
19
19
|
|
|
20
|
+
from nepher_cli.commands.highlight import highlight as highlight_group
|
|
20
21
|
from nepher_cli.config import TOURNAMENT_BACKEND
|
|
21
22
|
from nepher_cli.core.credentials import get_auth_headers, get_stored_api_key
|
|
22
23
|
from nepher_cli.core.http import parse_error_body
|
|
@@ -314,6 +315,9 @@ def tournament() -> None:
|
|
|
314
315
|
"""Browse tournaments, check leaderboards, validate agents, and submit to Subnet 49."""
|
|
315
316
|
|
|
316
317
|
|
|
318
|
+
tournament.add_command(highlight_group)
|
|
319
|
+
|
|
320
|
+
|
|
317
321
|
@tournament.command("list")
|
|
318
322
|
@click.option("--active-only", is_flag=True, help="Show only active tournaments.")
|
|
319
323
|
@click.option("--limit", type=int, default=50, show_default=True, help="Maximum tournaments to return.")
|
|
@@ -524,8 +528,9 @@ def tournament_submit(
|
|
|
524
528
|
Your Nepher account is identified by the API key (--api-key, NEPHER_API_KEY,
|
|
525
529
|
or credentials from npcli account login). The wallet hotkey signs the archive.
|
|
526
530
|
|
|
527
|
-
Requires [bold]bittensor[/bold] for wallet signing:
|
|
528
|
-
pip install bittensor
|
|
531
|
+
Requires [bold]bittensor-wallet[/bold] for wallet signing:
|
|
532
|
+
pip install bittensor-wallet
|
|
533
|
+
# or: pip install "nepher-cli[bittensor]"
|
|
529
534
|
"""
|
|
530
535
|
resolved_key = _resolve_api_key(api_key)
|
|
531
536
|
resolved_url = api_url or TOURNAMENT_BACKEND
|
|
@@ -67,6 +67,20 @@ def authed_post(
|
|
|
67
67
|
return client.post(url, headers=headers, json=json_body, timeout=timeout)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def authed_patch(
|
|
71
|
+
url: str,
|
|
72
|
+
*,
|
|
73
|
+
api_key: str | None = None,
|
|
74
|
+
json_body: dict[str, Any] | None = None,
|
|
75
|
+
) -> httpx.Response:
|
|
76
|
+
"""PATCH with auth headers resolved from credential store or override."""
|
|
77
|
+
from nepher_cli.core.credentials import get_auth_headers
|
|
78
|
+
|
|
79
|
+
headers = get_auth_headers(api_key)
|
|
80
|
+
with httpx.Client() as client:
|
|
81
|
+
return client.patch(url, headers=headers, json=json_body, timeout=60.0)
|
|
82
|
+
|
|
83
|
+
|
|
70
84
|
def authed_delete(url: str, *, api_key: str | None = None) -> httpx.Response:
|
|
71
85
|
"""DELETE with auth headers resolved from credential store or override."""
|
|
72
86
|
from nepher_cli.core.credentials import get_auth_headers
|
|
@@ -5,9 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import time
|
|
6
6
|
|
|
7
7
|
_BITTENSOR_HINT = (
|
|
8
|
-
"Agent submission requires [bold]bittensor[/bold] for wallet signing.\n\n"
|
|
8
|
+
"Agent submission requires [bold]bittensor-wallet[/bold] for wallet signing.\n\n"
|
|
9
9
|
"Install it with:\n"
|
|
10
|
-
" [bold]pip install bittensor[/bold]\n\n"
|
|
10
|
+
" [bold]pip install bittensor-wallet[/bold]\n\n"
|
|
11
11
|
"Or if you installed nepher-cli via pip:\n"
|
|
12
12
|
" [bold]pip install \"nepher-cli[bittensor]\"[/bold]"
|
|
13
13
|
)
|
|
@@ -26,7 +26,7 @@ def _require_wallet_class():
|
|
|
26
26
|
except ImportError:
|
|
27
27
|
from rich.console import Console
|
|
28
28
|
Console(stderr=True).print(
|
|
29
|
-
f"[red]bittensor not installed.[/red]\n\n{_BITTENSOR_HINT}"
|
|
29
|
+
f"[red]bittensor-wallet not installed.[/red]\n\n{_BITTENSOR_HINT}"
|
|
30
30
|
)
|
|
31
31
|
raise SystemExit(1)
|
|
32
32
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|