Skip to content

Commit

Permalink
Add IQ metadata (#132)
Browse files Browse the repository at this point in the history
Co-authored-by: Auto-format Bot <[email protected]>
  • Loading branch information
mjvogelsong and Auto-format Bot authored Nov 10, 2023
1 parent b79c307 commit 6b978ad
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 6 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ docs-comprehensive: apidocs
cd docs && npm run build

apidocs:
cd docs && npm install
poetry run make html

html:
Expand Down
4 changes: 3 additions & 1 deletion generated/docs/ImageQueriesApi.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ with openapi_client.ApiClient(configuration) as api_client:
human_review = "human_review_example" # str | If set to `DEFAULT`, use the regular escalation logic (i.e., send the image query for human review if the ML model is not confident). If set to `ALWAYS`, always send the image query for human review even if the ML model is confident. If set to `NEVER`, never send the image query for human review even if the ML model is not confident. (optional)
patience_time = 3.14 # float | How long to wait for a confident response. (optional)
want_async = "want_async_example" # str | If \"true\" then submitting an image query returns immediately without a result. The result will be computed asynchronously and can be retrieved later. (optional)
metadata = "metadata_example" # str | A dictionary of custom key/value metadata to associate with the image query (limited to 1KB). (optional)
body = open('@path/to/image.jpeg', 'rb') # file_type | (optional)

# example passing only required values which don't have defaults set
Expand All @@ -220,7 +221,7 @@ with openapi_client.ApiClient(configuration) as api_client:
# example passing only required values which don't have defaults set
# and optional values
try:
api_response = api_instance.submit_image_query(detector_id, human_review=human_review, patience_time=patience_time, want_async=want_async, body=body)
api_response = api_instance.submit_image_query(detector_id, human_review=human_review, patience_time=patience_time, want_async=want_async, metadata=metadata, body=body)
pprint(api_response)
except openapi_client.ApiException as e:
print("Exception when calling ImageQueriesApi->submit_image_query: %s\n" % e)
Expand All @@ -235,6 +236,7 @@ Name | Type | Description | Notes
**human_review** | **str**| If set to &#x60;DEFAULT&#x60;, use the regular escalation logic (i.e., send the image query for human review if the ML model is not confident). If set to &#x60;ALWAYS&#x60;, always send the image query for human review even if the ML model is confident. If set to &#x60;NEVER&#x60;, never send the image query for human review even if the ML model is not confident. | [optional]
**patience_time** | **float**| How long to wait for a confident response. | [optional]
**want_async** | **str**| If \&quot;true\&quot; then submitting an image query returns immediately without a result. The result will be computed asynchronously and can be retrieved later. | [optional]
**metadata** | **str**| A dictionary of custom key/value metadata to associate with the image query (limited to 1KB). | [optional]
**body** | **file_type**| | [optional]

### Return type
Expand Down
1 change: 1 addition & 0 deletions generated/docs/ImageQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Name | Type | Description | Notes
**detector_id** | **str** | Which detector was used on this image query? | [readonly]
**result_type** | **bool, date, datetime, dict, float, int, list, str, none_type** | What type of result are we returning? | [readonly]
**result** | **bool, date, datetime, dict, float, int, list, str, none_type** | | [optional] [readonly]
**metadata** | **{str: (bool, date, datetime, dict, float, int, list, str, none_type)}, none_type** | A dictionary of custom key/value metadata to associate with the image query (limited to 1KB). | [optional] [readonly]
**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)
Expand Down
8 changes: 6 additions & 2 deletions generated/model.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# generated by datamodel-codegen:
# filename: public-api.yaml
# timestamp: 2023-10-16T23:29:00+00:00
# timestamp: 2023-11-09T05:00:29+00:00

from __future__ import annotations

from datetime import datetime
from enum import Enum
from typing import List, Optional
from typing import Any, Dict, List, Optional

from pydantic import AnyUrl, BaseModel, Field, confloat, constr

