Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow submiting images alongside notes in the sdk #232

Merged
merged 15 commits into from
Aug 15, 2024
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)
Comment on lines +202 to +204
Copy link
Member

Choose a reason for hiding this comment

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

Will we ever resolve this TODO? Or is it a permanent issue? If permanent I'm not sure you need to include how we would have solved it if the generator worked.

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 very much would like to resolve the issue, but it really just doesn't make sense to sink more time right now. This may be resolved in the openapi-generator project. It shouldn't affect us until we decide we want to start porting the SDK into other languages (so not very soon)

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
Loading