Files
python-sdk/examples/client.py
T
zeekay 4998a916fd the client sends the credential the document now declares
hanzoai/cloud@113cf7d6 declares components.securitySchemes.bearer and applies it
document-wide, so regenerating writes a populated Configuration.auth_settings()
and auth_settings=['bearer'] into 2498 call sites; ApiClient._apply_auth_params
sets Authorization: Bearer <token> from access_token, which was read by nothing
before. Four call sites carry an empty list — exactly the operations the document
marks security: [] (get_models, get_models_providers, get_commands,
get_openapi_json) — so the public routes still go out with no credential.

examples/client.py drops the header_name/header_value compensation and passes
access_token. examples/models is a new flow on the public catalog: it runs before
a reader has a key, which makes the install checkable in one command.

Proven on api.hanzo.ai, not by reading: examples.models prints 112 models with no
credential; examples.hello returns the caller's keys with a real sk- and 403
"sign in to manage API keys" with a bogus one.
2026-08-14 17:22:30 -07:00

95 lines
4.0 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_API_KEY
"""
from __future__ import annotations
import os
import sys
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 api_key() -> str:
"""Fail loudly and early when the key is absent or of the wrong shape.
Without this the SDK sends an unauthenticated request and the flow dies on a
403 several frames deep, which reads like an API bug rather than an unset
shell variable.
A ``pk-`` is rejected here for the same reason. It is the PUBLISHABLE shape:
cloud resolves it to an org so a browser beacon can be attributed, but the
identity boundary refuses it outright, so it never becomes a principal. Every
route below wants one — ``/v1/tools`` says so in as many words, "a validated
principal is required" — so a ``pk-`` collects the same 403 as no key at all,
and that 403 reads like a revoked key rather than the wrong shape of key.
"""
key = os.environ.get("HANZO_API_KEY")
if not key:
raise SystemExit("HANZO_API_KEY is not set — export an sk- cloud key or an IAM access token")
if key.startswith("pk-"):
raise SystemExit("HANZO_API_KEY is a pk- (publishable) key, which authenticates nobody — use an sk-")
return key
def client() -> ApiClient:
"""An ApiClient carrying the credential the generated code knows how to send.
``access_token`` is the whole configuration. The document declares one
security scheme — ``bearer``, HTTP bearer — and applies it to every operation
except the four marked ``security: []``, so the generator wrote a populated
``Configuration.auth_settings()`` and an ``auth_settings=['bearer']`` into
2498 call sites. ``ApiClient._apply_auth_params`` reads that and sets
``Authorization: Bearer <token>`` on the way out.
This used to pass ``header_name``/``header_value`` on the ApiClient, because
the document declared no scheme at all: ``auth_settings()`` returned ``{}``,
``access_token`` was read by nothing, and a client built the obvious way sent
no ``Authorization`` header. That is fixed at the source — in the document —
which is where every other language got the same fix.
The header goes only where the document says it belongs. A flow that calls a
``security: []`` operation through this client sends no credential on that
call, which is the correct behaviour and not a hole: those four operations
are public.
"""
return ApiClient(Configuration(host=BASE_URL, access_token=api_key()))
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