Files
python-sdk/examples/client.py
T
zeekay 7a5968f9dc a pk- key authenticates nobody, so stop telling readers it reads
The README offered the two key shapes as a permission split — pk- to read,
sk- to write. cloud's own identity boundary says otherwise: middleware_identity
returns nil for any pk- before it consults the key store, because a key meant
for a browser bundle must not read. A pk- resolves to an org so a beacon can be
attributed to a tenant, and that is all it does.

Following the old sentence, a reader exports a pk- and every one of the five
flows answers 403 — /v1/tools spells out why, "a validated principal is
required" — and that refusal reads like a revoked key rather than the wrong
shape of key. examples/client.py now refuses a pk- before the request goes out,
the same argument that already rejects an unsendable access_token.

Also: three status codes in example docstrings had drifted from what the API
answers now (/v1/kv/keys/* 404s on every verb, /v1/automations/mcp 404s), and
two dead files went with them — examples/.keep, whose directory has held real
flows for a while, and scripts/mock, which reads a .stats.yml this repo does
not have and which scripts/test already says is unnecessary.
2026-08-14 14:45:57 -07:00

78 lines
3.1 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 five.
Run any flow from the repo root::
python -m examples.hello
"""
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
401 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 JWT")
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 bound to the configured host, carrying the key as a bearer.
The header is set here rather than through ``Configuration(access_token=…)``,
and that is a correction, not a preference. The document declares no security
scheme, so the generator wrote an empty ``auth_settings`` into every
operation, ``Configuration.auth_settings()`` returns ``{}``, and
``access_token`` is read by nothing. Measured on this tree: serializing
``get_keys`` from a Configuration built with ``access_token="sk-test"``
produces a request with no ``Authorization`` header at all. Every flow built
that way calls anonymously and collects a 403 that reads like a key problem.
``header_name``/``header_value`` is the generated client's own way to send a
header the document did not describe, so it stays the one way here until the
document carries the scheme.
"""
return ApiClient(Configuration(host=BASE_URL), "Authorization", f"Bearer {api_key()}")
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