mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
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.
78 lines
3.1 KiB
Python
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
|