From b89949bfb2c8421ec6c4a35671f934c32a33fece Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 9 Jun 2025 20:34:26 +0000 Subject: [PATCH 1/6] Add delete_detector method to Groundlight SDK MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement delete_detector() method in client.py that accepts both Detector objects and ID strings - Add comprehensive integration test covering deletion by object, by ID, and error handling - Follow existing SDK patterns for parameter handling and error conversion - Include proper documentation with usage examples and warnings about irreversible deletion 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/groundlight/client.py | 37 +++++++++++++++++++++++++++ test/integration/test_groundlight.py | 38 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/src/groundlight/client.py b/src/groundlight/client.py index a603edd3..28046908 100644 --- a/src/groundlight/client.py +++ b/src/groundlight/client.py @@ -1460,6 +1460,43 @@ def update_detector_escalation_type(self, detector: Union[str, Detector], escala patched_detector_request=PatchedDetectorRequest(escalation_type=escalation_type), ) + def delete_detector(self, detector: Union[str, Detector]) -> None: + """ + Delete a detector. This permanently removes the detector and all its associated data. + + .. warning:: + This operation is irreversible. Once a detector is deleted, it cannot be recovered. + All associated image queries and training data will also be permanently deleted. + + **Example usage**:: + + gl = Groundlight() + + # Using a detector object + detector = gl.get_detector("det_abc123") + gl.delete_detector(detector) + + # Using a detector ID string directly + gl.delete_detector("det_abc123") + + :param detector: Either a Detector object or a detector ID string starting with "det_". + The detector to delete. + + :return: None + :raises NotFoundError: If the detector with the given ID does not exist + :raises ApiTokenError: If API token is invalid + :raises GroundlightClientError: For other API errors + """ + if isinstance(detector, Detector): + detector_id = detector.id + else: + detector_id = str(detector) + + try: + self.detectors_api.delete_detector(id=detector_id, _request_timeout=DEFAULT_REQUEST_TIMEOUT) + except NotFoundException as e: + raise NotFoundError(f"Detector with id '{detector_id}' not found") from e + def create_counting_detector( # noqa: PLR0913 # pylint: disable=too-many-arguments, too-many-locals self, name: str, diff --git a/test/integration/test_groundlight.py b/test/integration/test_groundlight.py index c68e47ea..bc9b3f59 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -848,3 +848,41 @@ def test_multiclass_detector(gl: Groundlight): mc_iq = gl.submit_image_query(created_detector, "test/assets/dog.jpeg") assert mc_iq.result.label is not None assert mc_iq.result.label in class_names + + +def test_delete_detector(gl: Groundlight): + """ + Test deleting a detector by both ID and object, and verify proper error handling. + """ + # Create a detector to delete + name = f"Test delete detector {datetime.utcnow()}" + query = "Is there a dog to delete?" + pipeline_config = "never-review" + detector = gl.create_detector(name=name, query=query, pipeline_config=pipeline_config) + + # Verify the detector exists + retrieved_detector = gl.get_detector(detector.id) + assert retrieved_detector.id == detector.id + + # Delete using detector object + gl.delete_detector(detector) + + # Verify the detector is actually deleted + with pytest.raises(NotFoundError): + gl.get_detector(detector.id) + + # Create another detector to test deletion by ID string + name2 = f"Test delete detector 2 {datetime.utcnow()}" + detector2 = gl.create_detector(name=name2, query=query, pipeline_config=pipeline_config) + + # Delete using detector ID string + gl.delete_detector(detector2.id) + + # Verify the second detector is also deleted + with pytest.raises(NotFoundError): + gl.get_detector(detector2.id) + + # Test deleting a non-existent detector raises NotFoundError + fake_detector_id = "det_fake123456789" + with pytest.raises(NotFoundError): + gl.delete_detector(fake_detector_id) From a6382e069259a250671e1203b7bd28ec29fb5b21 Mon Sep 17 00:00:00 2001 From: Auto-format Bot Date: Mon, 9 Jun 2025 20:35:43 +0000 Subject: [PATCH 2/6] Automatically reformatting code --- test/integration/test_groundlight.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/integration/test_groundlight.py b/test/integration/test_groundlight.py index bc9b3f59..50a33a62 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -859,29 +859,29 @@ def test_delete_detector(gl: Groundlight): query = "Is there a dog to delete?" pipeline_config = "never-review" detector = gl.create_detector(name=name, query=query, pipeline_config=pipeline_config) - + # Verify the detector exists retrieved_detector = gl.get_detector(detector.id) assert retrieved_detector.id == detector.id - + # Delete using detector object gl.delete_detector(detector) - + # Verify the detector is actually deleted with pytest.raises(NotFoundError): gl.get_detector(detector.id) - + # Create another detector to test deletion by ID string name2 = f"Test delete detector 2 {datetime.utcnow()}" detector2 = gl.create_detector(name=name2, query=query, pipeline_config=pipeline_config) - + # Delete using detector ID string gl.delete_detector(detector2.id) - + # Verify the second detector is also deleted with pytest.raises(NotFoundError): gl.get_detector(detector2.id) - + # Test deleting a non-existent detector raises NotFoundError fake_detector_id = "det_fake123456789" with pytest.raises(NotFoundError): From d8af357c5468718ef40a890a91a46c5d9be62ca8 Mon Sep 17 00:00:00 2001 From: brandon Date: Wed, 11 Jun 2025 12:35:18 -0700 Subject: [PATCH 3/6] removing test_delete_detector redundancy with test_get_detector --- test/integration/test_groundlight.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/integration/test_groundlight.py b/test/integration/test_groundlight.py index 50a33a62..5354a706 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -860,10 +860,6 @@ def test_delete_detector(gl: Groundlight): pipeline_config = "never-review" detector = gl.create_detector(name=name, query=query, pipeline_config=pipeline_config) - # Verify the detector exists - retrieved_detector = gl.get_detector(detector.id) - assert retrieved_detector.id == detector.id - # Delete using detector object gl.delete_detector(detector) From 91822eea66cbb3801791139bb2915f43d2fcec3a Mon Sep 17 00:00:00 2001 From: brandon Date: Wed, 11 Jun 2025 12:55:44 -0700 Subject: [PATCH 4/6] add testing for iqs on the deleted detector --- test/integration/test_groundlight.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/integration/test_groundlight.py b/test/integration/test_groundlight.py index 5354a706..d17df942 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -15,6 +15,7 @@ from groundlight.optional_imports import * from groundlight.status_codes import is_user_error from ksuid import KsuidMs +from groundlight_openapi_client.exceptions import NotFoundException from model import ( BinaryClassificationResult, BoundingBoxResult, @@ -867,9 +868,10 @@ def test_delete_detector(gl: Groundlight): with pytest.raises(NotFoundError): gl.get_detector(detector.id) - # Create another detector to test deletion by ID string + # Create another detector to test deletion by ID string and that an attached image query is deleted name2 = f"Test delete detector 2 {datetime.utcnow()}" detector2 = gl.create_detector(name=name2, query=query, pipeline_config=pipeline_config) + gl.submit_image_query(detector2, "test/assets/dog.jpeg") # Delete using detector ID string gl.delete_detector(detector2.id) @@ -878,7 +880,11 @@ def test_delete_detector(gl: Groundlight): with pytest.raises(NotFoundError): gl.get_detector(detector2.id) + # Verify the image query is also deleted + with pytest.raises(NotFoundException): + gl.get_image_query(detector2.id) + # Test deleting a non-existent detector raises NotFoundError fake_detector_id = "det_fake123456789" with pytest.raises(NotFoundError): - gl.delete_detector(fake_detector_id) + gl.delete_detector(fake_detector_id) # type: ignore From 24680c163ee238b2e3d1ae11a4f0daaef897b2bf Mon Sep 17 00:00:00 2001 From: Auto-format Bot Date: Wed, 11 Jun 2025 19:56:33 +0000 Subject: [PATCH 5/6] Automatically reformatting code --- test/integration/test_groundlight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/test_groundlight.py b/test/integration/test_groundlight.py index d17df942..9404d71b 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -14,8 +14,8 @@ from groundlight.internalapi import ApiException, InternalApiError, NotFoundError from groundlight.optional_imports import * from groundlight.status_codes import is_user_error -from ksuid import KsuidMs from groundlight_openapi_client.exceptions import NotFoundException +from ksuid import KsuidMs from model import ( BinaryClassificationResult, BoundingBoxResult, From 5199c2cf5f122159ec88cedbe3de9374a5623401 Mon Sep 17 00:00:00 2001 From: brandon Date: Wed, 11 Jun 2025 13:17:52 -0700 Subject: [PATCH 6/6] sort imports --- test/integration/test_groundlight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/test_groundlight.py b/test/integration/test_groundlight.py index d17df942..9404d71b 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -14,8 +14,8 @@ from groundlight.internalapi import ApiException, InternalApiError, NotFoundError from groundlight.optional_imports import * from groundlight.status_codes import is_user_error -from ksuid import KsuidMs from groundlight_openapi_client.exceptions import NotFoundException +from ksuid import KsuidMs from model import ( BinaryClassificationResult, BoundingBoxResult,