Skip to content

Commit 2f2ebd5

Browse files
authored
Enable configuring HTTP transport retry behavior and add request_timeout parameter to get_detector (#386)
This PR includes two distinct changes. - Adds a new parameter `http_transport_retries` to the `Groundlight` constructor, enabling the retry behavior to be modified when initializing a `Groundlight` instance. - The retry behavior specified will apply to all API requests made through the `Groundlight` instance. - Currently no way is provided to configure the retry behavior per-request, or to change it once the `Groundlight` instance has been initialized (this is technically possible but awkward). - I added a test to verify that we can pass the parameter in and it gets passed through to the inner API client. - Adds a `request_timeout` parameter to the `get_detector` method which enables changing the request timeout values for the request. - An identical parameter was previously added for `submit_image_query`. Eventually I think we'll want to make this available for all methods, but for now we don't need that. - I added a test to verify that this parameter works properly. These changes are intended to be utilized by the edge endpoint to improve behavior under poor network connectivity.
1 parent af36416 commit 2f2ebd5

File tree

3 files changed

+64
-16
lines changed

3 files changed

+64
-16
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ python = ">=3.9,<4.0"
2121
python-dateutil = "^2.9.0"
2222
requests = "^2.28.2"
2323
typer = "^0.15.4"
24-
urllib3 = "^1.26.9"
24+
urllib3 = "^2.5.0"
2525

2626
[tool.poetry.group.dev.dependencies]
2727
datamodel-code-generator = "^0.22.1"

src/groundlight/client.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
PaginatedImageQueryList,
3838
)
3939
from urllib3.exceptions import InsecureRequestWarning
40+
from urllib3.util.retry import Retry
4041

4142
from groundlight.binary_labels import Label, convert_internal_label_to_display
4243
from groundlight.config import API_TOKEN_MISSING_HELP_MESSAGE, API_TOKEN_VARIABLE_NAME, DISABLE_TLS_VARIABLE_NAME
@@ -133,26 +134,31 @@ def __init__(
133134
endpoint: Optional[str] = None,
134135
api_token: Optional[str] = None,
135136
disable_tls_verification: Optional[bool] = None,
137+
http_transport_retries: Optional[Union[int, Retry]] = None,
136138
):
137139
"""
138140
Initialize a new Groundlight client instance.
139141
140142
:param endpoint: Optional custom API endpoint URL. If not specified, uses the default Groundlight endpoint.
141143
:param api_token: Authentication token for API access.
142-
If not provided, will attempt to read from the "GROUNDLIGHT_API_TOKEN" environment variable.
144+
If not provided, will attempt to read from the "GROUNDLIGHT_API_TOKEN" environment variable.
143145
:param disable_tls_verification: If True, disables SSL/TLS certificate verification for API calls.
144-
When not specified, checks the "DISABLE_TLS_VERIFY" environment variable
145-
(1=disable, 0=enable). Certificate verification is enabled by default.
146+
When not specified, checks the "DISABLE_TLS_VERIFY" environment variable (1=disable, 0=enable).
147+
Certificate verification is enabled by default.
146148
147-
Warning: Only disable verification when connecting to a Groundlight Edge
148-
Endpoint using self-signed certificates. For security, always keep
149-
verification enabled when using the Groundlight cloud service.
149+
Warning: Only disable verification when connecting to a Groundlight Edge Endpoint using self-signed
150+
certificates. For security, always keep verification enabled when using the Groundlight cloud service.
151+
:param http_transport_retries: Overrides urllib3 `PoolManager` retry policy for HTTP/HTTPS (forwarded to
152+
`Configuration.retries`). Not the same as SDK 5xx retries handled by `RequestsRetryDecorator`.
150153
151154
:return: Groundlight client
152155
"""
153156
# Specify the endpoint
154157
self.endpoint = sanitize_endpoint_url(endpoint)
155158
self.configuration = Configuration(host=self.endpoint)
159+
if http_transport_retries is not None:
160+
# Once we upgrade openapitools to ^7.7.0, retries can be passed into the constructor of Configuration above.
161+
self.configuration.retries = http_transport_retries
156162

157163
if not api_token:
158164
try:
@@ -264,7 +270,11 @@ def _user_is_privileged(self) -> bool:
264270
obj = self.user_api.who_am_i()
265271
return obj["is_superuser"]
266272

