diff --git a/.pylintrc b/.pylintrc index 991ed3af..0a1fba02 100644 --- a/.pylintrc +++ b/.pylintrc @@ -563,5 +563,5 @@ min-public-methods=2 # Exceptions that will emit a warning when being caught. Defaults to # "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +overgeneral-exceptions=builtins.BaseException, + builtins.Exception diff --git a/generated/docs/DetectorRequest.md b/generated/docs/DetectorRequest.md new file mode 100644 index 00000000..4ab13a90 --- /dev/null +++ b/generated/docs/DetectorRequest.md @@ -0,0 +1,17 @@ +# DetectorRequest + +Spec for serializing a detector object in the public API. + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**name** | **str** | A short, descriptive name for the detector. | +**confidence_threshold** | **float** | If the detector's prediction is below this confidence threshold, send the image query for human review. | [optional] if omitted the server will use the default value of 0.9 +**patience_time** | **float** | How long Groundlight will attempt to generate a confident prediction | [optional] if omitted the server will use the default value of 30.0 +**status** | **bool, date, datetime, dict, float, int, list, str, none_type** | | [optional] +**escalation_type** | **bool, date, datetime, dict, float, int, list, str, none_type** | Category that define internal proccess for labeling image queries * `STANDARD` - STANDARD * `NO_HUMAN_LABELING` - NO_HUMAN_LABELING | [optional] +**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/generated/groundlight_openapi_client/model/detector_request.py b/generated/groundlight_openapi_client/model/detector_request.py new file mode 100644 index 00000000..c7f708f1 --- /dev/null +++ b/generated/groundlight_openapi_client/model/detector_request.py @@ -0,0 +1,335 @@ +""" + Groundlight API + + Groundlight makes it simple to understand images. You can easily create computer vision detectors just by describing what you want to know using natural language. # noqa: E501 + + The version of the OpenAPI document: 0.15.3 + Contact: support@groundlight.ai + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 + +from groundlight_openapi_client.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel, +) +from groundlight_openapi_client.exceptions import ApiAttributeError + + +def lazy_import(): + from groundlight_openapi_client.model.blank_enum import BlankEnum + from groundlight_openapi_client.model.escalation_type_enum import EscalationTypeEnum + from groundlight_openapi_client.model.status_enum import StatusEnum + + globals()["BlankEnum"] = BlankEnum + globals()["EscalationTypeEnum"] = EscalationTypeEnum + globals()["StatusEnum"] = StatusEnum + + +class DetectorRequest(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = {} + + validations = { + ("name",): { + "max_length": 200, + "min_length": 1, + }, + ("confidence_threshold",): { + "inclusive_maximum": 1.0, + "inclusive_minimum": 0.0, + }, + ("patience_time",): { + "inclusive_maximum": 3600, + "inclusive_minimum": 0, + }, + } + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + lazy_import() + return ( + bool, + date, + datetime, + dict, + float, + int, + list, + str, + none_type, + ) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + lazy_import() + return { + "name": (str,), # noqa: E501 + "confidence_threshold": (float,), # noqa: E501 + "patience_time": (float,), # noqa: E501 + "status": ( + bool, + date, + datetime, + dict, + float, + int, + list, + str, + none_type, + ), # noqa: E501 + "escalation_type": ( + bool, + date, + datetime, + dict, + float, + int, + list, + str, + none_type, + ), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + attribute_map = { + "name": "name", # noqa: E501 + "confidence_threshold": "confidence_threshold", # noqa: E501 + "patience_time": "patience_time", # noqa: E501 + "status": "status", # noqa: E501 + "escalation_type": "escalation_type", # noqa: E501 + } + + read_only_vars = {} + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, name, *args, **kwargs): # noqa: E501 + """DetectorRequest - a model defined in OpenAPI + + Args: + name (str): A short, descriptive name for the detector. + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + confidence_threshold (float): If the detector's prediction is below this confidence threshold, send the image query for human review.. [optional] if omitted the server will use the default value of 0.9 # noqa: E501 + patience_time (float): How long Groundlight will attempt to generate a confident prediction. [optional] if omitted the server will use the default value of 30.0 # noqa: E501 + status (bool, date, datetime, dict, float, int, list, str, none_type): [optional] # noqa: E501 + escalation_type (bool, date, datetime, dict, float, int, list, str, none_type): Category that define internal proccess for labeling image queries * `STANDARD` - STANDARD * `NO_HUMAN_LABELING` - NO_HUMAN_LABELING. [optional] # noqa: E501 + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.name = name + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = set([ + "_data_store", + "_check_type", + "_spec_property_naming", + "_path_to_item", + "_configuration", + "_visited_composed_classes", + ]) + + @convert_js_args_to_python_args + def __init__(self, name, *args, **kwargs): # noqa: E501 + """DetectorRequest - a model defined in OpenAPI + + Args: + name (str): A short, descriptive name for the detector. + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + confidence_threshold (float): If the detector's prediction is below this confidence threshold, send the image query for human review.. [optional] if omitted the server will use the default value of 0.9 # noqa: E501 + patience_time (float): How long Groundlight will attempt to generate a confident prediction. [optional] if omitted the server will use the default value of 30.0 # noqa: E501 + status (bool, date, datetime, dict, float, int, list, str, none_type): [optional] # noqa: E501 + escalation_type (bool, date, datetime, dict, float, int, list, str, none_type): Category that define internal proccess for labeling image queries * `STANDARD` - STANDARD * `NO_HUMAN_LABELING` - NO_HUMAN_LABELING. [optional] # noqa: E501 + """ + + _check_type = kwargs.pop("_check_type", True) + _spec_property_naming = kwargs.pop("_spec_property_naming", False) + _path_to_item = kwargs.pop("_path_to_item", ()) + _configuration = kwargs.pop("_configuration", None) + _visited_composed_classes = kwargs.pop("_visited_composed_classes", ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." + % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.name = name + for var_name, var_value in kwargs.items(): + if ( + var_name not in self.attribute_map + and self._configuration is not None + and self._configuration.discard_unknown_keys + and self.additional_properties_type is None + ): + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError( + f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + "class with read only attributes." + ) diff --git a/generated/model.py b/generated/model.py index 9c12f113..3fed7b1c 100644 --- a/generated/model.py +++ b/generated/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: public-api.yaml -# timestamp: 2024-10-30T23:38:30+00:00 +# timestamp: 2024-11-21T00:58:02+00:00 from __future__ import annotations diff --git a/src/groundlight/client.py b/src/groundlight/client.py index 07d64804..fce4b400 100644 --- a/src/groundlight/client.py +++ b/src/groundlight/client.py @@ -14,6 +14,7 @@ from groundlight_openapi_client.exceptions import NotFoundException, UnauthorizedException from groundlight_openapi_client.model.detector_creation_input_request import DetectorCreationInputRequest from groundlight_openapi_client.model.label_value_request import LabelValueRequest +from groundlight_openapi_client.model.patched_detector_request import PatchedDetectorRequest from model import ( ROI, BinaryClassificationResult, @@ -864,15 +865,19 @@ def stop_inspection(self, inspection_id: str) -> str: """ return self.api_client.stop_inspection(inspection_id) - def update_detector_confidence_threshold(self, detector_id: str, confidence_threshold: float) -> None: + def update_detector_confidence_threshold(self, detector: Union[str, Detector], confidence_threshold: float) -> None: """ - Updates the confidence threshold of a detector given a detector_id. + Updates the confidence threshold for the given detector - :param detector_id: The id of the detector to update. + :param detector: the detector to update + :param confidence_threshold: the new confidence threshold - :param confidence_threshold: The new confidence threshold for the detector. - - :return None - :rtype None + :return: None """ - self.api_client.update_detector_confidence_threshold(detector_id, confidence_threshold) + if isinstance(detector, Detector): + detector = detector.id + if confidence_threshold < 0 or confidence_threshold > 1: + raise ValueError("confidence must be between 0 and 1") + self.detectors_api.update_detector( + detector, patched_detector_request=PatchedDetectorRequest(confidence_threshold=confidence_threshold) + ) diff --git a/src/groundlight/experimental_api.py b/src/groundlight/experimental_api.py index 8940f48d..69f35c47 100644 --- a/src/groundlight/experimental_api.py +++ b/src/groundlight/experimental_api.py @@ -22,10 +22,13 @@ from groundlight_openapi_client.model.condition_request import ConditionRequest from groundlight_openapi_client.model.count_mode_configuration import CountModeConfiguration from groundlight_openapi_client.model.detector_group_request import DetectorGroupRequest +from groundlight_openapi_client.model.escalation_type_enum import EscalationTypeEnum from groundlight_openapi_client.model.label_value_request import LabelValueRequest from groundlight_openapi_client.model.multi_class_mode_configuration import MultiClassModeConfiguration +from groundlight_openapi_client.model.patched_detector_request import PatchedDetectorRequest from groundlight_openapi_client.model.roi_request import ROIRequest from groundlight_openapi_client.model.rule_request import RuleRequest +from groundlight_openapi_client.model.status_enum import StatusEnum from groundlight_openapi_client.model.verb_enum import VerbEnum from model import ROI, BBoxGeometry, Detector, DetectorGroup, ImageQuery, ModeEnum, PaginatedRuleList, Rule @@ -308,6 +311,58 @@ def reset_detector(self, detector: Union[str, Detector]) -> None: detector = detector.id self.detector_reset_api.reset_detector(detector) + def update_detector_name(self, detector: Union[str, Detector], name: str) -> None: + """ + Updates the name of the given detector + + :param detector: the detector to update + :param name: the new name + + :return: None + """ + if isinstance(detector, Detector): + detector = detector.id + self.detectors_api.update_detector(detector, patched_detector_request=PatchedDetectorRequest(name=name)) + + def update_detector_status(self, detector: Union[str, Detector], enabled: bool) -> None: + """ + Updates the status of the given detector. If the detector is disabled, it will not receive new image queries + + :param detector: the detector to update + :param enabled: whether the detector is enabled, can be either True or False + + :return: None + """ + if isinstance(detector, Detector): + detector = detector.id + self.detectors_api.update_detector( + detector, + patched_detector_request=PatchedDetectorRequest(status=StatusEnum("ON") if enabled else StatusEnum("OFF")), + ) + + def update_detector_escalation_type(self, detector: Union[str, Detector], escalation_type: str) -> None: + """ + Updates the escalation type of the given detector + + This is particularly useful for turning off human labeling for billing or security purposes. + By setting a detector to "NO_HUMAN_LABELING", no image queries sent to this detector will be + sent to human labelers. + + :param detector: the detector to update + :param escalation_type: the new escalation type, can be "STANDARD" or "NO_HUMAN_LABELING" + + :return: None + """ + if isinstance(detector, Detector): + detector = detector.id + escalation_type = escalation_type.upper() + if escalation_type not in ["STANDARD", "NO_HUMAN_LABELING"]: + raise ValueError("escalation_type must be either 'STANDARD' or 'NO_HUMAN_LABELING'") + self.detectors_api.update_detector( + detector, + patched_detector_request=PatchedDetectorRequest(escalation_type=EscalationTypeEnum(escalation_type)), + ) + def create_counting_detector( # noqa: PLR0913 # pylint: disable=too-many-arguments, too-many-locals self, name: str, diff --git a/src/groundlight/internalapi.py b/src/groundlight/internalapi.py index 93ed7256..91fea60c 100644 --- a/src/groundlight/internalapi.py +++ b/src/groundlight/internalapi.py @@ -355,27 +355,3 @@ def stop_inspection(self, inspection_id: str) -> str: ) return response.json()["result"] - - @RequestsRetryDecorator() - def update_detector_confidence_threshold(self, detector_id: str, confidence_threshold: float) -> None: - """Updates the confidence threshold of a detector.""" - - # The API does not validate the confidence threshold, - # so we will validate it here and raise an exception if necessary. - if confidence_threshold < 0 or confidence_threshold > 1: - raise ValueError(f"Confidence threshold must be between 0 and 1. Got {confidence_threshold}.") - - url = f"{self.configuration.host}/predictors/{detector_id}" - - headers = self._headers() - - payload = {"confidence_threshold": confidence_threshold} - - response = requests.request("PATCH", url, headers=headers, json=payload, verify=self.configuration.verify_ssl) - - if not is_ok(response.status_code): - raise InternalApiError( - status=response.status_code, - reason=f"Error updating detector: {detector_id}.", - http_resp=response, - ) diff --git a/test/unit/test_experimental.py b/test/unit/test_experimental.py index 4cebbbd6..3ad0b5e1 100644 --- a/test/unit/test_experimental.py +++ b/test/unit/test_experimental.py @@ -2,7 +2,7 @@ import pytest from groundlight import ExperimentalApi -from model import ImageQuery +from model import Detector, ImageQuery def test_detector_groups(gl_experimental: ExperimentalApi): @@ -15,6 +15,56 @@ def test_detector_groups(gl_experimental: ExperimentalApi): assert created_group in all_groups +def test_update_detector_confidence_threshold(gl_experimental: ExperimentalApi, detector: Detector): + """ + verify that we can update the confidence of a detector + """ + new_confidence = 0.5 + gl_experimental.update_detector_confidence_threshold(detector.id, new_confidence) + updated_detector = gl_experimental.get_detector(detector.id) + assert updated_detector.confidence_threshold == new_confidence + newer_confidence = 0.9 + gl_experimental.update_detector_confidence_threshold(detector.id, newer_confidence) + updated_detector = gl_experimental.get_detector(detector.id) + assert updated_detector.confidence_threshold == newer_confidence + + +def test_update_detector_name(gl_experimental: ExperimentalApi, detector: Detector): + """ + verify that we can update the name of a detector + """ + new_name = f"Test {datetime.utcnow()}" + gl_experimental.update_detector_name(detector.id, new_name) + updated_detector = gl_experimental.get_detector(detector.id) + assert updated_detector.name == new_name + + +def test_update_detector_status(gl_experimental: ExperimentalApi): + """ + verify that we can update the status of a detector + """ + detector = gl_experimental.get_or_create_detector(f"test {datetime.utcnow()}", "Is there a dog?") + gl_experimental.update_detector_status(detector.id, False) + updated_detector = gl_experimental.get_detector(detector.id) + assert updated_detector.status.value == "OFF" + gl_experimental.update_detector_status(detector.id, True) + updated_detector = gl_experimental.get_detector(detector.id) + assert updated_detector.status.value == "ON" + + +def test_update_detector_escalation_type(gl_experimental: ExperimentalApi): + """ + verify that we can update the escalation type of a detector + """ + detector = gl_experimental.get_or_create_detector(f"test {datetime.utcnow()}", "Is there a dog?") + gl_experimental.update_detector_escalation_type(detector.id, "NO_HUMAN_LABELING") + updated_detector = gl_experimental.get_detector(detector.id) + updated_detector.escalation_type.value == "NO_HUMAN_LABELING" + gl_experimental.update_detector_escalation_type(detector.id, "STANDARD") + updated_detector = gl_experimental.get_detector(detector.id) + updated_detector.escalation_type.value == "STANDARD" + + @pytest.mark.skip( reason=( "Users currently don't have permission to turn object detection on their own. If you have questions, reach out"