From de898fa9fb04adf158ce63252edbe54e106ad018 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Mon, 25 Aug 2025 18:43:33 -0700 Subject: [PATCH 1/5] update file upload works --- jigsawstack/validate.py | 77 +++++++++++------- jigsawstack/vision.py | 170 ++++++++++++++++++++++++++++++++-------- 2 files changed, 188 insertions(+), 59 deletions(-) diff --git a/jigsawstack/validate.py b/jigsawstack/validate.py index 2298b00..6d75fc0 100644 --- a/jigsawstack/validate.py +++ b/jigsawstack/validate.py @@ -50,7 +50,6 @@ class NSFWParams(TypedDict): file_store_key: NotRequired[str] - class NSFWResponse(TypedDict): success: bool nsfw: bool @@ -107,25 +106,39 @@ def email(self, params: EmailValidationParams) -> EmailValidationResponse: verb="get", ).perform_with_content() return resp - - def nsfw(self, params: Union[NSFWParams, bytes]) -> NSFWResponse: - path="/validate/nsfw" - if isinstance(params, dict): + + @overload + def nsfw(self, params: NSFWParams) -> NSFWResponse: ... + @overload + def nsfw(self, file: bytes, options: NSFWParams = None) -> NSFWResponse: ... + + def nsfw( + self, + blob: Union[NSFWParams, bytes], + options: NSFWParams = None, + ) -> NSFWResponse: + if isinstance( + blob, dict + ): # If params is provided as a dict, we assume it's the first argument resp = Request( config=self.config, - path=path, - params=cast(Dict[Any, Any], params), + path="/validate/nsfw", + params=cast(Dict[Any, Any], blob), verb="post", ).perform_with_content() return resp - _headers = {"Content-Type": "application/octet-stream"} + options = options or {} + path = build_path(base_path="/validate/nsfw", params=options) + content_type = options.get("content_type", "application/octet-stream") + headers = {"Content-Type": content_type} + resp = Request( config=self.config, path=path, - params={}, #since we're already passing data. - data=params, - headers=_headers, + params=options, + data=blob, + headers=headers, verb="post", ).perform_with_content() return resp @@ -138,9 +151,7 @@ def profanity(self, params: ProfanityParams) -> ProfanityResponse: resp = Request( config=self.config, path=path, - params=cast( - Dict[Any, Any], params - ), + params=cast(Dict[Any, Any], params), verb="post", ).perform_with_content() return resp @@ -198,25 +209,39 @@ async def email(self, params: EmailValidationParams) -> EmailValidationResponse: verb="get", ).perform_with_content() return resp - - async def nsfw(self, params: Union[NSFWParams, bytes]) -> NSFWResponse: - path="/validate/nsfw" - if isinstance(params, dict): + + @overload + async def nsfw(self, params: NSFWParams) -> NSFWResponse: ... + @overload + async def nsfw(self, file: bytes, options: NSFWParams = None) -> NSFWResponse: ... + + async def nsfw( + self, + blob: Union[NSFWParams, bytes], + options: NSFWParams = None, + ) -> NSFWResponse: + if isinstance( + blob, dict + ): # If params is provided as a dict, we assume it's the first argument resp = await AsyncRequest( config=self.config, - path=path, - params=cast(Dict[Any, Any], params), + path="/validate/nsfw", + params=cast(Dict[Any, Any], blob), verb="post", ).perform_with_content() return resp - _headers = {"Content-Type": "application/octet-stream"} + options = options or {} + path = build_path(base_path="/validate/nsfw", params=options) + content_type = options.get("content_type", "application/octet-stream") + headers = {"Content-Type": content_type} + resp = await AsyncRequest( config=self.config, path=path, - params={}, - data=params, - headers=_headers, + params=options, + data=blob, + headers=headers, verb="post", ).perform_with_content() return resp @@ -229,9 +254,7 @@ async def profanity(self, params: ProfanityParams) -> ProfanityResponse: resp = await AsyncRequest( config=self.config, path=path, - params=cast( - Dict[Any, Any], params - ), + params=cast(Dict[Any, Any], params), verb="post", ).perform_with_content() return resp diff --git a/jigsawstack/vision.py b/jigsawstack/vision.py index f957615..ec0e44f 100644 --- a/jigsawstack/vision.py +++ b/jigsawstack/vision.py @@ -1,10 +1,9 @@ -from typing import Any, Dict, List, Union, cast, Optional -from typing_extensions import NotRequired, TypedDict, Literal -from typing import Any, Dict, List, cast +from typing import Any, Dict, List, Union, cast, Optional, overload from typing_extensions import NotRequired, TypedDict, Literal from .request import Request, RequestConfig from .async_request import AsyncRequest, AsyncRequestConfig from ._config import ClientConfig +from .helpers import build_path class Point(TypedDict): @@ -12,7 +11,7 @@ class Point(TypedDict): """ X coordinate of the point """ - + y: int """ Y coordinate of the point @@ -24,27 +23,27 @@ class BoundingBox(TypedDict): """ Top-left corner of the bounding box """ - + top_right: Point """ Top-right corner of the bounding box """ - + bottom_left: Point """ Bottom-left corner of the bounding box """ - + bottom_right: Point """ Bottom-right corner of the bounding box """ - + width: int """ Width of the bounding box """ - + height: int """ Height of the bounding box @@ -56,7 +55,7 @@ class GuiElement(TypedDict): """ Bounding box coordinates of the GUI element """ - + content: Union[str, None] """ Content of the GUI element, can be null if no object detected @@ -68,40 +67,39 @@ class DetectedObject(TypedDict): """ Bounding box coordinates of the detected object """ - + mask: NotRequired[str] """ URL or base64 string depending on return_type - only present for some objects """ - class ObjectDetectionParams(TypedDict): url: NotRequired[str] """ URL of the image to process """ - + file_store_key: NotRequired[str] """ File store key of the image to process """ - + prompts: NotRequired[List[str]] """ List of prompts for object detection """ - + features: NotRequired[List[Literal["object_detection", "gui"]]] """ List of features to enable: object_detection, gui """ - + annotated_image: NotRequired[bool] """ Whether to return an annotated image """ - + return_type: NotRequired[Literal["url", "base64"]] """ Format for returned images: url or base64 @@ -113,12 +111,12 @@ class ObjectDetectionResponse(TypedDict): """ URL or base64 string of annotated image (included only if annotated_image=true and objects/gui_elements exist) """ - + gui_elements: NotRequired[List[GuiElement]] """ List of detected GUI elements (included only if features includes "gui") """ - + objects: NotRequired[List[DetectedObject]] """ List of detected objects (included only if features includes "object_detection") @@ -163,22 +161,76 @@ def __init__( disable_request_logging=disable_request_logging, ) - def vocr(self, params: VOCRParams) -> OCRResponse: - path = "/vocr" + @overload + def vocr(self, params: VOCRParams) -> OCRResponse: ... + @overload + def vocr(self, file: bytes, options: VOCRParams = None) -> VOCRParams: ... + + def vocr( + self, + blob: Union[VOCRParams, bytes], + options: VOCRParams = None, + ) -> OCRResponse: + if isinstance( + blob, dict + ): # If params is provided as a dict, we assume it's the first argument + resp = Request( + config=self.config, + path="/vocr", + params=cast(Dict[Any, Any], blob), + verb="post", + ).perform_with_content() + return resp + + options = options or {} + path = build_path(base_path="/vocr", params=options) + content_type = options.get("content_type", "application/octet-stream") + headers = {"Content-Type": content_type} + resp = Request( config=self.config, path=path, - params=cast(Dict[Any, Any], params), + params=options, + data=blob, + headers=headers, verb="post", ).perform_with_content() return resp - def object_detection(self, params: ObjectDetectionParams) -> ObjectDetectionResponse: - path = "/object_detection" + @overload + def object_detection( + self, params: ObjectDetectionParams + ) -> ObjectDetectionResponse: ... + @overload + def object_detection( + self, file: bytes, options: ObjectDetectionParams = None + ) -> ObjectDetectionResponse: ... + + def object_detection( + self, + blob: Union[ObjectDetectionParams, bytes], + options: ObjectDetectionParams = None, + ) -> ObjectDetectionResponse: + if isinstance(blob, dict): + resp = Request( + config=self.config, + path="/object_detection", + params=cast(Dict[Any, Any], blob), + verb="post", + ).perform_with_content() + return resp + + options = options or {} + path = build_path(base_path="/object_detection", params=options) + content_type = options.get("content_type", "application/octet-stream") + headers = {"Content-Type": content_type} + resp = Request( config=self.config, path=path, - params=cast(Dict[Any, Any], params), + params=options, + data=blob, + headers=headers, verb="post", ).perform_with_content() return resp @@ -200,22 +252,76 @@ def __init__( disable_request_logging=disable_request_logging, ) - async def vocr(self, params: VOCRParams) -> OCRResponse: - path = "/vocr" - resp = await AsyncRequest( + @overload + async def vocr(self, params: VOCRParams) -> OCRResponse: ... + @overload + async def vocr(self, file: bytes, options: VOCRParams = None) -> VOCRParams: ... + + async def vocr( + self, + blob: Union[VOCRParams, bytes], + options: VOCRParams = None, + ) -> OCRResponse: + if isinstance(blob, dict): + resp = AsyncRequest( + config=self.config, + path="/vocr", + params=cast(Dict[Any, Any], blob), + verb="post", + ).perform_with_content() + return resp + + options = options or {} + path = build_path(base_path="/vocr", params=options) + content_type = options.get("content_type", "application/octet-stream") + headers = {"Content-Type": content_type} + + resp = Request( config=self.config, path=path, - params=cast(Dict[Any, Any], params), + params=options, + data=blob, + headers=headers, verb="post", ).perform_with_content() return resp - async def object_detection(self, params: ObjectDetectionParams) -> ObjectDetectionResponse: - path = "/object_detection" + @overload + async def object_detection( + self, params: ObjectDetectionParams + ) -> ObjectDetectionResponse: ... + @overload + async def object_detection( + self, file: bytes, options: ObjectDetectionParams = None + ) -> ObjectDetectionResponse: ... + + async def object_detection( + self, + blob: Union[ObjectDetectionParams, bytes], + options: ObjectDetectionParams = None, + ) -> ObjectDetectionResponse: + if isinstance( + blob, dict + ): # If params is provided as a dict, we assume it's the first argument + resp = await AsyncRequest( + config=self.config, + path="/object_detection", + params=cast(Dict[Any, Any], blob), + verb="post", + ).perform_with_content() + return resp + + options = options or {} + path = build_path(base_path="/object_detection", params=options) + content_type = options.get("content_type", "application/octet-stream") + headers = {"Content-Type": content_type} + resp = await AsyncRequest( config=self.config, path=path, - params=cast(Dict[Any, Any], params), + params=options, + data=blob, + headers=headers, verb="post", ).perform_with_content() return resp From e01b86e4159e1e7bcd8e876946e5afefbf69fbdd Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Mon, 25 Aug 2025 20:49:58 -0700 Subject: [PATCH 2/5] request headers for no logging works --- jigsawstack/request.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/jigsawstack/request.py b/jigsawstack/request.py index 624b31a..d37c186 100644 --- a/jigsawstack/request.py +++ b/jigsawstack/request.py @@ -12,7 +12,7 @@ class RequestConfig(TypedDict): api_url: str api_key: str - disable_request_logging: Union[bool, None] = False + headers: Dict[str, str] # This class wraps the HTTP request creation logic @@ -34,7 +34,9 @@ def __init__( self.api_key = config.get("api_key") self.data = data self.headers = headers - self.disable_request_logging = config.get("disable_request_logging") + self.disable_request_logging = config.get("headers", {}).get( + "x-jigsaw-no-request-log" + ) self.stream = stream def perform(self) -> Union[T, None]: @@ -50,13 +52,16 @@ def perform(self) -> Union[T, None]: """ resp = self.make_request(url=f"{self.api_url}{self.path}") - #for binary responses + # for binary responses if resp.status_code == 200: content_type = resp.headers.get("content-type", "") - if not resp.text or any(t in content_type for t in ["audio/", "image/", "application/octet-stream", "image/png"]): + if not resp.text or any( + t in content_type + for t in ["audio/", "image/", "application/octet-stream", "image/png"] + ): return cast(T, resp.content) - #for json resposes. + # for json resposes. if resp.status_code != 200: try: error = resp.json() @@ -94,7 +99,6 @@ def perform_file(self) -> Union[T, None]: raise_for_code_and_type( code=500, message="Failed to parse JigsawStack API response. Please try again.", - error_type="InternalServerError", ) if resp.status_code != 200: @@ -105,7 +109,7 @@ def perform_file(self) -> Union[T, None]: err=error.get("error"), ) - #for binary responses + # for binary responses if resp.status_code == 200: content_type = resp.headers.get("content-type", "") if "application/json" not in content_type: From 9e14bc4092fd0850c0650972fdccc033a66bed2d Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 27 Aug 2025 13:21:54 -0700 Subject: [PATCH 3/5] update to be in init instead of request --- jigsawstack/__init__.py | 12 +++++++++--- jigsawstack/async_request.py | 18 ++++++++++++++---- jigsawstack/request.py | 6 ++---- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/jigsawstack/__init__.py b/jigsawstack/__init__.py index e6a2f5f..3702bdb 100644 --- a/jigsawstack/__init__.py +++ b/jigsawstack/__init__.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Dict import os from .audio import Audio, AsyncAudio from .vision import Vision, AsyncVision @@ -27,13 +27,15 @@ class JigsawStack: classification: Classification api_key: str api_url: str - disable_request_logging: bool + headers: Dict[str, str] + # disable_request_logging: bool def __init__( self, api_key: Union[str, None] = None, api_url: Union[str, None] = None, - disable_request_logging: Union[bool, None] = None, + # disable_request_logging: Union[bool, None] = None, + headers: Union[Dict[str, str], None] = None, ) -> None: if api_key is None: api_key = os.environ.get("JIGSAWSTACK_API_KEY") @@ -51,6 +53,10 @@ def __init__( self.api_key = api_key self.api_url = api_url + self.headers = headers or {} + + disable_request_logging = self.headers.get("x-jigsaw-no-request-log") + self.audio = Audio( api_key=api_key, api_url=api_url, diff --git a/jigsawstack/async_request.py b/jigsawstack/async_request.py index fd48f20..033b39b 100644 --- a/jigsawstack/async_request.py +++ b/jigsawstack/async_request.py @@ -36,7 +36,9 @@ def __init__( self.disable_request_logging = config.get("disable_request_logging") self.stream = stream - def __convert_params(self, params: Union[Dict[Any, Any], List[Dict[Any, Any]]]) -> Dict[str, str]: + def __convert_params( + self, params: Union[Dict[Any, Any], List[Dict[Any, Any]]] + ) -> Dict[str, str]: """ Convert parameters to string values for URL encoding. """ @@ -45,10 +47,10 @@ def __convert_params(self, params: Union[Dict[Any, Any], List[Dict[Any, Any]]]) if isinstance(params, str): return params - + if isinstance(params, list): return {} # List params are only used in JSON body - + converted = {} for key, value in params.items(): if isinstance(value, bool): @@ -67,7 +69,15 @@ async def perform(self) -> Union[T, None]: # For binary responses if resp.status == 200: content_type = resp.headers.get("content-type", "") - if not resp.text or any(t in content_type for t in ["audio/", "image/", "application/octet-stream", "image/png"]): + if not resp.text or any( + t in content_type + for t in [ + "audio/", + "image/", + "application/octet-stream", + "image/png", + ] + ): content = await resp.read() return cast(T, content) diff --git a/jigsawstack/request.py b/jigsawstack/request.py index d37c186..e947f4c 100644 --- a/jigsawstack/request.py +++ b/jigsawstack/request.py @@ -12,7 +12,7 @@ class RequestConfig(TypedDict): api_url: str api_key: str - headers: Dict[str, str] + disable_request_logging: Union[bool, None] = False # This class wraps the HTTP request creation logic @@ -34,9 +34,7 @@ def __init__( self.api_key = config.get("api_key") self.data = data self.headers = headers - self.disable_request_logging = config.get("headers", {}).get( - "x-jigsaw-no-request-log" - ) + self.disable_request_logging = config.get("disable_request_logging") self.stream = stream def perform(self) -> Union[T, None]: From dd73baa14e80e8e006713fb5b573514981381587 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Thu, 28 Aug 2025 07:39:27 -0700 Subject: [PATCH 4/5] revert back to old request --- jigsawstack/request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/jigsawstack/request.py b/jigsawstack/request.py index e947f4c..55e8e7f 100644 --- a/jigsawstack/request.py +++ b/jigsawstack/request.py @@ -97,6 +97,7 @@ def perform_file(self) -> Union[T, None]: raise_for_code_and_type( code=500, message="Failed to parse JigsawStack API response. Please try again.", + error_type="InternalServerError", ) if resp.status_code != 200: From cb9979a85852f1b71d98f291606cb0781b4d766f Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Thu, 28 Aug 2025 09:48:22 -0700 Subject: [PATCH 5/5] update overload to be blob consistent throughout --- jigsawstack/audio.py | 6 ++---- jigsawstack/embedding.py | 4 ++-- jigsawstack/translate.py | 4 ++-- jigsawstack/validate.py | 4 ++-- jigsawstack/vision.py | 12 ++++++------ 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/jigsawstack/audio.py b/jigsawstack/audio.py index e3bcab7..c3d37e7 100644 --- a/jigsawstack/audio.py +++ b/jigsawstack/audio.py @@ -9,7 +9,6 @@ from .helpers import build_path - class SpeechToTextParams(TypedDict): url: NotRequired[str] file_store_key: NotRequired[str] @@ -57,7 +56,7 @@ def __init__( def speech_to_text(self, params: SpeechToTextParams) -> SpeechToTextResponse: ... @overload def speech_to_text( - self, file: bytes, options: Optional[SpeechToTextParams] = None + self, blob: bytes, options: Optional[SpeechToTextParams] = None ) -> SpeechToTextResponse: ... def speech_to_text( @@ -114,7 +113,7 @@ async def speech_to_text( ) -> SpeechToTextResponse: ... @overload async def speech_to_text( - self, file: bytes, options: Optional[SpeechToTextParams] = None + self, blob: bytes, options: Optional[SpeechToTextParams] = None ) -> SpeechToTextResponse: ... async def speech_to_text( @@ -145,4 +144,3 @@ async def speech_to_text( verb="post", ).perform_with_content() return resp - diff --git a/jigsawstack/embedding.py b/jigsawstack/embedding.py index a9c4d05..be09660 100644 --- a/jigsawstack/embedding.py +++ b/jigsawstack/embedding.py @@ -42,7 +42,7 @@ def __init__( @overload def execute(self, params: EmbeddingParams) -> EmbeddingResponse: ... @overload - def execute(self, file: bytes, options: EmbeddingParams = None) -> EmbeddingResponse: ... + def execute(self, blob: bytes, options: EmbeddingParams = None) -> EmbeddingResponse: ... def execute( self, @@ -95,7 +95,7 @@ def __init__( @overload async def execute(self, params: EmbeddingParams) -> EmbeddingResponse: ... @overload - async def execute(self, file: bytes, options: EmbeddingParams = None) -> EmbeddingResponse: ... + async def execute(self, blob: bytes, options: EmbeddingParams = None) -> EmbeddingResponse: ... async def execute( self, diff --git a/jigsawstack/translate.py b/jigsawstack/translate.py index 9b0bcfb..885cc0c 100644 --- a/jigsawstack/translate.py +++ b/jigsawstack/translate.py @@ -98,7 +98,7 @@ def text( @overload def image(self, params: TranslateImageParams) -> TranslateImageResponse: ... @overload - def image(self, file: bytes, options: TranslateImageParams = None) -> TranslateImageParams: ... + def image(self, blob: bytes, options: TranslateImageParams = None) -> TranslateImageParams: ... def image( self, @@ -160,7 +160,7 @@ async def text( @overload async def image(self, params: TranslateImageParams) -> TranslateImageResponse: ... @overload - async def image(self, file: bytes, options: TranslateImageParams = None) -> TranslateImageParams: ... + async def image(self, blob: bytes, options: TranslateImageParams = None) -> TranslateImageParams: ... async def image( self, diff --git a/jigsawstack/validate.py b/jigsawstack/validate.py index 6d75fc0..11f899d 100644 --- a/jigsawstack/validate.py +++ b/jigsawstack/validate.py @@ -110,7 +110,7 @@ def email(self, params: EmailValidationParams) -> EmailValidationResponse: @overload def nsfw(self, params: NSFWParams) -> NSFWResponse: ... @overload - def nsfw(self, file: bytes, options: NSFWParams = None) -> NSFWResponse: ... + def nsfw(self, blob: bytes, options: NSFWParams = None) -> NSFWResponse: ... def nsfw( self, @@ -213,7 +213,7 @@ async def email(self, params: EmailValidationParams) -> EmailValidationResponse: @overload async def nsfw(self, params: NSFWParams) -> NSFWResponse: ... @overload - async def nsfw(self, file: bytes, options: NSFWParams = None) -> NSFWResponse: ... + async def nsfw(self, blob: bytes, options: NSFWParams = None) -> NSFWResponse: ... async def nsfw( self, diff --git a/jigsawstack/vision.py b/jigsawstack/vision.py index 06ff9fb..3974e36 100644 --- a/jigsawstack/vision.py +++ b/jigsawstack/vision.py @@ -164,7 +164,7 @@ def __init__( @overload def vocr(self, params: VOCRParams) -> OCRResponse: ... @overload - def vocr(self, file: bytes, options: VOCRParams = None) -> VOCRParams: ... + def vocr(self, blob: bytes, options: VOCRParams = None) -> OCRResponse: ... def vocr( self, @@ -203,7 +203,7 @@ def object_detection( ) -> ObjectDetectionResponse: ... @overload def object_detection( - self, file: bytes, options: ObjectDetectionParams = None + self, blob: bytes, options: ObjectDetectionParams = None ) -> ObjectDetectionResponse: ... def object_detection( @@ -255,7 +255,7 @@ def __init__( @overload async def vocr(self, params: VOCRParams) -> OCRResponse: ... @overload - async def vocr(self, file: bytes, options: VOCRParams = None) -> VOCRParams: ... + async def vocr(self, blob: bytes, options: VOCRParams = None) -> OCRResponse: ... async def vocr( self, @@ -263,7 +263,7 @@ async def vocr( options: VOCRParams = None, ) -> OCRResponse: if isinstance(blob, dict): - resp = AsyncRequest( + resp = await AsyncRequest( config=self.config, path="/vocr", params=cast(Dict[Any, Any], blob), @@ -276,7 +276,7 @@ async def vocr( content_type = options.get("content_type", "application/octet-stream") headers = {"Content-Type": content_type} - resp = Request( + resp = await AsyncRequest( config=self.config, path=path, params=options, @@ -292,7 +292,7 @@ async def object_detection( ) -> ObjectDetectionResponse: ... @overload async def object_detection( - self, file: bytes, options: ObjectDetectionParams = None + self, blob: bytes, options: ObjectDetectionParams = None ) -> ObjectDetectionResponse: ... async def object_detection(