hugging-bay 0.1.0__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.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: hugging-bay
3
+ Version: 0.1.0
4
+ Summary: Thin, typed Python client for the Hugging Bay API — verified open-model discovery, safety, and lockfiles.
5
+ Author: Hugging Bay
6
+ License: MIT
7
+ Project-URL: Homepage, https://huggingbay.xyz
8
+ Project-URL: Repository, https://github.com/barneywohl/hugging-bay
9
+ Keywords: hugging-bay,models,safety,gguf,llm,provenance
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: httpx>=0.24
13
+
14
+ # hugging-bay
15
+
16
+ A thin, typed Python client for the [Hugging Bay](https://huggingbay.xyz) API:
17
+ verified open-model discovery, safe-to-run verdicts, provenance passports, and
18
+ offline lockfile verification.
19
+
20
+ The client is intentionally minimal — one method per endpoint, no client-side
21
+ magic. Verdicts, fits, and identities are computed by the server and returned
22
+ as parsed JSON.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install hugging-bay
28
+ ```
29
+
30
+ Requires Python 3.9+. The only runtime dependency is `httpx`.
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ from hugging_bay import Client
36
+
37
+ client = Client() # defaults to https://huggingbay.xyz
38
+ # Optional auth: Client(token="sk-...") sends Authorization: Bearer <token>.
39
+ # The token is never printed in repr()/str() and never logged.
40
+ ```
41
+
42
+ ### 1. Find a commercial-safe model for a rig
43
+
44
+ ```python
45
+ from hugging_bay import Client
46
+
47
+ client = Client()
48
+ result = client.find_model(
49
+ task="coding",
50
+ gpu="rtx4090-24",
51
+ ctx=8192,
52
+ device_family="nvidia",
53
+ commercial=True,
54
+ limit=3,
55
+ )
56
+ best = result["bestPick"]
57
+ print(best["repo"], "-> safe-to-run:", best["safety"]["verdict"])
58
+ ```
59
+
60
+ ### 2. Verify a source URL
61
+
62
+ ```python
63
+ from hugging_bay import Client, HuggingBayError
64
+
65
+ client = Client()
66
+ try:
67
+ resolved = client.resolve("https://huggingface.co/meta-llama/Llama-3.1-8B")
68
+ print("resolved artifact:", resolved.get("artifactId") or resolved)
69
+ except HuggingBayError as err:
70
+ print("could not resolve:", err.status, err.error)
71
+ ```
72
+
73
+ ### 3. Pull plan, then verify the lock against local files
74
+
75
+ ```python
76
+ from hugging_bay import Client, verify_lock
77
+
78
+ client = Client()
79
+ artifact_id = "art_123"
80
+
81
+ # The hosted download plan (signed manifest + per-file hashes).
82
+ plan = client.download_plan(artifact_id)
83
+
84
+ # ... download the files into ./models/art_123 using the plan ...
85
+
86
+ # Recompute local hashes and compare to the bay.lock document — pure stdlib,
87
+ # no network.
88
+ lock = client.lock(artifact_id)
89
+ report = verify_lock(lock, root_dir="./models/art_123")
90
+ if report["ok"]:
91
+ print(f"verified {report['checked']} files")
92
+ else:
93
+ for mismatch in report["mismatches"]:
94
+ print("BAD:", mismatch["path"], mismatch["reason"])
95
+ ```
96
+
97
+ ### 4. Diagnose a failing run
98
+
99
+ ```python
100
+ from hugging_bay import Client
101
+
102
+ client = Client()
103
+ diagnosis = client.doctor(
104
+ log_text="llama_model_load: error loading model: CUDA out of memory",
105
+ runtime="llama.cpp",
106
+ )
107
+ if diagnosis["topFix"]:
108
+ print("top fix:", diagnosis["topFix"])
109
+ for alt in diagnosis.get("smallerHostedAlternatives", []):
110
+ print("smaller option:", alt)
111
+ ```
112
+
113
+ ## API surface
114
+
115
+ | Method | Endpoint |
116
+ | --- | --- |
117
+ | `find_model(task, gpu, ctx, device_family, commercial, limit)` | `GET /api/agents/find-model` |
118
+ | `artifact(id)` | `GET /api/v1/artifacts/{id}` |
119
+ | `safety(id)` | `GET /api/v1/artifacts/{id}/safety` |
120
+ | `bundle(id)` | `GET /api/artifacts/{id}/bundle` |
121
+ | `passport(id)` | `GET /api/artifacts/{id}/passport` |
122
+ | `resolve(repo_or_url)` | `GET /api/resolve?repo=` |
123
+ | `download_plan(id)` | `GET /api/v1/artifacts/{id}/download-plan` |
124
+ | `lock(id)` | `GET /api/artifacts/{id}/lock` |
125
+ | `doctor(log_text, runtime)` | `POST /api/doctor` |
126
+ | `recipes()` / `recipe(slug)` | `GET /api/recipes[/{slug}]` |
127
+ | `verify_lock(lock_dict, root_dir)` | offline, stdlib only |
128
+
129
+ ## Errors
130
+
131
+ Any response with status `>= 400` raises `HuggingBayError(status, body)`. The
132
+ server's `error` field is exposed as `.error`; the parsed body is on `.body`.
133
+ On a `503` with a `Retry-After` header the client waits once and retries a
134
+ single time before raising.
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,125 @@
1
+ # hugging-bay
2
+
3
+ A thin, typed Python client for the [Hugging Bay](https://huggingbay.xyz) API:
4
+ verified open-model discovery, safe-to-run verdicts, provenance passports, and
5
+ offline lockfile verification.
6
+
7
+ The client is intentionally minimal — one method per endpoint, no client-side
8
+ magic. Verdicts, fits, and identities are computed by the server and returned
9
+ as parsed JSON.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install hugging-bay
15
+ ```
16
+
17
+ Requires Python 3.9+. The only runtime dependency is `httpx`.
18
+
19
+ ## Usage
20
+
21
+ ```python
22
+ from hugging_bay import Client
23
+
24
+ client = Client() # defaults to https://huggingbay.xyz
25
+ # Optional auth: Client(token="sk-...") sends Authorization: Bearer <token>.
26
+ # The token is never printed in repr()/str() and never logged.
27
+ ```
28
+
29
+ ### 1. Find a commercial-safe model for a rig
30
+
31
+ ```python
32
+ from hugging_bay import Client
33
+
34
+ client = Client()
35
+ result = client.find_model(
36
+ task="coding",
37
+ gpu="rtx4090-24",
38
+ ctx=8192,
39
+ device_family="nvidia",
40
+ commercial=True,
41
+ limit=3,
42
+ )
43
+ best = result["bestPick"]
44
+ print(best["repo"], "-> safe-to-run:", best["safety"]["verdict"])
45
+ ```
46
+
47
+ ### 2. Verify a source URL
48
+
49
+ ```python
50
+ from hugging_bay import Client, HuggingBayError
51
+
52
+ client = Client()
53
+ try:
54
+ resolved = client.resolve("https://huggingface.co/meta-llama/Llama-3.1-8B")
55
+ print("resolved artifact:", resolved.get("artifactId") or resolved)
56
+ except HuggingBayError as err:
57
+ print("could not resolve:", err.status, err.error)
58
+ ```
59
+
60
+ ### 3. Pull plan, then verify the lock against local files
61
+
62
+ ```python
63
+ from hugging_bay import Client, verify_lock
64
+
65
+ client = Client()
66
+ artifact_id = "art_123"
67
+
68
+ # The hosted download plan (signed manifest + per-file hashes).
69
+ plan = client.download_plan(artifact_id)
70
+
71
+ # ... download the files into ./models/art_123 using the plan ...
72
+
73
+ # Recompute local hashes and compare to the bay.lock document — pure stdlib,
74
+ # no network.
75
+ lock = client.lock(artifact_id)
76
+ report = verify_lock(lock, root_dir="./models/art_123")
77
+ if report["ok"]:
78
+ print(f"verified {report['checked']} files")
79
+ else:
80
+ for mismatch in report["mismatches"]:
81
+ print("BAD:", mismatch["path"], mismatch["reason"])
82
+ ```
83
+
84
+ ### 4. Diagnose a failing run
85
+
86
+ ```python
87
+ from hugging_bay import Client
88
+
89
+ client = Client()
90
+ diagnosis = client.doctor(
91
+ log_text="llama_model_load: error loading model: CUDA out of memory",
92
+ runtime="llama.cpp",
93
+ )
94
+ if diagnosis["topFix"]:
95
+ print("top fix:", diagnosis["topFix"])
96
+ for alt in diagnosis.get("smallerHostedAlternatives", []):
97
+ print("smaller option:", alt)
98
+ ```
99
+
100
+ ## API surface
101
+
102
+ | Method | Endpoint |
103
+ | --- | --- |
104
+ | `find_model(task, gpu, ctx, device_family, commercial, limit)` | `GET /api/agents/find-model` |
105
+ | `artifact(id)` | `GET /api/v1/artifacts/{id}` |
106
+ | `safety(id)` | `GET /api/v1/artifacts/{id}/safety` |
107
+ | `bundle(id)` | `GET /api/artifacts/{id}/bundle` |
108
+ | `passport(id)` | `GET /api/artifacts/{id}/passport` |
109
+ | `resolve(repo_or_url)` | `GET /api/resolve?repo=` |
110
+ | `download_plan(id)` | `GET /api/v1/artifacts/{id}/download-plan` |
111
+ | `lock(id)` | `GET /api/artifacts/{id}/lock` |
112
+ | `doctor(log_text, runtime)` | `POST /api/doctor` |
113
+ | `recipes()` / `recipe(slug)` | `GET /api/recipes[/{slug}]` |
114
+ | `verify_lock(lock_dict, root_dir)` | offline, stdlib only |
115
+
116
+ ## Errors
117
+
118
+ Any response with status `>= 400` raises `HuggingBayError(status, body)`. The
119
+ server's `error` field is exposed as `.error`; the parsed body is on `.body`.
120
+ On a `503` with a `Retry-After` header the client waits once and retries a
121
+ single time before raising.
122
+
123
+ ## License
124
+
125
+ MIT
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hugging-bay"
7
+ version = "0.1.0"
8
+ description = "Thin, typed Python client for the Hugging Bay API — verified open-model discovery, safety, and lockfiles."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Hugging Bay" }]
13
+ keywords = ["hugging-bay", "models", "safety", "gguf", "llm", "provenance"]
14
+ dependencies = ["httpx>=0.24"]
15
+
16
+ [project.urls]
17
+ Homepage = "https://huggingbay.xyz"
18
+ Repository = "https://github.com/barneywohl/hugging-bay"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["src"]
22
+
23
+ [tool.setuptools.package-data]
24
+ hugging_bay = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """Hugging Bay Python SDK — a thin, typed client for the Hugging Bay API."""
2
+
3
+ from .client import Client, HuggingBayError
4
+ from .lock import verify_lock
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = ["Client", "HuggingBayError", "verify_lock", "__version__"]
@@ -0,0 +1,228 @@
1
+ """Client for the Hugging Bay HTTP API.
2
+
3
+ The client is deliberately thin: one method per endpoint, no client-side
4
+ business logic, no response reshaping beyond returning parsed JSON. The server
5
+ is the source of truth for verdicts, fits, and identities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import time
11
+ from typing import Any, Dict, Optional
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+
16
+ __all__ = ["Client", "HuggingBayError"]
17
+
18
+ _DEFAULT_BASE_URL = "https://huggingbay.xyz"
19
+
20
+
21
+ class HuggingBayError(Exception):
22
+ """Raised when the API returns a >=400 status.
23
+
24
+ Attributes:
25
+ status: HTTP status code.
26
+ body: parsed JSON body (dict) when available, else the raw text.
27
+ error: the server's ``error`` field when present, else ``None``.
28
+ """
29
+
30
+ def __init__(self, status: int, body: Any) -> None:
31
+ self.status = status
32
+ self.body = body
33
+ self.error: Optional[str] = None
34
+ if isinstance(body, dict):
35
+ value = body.get("error")
36
+ if isinstance(value, str):
37
+ self.error = value
38
+ message = self.error or (body if isinstance(body, str) else "request failed")
39
+ super().__init__(f"HTTP {status}: {message}")
40
+
41
+
42
+ class Client:
43
+ """Synchronous Hugging Bay API client.
44
+
45
+ Args:
46
+ base_url: API origin. Defaults to the public production host.
47
+ token: optional bearer token; sent as ``Authorization: Bearer <token>``.
48
+ The token is never included in ``repr()`` / ``str()`` and never logged.
49
+ timeout: per-request timeout in seconds.
50
+ """
51
+
52
+ def __init__(
53
+ self,
54
+ base_url: str = _DEFAULT_BASE_URL,
55
+ token: Optional[str] = None,
56
+ timeout: float = 20,
57
+ *,
58
+ transport: Optional[httpx.BaseTransport] = None,
59
+ ) -> None:
60
+ self.base_url = base_url.rstrip("/")
61
+ self._token = token
62
+ headers = {"Accept": "application/json", "User-Agent": "hugging-bay-python/0.1.0"}
63
+ if token:
64
+ headers["Authorization"] = f"Bearer {token}"
65
+ # `transport` is an injection point for tests (httpx.MockTransport).
66
+ self._http = httpx.Client(
67
+ base_url=self.base_url,
68
+ headers=headers,
69
+ timeout=timeout,
70
+ transport=transport,
71
+ )
72
+
73
+ # -- lifecycle ---------------------------------------------------------
74
+
75
+ def close(self) -> None:
76
+ self._http.close()
77
+
78
+ def __enter__(self) -> "Client":
79
+ return self
80
+
81
+ def __exit__(self, *exc: object) -> None:
82
+ self.close()
83
+
84
+ def __repr__(self) -> str:
85
+ # Never leak the token.
86
+ return f"Client(base_url={self.base_url!r}, token={'set' if self._token else None})"
87
+
88
+ __str__ = __repr__
89
+
90
+ # -- transport ---------------------------------------------------------
91
+
92
+ def _request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None,
93
+ json: Optional[Dict[str, Any]] = None, _retried: bool = False) -> Any:
94
+ clean_params = None
95
+ if params is not None:
96
+ clean_params = {k: v for k, v in params.items() if v is not None}
97
+ resp = self._http.request(method, path, params=clean_params, json=json)
98
+
99
+ # Honor Retry-After exactly once on a 503.
100
+ if resp.status_code == 503 and not _retried:
101
+ retry_after = resp.headers.get("Retry-After")
102
+ delay = _parse_retry_after(retry_after)
103
+ if delay is not None:
104
+ time.sleep(delay)
105
+ return self._request(method, path, params=params, json=json, _retried=True)
106
+
107
+ if resp.status_code >= 400:
108
+ raise HuggingBayError(resp.status_code, _safe_body(resp))
109
+ return _safe_body(resp)
110
+
111
+ def _get(self, path: str, **params: Any) -> Any:
112
+ return self._request("GET", path, params=params)
113
+
114
+ # -- endpoints ---------------------------------------------------------
115
+
116
+ def find_model(
117
+ self,
118
+ task: str,
119
+ gpu: Optional[str] = None,
120
+ ctx: Optional[int] = None,
121
+ device_family: Optional[str] = None,
122
+ commercial: bool = False,
123
+ limit: int = 3,
124
+ ) -> Dict[str, Any]:
125
+ """GET /api/agents/find-model — verified recommendation for a task/rig."""
126
+ return self._get(
127
+ "/api/agents/find-model",
128
+ task=task,
129
+ gpu=gpu,
130
+ ctx=ctx,
131
+ deviceFamily=device_family,
132
+ commercial="true" if commercial else None,
133
+ limit=limit,
134
+ )
135
+
136
+ def artifact(self, id: str) -> Dict[str, Any]:
137
+ """GET /api/v1/artifacts/{id} — one public artifact."""
138
+ return self._get(f"/api/v1/artifacts/{id}")
139
+
140
+ def safety(self, id: str) -> Dict[str, Any]:
141
+ """GET /api/v1/artifacts/{id}/safety — per-artifact safe-to-run verdict."""
142
+ return self._get(f"/api/v1/artifacts/{id}/safety")
143
+
144
+ def bundle(self, id: str) -> Dict[str, Any]:
145
+ """GET /api/artifacts/{id}/bundle — trust bundle state."""
146
+ return self._get(f"/api/artifacts/{id}/bundle")
147
+
148
+ def passport(self, id: str) -> Dict[str, Any]:
149
+ """GET /api/artifacts/{id}/passport — artifact identity passport."""
150
+ return self._get(f"/api/artifacts/{id}/passport")
151
+
152
+ def resolve(self, repo_or_url: str) -> Dict[str, Any]:
153
+ """GET /api/resolve?repo= — resolve an owner/name or upstream URL."""
154
+ return self._get("/api/resolve", repo=repo_or_url)
155
+
156
+ def download_plan(self, id: str) -> Dict[str, Any]:
157
+ """GET /api/v1/artifacts/{id}/download-plan — hosted download plan + hashes."""
158
+ return self._get(f"/api/v1/artifacts/{id}/download-plan")
159
+
160
+ def download_completion(
161
+ self,
162
+ id: str,
163
+ path: str,
164
+ *,
165
+ sha256: str,
166
+ size_bytes: int,
167
+ completion_token: str,
168
+ ) -> Dict[str, Any]:
169
+ """POST /api/download-completions/{id}/{path} — attest a completed pull.
170
+
171
+ Call ONLY after the file has been downloaded in full and its SHA-256
172
+ recomputed locally to equal the download-plan's file.sha256.
173
+ ``completion_token`` is the single-use capability from the
174
+ ``X-Hugging-Bay-Completion-Token`` header of the download redirect;
175
+ without it the pull is not counted. Redirect issuance is a download
176
+ initiation, not a completed pull, and probes never count.
177
+ """
178
+ encoded_path = "/".join(quote(part, safe="") for part in str(path).split("/"))
179
+ return self._request(
180
+ "POST",
181
+ f"/api/download-completions/{id}/{encoded_path}",
182
+ json={
183
+ "sha256": sha256,
184
+ "sizeBytes": int(size_bytes),
185
+ "completionToken": completion_token,
186
+ },
187
+ )
188
+
189
+ def lock(self, id: str) -> Dict[str, Any]:
190
+ """GET /api/artifacts/{id}/lock — a bay.lock document (dict)."""
191
+ return self._get(f"/api/artifacts/{id}/lock")
192
+
193
+ def doctor(self, log_text: str, runtime: Optional[str] = None) -> Dict[str, Any]:
194
+ """POST /api/doctor — diagnose a failing run from its log output."""
195
+ body: Dict[str, Any] = {"log": log_text}
196
+ if runtime is not None:
197
+ body["runner"] = runtime
198
+ return self._request("POST", "/api/doctor", json=body)
199
+
200
+ def recipes(self) -> Dict[str, Any]:
201
+ """GET /api/recipes — index of locked task recipes."""
202
+ return self._get("/api/recipes")
203
+
204
+ def recipe(self, slug: str) -> Dict[str, Any]:
205
+ """GET /api/recipes/{slug} — one locked recipe by slug."""
206
+ return self._get(f"/api/recipes/{slug}")
207
+
208
+
209
+ def _safe_body(resp: httpx.Response) -> Any:
210
+ try:
211
+ return resp.json()
212
+ except ValueError:
213
+ return resp.text
214
+
215
+
216
+ def _parse_retry_after(value: Optional[str]) -> Optional[float]:
217
+ """Parse a Retry-After header. Only the delta-seconds form is honored;
218
+ an HTTP-date form is ignored (returns None) to avoid clock-skew guessing."""
219
+ if not value:
220
+ return None
221
+ try:
222
+ seconds = float(value.strip())
223
+ except (TypeError, ValueError):
224
+ return None
225
+ if seconds < 0:
226
+ return None
227
+ # Guard against a hostile/huge value blocking the caller indefinitely.
228
+ return min(seconds, 60.0)