Expand Down Expand Up @@ -70,6 +70,10 @@ class ImageQuery(BaseModel):
detector_id: str = Field(..., description="Which detector was used on this image query?")
result_type: ResultTypeEnum = Field(..., description="What type of result are we returning?")
result: Optional[ClassificationResult] = None
metadata: Optional[Dict[str, Any]] = Field(
None,
description="A dictionary of custom key/value metadata to associate with the image query (limited to 1KB).",
)


class PaginatedDetectorList(BaseModel):
Expand Down
5 changes: 5 additions & 0 deletions generated/openapi_client/api/image_queries_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def __init__(self, api_client=None):
"human_review",
"patience_time",
"want_async",
"metadata",
"body",
],
"required": [
Expand All @@ -151,19 +152,22 @@ def __init__(self, api_client=None):
"human_review": (str,),
"patience_time": (float,),
"want_async": (str,),
"metadata": (str,),
"body": (file_type,),
},
"attribute_map": {
"detector_id": "detector_id",
"human_review": "human_review",
"patience_time": "patience_time",
"want_async": "want_async",
"metadata": "metadata",
},
"location_map": {
"detector_id": "query",
"human_review": "query",
"patience_time": "query",
"want_async": "query",
"metadata": "query",
"body": "body",
},
"collection_format_map": {},
Expand Down Expand Up @@ -304,6 +308,7 @@ def submit_image_query(self, detector_id, **kwargs):
human_review (str): If set to `DEFAULT`, use the regular escalation logic (i.e., send the image query for human review if the ML model is not confident). If set to `ALWAYS`, always send the image query for human review even if the ML model is confident. If set to `NEVER`, never send the image query for human review even if the ML model is not confident. . [optional]
patience_time (float): How long to wait for a confident response.. [optional]
want_async (str): If \"true\" then submitting an image query returns immediately without a result. The result will be computed asynchronously and can be retrieved later.. [optional]
metadata (str): A dictionary of custom key/value metadata to associate with the image query (limited to 1KB).. [optional]
body (file_type): [optional]
_return_http_data_only (bool): response data without head status
code and headers. Default is True.
Expand Down
8 changes: 8 additions & 0 deletions generated/openapi_client/model/image_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ def openapi_types():
str,
none_type,
), # noqa: E501
"metadata": (
{str: (bool, date, datetime, dict, float, int, list, str, none_type)},
none_type,
), # noqa: E501
}

@cached_property
Expand All @@ -152,6 +156,7 @@ def discriminator():
"detector_id": "detector_id", # noqa: E501
"result_type": "result_type", # noqa: E501
"result": "result", # noqa: E501
"metadata": "metadata", # noqa: E501
}

read_only_vars = {
Expand All @@ -162,6 +167,7 @@ def discriminator():
"detector_id", # noqa: E501
"result_type", # noqa: E501
"result", # noqa: E501
"metadata", # noqa: E501
}

