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..9404d71b 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -14,6 +14,7 @@ from groundlight.internalapi import ApiException, InternalApiError, NotFoundError from groundlight.optional_imports import * from groundlight.status_codes import is_user_error +from groundlight_openapi_client.exceptions import NotFoundException from ksuid import KsuidMs from model import ( BinaryClassificationResult, @@ -848,3 +849,42 @@ 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) + + # 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 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) + + # Verify the second detector is also deleted + 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) # type: ignore