Skip to content

Commit 62599d9

Browse files
Merge pull request #2823 from VWS-Python/adamtheturtle/custom-timeouts
Add custom request timeout support for VWS and CloudRecoService
2 parents f39f067 + a69677c commit 62599d9

File tree

5 files changed

+145
-7
lines changed

5 files changed

+145
-7
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ Changelog
44
Next
55
----
66

7+
* Add ``request_timeout_seconds`` parameter to ``VWS`` and ``CloudRecoService``, allowing customization of the request timeout. This accepts a float or a ``(connect, read)`` tuple, matching the ``requests`` library's timeout interface. The default remains 30 seconds.
8+
79
2025.03.10.1
810
------------
911

src/vws/query.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from urllib.parse import urljoin
99

1010
import requests
11-
from beartype import beartype
11+
from beartype import BeartypeConf, beartype
1212
from urllib3.filepost import encode_multipart_formdata
1313
from vws_auth_tools import authorization_header, rfc_1123_date
1414

@@ -40,7 +40,7 @@ def _get_image_data(image: _ImageType) -> bytes:
4040
return image_data
4141

4242

43-
@beartype
43+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
4444
class CloudRecoService:
4545
"""An interface to the Vuforia Cloud Recognition Web APIs."""
4646

@@ -49,16 +49,22 @@ def __init__(
4949
client_access_key: str,
5050
client_secret_key: str,
5151
base_vwq_url: str = "https://cloudreco.vuforia.com",
52+
request_timeout_seconds: float | tuple[float, float] = 30.0,
5253
) -> None:
5354
"""
5455
Args:
5556
client_access_key: A VWS client access key.
5657
client_secret_key: A VWS client secret key.
5758
base_vwq_url: The base URL for the VWQ API.
59+
request_timeout_seconds: The timeout for each HTTP request, as
60+
used by ``requests.request``. This can be a float to set
61+
both the connect and read timeouts, or a (connect, read)
62+
tuple.
5863
"""
5964
self._client_access_key = client_access_key
6065
self._client_secret_key = client_secret_key
6166
self._base_vwq_url = base_vwq_url
67+
self._request_timeout_seconds = request_timeout_seconds
6268

