diff --git a/src/tether/__init__.py b/src/tether/__init__.py index d278e95..484e305 100644 --- a/src/tether/__init__.py +++ b/src/tether/__init__.py @@ -29,8 +29,31 @@ "SUPPORTED_MODEL_TYPES", "UNSUPPORTED_MODEL_MESSAGE", "load_fixtures", + # Customer SDK surface — re-exported from tether.client so a bare + # `pip install fastcrest-tether` gives `from tether import TetherClient`. + # httpx is a base dep, so this is cheap (no torch); lazy-loaded below. + "TetherClient", + "TetherAsyncClient", + "TetherClientError", + "TetherAuthError", + "TetherServerDegradedError", + "TetherServerNotReadyError", + "TetherValidationError", + "encode_image", ] +# Names re-exported from tether.client (lazy — see __getattr__). +_CLIENT_EXPORTS = frozenset({ + "TetherClient", + "TetherAsyncClient", + "TetherClientError", + "TetherAuthError", + "TetherServerDegradedError", + "TetherServerNotReadyError", + "TetherValidationError", + "encode_image", +}) + # ─── ORT-TRT EP first-class support (v0.7) ────────────────────────────────── # ORT-TRT EP needs libnvinfer.so.10 (from the `tensorrt` pip pkg) + CUDA libs @@ -125,7 +148,6 @@ def _eager_dlopen_nvidia_libs() -> None: cached handle. No-op on macOS/Windows or when libs don't exist. """ import ctypes - import glob import os import sys @@ -193,4 +215,7 @@ def __getattr__(name: str): if name == "load_fixtures": from tether.fixtures import load_fixtures return load_fixtures + if name in _CLIENT_EXPORTS: + import tether.client as _client + return getattr(_client, name) raise AttributeError(f"module 'tether' has no attribute {name!r}") diff --git a/src/tether/client/__init__.py b/src/tether/client/__init__.py index 8f860c6..54f4c21 100644 --- a/src/tether/client/__init__.py +++ b/src/tether/client/__init__.py @@ -42,6 +42,41 @@ encode_image, ) +class ReflexClient(TetherClient): + """Deprecated alias for :class:`TetherClient`. Removed in v0.14.0. + + Kept so pre-rename code (`from tether.client import ReflexClient`) keeps + working through the v0.13.x compat window, matching the `reflex` import + shim's removal schedule. + """ + + def __init__(self, *args, **kwargs): + import warnings + + warnings.warn( + "ReflexClient is deprecated; use TetherClient. " + "The alias is removed in tether v0.14.0.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + +class ReflexAsyncClient(TetherAsyncClient): + """Deprecated alias for :class:`TetherAsyncClient`. Removed in v0.14.0.""" + + def __init__(self, *args, **kwargs): + import warnings + + warnings.warn( + "ReflexAsyncClient is deprecated; use TetherAsyncClient. " + "The alias is removed in tether v0.14.0.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(*args, **kwargs) + + __all__ = [ "TetherClient", "TetherAsyncClient", @@ -51,4 +86,7 @@ "TetherServerNotReadyError", "TetherValidationError", "encode_image", + # Deprecated rename-compat aliases (removed v0.14.0). + "ReflexClient", + "ReflexAsyncClient", ] diff --git a/src/tether/client/client.py b/src/tether/client/client.py index 4f95d23..4ff4821 100644 --- a/src/tether/client/client.py +++ b/src/tether/client/client.py @@ -99,6 +99,18 @@ def encode_image(image: Any, jpeg_quality: int = 85) -> str: if len(image) >= 8 and image[:8] == b"\x89PNG\r\n\x1a\n": return base64.b64encode(image).decode("ascii") return base64.b64encode(image).decode("ascii") + # The only remaining valid inputs are numpy.ndarray and PIL.Image, both of + # which need Pillow. Reject obviously-unsupported types (dict, int, list…) + # BEFORE requiring Pillow, so the error names the real problem rather than + # blaming a missing optional dep on a caller who passed garbage. + _cls = type(image) + _image_like = ( + _cls.__module__.split(".")[0] in ("PIL", "numpy") + or hasattr(image, "__array_interface__") + or (hasattr(image, "save") and hasattr(image, "size")) + ) + if not _image_like: + raise TetherClientError(f"unsupported image type: {_cls.__name__}") try: from PIL import Image as PILImage except ImportError: @@ -211,7 +223,6 @@ def close(self) -> None: # ---- Internals ------------------------------------------------------- def _request_with_retry(self, method: str, path: str, **kw) -> httpx.Response: - last_exc: TetherClientError | None = None attempt = 0 backoff = self.initial_backoff_s while True: @@ -244,7 +255,6 @@ def _request_with_retry(self, method: str, path: str, **kw) -> httpx.Response: ) time.sleep(wait) attempt += 1 - last_exc = err # ---- Public surface --------------------------------------------------