diff --git a/generated/model.py b/generated/model.py index 1e5f419e..24a7d303 100644 --- a/generated/model.py +++ b/generated/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: public-api.yaml -# timestamp: 2023-11-08T22:11:51+00:00 +# timestamp: 2023-11-09T05:00:29+00:00 from __future__ import annotations diff --git a/src/groundlight/client.py b/src/groundlight/client.py index 3d873f2a..d113a096 100644 --- a/src/groundlight/client.py +++ b/src/groundlight/client.py @@ -290,7 +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: Optional[dict] = None, + metadata: Union[dict, str, None] = None, ) -> ImageQuery: """ Evaluates an image with Groundlight. @@ -336,10 +336,10 @@ 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 of custom key/value metadata to associate with the image - query (limited to 1KB). You can retrieve this metadata later by calling + :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 + :type metadata: dict or str :return: ImageQuery :rtype: ImageQuery @@ -399,7 +399,7 @@ def ask_confident( # noqa: PLR0913 # pylint: disable=too-many-arguments image: Union[str, bytes, Image.Image, BytesIO, BufferedReader, np.ndarray], confidence_threshold: Optional[float] = None, wait: Optional[float] = None, - metadata: Optional[dict] = None, + metadata: Union[dict, str, None] = None, ) -> ImageQuery: """ Evaluates an image with Groundlight waiting until an answer above the confidence threshold @@ -425,10 +425,10 @@ def ask_confident( # noqa: PLR0913 # pylint: disable=too-many-arguments :param wait: How long to wait (in seconds) for a confident answer. :type wait: float - :param metadata: A dictionary of custom key/value metadata to associate with the image - query (limited to 1KB). You can retrieve this metadata later by calling + :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 + :type metadata: dict or str :return: ImageQuery :rtype: ImageQuery @@ -448,7 +448,7 @@ def ask_ml( detector: Union[Detector, str], image: Union[str, bytes, Image.Image, BytesIO, BufferedReader, np.ndarray], wait: Optional[float] = None, - metadata: Optional[dict] = None, + metadata: Union[dict, str, None] = None, ) -> ImageQuery: """ Evaluates an image with Groundlight, getting the first answer Groundlight can provide. @@ -469,10 +469,10 @@ def ask_ml( :param wait: How long to wait (in seconds) for any answer. :type wait: float - :param metadata: A dictionary of custom key/value metadata to associate with the image - query (limited to 1KB). You can retrieve this metadata later by calling + :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 + :type metadata: dict or str :return: ImageQuery :rtype: ImageQuery @@ -495,7 +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: Optional[dict] = None, + metadata: Union[dict, str, None] = None, ) -> ImageQuery: """ Convenience method for submitting an `ImageQuery` asynchronously. This is equivalent to calling @@ -537,10 +537,10 @@ 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 of custom key/value metadata to associate with the image - query (limited to 1KB). You can retrieve this metadata later by calling + :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 + :type metadata: dict or str :return: ImageQuery :rtype: ImageQuery diff --git a/src/groundlight/encodings.py b/src/groundlight/encodings.py index f1fc6cf9..880c3694 100644 --- a/src/groundlight/encodings.py +++ b/src/groundlight/encodings.py @@ -1,13 +1,13 @@ import base64 import json import sys -from typing import Dict, Optional +from typing import Dict, Optional, Union -def url_encode_dict(maybe_dict: Dict, name: str, size_limit_bytes: Optional[int] = None) -> str: +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 to encode. + :param maybe_dict: The dictionary or JSON string to encode. :type maybe_dict: dict :param name: The name of the dictionary, for use in the error message. @@ -17,14 +17,21 @@ def url_encode_dict(maybe_dict: Dict, name: str, size_limit_bytes: Optional[int] If `None`, no size limit is enforced. :type size_limit_bytes: int or None - :raises TypeError: If `maybe_dict` is not a dictionary. + :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: + maybe_dict = json.loads(maybe_dict) + except json.JSONDecodeError: + raise TypeError(f"`{name}` must be a dictionary or JSON string: got {original_type}") + if not isinstance(maybe_dict, dict): - raise TypeError(f"`{name}` must be a dictionary: got {type(maybe_dict)}") + raise TypeError(f"`{name}` must be a dictionary or JSON string: got {original_type}") data_json = json.dumps(maybe_dict) diff --git a/test/integration/test_groundlight.py b/test/integration/test_groundlight.py index 01dc067e..4bded1c2 100644 --- a/test/integration/test_groundlight.py +++ b/test/integration/test_groundlight.py @@ -1,6 +1,7 @@ # Optional star-imports are weird and not usually recommended ... # ruff: noqa: F403,F405 # pylint: disable=wildcard-import,unused-wildcard-import,redefined-outer-name,import-outside-toplevel +import json import time from datetime import datetime from typing import Any, Dict, Optional @@ -251,16 +252,19 @@ def test_submit_image_query_with_human_review_param(gl: Groundlight, detector: D assert is_valid_display_result(_image_query.result) -@pytest.mark.parametrize("metadata", [None, {}, {"a": 1}]) +@pytest.mark.parametrize("metadata", [None, {}, {"a": 1}, '{"a": 1}']) def test_submit_image_query_with_metadata( gl: Groundlight, detector: Detector, image: str, metadata: Optional[Dict[str, Any]] ): + # We expect the returned value to be a dict + expected_metadata: Optional[Dict] = json.loads(metadata) if isinstance(metadata, str) else metadata + iq = gl.submit_image_query(detector=detector.id, image=image, human_review="NEVER", metadata=metadata) - assert iq.metadata == metadata + assert iq.metadata == expected_metadata # Test that we can retrieve the metadata from the server at a later time retrieved_iq = gl.get_image_query(id=iq.id) - assert retrieved_iq.metadata == metadata + assert retrieved_iq.metadata == expected_metadata def test_ask_methods_with_metadata(gl: Groundlight, detector: Detector, image: str):