Files
zeekayandHanzo Dev 7824191215 feat: budget, policy, audit, search, kb and graph on one credential
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]>
2026-09-10 15:01:41 -07:00

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