Skip to content

Commit e9231c8

Browse files
committed
first pass
1 parent bda1afb commit e9231c8

File tree

8 files changed

+93
-32
lines changed

8 files changed

+93
-32
lines changed

test/integration/test_groundlight.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
PaginatedDetectorList,
2727
PaginatedImageQueryList,
2828
)
29+
from test.retry_decorator import retry_on_failure
2930
from urllib3.exceptions import ConnectTimeoutError, MaxRetryError, ReadTimeoutError
3031
from urllib3.util.retry import Retry
3132

@@ -273,20 +274,23 @@ def test_get_detector_by_name(gl: Groundlight, detector: Detector):
273274
gl.get_detector_by_name(name="not a real name")
274275

275276

277+
@retry_on_failure()
276278
def test_ask_confident(gl: Groundlight, detector: Detector):
277279
_image_query = gl.ask_confident(detector=detector.id, image="test/assets/dog.jpeg", wait=10)
278280
assert str(_image_query)
279281
assert isinstance(_image_query, ImageQuery)
280282
assert is_valid_display_result(_image_query.result)
281283

282284

285+
@retry_on_failure()
283286
def test_ask_ml(gl: Groundlight, detector: Detector):
284287
_image_query = gl.ask_ml(detector=detector.id, image="test/assets/dog.jpeg", wait=10)
285288
assert str(_image_query)
286289
assert isinstance(_image_query, ImageQuery)
287290
assert is_valid_display_result(_image_query.result)
288291

289292

293+
@retry_on_failure()
290294
def test_submit_image_query(gl: Groundlight, detector: Detector):
291295
def validate_image_query(_image_query: ImageQuery):
292296
assert str(_image_query)
@@ -314,6 +318,7 @@ def validate_image_query(_image_query: ImageQuery):
314318
assert _image_query.result.confidence >= IQ_IMPROVEMENT_THRESHOLD
315319

316320

