Skip to content

Commit 8825c42

Browse files
authored
Voice SDK url parameter handling (#76)
## What's Changed? - better handling for `sm-app` and other URL parameters provided by the client. - ensure that URL parameters are parsed correctly.
1 parent 81f093f commit 8825c42

File tree

2 files changed

+107
-6
lines changed

2 files changed

+107
-6
lines changed

sdk/voice/speechmatics/voice/_client.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
from typing import Callable
1515
from typing import Optional
1616
from typing import Union
17+
from urllib.parse import parse_qs
1718
from urllib.parse import urlencode
19+
from urllib.parse import urlparse
20+
from urllib.parse import urlunparse
1821

1922
from speechmatics.rt import AsyncClient
2023
from speechmatics.rt import AudioEncoding
@@ -1914,12 +1917,21 @@ def _get_endpoint_url(self, url: str, app: Optional[str] = None) -> str:
19141917
app: The application name to use in the endpoint URL.
19151918
19161919
Returns:
1917-
str: The formatted endpoint URL.
1920+
str: The formatted endpoint URL.
19181921
"""
19191922

1920-
query_params = {}
1921-
query_params["sm-app"] = app or f"voice-sdk/{__version__}"
1922-
query_params["sm-voice-sdk"] = f"{__version__}"
1923-
query = urlencode(query_params)
1923+
# Parse the URL to extract existing query parameters
1924+
parsed = urlparse(url)
19241925

1925-
return f"{url}?{query}"
1926+
# Extract existing params into a dict of lists, keeping params without values
1927+
params = parse_qs(parsed.query, keep_blank_values=True)
1928+
1929+
# Use the provided app name, or fallback to existing value, or use the default string
1930+
existing_app = params.get("sm-app", [None])[0]
1931+
app_name = app or existing_app or f"voice-sdk/{__version__}"
1932+
params["sm-app"] = [app_name]
1933+
params["sm-voice-sdk"] = [__version__]
1934+
1935+
# Re-encode the query string and reconstruct
1936+
updated_query = urlencode(params, doseq=True)
1937+
return urlunparse(parsed._replace(query=updated_query))

tests/voice/test_16_url.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
from urllib.parse import parse_qs
4+
from urllib.parse import urlparse
5+
6+
import pytest
7+
from _utils import get_client
8+
9+
from speechmatics.voice import __version__
10+
11+
12+
@dataclass
13+
class URLExample:
14+
input_url: str
15+
input_app: Optional[str] = None
16+
17+
18+
URLS: list[URLExample] = [
19+
URLExample(
20+
input_url="wss://dummy/ep",
21+
input_app="dummy-0.1.2",
22+
),
23+
URLExample(
24+
input_url="wss://dummy:1234/ep?client=amz",
25+
input_app="dummy-0.1.2",
26+
),
27+
URLExample(
28+
input_url="wss://dummy/ep?sm-app=dummy",
29+
),
30+
URLExample(
31+
input_url="ws://localhost:8080/ep?sm-app=dummy",
32+
input_app="dummy-0.1.2",
33+
),
34+
URLExample(
35+
input_url="http://dummy/ep/v1/",
36+
input_app="dummy-0.1.2",
37+
),
38+
URLExample(
39+
input_url="wss://dummy/ep",
40+
),
41+
URLExample(
42+
input_url="wss://dummy/ep",
43+
input_app="client/a#b:c^d",
44+
),
45+
]
46+
47+
48+
@pytest.mark.asyncio
49+
@pytest.mark.parametrize("test", URLS, ids=lambda s: s.input_url)
50+
async def test_url_endpoints(test: URLExample):
51+
"""Test URL endpoint construction."""
52+
53+
# Client
54+
client = await get_client(
55+
api_key="DUMMY",
56+
connect=False,
57+
)
58+
59+
# Parse the input parameters
60+
input_parsed = urlparse(test.input_url)
61+
input_params = parse_qs(input_parsed.query, keep_blank_values=True)
62+
63+
# URL test
64+
generated_url = client._get_endpoint_url(test.input_url, test.input_app)
65+
66+
# Parse the URL
67+
parsed_url = urlparse(generated_url)
68+
parsed_params = parse_qs(parsed_url.query, keep_blank_values=True)
69+
70+
# Check the url scheme, netloc and path are preserved
71+
assert parsed_url.scheme == input_parsed.scheme
72+
assert parsed_url.netloc == input_parsed.netloc
73+
assert parsed_url.path == input_parsed.path
74+
75+
# Validate `sm-app`
76+
if test.input_app:
77+
assert parsed_params["sm-app"] == [test.input_app]
78+
elif "sm-app" in input_params:
79+
assert parsed_params["sm-app"] == [input_params["sm-app"][0]]
80+
else:
81+
assert parsed_params["sm-app"] == [f"voice-sdk/{__version__}"]
82+
83+
# Validate `sm-voice-sdk`
84+
assert parsed_params["sm-voice-sdk"] == [__version__]
85+
86+
# Check other original params are preserved
87+
for key, value in input_params.items():
88+
if key not in ["sm-app", "sm-voice-sdk"]:
89+
assert parsed_params[key] == value

0 commit comments

Comments
 (0)