mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
The Python client now reaches what the Rust crate does and the server serves. Research covers every /v1/research route: record() files benchmarks, studies, runs and papers in one batch a kind; runs(), get_run() and compare() read them; grant(), revise() and retract() move consent and versions; the experiment plane and artifacts as before. Arena covers /v1/benchmark: catalog, leaderboard, claims (written with protocol and source), history, compare, presets, and runs, which queue a paid execution and are refused without confirm=True. wire holds the crate's Run, Benchmark, Measure, Study, Paper and Comparison field for field; canonical writes RFC 8785 bytes equal to the crate's (tests/vectors/rust.json, written by the crate); lineage and wal are the crate's records, and a WAL the crate wrote verifies here. capture() reads the harness commit (the diff's SHA-256 when dirty), the weights' SHA-256, host, CPU, GPU, device, dtype and packages, and apply() folds them into a run. runs(org=) sends no credential: the server answers ?org= only to a caller with no org. An org the token does not belong to is refused before the request, since the server would act in the home org. The command is hanzo-research; the native CLI owns `hanzo`.
79 lines
2.5 KiB
Python
79 lines
2.5 KiB
Python
"""Errors a call raises, one class a kind of refusal, each carrying the server's own sentence."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
__all__ = ["Error", "Denied", "Missing", "Invalid", "Limited", "Unavailable", "refusal"]
|
|
|
|
|
|
class Error(Exception):
|
|
"""A call that did not succeed: the HTTP status (0 when no answer came), what the server said, and where."""
|
|
|
|
def __init__(self, message: str, status: int = 0, method: str = "", path: str = "", body: Any = None):
|
|
super().__init__(f"{method} {path}: {status}: {message}" if method else message)
|
|
self.message = message
|
|
self.status = status
|
|
self.method = method
|
|
self.path = path
|
|
self.body = body
|
|
|
|
|
|
class Denied(Error):
|
|
"""401 or 403: no credential, an expired one, or a caller this org or route does not admit."""
|
|
|
|
|
|
class Missing(Error):
|
|
"""404: no such record. A record another org holds reads the same, by design."""
|
|
|
|
|
|
class Invalid(Error):
|
|
"""400, 409 or 422: the request does not fit the route; the message names the field."""
|
|
|
|
|
|
class Limited(Error):
|
|
"""413 or 429: too large, or too many; `retry` is the server's wait in seconds when it gave one."""
|
|
|
|
retry: float | None = None
|
|
|
|
|
|
class Unavailable(Error):
|
|
"""502, 503, 504, or no answer at all: retry later."""
|
|
|
|
|
|
def refusal(status: int, raw: bytes, method: str, path: str, retry: float | None = None) -> Error:
|
|
"""The typed error for a non-2xx answer, its message read from whichever envelope the server used."""
|
|
body: Any = None
|
|
message = ""
|
|
try:
|
|
body = json.loads(raw)
|
|
except ValueError:
|
|
message = raw[:1024].decode("utf-8", "replace").strip()
|
|
if isinstance(body, dict):
|
|
e = body.get("error")
|
|
if isinstance(e, dict):
|
|
message = str(e.get("message") or e.get("code") or "")
|
|
elif e:
|
|
message = str(e)
|
|
# typed ops answer RFC 7807 ({detail, title}); the blob route and the gateway {error}
|
|
message = message or str(
|
|
body.get("detail") or body.get("message") or body.get("msg") or body.get("title") or ""
|
|
)
|
|
message = message or f"HTTP {status}"
|
|
cls: type[Error] = Error
|
|
if status in (401, 403):
|
|
cls = Denied
|
|
elif status == 404:
|
|
cls = Missing
|
|
elif status in (400, 409, 422):
|
|
cls = Invalid
|
|
elif status in (413, 429):
|
|
cls = Limited
|
|
elif status in (502, 503, 504):
|
|
cls = Unavailable
|
|
err = cls(message, status, method, path, body)
|
|
if isinstance(err, Limited):
|
|
err.retry = retry
|
|
return err
|