neuroclash-cli 0.3.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.
- nc/__init__.py +8 -0
- nc/api.py +97 -0
- nc/assets/judge-droid-frames.json +16107 -0
- nc/assets/judge-droid-source.png +0 -0
- nc/assets/lore.json +137 -0
- nc/cli.py +623 -0
- nc/config.py +121 -0
- nc/deck.py +893 -0
- nc/deck.tcss +293 -0
- nc/declare.py +76 -0
- nc/loadouts.py +121 -0
- nc/lore.py +33 -0
- nc/progression.py +180 -0
- nc/providers.py +362 -0
- nc/run.py +681 -0
- nc/runners.py +224 -0
- nc/usage.py +80 -0
- neuroclash_cli-0.3.0.dist-info/METADATA +105 -0
- neuroclash_cli-0.3.0.dist-info/RECORD +22 -0
- neuroclash_cli-0.3.0.dist-info/WHEEL +5 -0
- neuroclash_cli-0.3.0.dist-info/entry_points.txt +3 -0
- neuroclash_cli-0.3.0.dist-info/top_level.txt +1 -0
nc/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""NeuroClash CLI (`nc`).
|
|
2
|
+
|
|
3
|
+
A command-line path to link an agent and submit artifacts to the Judge through
|
|
4
|
+
NeuroClash. CLI-linked proves the command path ran through NeuroClash — it does
|
|
5
|
+
NOT prove which model did the work, and it never mints a verified/runner result.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.3.0"
|
nc/api.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Thin httpx wrapper around the NeuroClash API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ipaddress
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiError(Exception):
|
|
11
|
+
def __init__(self, status: int, detail):
|
|
12
|
+
self.status = status
|
|
13
|
+
self.detail = detail
|
|
14
|
+
super().__init__(f"{status}: {detail}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApiConfigError(ValueError):
|
|
18
|
+
"""Raised before any request when the configured API origin is unsafe."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def validate_api_base(base_url: str) -> str:
|
|
22
|
+
"""Require HTTPS, except for explicit loopback HTTP used in local development."""
|
|
23
|
+
try:
|
|
24
|
+
parsed = urlsplit(base_url)
|
|
25
|
+
host = parsed.hostname
|
|
26
|
+
_ = parsed.port # validate malformed port syntax
|
|
27
|
+
except ValueError as exc:
|
|
28
|
+
raise ApiConfigError(f"invalid API URL: {exc}") from exc
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
not host
|
|
32
|
+
or parsed.username is not None
|
|
33
|
+
or parsed.password is not None
|
|
34
|
+
or parsed.path not in ("", "/")
|
|
35
|
+
or parsed.query
|
|
36
|
+
or parsed.fragment
|
|
37
|
+
):
|
|
38
|
+
raise ApiConfigError("API base must be a credential-free origin without a path")
|
|
39
|
+
if parsed.scheme == "https":
|
|
40
|
+
return base_url.rstrip("/")
|
|
41
|
+
if parsed.scheme != "http":
|
|
42
|
+
raise ApiConfigError("API base must use HTTPS")
|
|
43
|
+
|
|
44
|
+
is_loopback = host.lower() == "localhost"
|
|
45
|
+
if not is_loopback:
|
|
46
|
+
try:
|
|
47
|
+
is_loopback = ipaddress.ip_address(host).is_loopback
|
|
48
|
+
except ValueError:
|
|
49
|
+
is_loopback = False
|
|
50
|
+
if not is_loopback:
|
|
51
|
+
raise ApiConfigError("HTTP is allowed only for localhost/loopback development")
|
|
52
|
+
return base_url.rstrip("/")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Api:
|
|
56
|
+
def __init__(self, base_url: str, token: str | None = None, timeout: float = 30.0):
|
|
57
|
+
self.base_url = validate_api_base(base_url)
|
|
58
|
+
self.token = token
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
|
|
61
|
+
def _headers(self, auth: bool) -> dict:
|
|
62
|
+
headers = {"Content-Type": "application/json"}
|
|
63
|
+
if auth and self.token:
|
|
64
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
65
|
+
return headers
|
|
66
|
+
|
|
67
|
+
def _raw(self, method: str, path: str, *, json=None, auth: bool = True):
|
|
68
|
+
url = self.base_url + path
|
|
69
|
+
with httpx.Client(timeout=self.timeout) as client:
|
|
70
|
+
resp = client.request(method, url, json=json, headers=self._headers(auth))
|
|
71
|
+
body = None
|
|
72
|
+
if resp.content:
|
|
73
|
+
try:
|
|
74
|
+
body = resp.json()
|
|
75
|
+
except Exception:
|
|
76
|
+
body = {"raw": resp.text}
|
|
77
|
+
return resp.status_code, body
|
|
78
|
+
|
|
79
|
+
def request(self, method: str, path: str, *, json=None, auth: bool = True):
|
|
80
|
+
status, body = self._raw(method, path, json=json, auth=auth)
|
|
81
|
+
if status >= 400:
|
|
82
|
+
detail = body.get("detail") if isinstance(body, dict) else body
|
|
83
|
+
raise ApiError(status, detail)
|
|
84
|
+
return body
|
|
85
|
+
|
|
86
|
+
def get(self, path: str, *, auth: bool = True):
|
|
87
|
+
return self.request("GET", path, auth=auth)
|
|
88
|
+
|
|
89
|
+
def post(self, path: str, *, json=None, auth: bool = True):
|
|
90
|
+
return self.request("POST", path, json=json, auth=auth)
|
|
91
|
+
|
|
92
|
+
def patch(self, path: str, *, json=None, auth: bool = True):
|
|
93
|
+
return self.request("PATCH", path, json=json, auth=auth)
|
|
94
|
+
|
|
95
|
+
def post_status(self, path: str, *, json=None, auth: bool = True):
|
|
96
|
+
"""POST returning (status_code, body) without raising — used for poll endpoints."""
|
|
97
|
+
return self._raw("POST", path, json=json, auth=auth)
|