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 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)