mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
Six capabilities hang off the client as first-class methods rather than as generated operations a caller assembles for itself: budget (allowance, wallet, tier, charged ledger), policy (one authorization question), audit (the org's trail, read-only by construction), search (one ranked set over every corpus), kb (the corpus you write and the connectors that fill it), and graph (assertions with provenance and time). The same six words, the same method names and the same answer type as the Go and TypeScript clients. A refused budget and a refused policy are answers. Answer is a union of three frozen types with a discriminant — Ok, Denied, Held — so a caller reads "denied, and here is why" without catching, and reading .value off a refusal raises it rather than handing back a half-answer. One rule maps HTTP to an arm: a 202 whose body says held is a hold, 402 and the four refusal codes on 403 are refusals, and a status that decided nothing raises. Every arm carries the x-request-id, which is the word audit.Event.request uses too — the join that lets a caller find the row the server wrote for the call it just made. Auth is IAM and nothing else. The client exchanges its own clientId and clientSecret for a short-lived access token (client_credentials, client_secret_basic, RFC 8707 resource), holds it until shortly before expiry, and re-mints once on a 401. It no longer accepts a bearer, so HANZO_API_KEY, Configuration(access_token=) construction and result() are gone rather than deprecated: two ways to hold one credential is the defect this removes. as_ still scopes to a subject and now mints from the client's own token. hanzoai resolves its public names on first use, and the capability layer is written over the wire rather than over the generated client, so importing the six costs milliseconds instead of 2,700 generated modules. ruff and ruff format are clean, mypy is clean over the thirteen new and changed modules, and 106 tests pass. The two failures in test_smoke are the ToolsApi name the current document no longer carries, and they fail at HEAD too. The mint was exercised against the live hanzo.id, which answers invalid_client for a credential it does not know; the six were exercised against a stub asserting the exact request and decoding the shapes cloud's own schemas declare. examples/six runs the whole thing as a caller would. Co-authored-by: Hanzo Dev <[email protected]>
68 lines
2.5 KiB
Python
68 lines
2.5 KiB
Python
"""The one place an example learns where the API is and who it is.
|
|
|
|
Every flow imports this and nothing else builds a client, so there is a single
|
|
answer to "which base URL?" and "which env var?" across all six.
|
|
|
|
Run any flow from the repo root::
|
|
|
|
python -m examples.models # no credential — GET /v1/models is public
|
|
python -m examples.hello # needs HANZO_CLIENT_ID and HANZO_CLIENT_SECRET
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
from hanzoai import Client
|
|
from hanzoai.cloud import ApiClient, Configuration
|
|
from hanzoai.cloud.exceptions import ApiException
|
|
|
|
#: Default host. ``HANZO_BASE_URL`` overrides it (staging, a local cloud, a tunnel).
|
|
BASE_URL = os.environ.get("HANZO_BASE_URL", "https://api.hanzo.ai")
|
|
|
|
#: ``zen4`` is the flagship the spec documents as its own example value.
|
|
MODEL = os.environ.get("HANZO_MODEL", "zen4")
|
|
|
|
|
|
def client() -> Client:
|
|
"""A client holding this caller's IAM credential.
|
|
|
|
``Client`` exchanges ``HANZO_CLIENT_ID`` and ``HANZO_CLIENT_SECRET`` for a
|
|
short-lived access token and presents that. It takes no bearer, because a
|
|
credential handed to an SDK is a credential nobody rotates and one that says
|
|
nothing about who is calling.
|
|
|
|
It is an ``ApiClient``, so every generated operation takes it unchanged, and
|
|
it answers the six capabilities besides. ``Configuration.auth_settings()``
|
|
is where the token joins a generated request: the document declares one
|
|
security scheme, ``bearer``, and applies it to every operation except the
|
|
handful marked ``security: []``, so the credential goes where the document
|
|
says it belongs and nowhere else.
|
|
"""
|
|
return Client(base=BASE_URL)
|
|
|
|
|
|
def public() -> ApiClient:
|
|
"""An ApiClient with no credential, for the operations that need none.
|
|
|
|
Four of the document's 2479 operations carry ``security: []`` — ``GET
|
|
/v1/models``, ``GET /v1/models/providers``, ``GET /v1/commands``, ``GET
|
|
/v1/openapi.json``. They are the public face of the API, and they answer 200
|
|
to a caller who has nothing. ``examples.models`` runs on this.
|
|
"""
|
|
return ApiClient(Configuration(host=BASE_URL))
|
|
|
|
|
|
def run(main) -> None:
|
|
"""Invoke a flow and report an API failure the way a caller can act on.
|
|
|
|
ApiException stringifies to the status line alone; the server's explanation
|
|
is in ``.body``, which is the part worth printing.
|
|
"""
|
|
try:
|
|
main()
|
|
except ApiException as e:
|
|
print(f"HTTP {e.status}: {e.body}", file=sys.stderr)
|
|
raise SystemExit(1) from e
|