_composed_schemas = {}
Expand Down Expand Up @@ -211,6 +217,7 @@ def _from_openapi_data(cls, id, type, created_at, query, detector_id, result_typ
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
result (bool, date, datetime, dict, float, int, list, str, none_type): [optional] # noqa: E501
metadata ({str: (bool, date, datetime, dict, float, int, list, str, none_type)}, none_type): A dictionary of custom key/value metadata to associate with the image query (limited to 1KB).. [optional] # noqa: E501
"""

_check_type = kwargs.pop("_check_type", True)
Expand Down Expand Up @@ -304,6 +311,7 @@ def __init__(self, *args, **kwargs): # noqa: E501
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
result (bool, date, datetime, dict, float, int, list, str, none_type): [optional] # noqa: E501
metadata ({str: (bool, date, datetime, dict, float, int, list, str, none_type)}, none_type): A dictionary of custom key/value metadata to associate with the image query (limited to 1KB).. [optional] # noqa: E501
"""

_check_type = kwargs.pop("_check_type", True)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ packages = [
{include = "**/*.py", from = "src"},
]
readme = "README.md"
version = "0.12.1"
version = "0.13.0"

[tool.poetry.dependencies]
# For certifi, use ">=" instead of "^" since it upgrades its "major version" every year, not really following semver
Expand Down
15 changes: 15 additions & 0 deletions spec/public-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,14 @@ paths:
schema:
type: string
description: If "true" then submitting an image query returns immediately without a result. The result will be computed asynchronously and can be retrieved later.
- in: query
name: metadata
schema:
type: string
required: false
description:
A dictionary of custom key/value metadata to associate with the image
query (limited to 1KB).
tags:
- image-queries
requestBody:
Expand Down Expand Up @@ -339,6 +347,13 @@ components:
allOf:
- $ref: "#/components/schemas/ClassificationResult"
readOnly: true
metadata:
type: object
readOnly: true
nullable: true
description:
A dictionary of custom key/value metadata to associate with the image
query (limited to 1KB).
required:
- created_at
- detector_id
Expand Down
36 changes: 35 additions & 1 deletion src/groundlight/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from groundlight.binary_labels import Label, convert_display_label_to_internal, convert_internal_label_to_display
from groundlight.config import API_TOKEN_HELP_MESSAGE, API_TOKEN_VARIABLE_NAME
from groundlight.encodings import url_encode_dict
from groundlight.images import ByteStreamWrapper, parse_supported_image_types
from groundlight.internalapi import (
GroundlightApiClient,
Expand Down Expand Up @@ -289,6 +290,7 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t
human_review: Optional[str] = None,
want_async: bool = False,
inspection_id: Optional[str] = None,
metadata: Union[dict, str, None] = None,
) -> ImageQuery:
"""
Evaluates an image with Groundlight.
Expand Down Expand Up @@ -334,6 +336,11 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t
this is the ID of the inspection to associate with the image query.
:type inspection_id: str
:param metadata: A dictionary or JSON string of custom key/value metadata to associate with
the image query (limited to 1KB). You can retrieve this metadata later by calling
`get_image_query()`.
:type metadata: dict or str
:return: ImageQuery
:rtype: ImageQuery
"""
Expand All @@ -360,6 +367,12 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t
)
params["want_async"] = str(bool(want_async))

if metadata is not None:
# Currently, our backend server puts the image in the body data of the API request,
# which means we need to put the metadata in the query string. To do that safely, we
# url- and base64-encode the metadata.
params["metadata"] = url_encode_dict(metadata, name="metadata", size_limit_bytes=1024)

