Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/groundlight/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea if this line is correct. I assume you've already verified that it is?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is correct. It's a pretty strict pattern match from the create_detector method

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,
Expand Down
40 changes: 40 additions & 0 deletions test/integration/test_groundlight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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?"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this name is so sad

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bad claude

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to change it originally, but then I thought about the silly test strings I've used and figured I'd let Claude have its fun

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to assume that Claude was being funny here, and not just confused.

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
Loading