mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
This distribution shipped two complete generated clients under one import root. `hanzoai.cloud` is the current one, generated from hanzoai/cloud's openapi.yaml at the ref .spec-lock names. `hanzoai.api` / `hanzoai.models` was another, and the package root re-exported its 2,202 names flat — so the obvious spelling, `from hanzoai import ApiClient`, reached the second client, while `hanzoai.cloud.ApiClient` reached the first. Nine core names existed twice as distinct classes bound to distinct documents, and which one a caller got depended on which import line they happened to write. The second lineage was not an older copy of the first. It was a projection of a document that no longer exists: of the 1,695 routes it binds, 243 are still in the document at v1.801.495 and 1,452 are not. Eighty-six percent of it builds a request the server answers with a 404. It had been broken for as long as it had been shipping, and nothing said so, because a client only fails at the call. So it goes, along with its runtime — api_client, api_response, configuration, exceptions, rest — which `hanzoai.cloud` carries its own copies of. The package root now exports `cloud` and nothing else. Nothing is re-exported flat on purpose: a name that appears in two places is a name that can disagree with itself, and the namespace already says which document a symbol came from. The smoke tests pinned the deleted surface — they asserted more than 300 api modules and imported `hanzoai.api.tracker_projects_api` by name, which is how a suite can stay green over a client that 404s. They now assert the surviving one, plus the invariant that made this possible in the first place: exactly one generated package under `hanzoai`, and none of the nine core names re-exported flat beside it.
106 lines
4.0 KiB
Python
106 lines
4.0 KiB
Python
"""Smoke tests for the generated `hanzoai` client.
|
|
|
|
The client is `hanzoai.cloud`, generated from hanzoai/cloud's openapi.yaml at the
|
|
ref .spec-lock names. These assert the package imports and that the unified
|
|
api.hanzo.ai/v1 product surface is present — not network behaviour.
|
|
"""
|
|
import hanzoai
|
|
import hanzoai.cloud
|
|
|
|
|
|
def test_client_constructs():
|
|
cfg = hanzoai.cloud.Configuration(host="https://api.hanzo.ai")
|
|
cfg.access_token = "hk-test"
|
|
client = hanzoai.cloud.ApiClient(cfg)
|
|
assert client is not None
|
|
assert cfg.host == "https://api.hanzo.ai"
|
|
|
|
|
|
def test_product_apis_present():
|
|
from hanzoai.cloud.api.tracker_api import TrackerApi
|
|
from hanzoai.cloud.api.chat_api import ChatApi
|
|
from hanzoai.cloud.api.crm_api import CrmApi
|
|
|
|
client = hanzoai.cloud.ApiClient(hanzoai.cloud.Configuration(host="https://api.hanzo.ai"))
|
|
for cls in (TrackerApi, ChatApi, CrmApi):
|
|
assert cls(client) is not None
|
|
|
|
|
|
def test_full_surface_breadth():
|
|
import glob
|
|
import os
|
|
|
|
api_dir = os.path.dirname(hanzoai.cloud.api.__file__)
|
|
modules = glob.glob(os.path.join(api_dir, "*_api.py"))
|
|
# One API group per product in the document, not the ~40 of the LLM-gateway
|
|
# client this replaced.
|
|
assert len(modules) > 150, f"only {len(modules)} api modules — surface too small"
|
|
|
|
|
|
def test_one_client_only():
|
|
"""There is exactly one generated client in this distribution.
|
|
|
|
`hanzoai.api` / `hanzoai.models` was a second one, re-exported flat from the
|
|
package root: 1,695 routes, of which 1,452 no longer exist in the document.
|
|
A caller reaching for the obvious flat name got a client that 404s. This
|
|
refuses its return — under that name or any other name beside `cloud`.
|
|
"""
|
|
import pkgutil
|
|
|
|
generated = {
|
|
m.name
|
|
for m in pkgutil.iter_modules(hanzoai.__path__)
|
|
if m.ispkg and m.name in {"cloud", "api", "models"}
|
|
}
|
|
assert generated == {"cloud"}, f"more than one generated client: {sorted(generated)}"
|
|
|
|
|
|
def test_version_is_single_sourced():
|
|
"""`hanzoai.__version__` must be the installed distribution, never a literal.
|
|
|
|
The generator writes `__version__ = "1.0.0"` (its default) into the package
|
|
__init__; that literal disagreed with pyproject for every release.
|
|
"""
|
|
from importlib.metadata import version
|
|
|
|
assert hanzoai.__version__ == version("hanzoai")
|
|
|
|
|
|
def test_cloud_package_imports():
|
|
"""`hanzoai.cloud` is the generated client — it must import cleanly.
|
|
|
|
This guarded a tag-casing collision in the retired hanzo.yaml lineage: tags
|
|
differing only by case (`AI`/`ai`, `API Keys`/`api-keys`, `MCP`/`mcp`) made
|
|
openapi-generator emit imports for classes it never wrote, so
|
|
`import hanzoai.cloud` raised ImportError. The client is a projection of
|
|
hanzoai/cloud's own emission now, which carries ONE spelling per tag, so the
|
|
class names lost their all-caps variants: `AIApi`/`APIKeysApi`/`MCPApi` are
|
|
`AiApi`/`KeysApi`/`McpApi`. The collision cannot recur while one document
|
|
with one spelling per tag is the only input; what this still pins is that
|
|
the top-level names resolve at all.
|
|
"""
|
|
import hanzoai.cloud
|
|
|
|
for name in ("AiApi", "KeysApi", "McpApi", "AdminApi"):
|
|
assert hasattr(hanzoai.cloud, name), f"hanzoai.cloud.{name} missing"
|
|
|
|
|
|
def test_admin_plugin_operator_surface():
|
|
"""The /v1/admin/plugins operator surface must be reachable from the SDK.
|
|
|
|
Four served routes: GET /v1/admin/plugins and POST
|
|
/v1/admin/plugins/{name}/{enable,disable,reload}. The `plugin_` prefix these
|
|
once carried came from the hand-merged master's `<svc>_` operationId
|
|
namespacing; cloud's emission names them `adminPlugins`,
|
|
`adminEnablePlugin`, `adminDisablePlugin`, `adminReloadPlugin`.
|
|
"""
|
|
from hanzoai.cloud.api.admin_api import AdminApi
|
|
|
|
for op in (
|
|
"admin_plugins",
|
|
"admin_enable_plugin",
|
|
"admin_disable_plugin",
|
|
"admin_reload_plugin",
|
|
):
|
|
assert callable(getattr(AdminApi, op, None)), f"AdminApi.{op} missing"
|