nepher-cli 0.2.6__tar.gz → 0.2.8__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.8/PKG-INFO +177 -0
- nepher_cli-0.2.8/README.md +149 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/pyproject.toml +1 -1
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/envhub.py +106 -52
- nepher_cli-0.2.8/src/nepher_cli/envhub/bundle.py +73 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/envhub/cache.py +8 -3
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/envhub/config.py +42 -19
- nepher_cli-0.2.6/PKG-INFO +0 -200
- nepher_cli-0.2.6/README.md +0 -172
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/.gitignore +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/__init__.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/__main__.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/cli.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/__init__.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/account.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/hackathon.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/highlight.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/simstore.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/commands/tournament.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/config.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/core/__init__.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/core/credentials.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/core/http.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/envhub/__init__.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/py.typed +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/__init__.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/agent_check.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/api.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/packer.py +0 -0
- {nepher_cli-0.2.6 → nepher_cli-0.2.8}/src/nepher_cli/tournament/wallet.py +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nepher-cli
|
|
3
|
+
Version: 0.2.8
|
|
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-wallet>=4.1.0; 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
|
+
<div align="center">
|
|
30
|
+
|
|
31
|
+
# nepher-cli
|
|
32
|
+
|
|
33
|
+
**The unified CLI for [Nepher Robotics](https://nepher.ai): accounts, tournaments, hackathons, EnvHub and SimStore.**
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/nepher-cli/)
|
|
36
|
+
[](https://www.python.org/downloads/)
|
|
37
|
+
[](#license)
|
|
38
|
+
|
|
39
|
+
[Install](#install) •
|
|
40
|
+
[Commands](#commands) •
|
|
41
|
+
[Tournaments](#tournaments) •
|
|
42
|
+
[EnvHub](#envhub) •
|
|
43
|
+
[Hackathons](#hackathons) •
|
|
44
|
+
[Account](#account) •
|
|
45
|
+
[Troubleshooting](#troubleshooting)
|
|
46
|
+
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install nepher-cli
|
|
55
|
+
pip install "nepher-cli[bittensor]" # adds wallet signing for tournament submit and coldkey registration
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This installs two equivalent commands, `npcli` and `nepher-cli`.
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npcli login --api-key nepher_xxxxxxxx
|
|
64
|
+
npcli whoami
|
|
65
|
+
npcli --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Get your API key at [nepher.ai/account](https://nepher.ai/account) under **API Keys**. In CI/CD, set `NEPHER_API_KEY` instead of logging in.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
| Command | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `npcli login` / `whoami` / `logout` | Manage authentication |
|
|
75
|
+
| `npcli account` | API keys and coldkey registration |
|
|
76
|
+
| `npcli tournament` | Browse tournaments, check and submit agents, view leaderboards |
|
|
77
|
+
| `npcli envhub` | Manage Isaac Lab environment bundles |
|
|
78
|
+
| `npcli hackathon` | Browse and submit to hackathons |
|
|
79
|
+
| `npcli simstore` | SimStore marketplace (coming soon) |
|
|
80
|
+
|
|
81
|
+
Run any command with `--help` for all options.
|
|
82
|
+
|
|
83
|
+
## Tournaments
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npcli tournament list # or list-active
|
|
87
|
+
npcli tournament status <tournament_id>
|
|
88
|
+
npcli tournament leaderboard <tournament_id>
|
|
89
|
+
npcli tournament check --path ./my-agent
|
|
90
|
+
npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`check` validates the agent directory (add `--verbose` for recommended-file warnings). `submit` requires `bittensor-wallet`.
|
|
94
|
+
|
|
95
|
+
<details>
|
|
96
|
+
<summary>Full <code>tournament submit</code> options</summary>
|
|
97
|
+
|
|
98
|
+
| Option | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `--path <path>` | Agent directory |
|
|
101
|
+
| `--wallet-name <str>` | Bittensor wallet name (default: `miner`) |
|
|
102
|
+
| `--wallet-hotkey <str>` | Bittensor wallet hotkey (default: `default`) |
|
|
103
|
+
| `--api-key <key>` | Nepher API key (falls back to stored credentials) |
|
|
104
|
+
| `--tournament-id <id>` | Target tournament, required when several are active |
|
|
105
|
+
| `--api-url <url>` | Override the tournament API URL |
|
|
106
|
+
| `-v`, `--verbose` | Verbose output |
|
|
107
|
+
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
## EnvHub
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npcli envhub list
|
|
114
|
+
npcli envhub download <env_id>
|
|
115
|
+
npcli envhub upload ./my-bundle --category navigation
|
|
116
|
+
npcli envhub cache list | info | clear
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Shares login, API and cache with the standalone [`nepher`](https://github.com/nepher-ai/envhub) package.
|
|
120
|
+
|
|
121
|
+
## Hackathons
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npcli hackathon list
|
|
125
|
+
npcli hackathon submit --title "My entry" --submission ./my-project --assets ./my-assets
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
<summary>Full <code>hackathon submit</code> options</summary>
|
|
130
|
+
|
|
131
|
+
| Option | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `--hackathon-id <uuid>` | Required when several hackathons are open |
|
|
134
|
+
| `--title <str>` | Entry title |
|
|
135
|
+
| `--description <markdown>` | Entry description |
|
|
136
|
+
| `--thumbnail <file>` | Cover image (JPEG, PNG, WebP, GIF) |
|
|
137
|
+
| `--submission <path>` | Project folder or `submission.zip` |
|
|
138
|
+
| `--assets <path>` | Assets folder or `assets.zip` (images, videos, PDFs) |
|
|
139
|
+
| `--public-source` | Mark source as public |
|
|
140
|
+
|
|
141
|
+
</details>
|
|
142
|
+
|
|
143
|
+
## Account
|
|
144
|
+
|
|
145
|
+
API key management needs a session from `npcli account login`. Don't pass `--api-key` to the `api-keys` commands; the account API rejects `X-API-Key` for key management.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npcli account login --api-key nepher_...
|
|
149
|
+
npcli account api-keys list
|
|
150
|
+
npcli account api-keys create --name "CI key" --platform hackathon
|
|
151
|
+
npcli account api-keys revoke <key_id>
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is not needed):
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npcli account register-coldkey --wallet <wallet_name>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Keys must start with `nepher_` and be active. `hackathon submit` needs Hackathon scope and `tournament submit` needs Tournament scope, or use an unrestricted key.
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
| Error | Fix |
|
|
165
|
+
|---|---|
|
|
166
|
+
| `npcli: command not found` | Check the install succeeded and Python's scripts directory is on `PATH` |
|
|
167
|
+
| `Not logged in` | `npcli login --api-key nepher_...` |
|
|
168
|
+
| `invalid api key format` | Key must start with `nepher_` |
|
|
169
|
+
| `api key expired` | Create a new key with `npcli account api-keys create` |
|
|
170
|
+
| `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
|
|
171
|
+
| `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
|
|
172
|
+
| `bittensor-wallet not installed` | `pip install bittensor-wallet` |
|
|
173
|
+
| `Conflict detected: 'scalecodec' ... 'cyscale'` | Leftover `scalecodec` from a Bittensor upgrade. Install `bittensor-wallet` so npcli doesn't need `btcli`, or repair `btcli` with `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT © 2026 Nepher Robotics
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# nepher-cli
|
|
4
|
+
|
|
5
|
+
**The unified CLI for [Nepher Robotics](https://nepher.ai): accounts, tournaments, hackathons, EnvHub and SimStore.**
|
|
6
|
+
|
|
7
|
+
[](https://pypi.org/project/nepher-cli/)
|
|
8
|
+
[](https://www.python.org/downloads/)
|
|
9
|
+
[](#license)
|
|
10
|
+
|
|
11
|
+
[Install](#install) •
|
|
12
|
+
[Commands](#commands) •
|
|
13
|
+
[Tournaments](#tournaments) •
|
|
14
|
+
[EnvHub](#envhub) •
|
|
15
|
+
[Hackathons](#hackathons) •
|
|
16
|
+
[Account](#account) •
|
|
17
|
+
[Troubleshooting](#troubleshooting)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install nepher-cli
|
|
27
|
+
pip install "nepher-cli[bittensor]" # adds wallet signing for tournament submit and coldkey registration
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This installs two equivalent commands, `npcli` and `nepher-cli`.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npcli login --api-key nepher_xxxxxxxx
|
|
36
|
+
npcli whoami
|
|
37
|
+
npcli --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Get your API key at [nepher.ai/account](https://nepher.ai/account) under **API Keys**. In CI/CD, set `NEPHER_API_KEY` instead of logging in.
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
| Command | Description |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `npcli login` / `whoami` / `logout` | Manage authentication |
|
|
47
|
+
| `npcli account` | API keys and coldkey registration |
|
|
48
|
+
| `npcli tournament` | Browse tournaments, check and submit agents, view leaderboards |
|
|
49
|
+
| `npcli envhub` | Manage Isaac Lab environment bundles |
|
|
50
|
+
| `npcli hackathon` | Browse and submit to hackathons |
|
|
51
|
+
| `npcli simstore` | SimStore marketplace (coming soon) |
|
|
52
|
+
|
|
53
|
+
Run any command with `--help` for all options.
|
|
54
|
+
|
|
55
|
+
## Tournaments
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npcli tournament list # or list-active
|
|
59
|
+
npcli tournament status <tournament_id>
|
|
60
|
+
npcli tournament leaderboard <tournament_id>
|
|
61
|
+
npcli tournament check --path ./my-agent
|
|
62
|
+
npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`check` validates the agent directory (add `--verbose` for recommended-file warnings). `submit` requires `bittensor-wallet`.
|
|
66
|
+
|
|
67
|
+
<details>
|
|
68
|
+
<summary>Full <code>tournament submit</code> options</summary>
|
|
69
|
+
|
|
70
|
+
| Option | Description |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `--path <path>` | Agent directory |
|
|
73
|
+
| `--wallet-name <str>` | Bittensor wallet name (default: `miner`) |
|
|
74
|
+
| `--wallet-hotkey <str>` | Bittensor wallet hotkey (default: `default`) |
|
|
75
|
+
| `--api-key <key>` | Nepher API key (falls back to stored credentials) |
|
|
76
|
+
| `--tournament-id <id>` | Target tournament, required when several are active |
|
|
77
|
+
| `--api-url <url>` | Override the tournament API URL |
|
|
78
|
+
| `-v`, `--verbose` | Verbose output |
|
|
79
|
+
|
|
80
|
+
</details>
|
|
81
|
+
|
|
82
|
+
## EnvHub
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npcli envhub list
|
|
86
|
+
npcli envhub download <env_id>
|
|
87
|
+
npcli envhub upload ./my-bundle --category navigation
|
|
88
|
+
npcli envhub cache list | info | clear
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Shares login, API and cache with the standalone [`nepher`](https://github.com/nepher-ai/envhub) package.
|
|
92
|
+
|
|
93
|
+
## Hackathons
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npcli hackathon list
|
|
97
|
+
npcli hackathon submit --title "My entry" --submission ./my-project --assets ./my-assets
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
<details>
|
|
101
|
+
<summary>Full <code>hackathon submit</code> options</summary>
|
|
102
|
+
|
|
103
|
+
| Option | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `--hackathon-id <uuid>` | Required when several hackathons are open |
|
|
106
|
+
| `--title <str>` | Entry title |
|
|
107
|
+
| `--description <markdown>` | Entry description |
|
|
108
|
+
| `--thumbnail <file>` | Cover image (JPEG, PNG, WebP, GIF) |
|
|
109
|
+
| `--submission <path>` | Project folder or `submission.zip` |
|
|
110
|
+
| `--assets <path>` | Assets folder or `assets.zip` (images, videos, PDFs) |
|
|
111
|
+
| `--public-source` | Mark source as public |
|
|
112
|
+
|
|
113
|
+
</details>
|
|
114
|
+
|
|
115
|
+
## Account
|
|
116
|
+
|
|
117
|
+
API key management needs a session from `npcli account login`. Don't pass `--api-key` to the `api-keys` commands; the account API rejects `X-API-Key` for key management.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npcli account login --api-key nepher_...
|
|
121
|
+
npcli account api-keys list
|
|
122
|
+
npcli account api-keys create --name "CI key" --platform hackathon
|
|
123
|
+
npcli account api-keys revoke <key_id>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is not needed):
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npcli account register-coldkey --wallet <wallet_name>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Keys must start with `nepher_` and be active. `hackathon submit` needs Hackathon scope and `tournament submit` needs Tournament scope, or use an unrestricted key.
|
|
133
|
+
|
|
134
|
+
## Troubleshooting
|
|
135
|
+
|
|
136
|
+
| Error | Fix |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `npcli: command not found` | Check the install succeeded and Python's scripts directory is on `PATH` |
|
|
139
|
+
| `Not logged in` | `npcli login --api-key nepher_...` |
|
|
140
|
+
| `invalid api key format` | Key must start with `nepher_` |
|
|
141
|
+
| `api key expired` | Create a new key with `npcli account api-keys create` |
|
|
142
|
+
| `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
|
|
143
|
+
| `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
|
|
144
|
+
| `bittensor-wallet not installed` | `pip install bittensor-wallet` |
|
|
145
|
+
| `Conflict detected: 'scalecodec' ... 'cyscale'` | Leftover `scalecodec` from a Bittensor upgrade. Install `bittensor-wallet` so npcli doesn't need `btcli`, or repair `btcli` with `pip uninstall scalecodec cyscale -y && pip install -U cyscale --force-reinstall` |
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT © 2026 Nepher Robotics
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nepher-cli"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.8"
|
|
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"
|
|
@@ -7,7 +7,9 @@ same operations from inside the unified npcli interface.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import contextlib
|
|
10
11
|
import json
|
|
12
|
+
import os
|
|
11
13
|
import shutil
|
|
12
14
|
import tempfile
|
|
13
15
|
import zipfile
|
|
@@ -21,18 +23,39 @@ from rich.console import Console
|
|
|
21
23
|
from nepher_cli.config import ENVHUB_BACKEND
|
|
22
24
|
from nepher_cli.core.credentials import get_auth_headers, get_stored_api_key
|
|
23
25
|
from nepher_cli.core.http import parse_error_body
|
|
26
|
+
from nepher_cli.envhub.bundle import extract_bundle, iter_bundle_files, safe_env_id
|
|
24
27
|
from nepher_cli.envhub.cache import is_cached_env, list_cached_env_dirs, resolve_cache_dir
|
|
28
|
+
from nepher_cli.envhub.config import (
|
|
29
|
+
get_api_url as get_envhub_api_url,
|
|
30
|
+
)
|
|
25
31
|
from nepher_cli.envhub.config import (
|
|
26
32
|
get_value as get_envhub_config_value,
|
|
33
|
+
)
|
|
34
|
+
from nepher_cli.envhub.config import (
|
|
27
35
|
list_values as list_envhub_config_values,
|
|
36
|
+
)
|
|
37
|
+
from nepher_cli.envhub.config import (
|
|
28
38
|
mask_secret as mask_envhub_config_secret,
|
|
39
|
+
)
|
|
40
|
+
from nepher_cli.envhub.config import (
|
|
29
41
|
parse_config_value,
|
|
42
|
+
)
|
|
43
|
+
from nepher_cli.envhub.config import (
|
|
30
44
|
reset_config as reset_envhub_config,
|
|
45
|
+
)
|
|
46
|
+
from nepher_cli.envhub.config import (
|
|
31
47
|
set_value as set_envhub_config_value,
|
|
32
48
|
)
|
|
33
49
|
|
|
34
50
|
console = Console(stderr=True)
|
|
35
51
|
|
|
52
|
+
# A numeric write timeout switches the SSL socket into timeout mode. On Windows
|
|
53
|
+
# that makes OpenSSL raise SSLWantWriteError as soon as the TCP window fills,
|
|
54
|
+
# which requests/httpcore surface as a failed upload ("Max retries exceeded"
|
|
55
|
+
# / SSLWantWriteError) instead of waiting for the server to read. Leave the
|
|
56
|
+
# write unbounded; bound connect and the wait for the upload response.
|
|
57
|
+
_UPLOAD_TIMEOUT = httpx.Timeout(connect=30.0, read=3600.0, write=None, pool=30.0)
|
|
58
|
+
|
|
36
59
|
|
|
37
60
|
def _headers(api_key: str | None) -> dict[str, str]:
|
|
38
61
|
h = get_auth_headers(api_key)
|
|
@@ -44,7 +67,16 @@ def _headers(api_key: str | None) -> dict[str, str]:
|
|
|
44
67
|
|
|
45
68
|
|
|
46
69
|
def _base() -> str:
|
|
47
|
-
|
|
70
|
+
"""EnvHub API base, shared with the ``nepher`` CLI's ``api_url`` setting."""
|
|
71
|
+
return get_envhub_api_url() or ENVHUB_BACKEND.rstrip("/")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _env_id_or_exit(env_id: str) -> str:
|
|
75
|
+
try:
|
|
76
|
+
return safe_env_id(env_id)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
console.print(f"[red]{e}[/red]")
|
|
79
|
+
raise SystemExit(1) from e
|
|
48
80
|
|
|
49
81
|
|
|
50
82
|
@click.group("envhub")
|
|
@@ -146,6 +178,7 @@ def envhub_list(
|
|
|
146
178
|
@envhub.command("download")
|
|
147
179
|
@click.argument("env_id")
|
|
148
180
|
@click.option("--cache-dir", type=click.Path(), default=None, help="Override local cache directory.")
|
|
181
|
+
@click.option("--category", default=None, help="Use the category-specific cache directory from config.")
|
|
149
182
|
@click.option("--force", is_flag=True, help="Re-download even if already cached.")
|
|
150
183
|
@click.option(
|
|
151
184
|
"--api-key",
|
|
@@ -155,7 +188,9 @@ def envhub_list(
|
|
|
155
188
|
help="API key for authentication. Sent directly as X-API-Key so the "
|
|
156
189
|
"download succeeds regardless of any cached access-token expiry.",
|
|
157
190
|
)
|
|
158
|
-
def envhub_download(
|
|
191
|
+
def envhub_download(
|
|
192
|
+
env_id: str, cache_dir: str | None, category: str | None, force: bool, api_key: str | None
|
|
193
|
+
) -> None:
|
|
159
194
|
"""Download an environment bundle and cache it locally.
|
|
160
195
|
|
|
161
196
|
The bundle is extracted to ~/.nepher/cache/<env_id>/ by default.
|
|
@@ -164,7 +199,8 @@ def envhub_download(env_id: str, cache_dir: str | None, force: bool, api_key: st
|
|
|
164
199
|
is sent directly to EnvHub, which verifies it against account-backend — the
|
|
165
200
|
download therefore works even if a previously cached JWT has expired.
|
|
166
201
|
"""
|
|
167
|
-
|
|
202
|
+
env_id = _env_id_or_exit(env_id)
|
|
203
|
+
cache_root = resolve_cache_dir(cache_dir, category=category)
|
|
168
204
|
env_cache = cache_root / env_id
|
|
169
205
|
|
|
170
206
|
if is_cached_env(env_cache) and not force:
|
|
@@ -174,31 +210,33 @@ def envhub_download(env_id: str, cache_dir: str | None, force: bool, api_key: st
|
|
|
174
210
|
headers = _headers(api_key)
|
|
175
211
|
url = f"{_base()}/api/v1/envs/{env_id}/download"
|
|
176
212
|
console.print(f"Downloading [bold]{env_id}[/bold]...")
|
|
213
|
+
tmp_fd, tmp_name = tempfile.mkstemp(suffix=".zip")
|
|
214
|
+
os.close(tmp_fd)
|
|
215
|
+
tmp_path = Path(tmp_name)
|
|
177
216
|
try:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
tmp_path
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
raise SystemExit(1)
|
|
217
|
+
try:
|
|
218
|
+
with httpx.stream("GET", url, headers=headers, timeout=600.0, follow_redirects=True) as r:
|
|
219
|
+
if r.status_code != 200:
|
|
220
|
+
body = r.read().decode(errors="replace")
|
|
221
|
+
console.print(f"[red]{parse_error_body(body) or body.strip() or f'HTTP {r.status_code}'}[/red]")
|
|
222
|
+
raise SystemExit(1)
|
|
223
|
+
|
|
224
|
+
with open(tmp_path, "wb") as tmp:
|
|
225
|
+
for chunk in r.iter_bytes(chunk_size=1024 * 1024):
|
|
226
|
+
tmp.write(chunk)
|
|
227
|
+
except httpx.HTTPError as e:
|
|
228
|
+
console.print(f"[red]Network error[/red]: {e}")
|
|
229
|
+
raise SystemExit(1) from e
|
|
230
|
+
|
|
231
|
+
console.print("Extracting bundle...")
|
|
232
|
+
try:
|
|
233
|
+
extract_bundle(tmp_path, env_cache)
|
|
234
|
+
except zipfile.BadZipFile as e:
|
|
235
|
+
console.print("[red]Downloaded file is not a valid ZIP archive.[/red]")
|
|
236
|
+
raise SystemExit(1) from e
|
|
237
|
+
except (ValueError, OSError) as e:
|
|
238
|
+
console.print(f"[red]Invalid bundle[/red]: {e}")
|
|
239
|
+
raise SystemExit(1) from e
|
|
202
240
|
finally:
|
|
203
241
|
tmp_path.unlink(missing_ok=True)
|
|
204
242
|
|
|
@@ -239,13 +277,11 @@ def envhub_upload(path: str, category: str, benchmark: bool, force: bool, thumbn
|
|
|
239
277
|
raise SystemExit(1)
|
|
240
278
|
console.print("Zipping bundle...")
|
|
241
279
|
tmp_fd, tmp_name = tempfile.mkstemp(suffix=".zip")
|
|
242
|
-
import os
|
|
243
280
|
os.close(tmp_fd)
|
|
244
281
|
tmp_zip = Path(tmp_name)
|
|
245
282
|
with zipfile.ZipFile(tmp_zip, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
246
|
-
for file in bundle_path
|
|
247
|
-
|
|
248
|
-
zf.write(file, file.relative_to(bundle_path))
|
|
283
|
+
for file, arcname in iter_bundle_files(bundle_path):
|
|
284
|
+
zf.write(file, arcname.as_posix())
|
|
249
285
|
upload_path = tmp_zip
|
|
250
286
|
else:
|
|
251
287
|
upload_path = bundle_path
|
|
@@ -257,15 +293,16 @@ def envhub_upload(path: str, category: str, benchmark: bool, force: bool, thumbn
|
|
|
257
293
|
if force:
|
|
258
294
|
data_fields["force"] = "true"
|
|
259
295
|
|
|
260
|
-
|
|
296
|
+
data_fields["duplicate_policy"] = "reject"
|
|
297
|
+
with contextlib.ExitStack() as stack:
|
|
298
|
+
f = stack.enter_context(open(upload_path, "rb"))
|
|
261
299
|
files: dict[str, Any] = {"bundle": (upload_path.name, f, "application/zip")}
|
|
262
300
|
if thumbnail:
|
|
263
301
|
thumb_path = Path(thumbnail)
|
|
264
302
|
suffix = thumb_path.suffix.lower()
|
|
265
303
|
content_type = "image/png" if suffix == ".png" else ("image/webp" if suffix == ".webp" else "image/jpeg")
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
data_fields["duplicate_policy"] = "reject"
|
|
304
|
+
thumb_file = stack.enter_context(open(thumb_path, "rb"))
|
|
305
|
+
files["thumbnail"] = (thumb_path.name, thumb_file, content_type)
|
|
269
306
|
|
|
270
307
|
try:
|
|
271
308
|
r = httpx.post(
|
|
@@ -273,9 +310,9 @@ def envhub_upload(path: str, category: str, benchmark: bool, force: bool, thumbn
|
|
|
273
310
|
headers=headers,
|
|
274
311
|
data=data_fields,
|
|
275
312
|
files=files,
|
|
276
|
-
timeout=
|
|
313
|
+
timeout=_UPLOAD_TIMEOUT,
|
|
277
314
|
)
|
|
278
|
-
except httpx.
|
|
315
|
+
except httpx.HTTPError as e:
|
|
279
316
|
console.print(f"[red]Network error[/red]: {e}")
|
|
280
317
|
raise SystemExit(1) from e
|
|
281
318
|
|
|
@@ -325,15 +362,15 @@ def cache_clear(env_id: str | None, cache_dir: str | None) -> None:
|
|
|
325
362
|
"""Clear cache — all environments or a specific one."""
|
|
326
363
|
root = resolve_cache_dir(cache_dir)
|
|
327
364
|
if env_id:
|
|
328
|
-
target = root / env_id
|
|
365
|
+
target = root / _env_id_or_exit(env_id)
|
|
329
366
|
if target.exists():
|
|
330
367
|
shutil.rmtree(target)
|
|
331
368
|
console.print(f"[green]Cleared cache for[/green] {env_id}")
|
|
332
369
|
else:
|
|
333
370
|
console.print(f"[yellow]{env_id} is not cached.[/yellow]")
|
|
334
371
|
else:
|
|
335
|
-
|
|
336
|
-
shutil.rmtree(
|
|
372
|
+
for entry in list_cached_env_dirs(root):
|
|
373
|
+
shutil.rmtree(entry)
|
|
337
374
|
console.print("[green]Cleared all cached environments.[/green]")
|
|
338
375
|
|
|
339
376
|
|
|
@@ -363,14 +400,20 @@ def cache_info(cache_dir: str | None) -> None:
|
|
|
363
400
|
@click.argument("new_path", type=click.Path())
|
|
364
401
|
@click.option("--cache-dir", type=click.Path(), default=None)
|
|
365
402
|
def cache_migrate(new_path: str, cache_dir: str | None) -> None:
|
|
366
|
-
"""Move
|
|
403
|
+
"""Move cached environments to NEW_PATH and make it the configured cache_dir."""
|
|
367
404
|
old_root = resolve_cache_dir(cache_dir)
|
|
368
|
-
new_root = Path(new_path)
|
|
369
|
-
if
|
|
370
|
-
console.print("[
|
|
405
|
+
new_root = Path(new_path).expanduser().resolve()
|
|
406
|
+
if new_root == old_root:
|
|
407
|
+
console.print("[dim]Cache is already at that location.[/dim]")
|
|
371
408
|
return
|
|
372
|
-
new_root.
|
|
373
|
-
|
|
409
|
+
new_root.mkdir(parents=True, exist_ok=True)
|
|
410
|
+
for entry in list_cached_env_dirs(old_root):
|
|
411
|
+
target = new_root / entry.name
|
|
412
|
+
if target.exists():
|
|
413
|
+
console.print(f"[red]{target} already exists; aborting.[/red]")
|
|
414
|
+
raise SystemExit(1)
|
|
415
|
+
shutil.move(str(entry), str(target))
|
|
416
|
+
set_envhub_config_value("cache_dir", str(new_root))
|
|
374
417
|
console.print(f"[green]Cache migrated to[/green] {new_root}")
|
|
375
418
|
|
|
376
419
|
|
|
@@ -402,8 +445,8 @@ def envhub_view(env_id: str, category: str | None, scene: str | None, cache_dir:
|
|
|
402
445
|
)
|
|
403
446
|
raise SystemExit(1)
|
|
404
447
|
|
|
405
|
-
root = resolve_cache_dir(cache_dir)
|
|
406
|
-
env_path = root / env_id
|
|
448
|
+
root = resolve_cache_dir(cache_dir, category=category)
|
|
449
|
+
env_path = root / _env_id_or_exit(env_id)
|
|
407
450
|
if not is_cached_env(env_path):
|
|
408
451
|
console.print(
|
|
409
452
|
f"[yellow]{env_id} is not cached.[/yellow] "
|
|
@@ -412,7 +455,7 @@ def envhub_view(env_id: str, category: str | None, scene: str | None, cache_dir:
|
|
|
412
455
|
raise SystemExit(1)
|
|
413
456
|
|
|
414
457
|
try:
|
|
415
|
-
env = load_env(env_id, category)
|
|
458
|
+
env = load_env(env_id, category, cache_dir=root) if cache_dir else load_env(env_id, category)
|
|
416
459
|
except Exception as e:
|
|
417
460
|
console.print(f"[red]Failed to load environment[/red]: {e}")
|
|
418
461
|
raise SystemExit(1) from e
|
|
@@ -428,8 +471,15 @@ def envhub_view(env_id: str, category: str | None, scene: str | None, cache_dir:
|
|
|
428
471
|
console.print(f"Launching scene [bold]{scene}[/bold] in Isaac Sim...")
|
|
429
472
|
# Actual rendering requires the full IsaacLab runtime; delegate back to the
|
|
430
473
|
# installed nepher package's view script which handles AppLauncher setup.
|
|
431
|
-
import subprocess
|
|
432
|
-
|
|
474
|
+
import subprocess
|
|
475
|
+
import sys
|
|
476
|
+
|
|
477
|
+
cmd = [sys.executable, "-m", "nepher", "view", env_id, "--scene", scene]
|
|
478
|
+
if category:
|
|
479
|
+
cmd += ["--category", category]
|
|
480
|
+
if cache_dir:
|
|
481
|
+
cmd += ["--cache-dir", str(root)]
|
|
482
|
+
raise SystemExit(subprocess.run(cmd, check=False).returncode)
|
|
433
483
|
|
|
434
484
|
|
|
435
485
|
# ---------------------------------------------------------------------------
|
|
@@ -459,7 +509,11 @@ def config_get(key: str) -> None:
|
|
|
459
509
|
def config_set(key: str, value: str) -> None:
|
|
460
510
|
"""Set a configuration value."""
|
|
461
511
|
parsed = parse_config_value(value)
|
|
462
|
-
|
|
512
|
+
try:
|
|
513
|
+
set_envhub_config_value(key, parsed)
|
|
514
|
+
except ValueError as e:
|
|
515
|
+
console.print(f"[red]{e}[/red]")
|
|
516
|
+
raise SystemExit(1) from e
|
|
463
517
|
display = mask_envhub_config_secret(key, parsed) if isinstance(parsed, str) else parsed
|
|
464
518
|
console.print(f"[green]Set[/green] {key} = {display}")
|
|
465
519
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""EnvHub bundle helpers — safe extraction and zipping (same rules as ``nepher``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import zipfile
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
11
|
+
|
|
12
|
+
_ENV_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
13
|
+
|
|
14
|
+
EXCLUDED_NAMES = {".git", ".hg", ".svn", "__pycache__", ".DS_Store", ".env", ".venv", "venv"}
|
|
15
|
+
EXCLUDED_SUFFIXES = {".pyc", ".pyo"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def safe_env_id(env_id: str) -> str:
|
|
19
|
+
"""Return ``env_id`` if it is a single safe path segment, else raise ValueError."""
|
|
20
|
+
if not _ENV_ID_RE.match(env_id) or ".." in env_id:
|
|
21
|
+
raise ValueError(f"Invalid environment ID: {env_id!r}")
|
|
22
|
+
return env_id
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _check_member(dest: Path, name: str) -> None:
|
|
26
|
+
win = PureWindowsPath(name)
|
|
27
|
+
if PurePosixPath(name).is_absolute() or win.is_absolute() or win.drive:
|
|
28
|
+
raise ValueError(f"Unsafe path in bundle: {name!r}")
|
|
29
|
+
if not (dest / name).resolve().is_relative_to(dest):
|
|
30
|
+
raise ValueError(f"Unsafe path in bundle: {name!r}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_bundle(zip_path: Path, dest_dir: Path) -> None:
|
|
34
|
+
"""Extract a bundle into ``dest_dir`` atomically.
|
|
35
|
+
|
|
36
|
+
Members that would land outside the destination are rejected, the
|
|
37
|
+
uncompressed size is checked against free disk space, and the result only
|
|
38
|
+
replaces ``dest_dir`` once it contains a root ``manifest.yaml``.
|
|
39
|
+
"""
|
|
40
|
+
dest_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
staging = Path(tempfile.mkdtemp(prefix=f".{dest_dir.name}.", dir=dest_dir.parent))
|
|
42
|
+
try:
|
|
43
|
+
with zipfile.ZipFile(zip_path, "r") as zf:
|
|
44
|
+
members = zf.infolist()
|
|
45
|
+
root = staging.resolve()
|
|
46
|
+
for member in members:
|
|
47
|
+
_check_member(root, member.filename)
|
|
48
|
+
total = sum(m.file_size for m in members)
|
|
49
|
+
free = shutil.disk_usage(staging).free
|
|
50
|
+
if total > free:
|
|
51
|
+
need, have = total / 1024**3, free / 1024**3
|
|
52
|
+
raise ValueError(f"Bundle needs {need:.2f} GB but only {have:.2f} GB is free")
|
|
53
|
+
zf.extractall(staging, members=members)
|
|
54
|
+
|
|
55
|
+
if not (staging / "manifest.yaml").is_file():
|
|
56
|
+
raise ValueError("manifest.yaml not found at the root of the bundle")
|
|
57
|
+
|
|
58
|
+
if dest_dir.exists():
|
|
59
|
+
shutil.rmtree(dest_dir)
|
|
60
|
+
staging.rename(dest_dir)
|
|
61
|
+
finally:
|
|
62
|
+
if staging.exists():
|
|
63
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def iter_bundle_files(bundle_dir: Path) -> Iterator[tuple[Path, Path]]:
|
|
67
|
+
"""Yield ``(path, archive_name)`` for files to include when zipping a bundle."""
|
|
68
|
+
for path in sorted(bundle_dir.rglob("*")):
|
|
69
|
+
rel = path.relative_to(bundle_dir)
|
|
70
|
+
if any(part in EXCLUDED_NAMES for part in rel.parts):
|
|
71
|
+
continue
|
|
72
|
+
if path.is_file() and path.suffix not in EXCLUDED_SUFFIXES:
|
|
73
|
+
yield path, rel
|
|
@@ -24,11 +24,16 @@ def resolve_cache_dir(cache_dir: str | Path | None = None, category: str | None
|
|
|
24
24
|
if cache_dir:
|
|
25
25
|
return Path(cache_dir).expanduser().resolve()
|
|
26
26
|
|
|
27
|
+
# Read the env var directly: an already-initialised ``nepher`` config may
|
|
28
|
+
# predate it. Category-specific cache_dir still wins, as in ``nepher``.
|
|
29
|
+
env_override = os.getenv("NEPHER_CACHE_DIR")
|
|
30
|
+
if env_override and not category:
|
|
31
|
+
return Path(env_override).expanduser().resolve()
|
|
32
|
+
|
|
27
33
|
nepher_dir = _nepher_cache_dir(category)
|
|
28
34
|
if nepher_dir is not None:
|
|
29
35
|
return nepher_dir
|
|
30
36
|
|
|
31
|
-
env_override = os.getenv("NEPHER_CACHE_DIR")
|
|
32
37
|
if env_override:
|
|
33
38
|
return Path(env_override).expanduser().resolve()
|
|
34
39
|
|
|
@@ -50,7 +55,7 @@ def is_cached_env(path: Path) -> bool:
|
|
|
50
55
|
|
|
51
56
|
|
|
52
57
|
def list_cached_env_dirs(root: Path) -> list[Path]:
|
|
53
|
-
"""List cached environment directories under ``root
|
|
58
|
+
"""List cached environment directories under ``root`` (skips in-progress staging dirs)."""
|
|
54
59
|
if not root.exists():
|
|
55
60
|
return []
|
|
56
|
-
return sorted(p for p in root.iterdir() if is_cached_env(p))
|
|
61
|
+
return sorted(p for p in root.iterdir() if not p.name.startswith(".") and is_cached_env(p))
|
|
@@ -17,18 +17,32 @@ DEFAULT_CONFIG: dict[str, Any] = {
|
|
|
17
17
|
|
|
18
18
|
LIST_KEYS = ("api_url", "cache_dir", "default_category")
|
|
19
19
|
|
|
20
|
+
# Older ``nepher`` releases persisted this default into config.toml; treat it as unset.
|
|
21
|
+
LEGACY_API_URLS = frozenset({"https://envhub-api.nepher.ai", "https://envhub-api.nepher.ai/"})
|
|
22
|
+
|
|
23
|
+
SECRET_KEYS = ("api_key",)
|
|
24
|
+
|
|
20
25
|
|
|
21
26
|
def find_config_file() -> Path | None:
|
|
22
27
|
"""Return the active nepher config file path, if any."""
|
|
23
28
|
cwd_config = Path.cwd() / ".nepherrc"
|
|
24
29
|
if cwd_config.exists():
|
|
25
30
|
return cwd_config
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
for name in ("config.toml", "config.json"):
|
|
32
|
+
home_config = Path.home() / ".nepher" / name
|
|
33
|
+
if home_config.exists():
|
|
34
|
+
return home_config
|
|
29
35
|
return None
|
|
30
36
|
|
|
31
37
|
|
|
38
|
+
def _load_toml(text: str) -> dict[str, Any]:
|
|
39
|
+
try:
|
|
40
|
+
import tomllib
|
|
41
|
+
except ImportError:
|
|
42
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
43
|
+
return tomllib.loads(text)
|
|
44
|
+
|
|
45
|
+
|
|
32
46
|
def load_config_file() -> dict[str, Any]:
|
|
33
47
|
"""Load values from the nepher config file (no defaults or env overrides)."""
|
|
34
48
|
config_file = find_config_file()
|
|
@@ -36,24 +50,23 @@ def load_config_file() -> dict[str, Any]:
|
|
|
36
50
|
return {}
|
|
37
51
|
|
|
38
52
|
try:
|
|
39
|
-
|
|
53
|
+
text = config_file.read_text(encoding="utf-8")
|
|
54
|
+
if config_file.suffix == ".json":
|
|
55
|
+
data = json.loads(text)
|
|
56
|
+
elif config_file.suffix == ".toml":
|
|
57
|
+
data = _load_toml(text)
|
|
58
|
+
else:
|
|
40
59
|
try:
|
|
41
|
-
|
|
42
|
-
except
|
|
43
|
-
|
|
44
|
-
import tomli as tomllib # type: ignore[no-redef]
|
|
45
|
-
except ImportError:
|
|
46
|
-
return {}
|
|
47
|
-
with open(config_file, "rb") as f:
|
|
48
|
-
data = tomllib.load(f)
|
|
49
|
-
return data if isinstance(data, dict) else {}
|
|
50
|
-
if config_file.suffix == ".json" or config_file.name == ".nepherrc":
|
|
51
|
-
with open(config_file, encoding="utf-8") as f:
|
|
52
|
-
data = json.load(f)
|
|
53
|
-
return data if isinstance(data, dict) else {}
|
|
60
|
+
data = _load_toml(text)
|
|
61
|
+
except Exception:
|
|
62
|
+
data = json.loads(text)
|
|
54
63
|
except Exception:
|
|
55
64
|
return {}
|
|
56
|
-
|
|
65
|
+
if not isinstance(data, dict):
|
|
66
|
+
return {}
|
|
67
|
+
if data.get("api_url") in LEGACY_API_URLS:
|
|
68
|
+
data.pop("api_url")
|
|
69
|
+
return data
|
|
57
70
|
|
|
58
71
|
|
|
59
72
|
def _apply_env_overrides(config: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -88,9 +101,17 @@ def get_value(key: str, default: Any = None) -> Any:
|
|
|
88
101
|
try:
|
|
89
102
|
from nepher.config import get_config
|
|
90
103
|
|
|
91
|
-
|
|
104
|
+
value = get_config().get(key, default)
|
|
92
105
|
except ImportError:
|
|
93
106
|
return _fallback_get(key, default)
|
|
107
|
+
if key == "api_url" and value in LEGACY_API_URLS:
|
|
108
|
+
return _fallback_get(key, default)
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_api_url() -> str:
|
|
113
|
+
"""Effective EnvHub API base URL (env vars > config file > default)."""
|
|
114
|
+
return str(get_value("api_url") or DEFAULT_CONFIG["api_url"]).rstrip("/")
|
|
94
115
|
|
|
95
116
|
|
|
96
117
|
def list_values() -> dict[str, Any]:
|
|
@@ -111,6 +132,8 @@ def parse_config_value(value: str) -> Any:
|
|
|
111
132
|
|
|
112
133
|
def set_value(key: str, value: Any) -> None:
|
|
113
134
|
"""Persist a configuration value."""
|
|
135
|
+
if key in SECRET_KEYS:
|
|
136
|
+
raise ValueError("API keys are not stored in config; use 'npcli login' or NEPHER_API_KEY.")
|
|
114
137
|
try:
|
|
115
138
|
from nepher.config import set_config
|
|
116
139
|
|
nepher_cli-0.2.6/PKG-INFO
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: nepher-cli
|
|
3
|
-
Version: 0.2.6
|
|
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-wallet>=4.1.0; 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 [nepher.ai/account](https://nepher.ai/account) → 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-wallet` for wallet signing):
|
|
91
|
-
|
|
92
|
-
```bash
|
|
93
|
-
pip install bittensor-wallet
|
|
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
|
-
`list` / `create` / `revoke` need a **JWT session** from `npcli account login`.
|
|
150
|
-
Do **not** pass `--api-key` on these commands (that flag sends `X-API-Key`, which the account API rejects for key management).
|
|
151
|
-
|
|
152
|
-
```bash
|
|
153
|
-
npcli account login --api-key nepher_...
|
|
154
|
-
npcli account api-keys list
|
|
155
|
-
npcli account api-keys create --name "CI key" --platform hackathon
|
|
156
|
-
npcli account api-keys revoke <key_id>
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Coldkey Registration
|
|
160
|
-
|
|
161
|
-
Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is optional fallback):
|
|
162
|
-
|
|
163
|
-
```bash
|
|
164
|
-
pip install bittensor-wallet
|
|
165
|
-
npcli account register-coldkey --wallet <wallet_name>
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
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.
|
|
169
|
-
|
|
170
|
-
## API Key Requirements
|
|
171
|
-
|
|
172
|
-
- Must start with `nepher_`
|
|
173
|
-
- Must be active and not expired
|
|
174
|
-
- `hackathon submit` requires **Hackathon** scope (or an unrestricted key)
|
|
175
|
-
- `tournament submit` requires **Tournament** scope (or an unrestricted key)
|
|
176
|
-
|
|
177
|
-
## Troubleshooting
|
|
178
|
-
|
|
179
|
-
| Error | Fix |
|
|
180
|
-
|-------|-----|
|
|
181
|
-
| `npcli: command not found` | Ensure `pip install` succeeded and Python scripts dir is on `PATH` |
|
|
182
|
-
| `invalid api key format` | Key must start with `nepher_` |
|
|
183
|
-
| `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
|
|
184
|
-
| `api key expired` | Run `npcli account api-keys create` |
|
|
185
|
-
| `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
|
|
186
|
-
| `bittensor-wallet not installed` | `pip install bittensor-wallet` (needed for `tournament submit` and `account register-coldkey`) |
|
|
187
|
-
| `Not logged in` | `npcli login --api-key nepher_...` |
|
|
188
|
-
| `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` |
|
|
189
|
-
|
|
190
|
-
Run any command with `--help` for full flag details:
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
npcli --help
|
|
194
|
-
npcli hackathon submit --help
|
|
195
|
-
npcli tournament submit --help
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
## License
|
|
199
|
-
|
|
200
|
-
MIT
|
nepher_cli-0.2.6/README.md
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
# nepher-cli
|
|
2
|
-
|
|
3
|
-
The unified CLI for [Nepher Robotics](https://nepher.ai) — accounts, hackathons, EnvHub, tournaments, and SimStore from a single tool.
|
|
4
|
-
|
|
5
|
-
**Requires:** Python 3.10+
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pip install nepher-cli
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Both `npcli` and `nepher-cli` entry points are registered after install.
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npcli --version
|
|
17
|
-
npcli --help
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Commands
|
|
21
|
-
|
|
22
|
-
| Command / Group | Description |
|
|
23
|
-
|-----------------|-------------|
|
|
24
|
-
| `npcli login` | Log in with a Nepher API key |
|
|
25
|
-
| `npcli whoami` | Show the currently authenticated user |
|
|
26
|
-
| `npcli logout` | Clear locally stored credentials |
|
|
27
|
-
| `npcli account` | API keys and coldkey registration |
|
|
28
|
-
| `npcli tournament` | Browse tournaments, submit agents, leaderboards |
|
|
29
|
-
| `npcli envhub` | Manage Isaac Lab environment bundles |
|
|
30
|
-
| `npcli hackathon` | Browse and submit to hackathons |
|
|
31
|
-
| `npcli simstore` | SimStore marketplace *(coming soon)* |
|
|
32
|
-
|
|
33
|
-
## Quick Start
|
|
34
|
-
|
|
35
|
-
### Authenticate
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npcli login --api-key nepher_xxxxxxxx
|
|
39
|
-
npcli whoami
|
|
40
|
-
npcli logout
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Get your API key at [nepher.ai/account](https://nepher.ai/account) → Account → API Keys.
|
|
44
|
-
For CI/CD, set `NEPHER_API_KEY=nepher_xxxxxxxx` instead of logging in.
|
|
45
|
-
|
|
46
|
-
### Tournaments
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
npcli tournament list
|
|
50
|
-
npcli tournament list-active
|
|
51
|
-
npcli tournament status <tournament_id>
|
|
52
|
-
npcli tournament leaderboard <tournament_id>
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
Check your agent directory structure (no extra dependencies needed):
|
|
56
|
-
|
|
57
|
-
```bash
|
|
58
|
-
npcli tournament check --path ./my-agent
|
|
59
|
-
npcli tournament check --path ./my-agent --verbose # also show recommended-file warnings
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Submit an agent (requires `bittensor-wallet` for wallet signing):
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
pip install bittensor-wallet
|
|
66
|
-
# or: pip install "nepher-cli[bittensor]"
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npcli tournament submit --path ./my-agent --wallet-name miner --wallet-hotkey default
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Full options for `submit`:
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
--path <path> Agent directory
|
|
77
|
-
--wallet-name <str> Bittensor wallet name (default: miner)
|
|
78
|
-
--wallet-hotkey <str> Bittensor wallet hotkey (default: default)
|
|
79
|
-
--api-key <key> Nepher API key (falls back to stored credentials)
|
|
80
|
-
--tournament-id <id> Target tournament ID (required when multiple are active)
|
|
81
|
-
--api-url <url> Override tournament API URL
|
|
82
|
-
-v / --verbose Verbose output
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### EnvHub
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
npcli envhub list
|
|
89
|
-
npcli envhub download <env_id>
|
|
90
|
-
npcli envhub upload ./my-bundle --category navigation
|
|
91
|
-
npcli envhub cache list
|
|
92
|
-
npcli envhub cache info
|
|
93
|
-
npcli envhub cache clear
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Hackathons
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
npcli hackathon list
|
|
100
|
-
|
|
101
|
-
npcli hackathon submit \
|
|
102
|
-
--title "My entry" \
|
|
103
|
-
--submission ./my-project \
|
|
104
|
-
--assets ./my-assets
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Full options:
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
--hackathon-id <uuid> Required when multiple hackathons are open
|
|
111
|
-
--title <str> Entry title
|
|
112
|
-
--description <markdown> Entry description
|
|
113
|
-
--thumbnail <file> Cover image (JPEG, PNG, WebP, GIF)
|
|
114
|
-
--submission <path> Project folder or submission.zip
|
|
115
|
-
--assets <path> Assets folder or assets.zip (images, videos, PDFs)
|
|
116
|
-
--public-source Mark source as public
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### API Keys
|
|
120
|
-
|
|
121
|
-
`list` / `create` / `revoke` need a **JWT session** from `npcli account login`.
|
|
122
|
-
Do **not** pass `--api-key` on these commands (that flag sends `X-API-Key`, which the account API rejects for key management).
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
npcli account login --api-key nepher_...
|
|
126
|
-
npcli account api-keys list
|
|
127
|
-
npcli account api-keys create --name "CI key" --platform hackathon
|
|
128
|
-
npcli account api-keys revoke <key_id>
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Coldkey Registration
|
|
132
|
-
|
|
133
|
-
Bind a Bittensor coldkey to your account (requires `bittensor-wallet`; `btcli` is optional fallback):
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
pip install bittensor-wallet
|
|
137
|
-
npcli account register-coldkey --wallet <wallet_name>
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
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.
|
|
141
|
-
|
|
142
|
-
## API Key Requirements
|
|
143
|
-
|
|
144
|
-
- Must start with `nepher_`
|
|
145
|
-
- Must be active and not expired
|
|
146
|
-
- `hackathon submit` requires **Hackathon** scope (or an unrestricted key)
|
|
147
|
-
- `tournament submit` requires **Tournament** scope (or an unrestricted key)
|
|
148
|
-
|
|
149
|
-
## Troubleshooting
|
|
150
|
-
|
|
151
|
-
| Error | Fix |
|
|
152
|
-
|-------|-----|
|
|
153
|
-
| `npcli: command not found` | Ensure `pip install` succeeded and Python scripts dir is on `PATH` |
|
|
154
|
-
| `invalid api key format` | Key must start with `nepher_` |
|
|
155
|
-
| `api key does not have hackathon access` | Enable Hackathon scope or use an unrestricted key |
|
|
156
|
-
| `api key expired` | Run `npcli account api-keys create` |
|
|
157
|
-
| `Several hackathons are accepting submissions` | Re-run with `--hackathon-id` |
|
|
158
|
-
| `bittensor-wallet not installed` | `pip install bittensor-wallet` (needed for `tournament submit` and `account register-coldkey`) |
|
|
159
|
-
| `Not logged in` | `npcli login --api-key nepher_...` |
|
|
160
|
-
| `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` |
|
|
161
|
-
|
|
162
|
-
Run any command with `--help` for full flag details:
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
npcli --help
|
|
166
|
-
npcli hackathon submit --help
|
|
167
|
-
npcli tournament submit --help
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
## License
|
|
171
|
-
|
|
172
|
-
MIT
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|