321+
@retry_on_failure()
317322
def test_submit_image_query_blocking(gl: Groundlight, detector: Detector):
318323
_image_query = gl.submit_image_query(
319324
detector=detector.id, image="test/assets/dog.jpeg", wait=10, human_review="NEVER"
@@ -323,13 +328,15 @@ def test_submit_image_query_blocking(gl: Groundlight, detector: Detector):
323328
assert is_valid_display_result(_image_query.result)
324329

325330

331+
@retry_on_failure()
326332
def test_submit_image_query_returns_yes(gl: Groundlight):
327333
# We use the "never-review" pipeline to guarantee a confident "yes" answer.
328334
detector = gl.get_or_create_detector(name="Always a dog", query="Is there a dog?", pipeline_config="never-review")
329335
image_query = gl.submit_image_query(detector=detector, image="test/assets/dog.jpeg", wait=10, human_review="NEVER")
330336
assert image_query.result.label == Label.YES
331337

332338

339+
@retry_on_failure()
333340
def test_submit_image_query_returns_text(gl: Groundlight):
334341
# We use the "never-review" pipeline to guarantee a confident "yes" answer.
335342
detector = gl.get_or_create_detector(
@@ -339,20 +346,23 @@ def test_submit_image_query_returns_text(gl: Groundlight):
339346
assert isinstance(image_query.text, str)
340347

341348

349+
@retry_on_failure()
342350
def test_submit_image_query_filename(gl: Groundlight, detector: Detector):
343351
_image_query = gl.submit_image_query(detector=detector.id, image="test/assets/dog.jpeg", human_review="NEVER")
344352
assert str(_image_query)
345353
assert isinstance(_image_query, ImageQuery)
346354
assert is_valid_display_result(_image_query.result)
347355

348356

357+
@retry_on_failure()
349358
def test_submit_image_query_png(gl: Groundlight, detector: Detector):
350359
_image_query = gl.submit_image_query(detector=detector.id, image="test/assets/cat.png", human_review="NEVER")
351360
assert str(_image_query)
352361
assert isinstance(_image_query, ImageQuery)
353362
assert is_valid_display_result(_image_query.result)
354363

355364

365+
@retry_on_failure()
356366
def test_submit_image_query_with_confidence_threshold(gl: Groundlight, detector: Detector):
357367
confidence_threshold = 0.5234 # Arbitrary specific value
358368
_image_query = gl.submit_image_query(
@@ -366,6 +376,7 @@ def test_submit_image_query_with_confidence_threshold(gl: Groundlight, detector:
366376

367377

368378
@pytest.mark.skip_for_edge_endpoint(reason="The edge-endpoint does not support passing an image query ID.")
379+
@retry_on_failure()
369380
def test_submit_image_query_with_id(gl: Groundlight, detector: Detector):
370381
# submit_image_query
371382
id = f"iq_{KsuidMs()}"
@@ -380,6 +391,7 @@ def test_submit_image_query_with_id(gl: Groundlight, detector: Detector):
380391
assert _image_query.metadata.get("is_from_edge")
381392

382393

394+
@retry_on_failure()
383395
def test_submit_image_query_with_human_review_param(gl: Groundlight, detector: Detector):
384396
# For now, this just tests that the image query is submitted successfully.
385397
# There should probably be a better way to check whether the image query was escalated for human review.
@@ -451,6 +463,7 @@ def test_create_detector_with_invalid_metadata(gl: Groundlight, metadata_list: A
451463

452464
@pytest.mark.skip_for_edge_endpoint(reason="The edge-endpoint does not support passing image query metadata.")
453465
@pytest.mark.parametrize("metadata", [None, {}, {"a": 1}, '{"a": 1}'])
466+
@retry_on_failure()
454467
def test_submit_image_query_with_metadata(
455468
gl: Groundlight, detector: Detector, image: str, metadata: Union[Dict, str, None]
456469
):
@@ -505,6 +518,7 @@ def test_submit_image_query_with_metadata_returns_user_error(gl: Groundlight, de
505518
assert is_user_error(exc_info.value.status)
506519

507520

521+
@retry_on_failure()
508522
def test_submit_image_query_jpeg_bytes(gl: Groundlight, detector: Detector):
509523
jpeg = open("test/assets/dog.jpeg", "rb").read()
510524
_image_query = gl.submit_image_query(detector=detector.id, image=jpeg, human_review="NEVER")
@@ -543,6 +557,7 @@ def test_submit_image_query_bad_jpeg_file(gl: Groundlight, detector: Detector):
543557

544558

545559
@pytest.mark.skipif(MISSING_PIL, reason="Needs pillow") # type: ignore
560+
@retry_on_failure()
546561
def test_submit_image_query_pil(gl: Groundlight, detector: Detector):
547562
# generates a pil image and submits it
548563
from PIL import Image
@@ -565,6 +580,7 @@ def test_submit_image_query_wait_and_want_async_causes_exception(gl: Groundlight
565580
)
566581

567582

583+
@retry_on_failure()
568584
def test_submit_image_query_with_want_async_workflow(gl: Groundlight, detector: Detector):
569585
"""
570586
Tests the workflow for submitting an image query with the want_async parameter set to True.
@@ -589,6 +605,7 @@ def test_submit_image_query_with_want_async_workflow(gl: Groundlight, detector:
589605
assert _image_query.result.label in VALID_DISPLAY_LABELS
590606

591607

608+
@retry_on_failure()
592609
def test_ask_async_workflow(gl: Groundlight, detector: Detector):
593610
"""
594611
Tests the workflow for submitting an image query with ask_async.
@@ -709,6 +726,7 @@ def test_enum_string_equality():
709726

710727

711728
@pytest.mark.skipif(MISSING_NUMPY or MISSING_PIL, reason="Needs numpy and pillow") # type: ignore
729+
@retry_on_failure()
712730
def test_submit_numpy_image(gl: Groundlight, detector: Detector):
713731
np_img = np.random.uniform(0, 255, (600, 800, 3)) # type: ignore
714732
_image_query = gl.submit_image_query(detector=detector.id, image=np_img, human_review="NEVER")
@@ -820,6 +838,7 @@ def test_update_detector_confidence_threshold_failure(gl: Groundlight, detector:
820838

821839

822840
@pytest.mark.skip_for_edge_endpoint(reason="The edge-endpoint does not support passing detector metadata.")
841+
@retry_on_failure()
823842
def test_submit_image_query_with_inspection_id_metadata_and_want_async(gl: Groundlight, detector: Detector, image: str):
824843
inspection_id = gl.start_inspection()
825844
metadata = {"key": "value"}
@@ -852,6 +871,7 @@ def test_submit_image_query_with_empty_inspection_id(gl: Groundlight, detector:
852871
)
853872

854873

874+
@retry_on_failure()
855875
def test_binary_detector(gl: Groundlight, detector_name: Callable):
856876
"""
857877
verify that we can create and submit to a binary detector
@@ -863,6 +883,7 @@ def test_binary_detector(gl: Groundlight, detector_name: Callable):
863883
assert binary_iq.result.label is not None
864884

865885

886+
@retry_on_failure()
866887
def test_counting_detector(gl: Groundlight, detector_name: Callable):
867888
"""
868889
verify that we can create and submit to a counting detector
@@ -874,6 +895,7 @@ def test_counting_detector(gl: Groundlight, detector_name: Callable):
874895
assert count_iq.result.count is not None
875896

876897

898+
@retry_on_failure()
877899
def test_counting_detector_async(gl: Groundlight, detector_name: Callable):
878900
"""
879901
verify that we can create and submit to a counting detector
@@ -893,6 +915,7 @@ def test_counting_detector_async(gl: Groundlight, detector_name: Callable):
893915
assert _image_query.result is not None
894916

895917

918+
@retry_on_failure()
896919
def test_multiclass_detector(gl: Groundlight, detector_name: Callable):
897920
"""
898921
verify that we can create and submit to a multi-class detector

test/retry_decorator.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Test-only helpers for retrying tests affected by transient cloud timing."""
2+
3+
import functools
4+
from typing import Any, Callable, Tuple, Type
5+
6+
7+
def retry_on_failure(
8+
*,
9+
max_attempts: int = 3,
10+
exception_types: Tuple[Type[BaseException], ...] = (Exception,),
11+
) -> Callable[[Callable[..., Any]], Callable[..., None]]:
12+
"""Run the wrapped test up to `max_attempts` times when it raises a listed exception."""
13+
14+
if max_attempts < 1:
15+
raise ValueError("max_attempts must be at least 1")
16+
17+
def decorator(fn: Callable[..., Any]) -> Callable[..., None]:
18+
@functools.wraps(fn)
19+
def wrapper(*args: object, **kwargs: object) -> None:
20+
for attempt in range(max_attempts):
21+
try:
22+
fn(*args, **kwargs)
23+
return
24+
except exception_types:
25+
if attempt == max_attempts - 1:
26+
raise
27+
28+
return wrapper
29+
30+
return decorator
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
from datetime import datetime
1+
from typing import Callable
22

33
from groundlight import ExperimentalApi
44

55
gl = ExperimentalApi()
66

77

8-
def test_invalid_endpoint_config():
8+
def test_invalid_endpoint_config(detector_name: Callable):
99
print(gl.make_generic_api_request(endpoint="/v1/me", method="GET"))
1010
print(gl.make_generic_api_request(endpoint="/v1/detectors", method="GET"))
11-
name = f"Test {datetime.utcnow()}"
12-
print(gl.make_generic_api_request(endpoint="/v1/detector-groups", method="POST", body={"name": name}))
11+
print(gl.make_generic_api_request(endpoint="/v1/detector-groups", method="POST", body={"name": detector_name()}))

test/unit/test_experimental.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55
from groundlight import ExperimentalApi
66
from model import Detector, ImageQuery
7+
from test.retry_decorator import retry_on_failure
78

89

910
def test_detector_groups(gl_experimental: ExperimentalApi, detector_name: Callable):
@@ -90,6 +91,7 @@ def test_submit_multiple_rois(gl_experimental: ExperimentalApi, image_query_one:
9091
gl_experimental.add_label(image_query_one, 3, [roi] * 3)
9192

9293

94+
@retry_on_failure()
9395
def test_text_recognition_detector(gl_experimental: ExperimentalApi, detector_name: Callable):
9496
"""
9597
verify that we can create and submit to a text recognition detector
@@ -103,6 +105,7 @@ def test_text_recognition_detector(gl_experimental: ExperimentalApi, detector_na
103105
assert mc_iq.result.text is not None
104106

105107

108+
@retry_on_failure()
106109
def test_bounding_box_detector(gl_experimental: ExperimentalApi, detector_name: Callable):
107110
"""
108111
Verify that we can create and submit to a bounding box detector
@@ -117,6 +120,7 @@ def test_bounding_box_detector(gl_experimental: ExperimentalApi, detector_name:
117120
assert bbox_iq.rois is not None
118121

119122

123+
@retry_on_failure()
120124
def test_bounding_box_detector_async(gl_experimental: ExperimentalApi, detector_name: Callable):
121125
"""
122126
Verify that we can create and submit to a bounding box detector with ask_async

test/unit/test_images.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from datetime import datetime
1+
from typing import Callable
22

33
import PIL
44
from groundlight import ExperimentalApi
5+
from test.retry_decorator import retry_on_failure
56

67

7-
def test_get_image(gl_experimental: ExperimentalApi):
8-
name = f"Test {datetime.utcnow()}"
9-
det = gl_experimental.get_or_create_detector(name, "test_query")
8+
@retry_on_failure()
9+
def test_get_image(gl_experimental: ExperimentalApi, detector_name: Callable):
10+
det = gl_experimental.get_or_create_detector(detector_name(), "test_query")
1011
iq = gl_experimental.submit_image_query(det, image="test/assets/dog.jpeg", wait=10)
1112
gl_experimental.get_image(iq.id)
1213
assert isinstance(PIL.Image.open(gl_experimental.get_image(iq.id)), PIL.Image.Image)

test/unit/test_internalapi.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
from typing import Callable
2+
13
from groundlight import ExperimentalApi
24
from groundlight.internalapi import iq_is_answered, iq_is_confident
35
from model import ImageQuery
46

57

6-
def test_iq_is_confident(gl_experimental: ExperimentalApi, initial_iq: ImageQuery):
7-
det = gl_experimental.get_or_create_detector("Test", "test_query")
8+
def test_iq_is_confident(gl_experimental: ExperimentalApi, initial_iq: ImageQuery, detector_name: Callable):
9+
det = gl_experimental.get_or_create_detector(detector_name(), "test_query")
810
iq = gl_experimental.ask_async(det, image="test/assets/dog.jpeg")
911
assert not iq_is_confident(iq, 0.9)
1012

1113
assert not iq_is_confident(initial_iq, 0.9)
1214

1315

14-
def test_iq_is_answered(gl_experimental: ExperimentalApi, initial_iq: ImageQuery):
15-
det = gl_experimental.get_or_create_detector("Test", "test_query")
16+
def test_iq_is_answered(gl_experimental: ExperimentalApi, initial_iq: ImageQuery, detector_name: Callable):
17+
det = gl_experimental.get_or_create_detector(detector_name(), "test_query")
1618
iq = gl_experimental.ask_async(det, image="test/assets/dog.jpeg")
1719
assert not iq_is_answered(iq)
1820

test/unit/test_labels.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
from datetime import datetime
1+
from typing import Callable
22

33
import pytest
44
from groundlight import ApiException, ExperimentalApi
5+
from test.retry_decorator import retry_on_failure
56

67

7-
def test_binary_labels(gl_experimental: ExperimentalApi):
8-
name = f"Test binary labels{datetime.utcnow()}"
9-
det = gl_experimental.create_detector(name, "test_query")
8+
@retry_on_failure()
9+
def test_binary_labels(gl_experimental: ExperimentalApi, detector_name: Callable):
10+
det = gl_experimental.create_detector(detector_name("Test binary labels"), "test_query")
1011
iq1 = gl_experimental.submit_image_query(det, "test/assets/cat.jpeg")
1112
gl_experimental.add_label(iq1, "YES")
1213
iq1 = gl_experimental.get_image_query(iq1.id)
@@ -21,9 +22,9 @@ def test_binary_labels(gl_experimental: ExperimentalApi):
2122
gl_experimental.add_label(iq1, "MAYBE")
2223

2324

24-
def test_counting_labels(gl_experimental: ExperimentalApi):
25-
name = f"Test binary labels{datetime.utcnow()}"
26-
det = gl_experimental.create_counting_detector(name, "test_query", "test_object_class")
25+
@retry_on_failure()
26+
def test_counting_labels(gl_experimental: ExperimentalApi, detector_name: Callable):
27+
det = gl_experimental.create_counting_detector(detector_name("Test counting labels"), "test_query", "test_object_class")
2728
iq1 = gl_experimental.submit_image_query(det, "test/assets/cat.jpeg")
2829

2930
gl_experimental.add_label(iq1, 0)
@@ -45,9 +46,11 @@ def test_counting_labels(gl_experimental: ExperimentalApi):
4546
gl_experimental.add_label(iq1, -999)
4647

4748

48-
def test_multiclass_labels(gl_experimental: ExperimentalApi):
49-
name = f"Test binary labels{datetime.utcnow()}"
50-
det = gl_experimental.create_multiclass_detector(name, "test_query", class_names=["apple", "banana", "cherry"])
49+
@retry_on_failure()
50+
def test_multiclass_labels(gl_experimental: ExperimentalApi, detector_name: Callable):
51+
det = gl_experimental.create_multiclass_detector(
52+
detector_name("Test multiclass labels"), "test_query", class_names=["apple", "banana", "cherry"]
53+
)
5154
iq1 = gl_experimental.submit_image_query(det, "test/assets/cat.jpeg")
5255
gl_experimental.add_label(iq1, "apple")
5356
iq1 = gl_experimental.get_image_query(iq1.id)
@@ -66,9 +69,9 @@ def test_multiclass_labels(gl_experimental: ExperimentalApi):
6669
gl_experimental.add_label(iq1, "MAYBE")
6770

6871

69-
def test_bounding_box_labels(gl_experimental: ExperimentalApi):
70-
name = f"Test bounding box labels{datetime.utcnow()}"
71-
det = gl_experimental.create_bounding_box_detector(name, "test_query", "test_class")
72+
@retry_on_failure()
73+
def test_bounding_box_labels(gl_experimental: ExperimentalApi, detector_name: Callable):
74+
det = gl_experimental.create_bounding_box_detector(detector_name("Test bounding box labels"), "test_query", "test_class")
7275
iq1 = gl_experimental.submit_image_query(det, "test/assets/cat.jpeg")
7376
gl_experimental.add_label(iq1, "NO_OBJECTS")
7477
iq1 = gl_experimental.get_image_query(iq1.id)
@@ -84,9 +87,9 @@ def test_bounding_box_labels(gl_experimental: ExperimentalApi):
8487
gl_experimental.add_label(iq1, "MAYBE")
8588

8689

87-
def test_text_recognition_labels(gl_experimental: ExperimentalApi):
88-
name = f"Test text recognition labels{datetime.utcnow()}"
89-
det = gl_experimental.create_text_recognition_detector(name, "test_query")
90+
@retry_on_failure()
91+
def test_text_recognition_labels(gl_experimental: ExperimentalApi, detector_name: Callable):
92+
det = gl_experimental.create_text_recognition_detector(detector_name("Test text recognition labels"), "test_query")
9093
iq1 = gl_experimental.submit_image_query(det, "test/assets/cat.jpeg")
9194
gl_experimental.add_label(iq1, "apple text")
9295
iq1 = gl_experimental.get_image_query(iq1.id)

test/unit/test_metrics_and_evaluation.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import time
2-
from datetime import datetime
2+
from typing import Callable
33

44
import pytest
55
from groundlight import ExperimentalApi
66

77

88
@pytest.mark.skip(reason="Slow")
9-
def test_metrics_and_evaluation(gl_experimental: ExperimentalApi):
10-
name = f"Test metrics and evaluation {datetime.utcnow()}"
11-
det = gl_experimental.create_detector(name, "test_query")
9+
def test_metrics_and_evaluation(gl_experimental: ExperimentalApi, detector_name: Callable):
10+
det = gl_experimental.create_detector(detector_name("Test metrics and evaluation"), "test_query")
1211
for i in range(10):
1312
iq = gl_experimental.submit_image_query(
1413
det, "test/assets/cat.jpeg", wait=0, patience_time=10, human_review="NEVER"

0 commit comments

Comments
 (0)