diff --git a/AGENTS.md b/AGENTS.md index 4a680b38..58a69508 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,7 +31,7 @@ Current permanently frozen files: - `src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider_v1.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider_v2.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider_v2language_hint.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_listen_provider_v1.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_listen_provider_v2.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_context_listen_provider_v1.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_context_listen_provider_v2.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_context_listen_provider_v2language_hint.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_listen_provider_v1.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_listen_provider_v2.py` — hand-written compatibility aliases for the 2026-05-14 spec dedup that consolidated `AgentV1SettingsAgent[Context]ListenProviderV{1,2,V2LanguageHint}` into top-level `DeepgramListenProvider*` types - `src/deepgram/types/deepgram_listen_provider_v2language_hint.py`, `src/deepgram/requests/deepgram_listen_provider_v2language_hint.py` — hand-written shims recreating the top-level `DeepgramListenProviderV2LanguageHint`/`...Params` type (`Union[str, List[str]]`) that Fern removed in the 2026-06-15 regen. The `*V2LanguageHint` listen-provider aliases above import from these and they remain part of the public import surface, so they are recreated by hand and frozen so Fern won't delete them again - `src/deepgram/listen/v2/types/listen_v2close_stream_type.py` — hand-written shim recreating `ListenV2CloseStreamType`, which Fern removed in the 2026-06-15 regen (docs #946). The original generated type wrongly allowed `Union[Literal["Finalize","CloseStream","KeepAlive"], Any]` (v2 copied v1's control-message enum); a CloseStream message's `type` can only ever be `"CloseStream"`. Recreated as the corrected `Literal["CloseStream"]` to preserve the public import path without resurrecting the invalid values. Re-exported from the three `listen` `__init__.py` files (temporarily frozen, below). -- `src/deepgram/transport_interface.py`, `src/deepgram/transport.py`, `src/deepgram/transports/` — custom transport layer +- `src/deepgram/transport_interface.py`, `src/deepgram/transport.py`, `src/deepgram/transports/` — custom transport layer. `_TARGET_MODULES` in `transport.py` must include every generated `client.py` and `raw_client.py` that uses the patchable WebSocket connector symbols; update it whenever a regen adds a WebSocket surface. `tests/custom/test_transport.py` enforces this so `transport_factory` cannot silently fall back to Deepgram Cloud. - `tests/custom/test_agent_history.py` — hand-written regression test for Agent History websocket payload parsing - `tests/custom/test_compat_aliases.py` — hand-written regression test for backward-compatible alias imports after regen renames - `tests/custom/test_query_encoder.py` — hand-written regression test that `core/query_encoder.py` coerces Python bools to lowercase `"true"`/`"false"` before `urlencode` so websocket query strings stay wire-correct diff --git a/src/deepgram/transport.py b/src/deepgram/transport.py index a7897725..2c81c76a 100644 --- a/src/deepgram/transport.py +++ b/src/deepgram/transport.py @@ -27,7 +27,7 @@ # --------------------------------------------------------------------------- # Module paths that contain the websocket references we need to patch. -# All 8 are auto-generated by Fern — we never modify their source. +# All 10 are auto-generated by Fern — we never modify their source. # --------------------------------------------------------------------------- _TARGET_MODULES = [ "deepgram.listen.v1.raw_client", @@ -36,6 +36,8 @@ "deepgram.listen.v2.client", "deepgram.speak.v1.raw_client", "deepgram.speak.v1.client", + "deepgram.speak.v2.raw_client", + "deepgram.speak.v2.client", "deepgram.agent.v1.raw_client", "deepgram.agent.v1.client", ] @@ -102,7 +104,7 @@ def install_transport( sync_factory: Optional[Callable] = None, async_factory: Optional[Callable] = None, ) -> None: - """Monkey-patch the 8 auto-generated modules to use custom transports. + """Monkey-patch the 10 auto-generated modules to use custom transports. Parameters ---------- diff --git a/tests/custom/test_transport.py b/tests/custom/test_transport.py index 84a262f7..931ed8fc 100644 --- a/tests/custom/test_transport.py +++ b/tests/custom/test_transport.py @@ -2,22 +2,23 @@ import json import sys -from typing import Any, Dict, Iterator, List +from pathlib import Path +from typing import Any, Dict, Iterator, List, Set from unittest.mock import MagicMock import pytest +import deepgram from deepgram.transport import ( + _TARGET_MODULES, AsyncTransport, SyncTransport, _AsyncTransportShim, _SyncTransportShim, - _TARGET_MODULES, install_transport, restore_transport, ) - # --------------------------------------------------------------------------- # Mock transport implementations # --------------------------------------------------------------------------- @@ -530,3 +531,50 @@ def test_async_transport_factory_auto_disables_reconnect(self): from deepgram.client import AsyncDeepgramClient client = AsyncDeepgramClient(api_key="test-key", transport_factory=factory) assert client.reconnect is False + + +# --------------------------------------------------------------------------- +# _TARGET_MODULES completeness +# --------------------------------------------------------------------------- + +_PATCHED_SYMBOLS = ("websockets_sync_client", "websockets_client_connect") + + +def _discover_websocket_modules() -> Set[str]: + """Return every `deepgram` module that references a patchable websocket symbol. + + Derived from the package source, deliberately not from `_TARGET_MODULES`. A + check that iterates `_TARGET_MODULES` can only confirm the list is internally + consistent; it cannot detect a websocket client missing from the list, + because the missing entry is never iterated. + """ + package_root = Path(deepgram.__file__).parent + discovered: Set[str] = set() + + for path in sorted(package_root.rglob("*.py")): + # transport.py names both symbols as its patch targets, so including it + # here would make the scan match itself. + if path.name == "transport.py": + continue + + source = path.read_text(encoding="utf-8") + if any(symbol in source for symbol in _PATCHED_SYMBOLS): + relative = path.relative_to(package_root).with_suffix("") + discovered.add(".".join(("deepgram",) + relative.parts)) + + return discovered + + +class TestTargetModuleCompleteness: + def test_every_websocket_module_is_registered_for_patching(self): + missing = sorted(_discover_websocket_modules() - set(_TARGET_MODULES)) + + assert not missing, ( + "websocket client module(s) absent from _TARGET_MODULES, so a custom " + "transport_factory is silently not applied to them: " + ", ".join(missing) + ) + + def test_discovery_locates_a_known_websocket_module(self): + # Guards the guard: if the scan silently found nothing, the completeness + # check above would pass vacuously. + assert "deepgram.listen.v1.raw_client" in _discover_websocket_modules()