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.
- nepher_cli/__init__.py +3 -0
- nepher_cli/__main__.py +4 -0
- nepher_cli/cli.py +59 -0
- nepher_cli/commands/__init__.py +1 -0
- nepher_cli/commands/account.py +527 -0
- nepher_cli/commands/envhub.py +466 -0
- nepher_cli/commands/hackathon.py +760 -0
- nepher_cli/commands/simstore.py +49 -0
- nepher_cli/commands/tournament.py +651 -0
- nepher_cli/config.py +25 -0
- nepher_cli/core/__init__.py +1 -0
- nepher_cli/core/credentials.py +243 -0
- nepher_cli/core/http.py +76 -0
- nepher_cli/envhub/__init__.py +1 -0
- nepher_cli/envhub/cache.py +56 -0
- nepher_cli/envhub/config.py +176 -0
- nepher_cli/py.typed +0 -0
- nepher_cli/tournament/__init__.py +1 -0
- nepher_cli/tournament/agent_check.py +60 -0
- nepher_cli/tournament/api.py +100 -0
- nepher_cli/tournament/packer.py +50 -0
- nepher_cli/tournament/wallet.py +89 -0
- nepher_cli-0.2.0.dist-info/METADATA +193 -0
- nepher_cli-0.2.0.dist-info/RECORD +26 -0
- nepher_cli-0.2.0.dist-info/WHEEL +4 -0
- nepher_cli-0.2.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Persistent credential storage for npcli.
|
|
2
|
+
|
|
3
|
+
Stores API key, JWT access/refresh tokens, and user metadata in
|
|
4
|
+
~/.nepher/credentials.json (or %APPDATA%\\nepher\\credentials.json on Windows).
|
|
5
|
+
|
|
6
|
+
Sensitive values (tokens) are also mirrored into the system keyring when
|
|
7
|
+
the ``keyring`` package is available, and the JSON file contains only
|
|
8
|
+
non-secret metadata plus the expiry timestamp in that case.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import keyring as _keyring
|
|
22
|
+
|
|
23
|
+
_HAS_KEYRING = True
|
|
24
|
+
except ImportError: # pragma: no cover
|
|
25
|
+
_HAS_KEYRING = False
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
from platformdirs import user_config_dir
|
|
29
|
+
|
|
30
|
+
def _config_dir() -> Path:
|
|
31
|
+
return Path(user_config_dir("nepher", appauthor=False))
|
|
32
|
+
|
|
33
|
+
except ImportError: # pragma: no cover
|
|
34
|
+
def _config_dir() -> Path: # type: ignore[misc]
|
|
35
|
+
return Path.home() / ".nepher"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_KEYRING_SERVICE = "npcli"
|
|
39
|
+
_KEYRING_ACCESS = "access_token"
|
|
40
|
+
_KEYRING_REFRESH = "refresh_token"
|
|
41
|
+
_KEYRING_API_KEY = "api_key"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _cred_path() -> Path:
|
|
45
|
+
d = _config_dir()
|
|
46
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
return d / "credentials.json"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _read_json() -> dict[str, Any]:
|
|
51
|
+
p = _cred_path()
|
|
52
|
+
if not p.exists():
|
|
53
|
+
return {}
|
|
54
|
+
try:
|
|
55
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
56
|
+
except (json.JSONDecodeError, OSError):
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _write_json(data: dict[str, Any]) -> None:
|
|
61
|
+
p = _cred_path()
|
|
62
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
p.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
64
|
+
try:
|
|
65
|
+
p.chmod(0o600)
|
|
66
|
+
except OSError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _kr_get(key: str) -> str | None:
|
|
71
|
+
if not _HAS_KEYRING:
|
|
72
|
+
return None
|
|
73
|
+
try:
|
|
74
|
+
return _keyring.get_password(_KEYRING_SERVICE, key)
|
|
75
|
+
except Exception:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _kr_set(key: str, value: str) -> bool:
|
|
80
|
+
if not _HAS_KEYRING:
|
|
81
|
+
return False
|
|
82
|
+
try:
|
|
83
|
+
_keyring.set_password(_KEYRING_SERVICE, key, value)
|
|
84
|
+
return True
|
|
85
|
+
except Exception:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _kr_delete(key: str) -> None:
|
|
90
|
+
if not _HAS_KEYRING:
|
|
91
|
+
return
|
|
92
|
+
try:
|
|
93
|
+
_keyring.delete_password(_KEYRING_SERVICE, key)
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def save_credentials(
|
|
99
|
+
*,
|
|
100
|
+
api_key: str,
|
|
101
|
+
access_token: str,
|
|
102
|
+
refresh_token: str,
|
|
103
|
+
expires_in: int,
|
|
104
|
+
user: dict[str, Any],
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Persist all credential material after a successful cli-login."""
|
|
107
|
+
expires_at = int(time.time()) + expires_in
|
|
108
|
+
|
|
109
|
+
stored_in_keyring = all([
|
|
110
|
+
_kr_set(_KEYRING_API_KEY, api_key),
|
|
111
|
+
_kr_set(_KEYRING_ACCESS, access_token),
|
|
112
|
+
_kr_set(_KEYRING_REFRESH, refresh_token),
|
|
113
|
+
])
|
|
114
|
+
|
|
115
|
+
meta: dict[str, Any] = {
|
|
116
|
+
"expires_at": expires_at,
|
|
117
|
+
"user": user,
|
|
118
|
+
"keyring": stored_in_keyring,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if not stored_in_keyring:
|
|
122
|
+
meta["api_key"] = api_key
|
|
123
|
+
meta["access_token"] = access_token
|
|
124
|
+
meta["refresh_token"] = refresh_token
|
|
125
|
+
|
|
126
|
+
_write_json(meta)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_credentials() -> dict[str, Any] | None:
|
|
130
|
+
"""Return the stored credential dict or None if nothing is saved."""
|
|
131
|
+
meta = _read_json()
|
|
132
|
+
if not meta:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
if meta.get("keyring"):
|
|
136
|
+
meta["api_key"] = _kr_get(_KEYRING_API_KEY)
|
|
137
|
+
meta["access_token"] = _kr_get(_KEYRING_ACCESS)
|
|
138
|
+
meta["refresh_token"] = _kr_get(_KEYRING_REFRESH)
|
|
139
|
+
|
|
140
|
+
if not meta.get("access_token") and not meta.get("api_key"):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
return meta
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def clear_credentials() -> None:
|
|
147
|
+
"""Delete all stored credential material."""
|
|
148
|
+
_kr_delete(_KEYRING_API_KEY)
|
|
149
|
+
_kr_delete(_KEYRING_ACCESS)
|
|
150
|
+
_kr_delete(_KEYRING_REFRESH)
|
|
151
|
+
p = _cred_path()
|
|
152
|
+
if p.exists():
|
|
153
|
+
p.unlink()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _is_expired(creds: dict[str, Any]) -> bool:
|
|
157
|
+
expires_at = creds.get("expires_at")
|
|
158
|
+
if not isinstance(expires_at, (int, float)):
|
|
159
|
+
return True
|
|
160
|
+
return time.time() > (expires_at - 60)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _refresh_access_token(refresh_token: str, account_base: str) -> dict[str, Any] | None:
|
|
164
|
+
"""Call /api/v1/auth/refresh and return updated token data, or None on failure."""
|
|
165
|
+
url = f"{account_base.rstrip('/')}/api/v1/auth/refresh"
|
|
166
|
+
try:
|
|
167
|
+
r = httpx.post(url, json={"refresh_token": refresh_token}, timeout=30.0)
|
|
168
|
+
except httpx.RequestError:
|
|
169
|
+
return None
|
|
170
|
+
if r.status_code != 200:
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
return r.json()
|
|
174
|
+
except Exception:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_auth_headers(
|
|
179
|
+
api_key_override: str | None = None,
|
|
180
|
+
*,
|
|
181
|
+
account_base: str | None = None,
|
|
182
|
+
) -> dict[str, str]:
|
|
183
|
+
"""Return HTTP auth headers for a request.
|
|
184
|
+
|
|
185
|
+
Priority:
|
|
186
|
+
1. ``api_key_override`` (from ``--api-key`` flag or ``NEPHER_API_KEY`` env var)
|
|
187
|
+
2. Stored JWT (auto-refreshed if expired)
|
|
188
|
+
3. Stored API key (fallback when no JWT)
|
|
189
|
+
4. Empty dict — command must handle the unauthenticated case.
|
|
190
|
+
"""
|
|
191
|
+
from nepher_cli.config import ACCOUNT_BACKEND
|
|
192
|
+
|
|
193
|
+
base = account_base or ACCOUNT_BACKEND
|
|
194
|
+
|
|
195
|
+
if api_key_override:
|
|
196
|
+
return {"X-API-Key": api_key_override}
|
|
197
|
+
|
|
198
|
+
creds = load_credentials()
|
|
199
|
+
if not creds:
|
|
200
|
+
return {}
|
|
201
|
+
|
|
202
|
+
access_token = creds.get("access_token")
|
|
203
|
+
refresh_token = creds.get("refresh_token")
|
|
204
|
+
api_key = creds.get("api_key")
|
|
205
|
+
|
|
206
|
+
if access_token and not _is_expired(creds):
|
|
207
|
+
return {"Authorization": f"Bearer {access_token}"}
|
|
208
|
+
|
|
209
|
+
if refresh_token:
|
|
210
|
+
refreshed = _refresh_access_token(refresh_token, base)
|
|
211
|
+
if refreshed and refreshed.get("access_token"):
|
|
212
|
+
new_access = refreshed["access_token"]
|
|
213
|
+
expires_in = refreshed.get("expires_in", 86400)
|
|
214
|
+
new_expires_at = int(time.time()) + expires_in
|
|
215
|
+
meta = _read_json()
|
|
216
|
+
meta["expires_at"] = new_expires_at
|
|
217
|
+
if meta.get("keyring"):
|
|
218
|
+
_kr_set(_KEYRING_ACCESS, new_access)
|
|
219
|
+
else:
|
|
220
|
+
meta["access_token"] = new_access
|
|
221
|
+
_write_json(meta)
|
|
222
|
+
return {"Authorization": f"Bearer {new_access}"}
|
|
223
|
+
|
|
224
|
+
if api_key:
|
|
225
|
+
return {"X-API-Key": api_key}
|
|
226
|
+
|
|
227
|
+
return {}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_stored_api_key() -> str | None:
|
|
231
|
+
"""Return the stored raw API key, or None."""
|
|
232
|
+
creds = load_credentials()
|
|
233
|
+
if creds:
|
|
234
|
+
return creds.get("api_key")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def whoami_from_cache() -> dict[str, Any] | None:
|
|
239
|
+
"""Return user metadata from the credentials cache without a network call."""
|
|
240
|
+
creds = load_credentials()
|
|
241
|
+
if creds:
|
|
242
|
+
return creds.get("user")
|
|
243
|
+
return None
|
nepher_cli/core/http.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Shared HTTP helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_error_body(text: str) -> str | None:
|
|
12
|
+
try:
|
|
13
|
+
data = json.loads(text)
|
|
14
|
+
except json.JSONDecodeError:
|
|
15
|
+
return None
|
|
16
|
+
if not isinstance(data, dict):
|
|
17
|
+
return None
|
|
18
|
+
if data.get("message"):
|
|
19
|
+
return str(data["message"])
|
|
20
|
+
if "detail" in data:
|
|
21
|
+
detail = data["detail"]
|
|
22
|
+
return str(detail) if detail is not None else None
|
|
23
|
+
if "error" in data:
|
|
24
|
+
err = data["error"]
|
|
25
|
+
if err == "http_error" and data.get("message"):
|
|
26
|
+
return str(data["message"])
|
|
27
|
+
return str(err) if err is not None else None
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def request_json(
|
|
32
|
+
client: httpx.Client,
|
|
33
|
+
method: str,
|
|
34
|
+
url: str,
|
|
35
|
+
*,
|
|
36
|
+
json_body: dict[str, Any] | None = None,
|
|
37
|
+
headers: dict[str, str] | None = None,
|
|
38
|
+
) -> httpx.Response:
|
|
39
|
+
return client.request(method, url, json=json_body, headers=headers, timeout=120.0)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def authed_get(url: str, *, api_key: str | None = None, params: dict[str, Any] | None = None) -> httpx.Response:
|
|
43
|
+
"""GET with auth headers resolved from credential store or override."""
|
|
44
|
+
from nepher_cli.core.credentials import get_auth_headers
|
|
45
|
+
|
|
46
|
+
headers = get_auth_headers(api_key)
|
|
47
|
+
with httpx.Client() as client:
|
|
48
|
+
return client.get(url, headers=headers, params=params, timeout=60.0)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def authed_post(
|
|
52
|
+
url: str,
|
|
53
|
+
*,
|
|
54
|
+
api_key: str | None = None,
|
|
55
|
+
json_body: dict[str, Any] | None = None,
|
|
56
|
+
data: dict[str, str] | None = None,
|
|
57
|
+
files: dict[str, Any] | None = None,
|
|
58
|
+
timeout: float = 120.0,
|
|
59
|
+
) -> httpx.Response:
|
|
60
|
+
"""POST with auth headers resolved from credential store or override."""
|
|
61
|
+
from nepher_cli.core.credentials import get_auth_headers
|
|
62
|
+
|
|
63
|
+
headers = get_auth_headers(api_key)
|
|
64
|
+
with httpx.Client() as client:
|
|
65
|
+
if files is not None:
|
|
66
|
+
return client.post(url, headers=headers, data=data, files=files, timeout=timeout)
|
|
67
|
+
return client.post(url, headers=headers, json=json_body, timeout=timeout)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def authed_delete(url: str, *, api_key: str | None = None) -> httpx.Response:
|
|
71
|
+
"""DELETE with auth headers resolved from credential store or override."""
|
|
72
|
+
from nepher_cli.core.credentials import get_auth_headers
|
|
73
|
+
|
|
74
|
+
headers = get_auth_headers(api_key)
|
|
75
|
+
with httpx.Client() as client:
|
|
76
|
+
return client.delete(url, headers=headers, timeout=30.0)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""EnvHub support code shared by ``npcli envhub`` commands."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Local EnvHub bundle cache paths — aligned with the ``nepher`` CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from nepher_cli.envhub.config import DEFAULT_CONFIG, load_config_file
|
|
9
|
+
|
|
10
|
+
_DEFAULT_CACHE_DIR = Path.home() / ".nepher" / "cache"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _nepher_cache_dir(category: str | None = None) -> Path | None:
|
|
14
|
+
try:
|
|
15
|
+
from nepher.config import get_config
|
|
16
|
+
|
|
17
|
+
return get_config().get_cache_dir(category=category)
|
|
18
|
+
except ImportError:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_cache_dir(cache_dir: str | Path | None = None, category: str | None = None) -> Path:
|
|
23
|
+
"""Resolve the local bundle cache directory (same rules as ``nepher``)."""
|
|
24
|
+
if cache_dir:
|
|
25
|
+
return Path(cache_dir).expanduser().resolve()
|
|
26
|
+
|
|
27
|
+
nepher_dir = _nepher_cache_dir(category)
|
|
28
|
+
if nepher_dir is not None:
|
|
29
|
+
return nepher_dir
|
|
30
|
+
|
|
31
|
+
env_override = os.getenv("NEPHER_CACHE_DIR")
|
|
32
|
+
if env_override:
|
|
33
|
+
return Path(env_override).expanduser().resolve()
|
|
34
|
+
|
|
35
|
+
config = {**DEFAULT_CONFIG, **load_config_file()}
|
|
36
|
+
if category:
|
|
37
|
+
cat_config = config.get("categories", {}).get(category, {})
|
|
38
|
+
path_str = cat_config.get("cache_dir") or config.get("cache_dir")
|
|
39
|
+
else:
|
|
40
|
+
path_str = config.get("cache_dir")
|
|
41
|
+
|
|
42
|
+
if path_str:
|
|
43
|
+
return Path(path_str).expanduser().resolve()
|
|
44
|
+
return _DEFAULT_CACHE_DIR.resolve()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_cached_env(path: Path) -> bool:
|
|
48
|
+
"""Return True when ``path`` is a cached environment bundle."""
|
|
49
|
+
return path.is_dir() and (path / "manifest.yaml").exists()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def list_cached_env_dirs(root: Path) -> list[Path]:
|
|
53
|
+
"""List cached environment directories under ``root``."""
|
|
54
|
+
if not root.exists():
|
|
55
|
+
return []
|
|
56
|
+
return sorted(p for p in root.iterdir() if is_cached_env(p))
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""EnvHub configuration — aligned with the ``nepher`` CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
11
|
+
"api_url": "https://envhub-api.nepher.ai",
|
|
12
|
+
"api_key": None,
|
|
13
|
+
"cache_dir": "~/.nepher/cache",
|
|
14
|
+
"default_category": None,
|
|
15
|
+
"categories": {},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
LIST_KEYS = ("api_url", "cache_dir", "default_category")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_config_file() -> Path | None:
|
|
22
|
+
"""Return the active nepher config file path, if any."""
|
|
23
|
+
cwd_config = Path.cwd() / ".nepherrc"
|
|
24
|
+
if cwd_config.exists():
|
|
25
|
+
return cwd_config
|
|
26
|
+
home_config = Path.home() / ".nepher" / "config.toml"
|
|
27
|
+
if home_config.exists():
|
|
28
|
+
return home_config
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_config_file() -> dict[str, Any]:
|
|
33
|
+
"""Load values from the nepher config file (no defaults or env overrides)."""
|
|
34
|
+
config_file = find_config_file()
|
|
35
|
+
if not config_file:
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if config_file.suffix == ".toml":
|
|
40
|
+
try:
|
|
41
|
+
import tomllib
|
|
42
|
+
except ImportError:
|
|
43
|
+
try:
|
|
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 {}
|
|
54
|
+
except Exception:
|
|
55
|
+
return {}
|
|
56
|
+
return {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _apply_env_overrides(config: dict[str, Any]) -> dict[str, Any]:
|
|
60
|
+
merged = dict(config)
|
|
61
|
+
if os.getenv("ENVHUB_API_URL"):
|
|
62
|
+
merged["api_url"] = os.getenv("ENVHUB_API_URL")
|
|
63
|
+
if os.getenv("NEPHER_API_KEY"):
|
|
64
|
+
merged["api_key"] = os.getenv("NEPHER_API_KEY")
|
|
65
|
+
if os.getenv("NEPHER_CACHE_DIR"):
|
|
66
|
+
merged["cache_dir"] = os.getenv("NEPHER_CACHE_DIR")
|
|
67
|
+
return merged
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _fallback_get(key: str, default: Any = None) -> Any:
|
|
71
|
+
config = _apply_env_overrides({**DEFAULT_CONFIG, **load_config_file()})
|
|
72
|
+
keys = key.split(".")
|
|
73
|
+
value: Any = config
|
|
74
|
+
for part in keys:
|
|
75
|
+
if isinstance(value, dict) and part in value:
|
|
76
|
+
value = value[part]
|
|
77
|
+
else:
|
|
78
|
+
return default
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_value(key: str, default: Any = None) -> Any:
|
|
83
|
+
"""Return an effective configuration value."""
|
|
84
|
+
try:
|
|
85
|
+
from nepher.config import get_config
|
|
86
|
+
|
|
87
|
+
return get_config().get(key, default)
|
|
88
|
+
except ImportError:
|
|
89
|
+
return _fallback_get(key, default)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def list_values() -> dict[str, Any]:
|
|
93
|
+
"""Return the standard keys shown by ``nepher config list``."""
|
|
94
|
+
return {key: get_value(key) for key in LIST_KEYS}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def parse_config_value(value: str) -> Any:
|
|
98
|
+
"""Parse a CLI string into a config value."""
|
|
99
|
+
if value.lower() in ("true", "false"):
|
|
100
|
+
return value.lower() == "true"
|
|
101
|
+
if value.isdigit():
|
|
102
|
+
return int(value)
|
|
103
|
+
if value.replace(".", "", 1).isdigit():
|
|
104
|
+
return float(value)
|
|
105
|
+
return value
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def set_value(key: str, value: Any) -> None:
|
|
109
|
+
"""Persist a configuration value."""
|
|
110
|
+
try:
|
|
111
|
+
from nepher.config import set_config
|
|
112
|
+
|
|
113
|
+
set_config(key, value, save=True)
|
|
114
|
+
return
|
|
115
|
+
except ImportError:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
config_path = Path.home() / ".nepher" / "config.toml"
|
|
119
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
data = load_config_file()
|
|
121
|
+
parts = key.split(".")
|
|
122
|
+
target = data
|
|
123
|
+
for part in parts[:-1]:
|
|
124
|
+
nested = target.get(part)
|
|
125
|
+
if not isinstance(nested, dict):
|
|
126
|
+
nested = {}
|
|
127
|
+
target[part] = nested
|
|
128
|
+
target = nested
|
|
129
|
+
target[parts[-1]] = value
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
import tomli_w
|
|
133
|
+
|
|
134
|
+
with open(config_path, "wb") as f:
|
|
135
|
+
tomli_w.dump(data, f)
|
|
136
|
+
except ImportError:
|
|
137
|
+
with open(config_path.with_suffix(".json"), "w", encoding="utf-8") as f:
|
|
138
|
+
json.dump(data, f, indent=2)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def reset_config() -> bool:
|
|
142
|
+
"""Delete the on-disk config file. Returns True when a file was removed."""
|
|
143
|
+
try:
|
|
144
|
+
from nepher.config import get_config
|
|
145
|
+
|
|
146
|
+
config = get_config()
|
|
147
|
+
config_file = getattr(config, "_config_file", None)
|
|
148
|
+
if config_file and config_file.exists():
|
|
149
|
+
config_file.unlink()
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
except ImportError:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
removed = False
|
|
156
|
+
for path in (Path.cwd() / ".nepherrc", Path.home() / ".nepher" / "config.toml"):
|
|
157
|
+
if path.exists():
|
|
158
|
+
path.unlink()
|
|
159
|
+
removed = True
|
|
160
|
+
json_path = Path.home() / ".nepher" / "config.json"
|
|
161
|
+
if json_path.exists():
|
|
162
|
+
json_path.unlink()
|
|
163
|
+
removed = True
|
|
164
|
+
return removed
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def mask_secret(key: str, value: Any) -> Any:
|
|
168
|
+
"""Mask secret keys for display."""
|
|
169
|
+
if value is None or not isinstance(value, str):
|
|
170
|
+
return value
|
|
171
|
+
lower = key.lower()
|
|
172
|
+
if any(token in lower for token in ("api_key", "secret", "password", "token")):
|
|
173
|
+
if len(value) < 8:
|
|
174
|
+
return "***"
|
|
175
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
176
|
+
return value
|
nepher_cli/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# tournament logic package — agent_check, packer, api, wallet
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Local agent directory structure check — no external dependencies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# Required items: relative path → "file" or "directory"
|
|
8
|
+
_REQUIRED: dict[str, str] = {
|
|
9
|
+
"best_policy": "directory",
|
|
10
|
+
"best_policy/best_policy.pt": "file",
|
|
11
|
+
"source": "directory",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Recommended (absent ones produce warnings, not errors)
|
|
15
|
+
_RECOMMENDED: dict[str, str] = {
|
|
16
|
+
"scripts/list_envs.py": "file",
|
|
17
|
+
"scripts/rsl_rl/play.py": "file",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check_agent_structure(
|
|
22
|
+
agent_path: Path,
|
|
23
|
+
) -> tuple[bool, list[str], list[str]]:
|
|
24
|
+
"""Check whether *agent_path* meets the expected agent layout.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
(is_valid, errors, warnings)
|
|
28
|
+
|
|
29
|
+
* ``is_valid`` – True when all required items are present and correct.
|
|
30
|
+
* ``errors`` – Required items that are missing or the wrong type.
|
|
31
|
+
* ``warnings`` – Recommended items that are absent (non-blocking).
|
|
32
|
+
"""
|
|
33
|
+
errors: list[str] = []
|
|
34
|
+
warnings: list[str] = []
|
|
35
|
+
|
|
36
|
+
if not agent_path.exists():
|
|
37
|
+
return False, [f"Path does not exist: {agent_path}"], warnings
|
|
38
|
+
|
|
39
|
+
if not agent_path.is_dir():
|
|
40
|
+
return False, [f"Not a directory: {agent_path}"], warnings
|
|
41
|
+
|
|
42
|
+
for rel_path, item_type in _REQUIRED.items():
|
|
43
|
+
full_path = agent_path / rel_path
|
|
44
|
+
if not full_path.exists():
|
|
45
|
+
errors.append(f"Required {item_type} missing: {rel_path}")
|
|
46
|
+
elif item_type == "directory" and not full_path.is_dir():
|
|
47
|
+
errors.append(f"Expected directory but found file: {rel_path}")
|
|
48
|
+
elif item_type == "file" and not full_path.is_file():
|
|
49
|
+
errors.append(f"Expected file but found directory: {rel_path}")
|
|
50
|
+
|
|
51
|
+
source_dir = agent_path / "source"
|
|
52
|
+
if source_dir.exists() and source_dir.is_dir():
|
|
53
|
+
if not any(d.is_dir() for d in source_dir.iterdir()):
|
|
54
|
+
errors.append("source/ directory must contain at least one task module subdirectory")
|
|
55
|
+
|
|
56
|
+
for rel_path, item_type in _RECOMMENDED.items():
|
|
57
|
+
if not (agent_path / rel_path).exists():
|
|
58
|
+
warnings.append(f"Recommended {item_type} missing: {rel_path}")
|
|
59
|
+
|
|
60
|
+
return len(errors) == 0, errors, warnings
|