267-
def get_detector(self, id: Union[str, Detector]) -> Detector: # pylint: disable=redefined-builtin
273+
def get_detector(
274+
self,
275+
id: Union[str, Detector], # pylint: disable=redefined-builtin
276+
request_timeout: Optional[Union[float, Tuple[float, float]]] = None,
277+
) -> Detector:
268278
"""
269279
Get a Detector by id.
270280
@@ -275,6 +285,8 @@ def get_detector(self, id: Union[str, Detector]) -> Detector: # pylint: disable
275285
print(detector)
276286
277287
:param id: the detector id
288+
:param request_timeout: The request timeout for the image query submission API request. Most users will not need
289+
to modify this. If not set, the default value will be used.
278290
279291
:return: Detector
280292
"""
@@ -283,7 +295,8 @@ def get_detector(self, id: Union[str, Detector]) -> Detector: # pylint: disable
283295
# Short-circuit
284296
return id
285297
try:
286-
obj = self.detectors_api.get_detector(id=id, _request_timeout=DEFAULT_REQUEST_TIMEOUT)
298+
request_timeout = request_timeout if request_timeout is not None else DEFAULT_REQUEST_TIMEOUT
299+
obj = self.detectors_api.get_detector(id=id, _request_timeout=request_timeout)
287300
except NotFoundException as e:
288301
raise NotFoundError(f"Detector with id '{id}' not found") from e
289302
return Detector.parse_obj(obj.to_dict())
@@ -662,7 +675,7 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t
662675
inspection_id: Optional[str] = None,
663676
metadata: Union[dict, str, None] = None,
664677
image_query_id: Optional[str] = None,
665-
request_timeout: Optional[float] = None,
678+
request_timeout: Optional[Union[float, Tuple[float, float]]] = None,
666679
) -> ImageQuery:
667680
"""
668681
Evaluates an image with Groundlight. This is the core method for getting predictions about images.
@@ -744,8 +757,8 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t
744757
:param image_query_id: The ID for the image query. This is to enable specific functionality
745758
and is not intended for general external use. If not set, a random ID
746759
will be generated.
747-
:param request_timeout: The total request timeout for the image query submission API request. Most users will
748-
not need to modify this. If not set, the default value will be used.
760+
:param request_timeout: The request timeout for the image query submission API request. Most users will not need
761+
to modify this. If not set, the default value will be used.
749762
750763
:return: ImageQuery with query details and result (if wait > 0)
751764
:raises ValueError: If wait > 0 when want_async=True

test/integration/test_groundlight.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
PaginatedDetectorList,
2828
PaginatedImageQueryList,
2929
)
30-
from urllib3.exceptions import ReadTimeoutError
30+
from urllib3.exceptions import ConnectTimeoutError, MaxRetryError, ReadTimeoutError
31+
from urllib3.util.retry import Retry
3132

3233
DEFAULT_CONFIDENCE_THRESHOLD = 0.9
3334
IQ_IMPROVEMENT_THRESHOLD = 0.75
@@ -80,6 +81,21 @@ def fixture_image() -> str:
8081
return "test/assets/dog.jpeg"
8182

8283

84+
def test_create_groundlight_with_retries():
85+
"""Verify that the `retries` parameter can be successfully passed to the `Groundlight` constructor."""
86+
# Set retries using int value
87+
num_retries = 25
88+
gl = Groundlight(http_transport_retries=num_retries)
89+
assert gl.configuration.retries == num_retries
90+
assert gl.api_client.configuration.retries == num_retries
91+
92+
# Set retries using Retry object
93+
retries = Retry(total=num_retries)
94+
gl = Groundlight(http_transport_retries=retries)
95+
assert gl.configuration.retries.total == retries.total
96+
assert gl.api_client.configuration.retries.total == retries.total
97+
98+
8399
def test_create_detector(gl: Groundlight):
84100
name = f"Test {datetime.utcnow()}" # Need a unique name
85101
query = "Is there a dog?"
@@ -222,6 +238,21 @@ def test_get_detector(gl: Groundlight, detector: Detector):
222238
assert isinstance(_detector, Detector)
223239

224240

241+
def test_get_detector_with_low_request_timeout(gl: Groundlight, detector: Detector):
242+
"""
243+
Verifies that get_detector respects the request_timeout parameter and raises a MaxRetryError when timeout is
244+
low. Verifies that request_timeout parameter can be a float or a tuple.
245+
"""
246+
with pytest.raises(MaxRetryError):
247+
# Setting a very low request_timeout value should result in a timeout.
248+
# NOTE: request_timeout=0 seems to have special behavior that does not result in a timeout.
249+
gl.get_detector(id=detector.id, request_timeout=1e-8)
250+
251+
with pytest.raises(MaxRetryError):
252+
# Ensure a tuple can be passed.
253+
gl.get_detector(id=detector.id, request_timeout=(1e-8, 1e-8))
254+
255+
225256
def test_get_detector_by_name(gl: Groundlight, detector: Detector):
226257
_detector = gl.get_detector_by_name(name=detector.name)
227258
assert str(_detector)
@@ -352,14 +383,18 @@ def test_submit_image_query_with_human_review_param(gl: Groundlight, detector: D
352383

353384
def test_submit_image_query_with_low_request_timeout(gl: Groundlight, detector: Detector, image: str):
354385
"""
355-
Test that submit_image_query respects the request_timeout parameter and raises a ReadTimeoutError when timeout is
356-
exceeded.
386+
Verifies that submit_image_query respects the request_timeout parameter and raises a ConnectTimeoutError or
387+
ReadTimeoutError when timeout is low. Verifies that request_timeout parameter can be a float or a tuple.
357388
"""
358-
with pytest.raises(ReadTimeoutError):
389+
with pytest.raises((ConnectTimeoutError, ReadTimeoutError)):
359390
# Setting a very low request_timeout value should result in a timeout.
360391
# NOTE: request_timeout=0 seems to have special behavior that does not result in a timeout.
361392
gl.submit_image_query(detector=detector, image=image, human_review="NEVER", request_timeout=1e-8)
362393

394+
with pytest.raises((ConnectTimeoutError, ReadTimeoutError)):
395+
# Ensure a tuple can be passed.
396+
gl.submit_image_query(detector=detector, image=image, human_review="NEVER", request_timeout=(5, 1e-8))
397+
363398

364399
@pytest.mark.skip_for_edge_endpoint(reason="The edge-endpoint does not support passing detector metadata.")
365400
def test_create_detector_with_metadata(gl: Groundlight):

0 commit comments

Comments
 (0)