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.
- hugging_bay-0.1.0/PKG-INFO +138 -0
- hugging_bay-0.1.0/README.md +125 -0
- hugging_bay-0.1.0/pyproject.toml +24 -0
- hugging_bay-0.1.0/setup.cfg +4 -0
- hugging_bay-0.1.0/src/hugging_bay/__init__.py +8 -0
- hugging_bay-0.1.0/src/hugging_bay/client.py +228 -0
- hugging_bay-0.1.0/src/hugging_bay/lock.py +492 -0
- hugging_bay-0.1.0/src/hugging_bay/py.typed +0 -0
- hugging_bay-0.1.0/src/hugging_bay.egg-info/PKG-INFO +138 -0
- hugging_bay-0.1.0/src/hugging_bay.egg-info/SOURCES.txt +13 -0
- hugging_bay-0.1.0/src/hugging_bay.egg-info/dependency_links.txt +1 -0
- hugging_bay-0.1.0/src/hugging_bay.egg-info/requires.txt +1 -0
- hugging_bay-0.1.0/src/hugging_bay.egg-info/top_level.txt +1 -0
- hugging_bay-0.1.0/tests/test_client.py +192 -0
- hugging_bay-0.1.0/tests/test_lock.py +276 -0
|
@@ -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,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)
|