nepher-cli 0.2.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.
@@ -0,0 +1,100 @@
1
+ """Tournament submission API calls — httpx only, no external dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from nepher_cli.core.http import parse_error_body
11
+
12
+ _DEFAULT_TIMEOUT = 30.0
13
+ _UPLOAD_TIMEOUT = 600.0 # 10 min — large agent ZIPs can be slow
14
+
15
+
16
+ def _json_headers(api_key: str) -> dict[str, str]:
17
+ return {
18
+ "X-API-Key": api_key,
19
+ "Content-Type": "application/json",
20
+ "Accept": "application/json",
21
+ }
22
+
23
+
24
+ def _raise_for_error(response: httpx.Response) -> None:
25
+ if response.status_code < 400:
26
+ return
27
+ msg = (
28
+ parse_error_body(response.text)
29
+ or response.text.strip()
30
+ or f"HTTP {response.status_code}"
31
+ )
32
+ raise RuntimeError(msg)
33
+
34
+
35
+ async def request_upload_token(
36
+ *,
37
+ api_key: str,
38
+ api_url: str,
39
+ miner_hotkey: str,
40
+ public_key: str,
41
+ file_info: str,
42
+ signature: str,
43
+ file_size: int,
44
+ tournament_id: str | None = None,
45
+ ) -> dict[str, Any]:
46
+ """POST /api/v1/agents/upload/verify → parsed JSON token dict.
47
+
48
+ The returned dict contains at least ``upload_token`` and ``tournament_id``.
49
+ """
50
+ body: dict[str, Any] = {
51
+ "miner_hotkey": miner_hotkey,
52
+ "public_key": public_key,
53
+ "file_info": file_info,
54
+ "signature": signature,
55
+ "file_size": file_size,
56
+ }
57
+ if tournament_id is not None:
58
+ body["tournament_id"] = tournament_id
59
+
60
+ url = f"{api_url.rstrip('/')}/api/v1/agents/upload/verify"
61
+ async with httpx.AsyncClient(
62
+ timeout=_DEFAULT_TIMEOUT,
63
+ headers=_json_headers(api_key),
64
+ ) as client:
65
+ response = await client.post(url, json=body)
66
+ _raise_for_error(response)
67
+ return response.json()
68
+
69
+
70
+ async def upload_agent(
71
+ *,
72
+ api_key: str,
73
+ api_url: str,
74
+ tournament_id: str,
75
+ upload_token: str,
76
+ miner_hotkey: str,
77
+ content_hash: str,
78
+ file_path: Path,
79
+ ) -> str:
80
+ """POST /api/v1/agents/upload/{tournament_id} (multipart) → agent_id string."""
81
+ url = f"{api_url.rstrip('/')}/api/v1/agents/upload/{tournament_id}"
82
+ headers = {
83
+ "X-API-Key": api_key,
84
+ "Accept": "application/json",
85
+ "X-Upload-Token": upload_token,
86
+ }
87
+ async with httpx.AsyncClient(timeout=httpx.Timeout(_UPLOAD_TIMEOUT)) as client:
88
+ with open(file_path, "rb") as fh:
89
+ response = await client.post(
90
+ url,
91
+ files={"file": (file_path.name, fh, "application/zip")},
92
+ data={"miner_hotkey": miner_hotkey, "content_hash": content_hash},
93
+ headers=headers,
94
+ )
95
+ _raise_for_error(response)
96
+ data = response.json()
97
+ agent_id = data.get("id") or data.get("agent_id")
98
+ if not agent_id:
99
+ raise RuntimeError(f"No agent ID in upload response: {data}")
100
+ return str(agent_id)
@@ -0,0 +1,50 @@
1
+ """Archive and checksum helpers — pure Python stdlib, no external dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import zipfile
7
+ from pathlib import Path
8
+
9
+ # Names and suffixes excluded from submission archives
10
+ _EXCLUDED_NAMES: frozenset[str] = frozenset({
11
+ "__pycache__", ".git", ".gitignore",
12
+ "logs", "outputs", ".env", "venv", ".venv", "node_modules",
13
+ })
14
+ _EXCLUDED_SUFFIXES: frozenset[str] = frozenset({".pyc", ".pyo"})
15
+
16
+
17
+ def _is_excluded(path_part: str) -> bool:
18
+ p = Path(path_part)
19
+ return p.name in _EXCLUDED_NAMES or p.suffix in _EXCLUDED_SUFFIXES
20
+
21
+
22
+ def zip_directory(source_dir: Path, output_path: Path) -> None:
23
+ """Create a ZIP archive of *source_dir* at *output_path*.
24
+
25
+ Files are added in sorted order for reproducibility. Common
26
+ build artefacts and VCS directories are excluded automatically.
27
+ """
28
+ output_path.parent.mkdir(parents=True, exist_ok=True)
29
+ with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
30
+ for file_path in sorted(source_dir.rglob("*")):
31
+ if not file_path.is_file():
32
+ continue
33
+ rel = file_path.relative_to(source_dir)
34
+ if any(_is_excluded(part) for part in rel.parts):
35
+ continue
36
+ zf.write(file_path, rel)
37
+
38
+
39
+ def compute_checksum(file_path: Path) -> str:
40
+ """Return the SHA-256 hex digest of *file_path*."""
41
+ h = hashlib.sha256()
42
+ with open(file_path, "rb") as f:
43
+ for chunk in iter(lambda: f.read(8192), b""):
44
+ h.update(chunk)
45
+ return h.hexdigest()
46
+
47
+
48
+ def get_file_size(file_path: Path) -> int:
49
+ """Return the size of *file_path* in bytes."""
50
+ return file_path.stat().st_size
@@ -0,0 +1,89 @@
1
+ """Bittensor wallet operations — lazy import so bittensor is an optional dependency."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ _BITTENSOR_HINT = (
8
+ "Agent submission requires [bold]bittensor[/bold] for wallet signing.\n\n"
9
+ "Install it with:\n"
10
+ " [bold]pip install bittensor[/bold]\n\n"
11
+ "Or if you installed nepher-cli via pip:\n"
12
+ " [bold]pip install \"nepher-cli[bittensor]\"[/bold]"
13
+ )
14
+
15
+
16
+ def _require_wallet_class():
17
+ """Return the ``bittensor_wallet.Wallet`` class or exit with a clear error.
18
+
19
+ ``bittensor_wallet`` is a standalone package that is installed automatically
20
+ as a dependency of ``bittensor``. Importing it directly keeps the check
21
+ lightweight — we do not need the full bittensor package at import time.
22
+ """
23
+ try:
24
+ from bittensor_wallet import Wallet # type: ignore[import]
25
+ return Wallet
26
+ except ImportError:
27
+ from rich.console import Console
28
+ Console(stderr=True).print(
29
+ f"[red]bittensor not installed.[/red]\n\n{_BITTENSOR_HINT}"
30
+ )
31
+ raise SystemExit(1)
32
+
33
+
34
+ def load_wallet(wallet_name: str, wallet_hotkey: str):
35
+ """Load a Bittensor wallet by name and hotkey name.
36
+
37
+ Exits with a user-friendly message if bittensor is not installed or the
38
+ hotkey file does not exist on this machine.
39
+ """
40
+ Wallet = _require_wallet_class()
41
+ wallet = Wallet(name=wallet_name, hotkey=wallet_hotkey)
42
+ if not wallet.hotkey_file.exists_on_device():
43
+ from rich.console import Console
44
+ Console(stderr=True).print(
45
+ f"[red]Hotkey '[bold]{wallet_hotkey}[/bold]' not found "
46
+ f"for wallet '[bold]{wallet_name}[/bold]'.[/red]\n"
47
+ "Check the wallet name and hotkey with [bold]btcli wallet list[/bold]."
48
+ )
49
+ raise SystemExit(1)
50
+ return wallet
51
+
52
+
53
+ def get_hotkey(wallet) -> str:
54
+ """Return the SS58 address of the wallet's hotkey."""
55
+ return wallet.hotkey.ss58_address
56
+
57
+
58
+ def get_public_key(wallet) -> str:
59
+ """Return the hex-encoded public key of the wallet's hotkey."""
60
+ return wallet.hotkey.public_key.hex()
61
+
62
+
63
+ def sign_message(wallet, message: str) -> str:
64
+ """Sign *message* with the wallet's hotkey and return a hex-encoded signature."""
65
+ return wallet.hotkey.sign(message.encode()).hex()
66
+
67
+
68
+ def create_file_info(miner_hotkey: str, content_hash: str, timestamp: int) -> str:
69
+ """Build the canonical file_info string: ``'hotkey:content_hash:timestamp'``."""
70
+ return f"{miner_hotkey}:{content_hash}:{timestamp}"
71
+
72
+
73
+ def prepare_submission_credentials(
74
+ wallet_name: str,
75
+ wallet_hotkey: str,
76
+ content_hash: str,
77
+ ) -> tuple[str, str, str, str]:
78
+ """Load wallet and produce all signing artefacts needed for a submission.
79
+
80
+ Returns:
81
+ ``(miner_hotkey, public_key, file_info, signature)``
82
+ """
83
+ wallet = load_wallet(wallet_name, wallet_hotkey)
84
+ miner_hotkey = get_hotkey(wallet)
85
+ public_key = get_public_key(wallet)
86
+ timestamp = int(time.time())
87
+ file_info = create_file_info(miner_hotkey, content_hash, timestamp)
88
+ signature = sign_message(wallet, file_info)
89
+ return miner_hotkey, public_key, file_info, signature
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: nepher-cli
3
+ Version: 0.2.0
4
+ Summary: npcli — unified Nepher command-line interface for accounts, hackathons, EnvHub, tournaments, and SimStore
5
+ Project-URL: Homepage, https://github.com/nepher-ai/nepher-cli
6
+ Author: Nepher
7
+ License: MIT
8
+ Keywords: bittensor,cli,envhub,hackathon,nepher,subnet,tournament
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: click>=8.1.0
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: keyring>=25.0.0
20
+ Requires-Dist: platformdirs>=4.0.0
21
+ Requires-Dist: rich>=13.7.0
22
+ Provides-Extra: bittensor
23
+ Requires-Dist: bittensor; extra == 'bittensor'
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.4.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # nepher-cli
30
+
31
+ The unified CLI for [Nepher Robotics](https://nepher.ai) — accounts, hackathons, EnvHub, tournaments, and SimStore from a single tool.
32
+
33
+ **Requires:** Python 3.10+
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ pip install nepher-cli
39
+ ```
40
+
41
+ Both `npcli` and `nepher-cli` entry points are registered after install.
42
+
43
+ ```bash
44
+ npcli --version
45
+ npcli --help
46
+ ```
47
+
48
+ ## Commands
49
+
50
+ | Command / Group | Description |
51
+ |-----------------|-------------|
52
+ | `npcli login` | Log in with a Nepher API key |
53
+ | `npcli whoami` | Show the currently authenticated user |
54
+ | `npcli logout` | Clear locally stored credentials |
55
+ | `npcli account` | API keys and coldkey registration |
56
+ | `npcli tournament` | Browse tournaments, submit agents, leaderboards |
57
+ | `npcli envhub` | Manage Isaac Lab environment bundles |
58
+ | `npcli hackathon` | Browse and submit to hackathons |
59
+ | `npcli simstore` | SimStore marketplace *(coming soon)* |
60
+
61
+ ## Quick Start
62
+
63
+ ### Authenticate
64
+
65
+ ```bash
66
+ npcli login --api-key nepher_xxxxxxxx
67
+ npcli whoami
68
+ npcli logout
69
+ ```
70
+
71
+ Get your API key at [account.nepher.ai](https://account.nepher.ai) → Account → API Keys.
72
+ For CI/CD, set `NEPHER_API_KEY=nepher_xxxxxxxx` instead of logging in.
73
+
74
+ ### Tournaments
75
+
76
+ ```bash
77
+ npcli tournament list
78
+ npcli tournament list-active
79
+ npcli tournament status <tournament_id>
80
+ npcli tournament leaderboard <tournament_id>
81
+ ```
82
+
83
+ Check your agent directory structure (no extra dependencies needed):
84
+
85
+ ```bash
86
+ npcli tournament check --path ./my-agent
87
+ npcli tournament check --path ./my-agent --verbose # also show recommended-file warnings
88
+ ```
89
+
90
+ Submit an agent (requires `bittensor` for wallet signing):
91
+
92
+ ```bash
93
+ pip install bittensor
94
+ # or: pip install "nepher-cli[bittensor]"
95
+ ```
96
+
97
+ ```bash
98
+ npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
99
+ ```
100
+
101
+ Full options for `submit`:
102
+
103
+ ```
104
+ --path <path> Agent directory
105
+ --wallet-name <str> Bittensor wallet name (default: miner)
106
+ --wallet-hotkey <str> Bittensor wallet hotkey (default: default)
107
+ --api-key <key> Nepher API key (falls back to stored credentials)
108
+ --tournament-id <id> Target tournament ID (required when multiple are active)
109
+ --api-url <url> Override tournament API URL
110
+ -v / --verbose Verbose output
111
+ ```
112
+
113
+ ### EnvHub
114
+
115
+ ```bash
116
+ npcli envhub list
117
+ npcli envhub download <env_id>
118
+ npcli envhub upload ./my-bundle --category navigation
119
+ npcli envhub cache list
120
+ npcli envhub cache info
121
+ npcli envhub cache clear
122
+ ```
123
+
124
+ ### Hackathons
125
+
126
+ ```bash
127
+ npcli hackathon list
128
+
129
+ npcli hackathon submit \
130
+ --title "My entry" \
131
+ --submission ./my-project \
132
+ --assets ./my-assets
133
+ ```
134
+
135
+ Full options:
136
+
137
+ ```
138
+ --hackathon-id <uuid> Required when multiple hackathons are open
139
+ --title <str> Entry title
140
+ --description <markdown> Entry description
141
+ --thumbnail <file> Cover image (JPEG, PNG, WebP, GIF)
142
+ --submission <path> Project folder or submission.zip
143
+ --assets <path> Assets folder or assets.zip (images, videos, PDFs)
144
+ --public-source Mark source as public
145
+ ```
146
+
147
+ ### API Keys
148
+
149
+ ```bash
150
+ npcli account api-keys list
151
+ npcli account api-keys create --name "CI key" --platform hackertone
152
+ npcli account api-keys revoke <key_id>
153
+ ```
154
+
155
+ ### Coldkey Registration
156
+
157
+ Bind a Bittensor coldkey to your account (requires `bittensor` + `btcli` on PATH):
158
+
159
+ ```bash
160
+ pip install bittensor
161
+ npcli account register-coldkey --wallet <wallet_name>
162
+ ```
163
+
164
+ ## API Key Requirements
165
+
166
+ - Must start with `nepher_`
167
+ - Must be active and not expired
168
+ - `hackathon submit` requires **Hackathon** scope (or an unrestricted key)
169
+ - `tournament submit` requires **Tournament** scope (or an unrestricted key)
170
+
171
+ ## Troubleshooting
172
+
173
+ | Error | Fix |
174
+ |-------|-----|
175
+ | `npcli: command not found` | Ensure `pip install` succeeded and Python scripts dir is on `PATH` |
176
+ | `invalid api key format` | Key must start with `nepher_` |
177
+ | `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
178
+ | `api key expired` | Run `npcli account api-keys create` |
179
+ | `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
180
+ | `bittensor not installed` | `pip install bittensor` (only needed for `tournament submit`) |
181
+ | `Not logged in` | `npcli login --api-key nepher_...` |
182
+
183
+ Run any command with `--help` for full flag details:
184
+
185
+ ```bash
186
+ npcli --help
187
+ npcli hackathon submit --help
188
+ npcli tournament submit --help
189
+ ```
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,26 @@
1
+ nepher_cli/__init__.py,sha256=L11vbIe673ZKYNG-FDL_KhGKcTmharqE6PQS9Wo-Rrw,81
2
+ nepher_cli/__main__.py,sha256=FMYmbifGU7eWdjSmv0RXSgXUO0EGzqsMkn2AA2dAws4,93
3
+ nepher_cli/cli.py,sha256=LUCi6ZjpcAjPiUKvKyabddASUoddzLK2aVJ3s5eqP3c,1873
4
+ nepher_cli/config.py,sha256=u3-D3XVSpmu_QCXaVqN7A4gXI5tkCp5P66M2SrWrscw,916
5
+ nepher_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ nepher_cli/commands/__init__.py,sha256=JXNTkej-suvV6Jc3JD0EbB1gd1tN4kRpghV9_B4Smj4,29
7
+ nepher_cli/commands/account.py,sha256=F76UrcCakYBYT9w-gGCaz66XrEFDlUEGCtLqPKQrtwA,19574
8
+ nepher_cli/commands/envhub.py,sha256=EapZjDahWwLFxHA5hWKGIJQkOJoLBlvzjaKjVZtOI2g,17984
9
+ nepher_cli/commands/hackathon.py,sha256=8YtoW-w9nmNbEN1hfmQwVNGeIkJXCLR5u5TT_8LiQHE,29042
10
+ nepher_cli/commands/simstore.py,sha256=8lrUIQEZzynPzQMWN9K8TWwoSWUUYBTPRO4NUTzi-OY,1339
11
+ nepher_cli/commands/tournament.py,sha256=pg4ZcriLZjF9lwJID4XK3-hwTWqVU8TLGyU_KKSN1C8,25374
12
+ nepher_cli/core/__init__.py,sha256=9XoS2YWSa28pFNeSvj0lQ6qcprhgXKVmwUZyQck0F44,55
13
+ nepher_cli/core/credentials.py,sha256=i6HLNLkawydJnzpjw_YRGqcMu5P3CvIuq8vsdKBwmhk,6723
14
+ nepher_cli/core/http.py,sha256=gGwIqd-rTSYASmwhSwamK6ksNUPTc3cmNztJzmer47M,2497
15
+ nepher_cli/envhub/__init__.py,sha256=l-VLi2YioXuRRJiJc7qfQlyaHIreQB30OXAENRjsl6M,64
16
+ nepher_cli/envhub/cache.py,sha256=fTxSdk3Kf4rbTD93juhUFhMNN7K4JK3lKCBDjJKMp90,1815
17
+ nepher_cli/envhub/config.py,sha256=Fa8KcihM9X5rW4xqz1ppzHUDjenn5o-bGGfpeGf-LvE,5395
18
+ nepher_cli/tournament/__init__.py,sha256=o-Ap1Pke9Gq4LrcQd33GP5Gu8o3K8amiXMRhDgM8Gbk,65
19
+ nepher_cli/tournament/agent_check.py,sha256=9k92L4e49Np54IDHEzIpKI0xjbDyY7YCU6hqNoYl3iU,2229
20
+ nepher_cli/tournament/api.py,sha256=141yi2elQ3bJB68m26kICMurT7uE9ewLhSxdpSTeeBo,2957
21
+ nepher_cli/tournament/packer.py,sha256=Xa6HMFjP5bRLDGr2Cws8DJFrQZoNCNBnWenVrjkQAjI,1742
22
+ nepher_cli/tournament/wallet.py,sha256=tfeUA7YOWr7GW0zMXVlaXDAIGxRw8wBLwFNcPOxwmwI,3250
23
+ nepher_cli-0.2.0.dist-info/METADATA,sha256=6_zB4KiNtO65Bzr8KihUpDrvi-GY67X3OXdCTISmHGU,5463
24
+ nepher_cli-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
25
+ nepher_cli-0.2.0.dist-info/entry_points.txt,sha256=QQ1l07o9WxwntxuU1K3Hnz7nRc0fFKRBRMAHJlkjfEQ,79
26
+ nepher_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ nepher-cli = nepher_cli.cli:main
3
+ npcli = nepher_cli.cli:main