diff --git a/generated/.openapi-generator/FILES b/generated/.openapi-generator/FILES index 0db4410e..fd6931f8 100644 --- a/generated/.openapi-generator/FILES +++ b/generated/.openapi-generator/FILES @@ -30,7 +30,6 @@ docs/LabelValueRequest.md docs/LabelsApi.md docs/ModeEnum.md docs/Note.md -docs/NoteRequest.md docs/NotesApi.md docs/PaginatedDetectorList.md docs/PaginatedImageQueryList.md @@ -82,7 +81,6 @@ groundlight_openapi_client/model/label_value.py groundlight_openapi_client/model/label_value_request.py groundlight_openapi_client/model/mode_enum.py groundlight_openapi_client/model/note.py -groundlight_openapi_client/model/note_request.py groundlight_openapi_client/model/paginated_detector_list.py groundlight_openapi_client/model/paginated_image_query_list.py groundlight_openapi_client/model/paginated_rule_list.py diff --git a/generated/README.md b/generated/README.md index 60492381..33bdaaf2 100644 --- a/generated/README.md +++ b/generated/README.md @@ -157,7 +157,6 @@ Class | Method | HTTP request | Description - [LabelValueRequest](docs/LabelValueRequest.md) - [ModeEnum](docs/ModeEnum.md) - [Note](docs/Note.md) - - [NoteRequest](docs/NoteRequest.md) - [PaginatedDetectorList](docs/PaginatedDetectorList.md) - [PaginatedImageQueryList](docs/PaginatedImageQueryList.md) - [PaginatedRuleList](docs/PaginatedRuleList.md) diff --git a/generated/docs/NotesApi.md b/generated/docs/NotesApi.md index 28e9f1da..5e0a4a0b 100644 --- a/generated/docs/NotesApi.md +++ b/generated/docs/NotesApi.md @@ -9,7 +9,7 @@ Method | HTTP request | Description # **create_note** -> create_note(detector_id, note_request) +> create_note(detector_id, content) @@ -23,7 +23,6 @@ Create a new note import time import groundlight_openapi_client from groundlight_openapi_client.api import notes_api -from groundlight_openapi_client.model.note_request import NoteRequest from pprint import pprint # Defining the host is optional and defaults to https://api.groundlight.ai/device-api # See configuration.py for a list of all supported configuration parameters. @@ -47,13 +46,19 @@ with groundlight_openapi_client.ApiClient(configuration) as api_client: # Create an instance of the API class api_instance = notes_api.NotesApi(api_client) detector_id = "detector_id_example" # str | the detector to associate the new note with - note_request = NoteRequest( - content="content_example", - ) # NoteRequest | + content = "content_example" # str | Text content of the note. + image = open('/path/to/file', 'rb') # file_type, none_type | (optional) # example passing only required values which don't have defaults set try: - api_instance.create_note(detector_id, note_request) + api_instance.create_note(detector_id, content) + except groundlight_openapi_client.ApiException as e: + print("Exception when calling NotesApi->create_note: %s\n" % e) + + # example passing only required values which don't have defaults set + # and optional values + try: + api_instance.create_note(detector_id, content, image=image) except groundlight_openapi_client.ApiException as e: print("Exception when calling NotesApi->create_note: %s\n" % e) ``` @@ -64,7 +69,8 @@ with groundlight_openapi_client.ApiClient(configuration) as api_client: Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- **detector_id** | **str**| the detector to associate the new note with | - **note_request** | [**NoteRequest**](NoteRequest.md)| | + **content** | **str**| Text content of the note. | + **image** | **file_type, none_type**| | [optional] ### Return type @@ -76,7 +82,7 @@ void (empty response body) ### HTTP request headers - - **Content-Type**: application/json, application/x-www-form-urlencoded, multipart/form-data + - **Content-Type**: multipart/form-data - **Accept**: Not defined diff --git a/generated/groundlight_openapi_client/api/notes_api.py b/generated/groundlight_openapi_client/api/notes_api.py index 00b937e7..14377076 100644 --- a/generated/groundlight_openapi_client/api/notes_api.py +++ b/generated/groundlight_openapi_client/api/notes_api.py @@ -22,7 +22,6 @@ validate_and_convert_types, ) from groundlight_openapi_client.model.all_notes import AllNotes -from groundlight_openapi_client.model.note_request import NoteRequest class NotesApi(object): @@ -48,36 +47,49 @@ def __init__(self, api_client=None): params_map={ "all": [ "detector_id", - "note_request", + "content", + "image", ], "required": [ "detector_id", - "note_request", + "content", + ], + "nullable": [ + "image", ], - "nullable": [], "enum": [], - "validation": [], + "validation": [ + "content", + ], }, root_map={ - "validations": {}, + "validations": { + ("content",): { + "min_length": 1, + }, + }, "allowed_values": {}, "openapi_types": { "detector_id": (str,), - "note_request": (NoteRequest,), + "content": (str,), + "image": ( + file_type, + none_type, + ), }, "attribute_map": { "detector_id": "detector_id", + "content": "content", + "image": "image", }, "location_map": { "detector_id": "query", - "note_request": "body", + "content": "form", + "image": "form", }, "collection_format_map": {}, }, - headers_map={ - "accept": [], - "content_type": ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"], - }, + headers_map={"accept": [], "content_type": ["multipart/form-data"]}, api_client=api_client, ) self.get_notes_endpoint = _Endpoint( @@ -121,21 +133,22 @@ def __init__(self, api_client=None): api_client=api_client, ) - def create_note(self, detector_id, note_request, **kwargs): + def create_note(self, detector_id, content, **kwargs): """create_note # noqa: E501 Create a new note # noqa: E501 This method makes a synchronous HTTP request by default. To make an asynchronous HTTP request, please pass async_req=True - >>> thread = api.create_note(detector_id, note_request, async_req=True) + >>> thread = api.create_note(detector_id, content, async_req=True) >>> result = thread.get() Args: detector_id (str): the detector to associate the new note with - note_request (NoteRequest): + content (str): Text content of the note. Keyword Args: + image (file_type, none_type): [optional] _return_http_data_only (bool): response data without head status code and headers. Default is True. _preload_content (bool): if False, the urllib3.HTTPResponse object @@ -178,7 +191,7 @@ def create_note(self, detector_id, note_request, **kwargs): kwargs["_content_type"] = kwargs.get("_content_type") kwargs["_host_index"] = kwargs.get("_host_index") kwargs["detector_id"] = detector_id - kwargs["note_request"] = note_request + kwargs["content"] = content return self.create_note_endpoint.call_with_http_info(**kwargs) def get_notes(self, detector_id, **kwargs): diff --git a/generated/groundlight_openapi_client/models/__init__.py b/generated/groundlight_openapi_client/models/__init__.py index 0491cc60..95e2b851 100644 --- a/generated/groundlight_openapi_client/models/__init__.py +++ b/generated/groundlight_openapi_client/models/__init__.py @@ -32,7 +32,6 @@ from groundlight_openapi_client.model.label_value_request import LabelValueRequest from groundlight_openapi_client.model.mode_enum import ModeEnum from groundlight_openapi_client.model.note import Note -from groundlight_openapi_client.model.note_request import NoteRequest from groundlight_openapi_client.model.paginated_detector_list import PaginatedDetectorList from groundlight_openapi_client.model.paginated_image_query_list import PaginatedImageQueryList from groundlight_openapi_client.model.paginated_rule_list import PaginatedRuleList diff --git a/generated/model.py b/generated/model.py index d439bf93..114ded38 100644 --- a/generated/model.py +++ b/generated/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: public-api.yaml -# timestamp: 2024-08-13T00:01:16+00:00 +# timestamp: 2024-08-14T20:35:47+00:00 from __future__ import annotations @@ -91,6 +91,7 @@ class Note(BaseModel): class NoteRequest(BaseModel): content: constr(min_length=1) = Field(..., description="Text content of the note.") + image: Optional[bytes] = None class ROI(BaseModel): diff --git a/spec/public-api.yaml b/spec/public-api.yaml index c6b3d8bb..d775fe86 100644 --- a/spec/public-api.yaml +++ b/spec/public-api.yaml @@ -534,12 +534,6 @@ paths: - notes requestBody: content: - application/json: - schema: - $ref: '#/components/schemas/NoteRequest' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/NoteRequest' multipart/form-data: schema: $ref: '#/components/schemas/NoteRequest' @@ -1020,6 +1014,11 @@ components: type: string minLength: 1 description: Text content of the note. + image: + type: string + format: binary + writeOnly: true + nullable: true required: - content PaginatedDetectorList: diff --git a/src/groundlight/client.py b/src/groundlight/client.py index 6cb787b2..848825c3 100644 --- a/src/groundlight/client.py +++ b/src/groundlight/client.py @@ -52,7 +52,7 @@ class ApiTokenError(GroundlightClientError): pass -class Groundlight: +class Groundlight: # pylint: disable=too-many-instance-attributes """ Client for accessing the Groundlight cloud service. @@ -731,13 +731,17 @@ def add_label( self, image_query: Union[ImageQuery, str], label: Union[Label, str], rois: Union[List[ROI], str, None] = None ): """ - Add a new label to an image query. This answers the detector's question. + Add a new label to an image query. This answers the detector's + question. - :param image_query: Either an ImageQuery object (returned from `submit_image_query`) - or an image_query id as a string. + :param image_query: Either an ImageQuery object (returned from + `ask_ml` or similar method) or an image_query id as a + string. - :param label: The string "YES" or the string "NO" in answer to the query. - :param rois: An option list of regions of interest (ROIs) to associate with the label. (This feature experimental) + :param label: The string "YES" or the string "NO" in answer to the + query. + :param rois: An option list of regions of interest (ROIs) to associate + with the label. (This feature experimental) :return: None """ diff --git a/src/groundlight/experimental_api.py b/src/groundlight/experimental_api.py index db97beda..2386dbe7 100644 --- a/src/groundlight/experimental_api.py +++ b/src/groundlight/experimental_api.py @@ -7,8 +7,10 @@ """ import json +from io import BufferedReader, BytesIO from typing import Any, Dict, List, Tuple, Union +import requests from groundlight_openapi_client.api.actions_api import ActionsApi from groundlight_openapi_client.api.detector_groups_api import DetectorGroupsApi from groundlight_openapi_client.api.image_queries_api import ImageQueriesApi @@ -19,13 +21,14 @@ from groundlight_openapi_client.model.condition_request import ConditionRequest from groundlight_openapi_client.model.detector_group_request import DetectorGroupRequest from groundlight_openapi_client.model.label_value_request import LabelValueRequest -from groundlight_openapi_client.model.note_request import NoteRequest from groundlight_openapi_client.model.roi_request import ROIRequest from groundlight_openapi_client.model.rule_request import RuleRequest from groundlight_openapi_client.model.verb_enum import VerbEnum from model import ROI, BBoxGeometry, Detector, DetectorGroup, ImageQuery, PaginatedRuleList, Rule from groundlight.binary_labels import Label, convert_display_label_to_internal +from groundlight.images import parse_supported_image_types +from groundlight.optional_imports import Image, np from .client import Groundlight @@ -180,16 +183,31 @@ def get_notes(self, detector: Union[str, Detector]) -> Dict[str, Any]: det_id = detector.id if isinstance(detector, Detector) else detector return self.notes_api.get_notes(det_id) - def create_note(self, detector: Union[str, Detector], note: Union[str, NoteRequest]) -> None: + def create_note( + self, + detector: Union[str, Detector], + note: str, + image: Union[str, bytes, Image.Image, BytesIO, BufferedReader, np.ndarray, None] = None, + ) -> None: """ Adds a note to a given detector :param detector: the detector to add the note to + :param note: the text content of the note + :param image: a path to an image to attach to the note """ det_id = detector.id if isinstance(detector, Detector) else detector - if isinstance(note, str): - note = NoteRequest(content=note) - self.notes_api.create_note(det_id, note) + if image is not None: + img_bytes = parse_supported_image_types(image) + # TODO: The openapi generator doesn't handle file submissions well at the moment, so we manually implement this + # kwargs = {"image": img_bytes} + # self.notes_api.create_note(det_id, note, **kwargs) + url = f"{self.endpoint}/v1/notes" + files = {"image": ("image.jpg", img_bytes, "image/jpeg")} if image is not None else None + data = {"content": note} + params = {"detector_id": det_id} + headers = {"x-api-token": self.configuration.api_key["ApiToken"]} + requests.post(url, headers=headers, data=data, files=files, params=params, timeout=60) # type: ignore def create_detector_group(self, name: str) -> DetectorGroup: """ @@ -234,18 +252,22 @@ def create_roi(self, label: str, top_left: Tuple[float, float], bottom_right: Tu ), ) + # pylint: disable=duplicate-code def add_label( self, image_query: Union[ImageQuery, str], label: Union[Label, str], rois: Union[List[ROI], str, None] = None ): """ - Experimental version of add_label. - Add a new label to an image query. This answers the detector's question. + Experimental version of add_label. Add a new label to an image query. + This answers the detector's question. - :param image_query: Either an ImageQuery object (returned from `submit_image_query`) - or an image_query id as a string. + :param image_query: Either an ImageQuery object (returned from + `submit_image_query`) or an image_query id as a + string. - :param label: The string "YES" or the string "NO" in answer to the query. - :param rois: An option list of regions of interest (ROIs) to associate with the label. (This feature experimental) + :param label: The string "YES" or the string "NO" in answer to the + query. + :param rois: An option list of regions of interest (ROIs) to associate + with the label. (This feature experimental) :return: None """ diff --git a/test/unit/test_notes.py b/test/unit/test_notes.py index b8189169..bf99e3cf 100644 --- a/test/unit/test_notes.py +++ b/test/unit/test_notes.py @@ -14,3 +14,15 @@ def test_notes(gl_experimental: ExperimentalApi): if notes[i].content == "test_note": found_note = True assert found_note + + +def test_note_with_image(gl_experimental: ExperimentalApi): + name = f"Test {datetime.utcnow()}" + det = gl_experimental.create_detector(name, "test_query") + gl_experimental.create_note(det, "test_note", "test/assets/cat.jpeg") + notes = (gl_experimental.get_notes(det).get("customer") or []) + (gl_experimental.get_notes(det).get("gl") or []) + found_note = False + for i in range(len(notes)): + if notes[i].content == "test_note": + found_note = True + assert found_note