diff --git a/src/speechify/client.py b/src/speechify/client.py index 4c47cb4..65bd7c5 100644 --- a/src/speechify/client.py +++ b/src/speechify/client.py @@ -71,7 +71,7 @@ def __init__( base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, version: typing.Optional[str] = "2026-09-13", - token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, max_retries: typing.Optional[int] = None, @@ -83,6 +83,8 @@ def __init__( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) _defaulted_max_retries = max_retries if max_retries is not None else 2 + if token is None: + token = os.getenv("SPEECHIFY_API_KEY") if token is None: raise ApiError( body="The client must be instantiated be either passing in token or setting SPEECHIFY_API_KEY" @@ -205,7 +207,7 @@ def __init__( base_url: typing.Optional[str] = None, environment: SpeechifyEnvironment = SpeechifyEnvironment.DEFAULT, version: typing.Optional[str] = "2026-09-13", - token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("SPEECHIFY_API_KEY"), + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, headers: typing.Optional[typing.Dict[str, str]] = None, async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, timeout: typing.Optional[float] = None, @@ -218,6 +220,8 @@ def __init__( timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read ) _defaulted_max_retries = max_retries if max_retries is not None else 2 + if token is None: + token = os.getenv("SPEECHIFY_API_KEY") if token is None: raise ApiError( body="The client must be instantiated be either passing in token or setting SPEECHIFY_API_KEY" diff --git a/tests/custom/test_environment_token.py b/tests/custom/test_environment_token.py new file mode 100644 index 0000000..bd5e37b --- /dev/null +++ b/tests/custom/test_environment_token.py @@ -0,0 +1,66 @@ +import importlib +from typing import Any + +import httpx +import pytest + +import speechify.client as client_module +from speechify.core.api_error import ApiError + + +@pytest.fixture +def imported_without_key(monkeypatch): + monkeypatch.delenv("SPEECHIFY_API_KEY", raising=False) + importlib.reload(client_module) + yield client_module + # Avoid leaving reloaded import-time defaults behind for other test files. + monkeypatch.undo() + importlib.reload(client_module) + + +@pytest.mark.parametrize("mode", ["sync", "async"]) +@pytest.mark.parametrize("scenario", ["late", "rotated", "removed", "explicit", "callable", "missing"]) +async def test_token_is_resolved_when_each_client_is_created(imported_without_key, monkeypatch, mode, scenario): + module = imported_without_key + if scenario in {"rotated", "removed"}: + monkeypatch.setenv("SPEECHIFY_API_KEY", "old-key") + importlib.reload(module) + if scenario in {"removed", "missing"}: + monkeypatch.delenv("SPEECHIFY_API_KEY", raising=False) + else: + monkeypatch.setenv("SPEECHIFY_API_KEY", "current-key") + + seen = [] + + def respond(request): + seen.append(request.headers["Authorization"]) + return httpx.Response(200, json={"models": [], "dialogue_models": []}) + + kwargs: dict[str, Any] = {} + if scenario == "explicit": + kwargs["token"] = "explicit-key" + elif scenario == "callable": + kwargs["token"] = lambda: "callable-key" + + transport = httpx.MockTransport(respond) + if mode == "sync": + with httpx.Client(transport=transport) as http_client: + if scenario in {"removed", "missing"}: + with pytest.raises(ApiError): + module.Speechify(httpx_client=http_client, **kwargs) + assert not seen + return + client = module.Speechify(httpx_client=http_client, **kwargs) + client.models.list() + else: + async with httpx.AsyncClient(transport=transport) as http_client: + if scenario in {"removed", "missing"}: + with pytest.raises(ApiError): + module.AsyncSpeechify(httpx_client=http_client, **kwargs) + assert not seen + return + client = module.AsyncSpeechify(httpx_client=http_client, **kwargs) + await client.models.list() + + expected = {"explicit": "explicit-key", "callable": "callable-key"}.get(scenario, "current-key") + assert seen == [f"Bearer {expected}"]