# If no inspection_id is provided, we submit the image query using image_queries_api (autogenerated via OpenAPI)
# However, our autogenerated code does not currently support inspection_id, so if an inspection_id was
# provided, we use the private API client instead.
Expand All @@ -380,12 +393,13 @@ def submit_image_query( # noqa: PLR0913 # pylint: disable=too-many-arguments, t

return self._fixup_image_query(image_query)

def ask_confident(
def ask_confident( # noqa: PLR0913 # pylint: disable=too-many-arguments
self,
detector: Union[Detector, str],
image: Union[str, bytes, Image.Image, BytesIO, BufferedReader, np.ndarray],
confidence_threshold: Optional[float] = None,
wait: Optional[float] = None,
metadata: Union[dict, str, None] = None,
) -> ImageQuery:
"""
Evaluates an image with Groundlight waiting until an answer above the confidence threshold
Expand All @@ -411,6 +425,11 @@ def ask_confident(
:param wait: How long to wait (in seconds) for a confident answer.
:type wait: float
:param metadata: A dictionary or JSON string of custom key/value metadata to associate with
the image query (limited to 1KB). You can retrieve this metadata later by calling
`get_image_query()`.
:type metadata: dict or str
:return: ImageQuery
:rtype: ImageQuery
"""
Expand All @@ -421,13 +440,15 @@ def ask_confident(
wait=wait,
patience_time=wait,
human_review=None,
metadata=metadata,
)

def ask_ml(
self,
detector: Union[Detector, str],
image: Union[str, bytes, Image.Image, BytesIO, BufferedReader, np.ndarray],
wait: Optional[float] = None,
metadata: Union[dict, str, None] = None,
) -> ImageQuery:
"""
Evaluates an image with Groundlight, getting the first answer Groundlight can provide.
Expand All @@ -448,13 +469,19 @@ def ask_ml(
:param wait: How long to wait (in seconds) for any answer.
:type wait: float
:param metadata: A dictionary or JSON string of custom key/value metadata to associate with
the image query (limited to 1KB). You can retrieve this metadata later by calling
`get_image_query()`.
:type metadata: dict or str
:return: ImageQuery
:rtype: ImageQuery
"""
iq = self.submit_image_query(
detector,
image,
wait=0,
metadata=metadata,
)
if iq_is_answered(iq):
return iq
Expand All @@ -468,6 +495,7 @@ def ask_async( # noqa: PLR0913 # pylint: disable=too-many-arguments
patience_time: Optional[float] = None,
confidence_threshold: Optional[float] = None,
human_review: Optional[str] = None,
metadata: Union[dict, str, None] = None,
) -> ImageQuery:
"""
Convenience method for submitting an `ImageQuery` asynchronously. This is equivalent to calling
Expand Down Expand Up @@ -509,6 +537,11 @@ def ask_async( # noqa: PLR0913 # pylint: disable=too-many-arguments
this is the ID of the inspection to associate with the image query.
:type inspection_id: str
:param metadata: A dictionary or JSON string of custom key/value metadata to associate with
the image query (limited to 1KB). You can retrieve this metadata later by calling
`get_image_query()`.
:type metadata: dict or str
:return: ImageQuery
:rtype: ImageQuery
Expand Down Expand Up @@ -552,6 +585,7 @@ def ask_async( # noqa: PLR0913 # pylint: disable=too-many-arguments
confidence_threshold=confidence_threshold,
human_review=human_review,
want_async=True,
metadata=metadata,
)

def wait_for_confident_result(
Expand Down
45 changes: 45 additions & 0 deletions src/groundlight/encodings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import base64
import json
import sys
from typing import Dict, Optional, Union


def url_encode_dict(maybe_dict: Union[Dict, str], name: str, size_limit_bytes: Optional[int] = None) -> str:
"""Encode a dictionary as a URL-safe, base64-encoded JSON string.
:param maybe_dict: The dictionary or JSON string to encode.
:type maybe_dict: dict or str
:param name: The name of the dictionary, for use in the error message.
:type name: str
:param size_limit_bytes: The maximum size of the dictionary, in bytes.
If `None`, no size limit is enforced.
:type size_limit_bytes: int or None
:raises TypeError: If `maybe_dict` is not a dictionary or JSON string.
:raises ValueError: If `maybe_dict` is too large.
:return: The URL-safe, base64-encoded JSON string.
:rtype: str
"""
original_type = type(maybe_dict)
if isinstance(maybe_dict, str):
try:
# It's a little inefficient to parse the JSON string, just to re-encode it later. But it
# allows us to check that we get a valid dictionary, and we remove any whitespace.
maybe_dict = json.loads(maybe_dict)
except json.JSONDecodeError as e:
raise TypeError(f"`{name}` must be a dictionary or JSON string: got {original_type}") from e

if not isinstance(maybe_dict, dict):
raise TypeError(f"`{name}` must be a dictionary or JSON string: got {original_type}")

data_json = json.dumps(maybe_dict)

if size_limit_bytes is not None:
size_bytes = sys.getsizeof(data_json)
if size_bytes > size_limit_bytes:
raise ValueError(f"`{name}` is too large: {size_bytes} bytes > {size_limit_bytes} bytes limit.")

return base64.urlsafe_b64encode(data_json.encode("utf-8")).decode("utf-8")
Loading

0 comments on commit 6b978ad

Please sign in to comment.