6369
def query(
6470
self,
@@ -141,8 +147,7 @@ def query(
141147
url=urljoin(base=self._base_vwq_url, url=request_path),
142148
headers=headers,
143149
data=content,
144-
# We should make the timeout customizable.
145-
timeout=30,
150+
timeout=self._request_timeout_seconds,
146151
)
147152
response = Response(
148153
text=requests_response.text,

src/vws/vws.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def _get_image_data(image: _ImageType) -> bytes:
5858
return image_data
5959

6060

61-
@beartype
61+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
6262
def _target_api_request(
6363
*,
6464
content_type: str,
@@ -68,6 +68,7 @@ def _target_api_request(
6868
data: bytes,
6969
request_path: str,
7070
base_vws_url: str,
71+
request_timeout_seconds: float | tuple[float, float],
7172
) -> Response:
7273
"""Make a request to the Vuforia Target API.
7374
@@ -82,6 +83,9 @@ def _target_api_request(
8283
request_path: The path to the endpoint which will be used in the
8384
request.
8485
base_vws_url: The base URL for the VWS API.
86+
request_timeout_seconds: The timeout for the request, as used by
87+
``requests.request``. This can be a float to set both the
88+
connect and read timeouts, or a (connect, read) tuple.
8589
8690
Returns:
8791
The response to the request made by `requests`.
@@ -111,8 +115,7 @@ def _target_api_request(
111115
url=url,
112116
headers=headers,
113117
data=data,
114-
# We should make the timeout customizable.
115-
timeout=30,
118+
timeout=request_timeout_seconds,
116119
)
117120

118121
return Response(
@@ -134,16 +137,22 @@ def __init__(
134137
server_access_key: str,
135138
server_secret_key: str,
136139
base_vws_url: str = "https://vws.vuforia.com",
140+
request_timeout_seconds: float | tuple[float, float] = 30.0,
137141
) -> None:
138142
"""
139143
Args:
140144
server_access_key: A VWS server access key.
141145
server_secret_key: A VWS server secret key.
142146
base_vws_url: The base URL for the VWS API.
147+
request_timeout_seconds: The timeout for each HTTP request, as
148+
used by ``requests.request``. This can be a float to set
149+
both the connect and read timeouts, or a (connect, read)
150+
tuple.
143151
"""
144152
self._server_access_key = server_access_key
145153
self._server_secret_key = server_secret_key
146154
self._base_vws_url = base_vws_url
155+
self._request_timeout_seconds = request_timeout_seconds
147156

148157
def make_request(
149158
self,
@@ -187,6 +196,7 @@ def make_request(
187196
data=data,
188197
request_path=request_path,
189198
base_vws_url=self._base_vws_url,
199+
request_timeout_seconds=self._request_timeout_seconds,
190200
)
191201

192202
if (

tests/test_query.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,61 @@ def test_default_timeout(
9292
assert not matches
9393

9494

95+
class TestCustomRequestTimeout:
96+
"""Tests for custom request timeout values."""
97+
98+
@staticmethod
99+
@pytest.mark.parametrize(
100+
argnames=(
101+
"custom_timeout",
102+
"response_delay_seconds",
103+
"expect_timeout",
104+
),
105+
argvalues=[
106+
(0.1, 0.09, False),
107+
(0.1, 0.11, True),
108+
((5.0, 0.1), 0.09, False),
109+
((5.0, 0.1), 0.11, True),
110+
],
111+
)
112+
def test_custom_timeout(
113+
image: io.BytesIO | BinaryIO,
114+
*,
115+
custom_timeout: float | tuple[float, float],
116+
response_delay_seconds: float,
117+
expect_timeout: bool,
118+
) -> None:
119+
"""Custom timeouts are honored for both float and tuple forms."""
120+
with (
121+
freeze_time() as frozen_datetime,
122+
MockVWS(
123+
response_delay_seconds=response_delay_seconds,
124+
sleep_fn=lambda seconds: (
125+
frozen_datetime.tick(
126+
delta=datetime.timedelta(seconds=seconds),
127+
),
128+
None,
129+
)[1],
130+
) as mock,
131+
):
132+
database = VuforiaDatabase()
133+
mock.add_database(database=database)
134+
cloud_reco_client = CloudRecoService(
135+
client_access_key=database.client_access_key,
136+
client_secret_key=database.client_secret_key,
137+
request_timeout_seconds=custom_timeout,
138+
)
139+
140+
if expect_timeout:
141+
with pytest.raises(
142+
expected_exception=requests.exceptions.Timeout,
143+
):
144+
cloud_reco_client.query(image=image)
145+
else:
146+
matches = cloud_reco_client.query(image=image)
147+
assert not matches
148+
149+
95150
class TestCustomBaseVWQURL:
96151
"""Tests for using a custom base VWQ URL."""
97152

tests/test_vws.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,72 @@ def test_default_timeout(
150150
)
151151

152152

153+
class TestCustomRequestTimeout:
154+
"""Tests for custom request timeout values."""
155+
156+
@staticmethod
157+
@pytest.mark.parametrize(
158+
argnames=(
159+
"custom_timeout",
160+
"response_delay_seconds",
161+
"expect_timeout",
162+
),
163+
argvalues=[
164+
(0.1, 0.09, False),
165+
(0.1, 0.11, True),
166+
((5.0, 0.1), 0.09, False),
167+
((5.0, 0.1), 0.11, True),
168+
],
169+
)
170+
def test_custom_timeout(
171+
image: io.BytesIO | BinaryIO,
172+
*,
173+
custom_timeout: float | tuple[float, float],
174+
response_delay_seconds: float,
175+
expect_timeout: bool,
176+
) -> None:
177+
"""Custom timeouts are honored for both float and tuple forms."""
178+
with (
179+
freeze_time() as frozen_datetime,
180+
MockVWS(
181+
response_delay_seconds=response_delay_seconds,
182+
sleep_fn=lambda seconds: (
183+
frozen_datetime.tick(
184+
delta=datetime.timedelta(seconds=seconds),
185+
),
186+
None,
187+
)[1],
188+
) as mock,
189+
):
190+
database = VuforiaDatabase()
191+
mock.add_database(database=database)
192+
vws_client = VWS(
193+
server_access_key=database.server_access_key,
194+
server_secret_key=database.server_secret_key,
195+
request_timeout_seconds=custom_timeout,
196+
)
197+
198+
if expect_timeout:
199+
with pytest.raises(
200+
expected_exception=requests.exceptions.Timeout,
201+
):
202+
vws_client.add_target(
203+
name="x",
204+
width=1,
205+
image=image,
206+
active_flag=True,
207+
application_metadata=None,
208+
)
209+
else:
210+
vws_client.add_target(
211+
name="x",
212+
width=1,
213+
image=image,
214+
active_flag=True,
215+
application_metadata=None,
216+
)
217+
218+
153219
class TestCustomBaseVWSURL:
154220
"""Tests for using a custom base VWS URL."""
155221

0 commit comments

Comments
 (0)