Skip to content

Commit

Permalink
Allow submiting images alongside notes in the sdk (#232)
Browse files Browse the repository at this point in the history
* Allows image submission alongside notes, bypassing the openapi generator
for now

---------

Co-authored-by: Auto-format Bot <[email protected]>
Co-authored-by: Ubuntu <[email protected]>
  • Loading branch information
3 people authored Aug 15, 2024
1 parent e4903c3 commit 5e83865
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 52 deletions.
2 changes: 0 additions & 2 deletions generated/.openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion generated/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 14 additions & 8 deletions generated/docs/NotesApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Method | HTTP request | Description


# **create_note**
> create_note(detector_id, note_request)
> create_note(detector_id, content)


Expand All @@ -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.
Expand All @@ -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)
```
Expand All @@ -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

Expand All @@ -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


Expand Down
45 changes: 29 additions & 16 deletions generated/groundlight_openapi_client/api/notes_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
1 change: 0 additions & 1 deletion generated/groundlight_openapi_client/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion generated/model.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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):
Expand Down
11 changes: 5 additions & 6 deletions spec/public-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 10 additions & 6 deletions src/groundlight/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
"""
Expand Down
44 changes: 33 additions & 11 deletions src/groundlight/experimental_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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
"""
Expand Down
12 changes: 12 additions & 0 deletions test/unit/test_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 5e83865

Please sign in to comment.