From d4cd423079a7a563a908158f2cab2ee17572226f Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 20 Aug 2025 10:52:10 -0700 Subject: [PATCH 1/4] classification added --- jigsawstack/__init__.py | 14 +++ jigsawstack/classification.py | 180 ++++++++++++++++++++++++++++++++++ tests/test_classification.py | 129 ++++++++++++++++++++++++ tests/test_file_store.py | 67 +++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 jigsawstack/classification.py create mode 100644 tests/test_classification.py create mode 100644 tests/test_file_store.py diff --git a/jigsawstack/__init__.py b/jigsawstack/__init__.py index bdb102e..8dafca5 100644 --- a/jigsawstack/__init__.py +++ b/jigsawstack/__init__.py @@ -15,6 +15,7 @@ from .embedding import Embedding, AsyncEmbedding from .exceptions import JigsawStackError from .image_generation import ImageGeneration, AsyncImageGeneration +from .classification import Classification, AsyncClassification class JigsawStack: @@ -25,6 +26,7 @@ class JigsawStack: web: Web search: Search prompt_engine: PromptEngine + classification: Classification api_key: str api_url: str disable_request_logging: bool @@ -118,6 +120,12 @@ def __init__( disable_request_logging=disable_request_logging, ).image_generation + self.classification = Classification( + api_key=api_key, + api_url=api_url, + disable_request_logging=disable_request_logging, + ) + class AsyncJigsawStack: @@ -229,6 +237,12 @@ def __init__( disable_request_logging=disable_request_logging, ).image_generation + self.classification = AsyncClassification( + api_key=api_key, + api_url=api_url, + disable_request_logging=disable_request_logging, + ) + # Create a global instance of the Web class diff --git a/jigsawstack/classification.py b/jigsawstack/classification.py new file mode 100644 index 0000000..69ed199 --- /dev/null +++ b/jigsawstack/classification.py @@ -0,0 +1,180 @@ +from typing import Any, Dict, List, Union, cast +from typing_extensions import NotRequired, TypedDict, Literal +from .request import Request, RequestConfig +from .async_request import AsyncRequest, AsyncRequestConfig +from ._config import ClientConfig + + +class DatasetItemText(TypedDict): + type: Literal["text"] + """ + Type of the dataset item: text + """ + + value: str + """ + Value of the dataset item + """ + + +class DatasetItemImage(TypedDict): + type: Literal["image"] + """ + Type of the dataset item: image + """ + + value: str + """ + Value of the dataset item + """ + + +class LabelItemText(TypedDict): + key: NotRequired[str] + """ + Optional key for the label + """ + + type: Literal["text"] + """ + Type of the label: text + """ + + value: str + """ + Value of the label + """ + + +class LabelItemImage(TypedDict): + key: NotRequired[str] + """ + Optional key for the label + """ + + type: Literal["image", "text"] + """ + Type of the label: image or text + """ + + value: str + """ + Value of the label + """ + + +class ClassificationTextParams(TypedDict): + dataset: List[DatasetItemText] + """ + List of text dataset items to classify + """ + + labels: List[LabelItemText] + """ + List of text labels for classification + """ + + multiple_labels: NotRequired[bool] + """ + Whether to allow multiple labels per item + """ + + +class ClassificationImageParams(TypedDict): + dataset: List[DatasetItemImage] + """ + List of image dataset items to classify + """ + + labels: List[LabelItemImage] + """ + List of labels for classification + """ + + multiple_labels: NotRequired[bool] + """ + Whether to allow multiple labels per item + """ + + +class ClassificationResponse(TypedDict): + predictions: List[Union[str, List[str]]] + """ + Classification predictions - single labels or multiple labels per item + """ + + + +class Classification(ClientConfig): + + config: RequestConfig + + def __init__( + self, + api_key: str, + api_url: str, + disable_request_logging: Union[bool, None] = False, + ): + super().__init__(api_key, api_url, disable_request_logging) + self.config = RequestConfig( + api_url=api_url, + api_key=api_key, + disable_request_logging=disable_request_logging, + ) + + def text(self, params: ClassificationTextParams) -> ClassificationResponse: + path = "/classification" + resp = Request( + config=self.config, + path=path, + params=cast(Dict[Any, Any], params), + verb="post", + ).perform_with_content() + return resp + def image(self, params: ClassificationImageParams) -> ClassificationResponse: + path = "/classification" + resp = Request( + config=self.config, + path=path, + params=cast(Dict[Any, Any], params), + verb="post", + ).perform_with_content() + return resp + + + +class AsyncClassification(ClientConfig): + config: AsyncRequestConfig + + def __init__( + self, + api_key: str, + api_url: str, + disable_request_logging: Union[bool, None] = False, + ): + super().__init__(api_key, api_url, disable_request_logging) + self.config = AsyncRequestConfig( + api_url=api_url, + api_key=api_key, + disable_request_logging=disable_request_logging, + ) + + async def text(self, params: ClassificationTextParams) -> ClassificationResponse: + path = "/classification" + resp = await AsyncRequest( + config=self.config, + path=path, + params=cast(Dict[Any, Any], params), + verb="post", + ).perform_with_content() + return resp + + async def image(self, params: ClassificationImageParams) -> ClassificationResponse: + path = "/classification" + resp = await AsyncRequest( + config=self.config, + path=path, + params=cast(Dict[Any, Any], params), + verb="post", + ).perform_with_content() + return resp \ No newline at end of file diff --git a/tests/test_classification.py b/tests/test_classification.py new file mode 100644 index 0000000..4908fe0 --- /dev/null +++ b/tests/test_classification.py @@ -0,0 +1,129 @@ +from unittest.mock import MagicMock +import unittest +from jigsawstack.exceptions import JigsawStackError +from jigsawstack import JigsawStack, AsyncJigsawStack +import asyncio +import logging + +import pytest + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +client = JigsawStack() +async_client = AsyncJigsawStack() + + +class TestClassificationAPI(unittest.TestCase): + def test_classification_text_success_response(self): + params = { + "dataset": [ + {"type": "text", "value": "Hello"}, + {"type": "text", "value": "World"} + ], + "labels": [ + {"type": "text", "value": "Greeting"}, + {"type": "text", "value": "Object"} + ] + } + try: + result = client.classification.text(params) + assert result["success"] == True + except JigsawStackError as e: + pytest.fail(f"Unexpected JigsawStackError: {e}") + + def test_classification_text_async_success_response(self): + async def _test(): + params = { + "dataset": [ + {"type": "text", "value": "Hello"}, + {"type": "text", "value": "World"} + ], + "labels": [ + {"type": "text", "value": "Greeting"}, + {"type": "text", "value": "Object"} + ] + } + try: + result = await async_client.classification.text(params) + assert result["success"] == True + except JigsawStackError as e: + pytest.fail(f"Unexpected JigsawStackError: {e}") + + asyncio.run(_test()) + + def test_classification_text_with_multiple_labels(self): + params = { + "dataset": [ + {"type": "text", "value": "This is a positive and happy message"} + ], + "labels": [ + {"type": "text", "value": "positive"}, + {"type": "text", "value": "negative"}, + {"type": "text", "value": "happy"}, + {"type": "text", "value": "sad"} + ], + "multiple_labels": True + } + try: + result = client.classification.text(params) + assert result["success"] == True + except JigsawStackError as e: + pytest.fail(f"Unexpected JigsawStackError: {e}") + + def test_classification_image_success_response(self): + params = { + "dataset": [ + {"type": "image", "value": "https://example.com/image1.jpg"}, + {"type": "image", "value": "https://example.com/image2.jpg"} + ], + "labels": [ + {"type": "text", "value": "Cat"}, + {"type": "text", "value": "Dog"} + ] + } + try: + result = client.classification.image(params) + assert result["success"] == True + except JigsawStackError as e: + pytest.fail(f"Unexpected JigsawStackError: {e}") + + def test_classification_image_async_success_response(self): + async def _test(): + params = { + "dataset": [ + {"type": "image", "value": "https://example.com/image1.jpg"}, + {"type": "image", "value": "https://example.com/image2.jpg"} + ], + "labels": [ + {"type": "text", "value": "Cat"}, + {"type": "text", "value": "Dog"} + ] + } + try: + result = await async_client.classification.image(params) + assert result["success"] == True + except JigsawStackError as e: + pytest.fail(f"Unexpected JigsawStackError: {e}") + + asyncio.run(_test()) + + def test_classification_image_with_multiple_labels(self): + params = { + "dataset": [ + {"type": "image", "value": "https://example.com/pet_image.jpg"} + ], + "labels": [ + {"type": "text", "value": "cute"}, + {"type": "text", "value": "fluffy"}, + {"type": "text", "value": "animal"}, + {"type": "text", "value": "pet"} + ], + "multiple_labels": True + } + try: + result = client.classification.image(params) + assert result["success"] == True + except JigsawStackError as e: + pytest.fail(f"Unexpected JigsawStackError: {e}") + diff --git a/tests/test_file_store.py b/tests/test_file_store.py new file mode 100644 index 0000000..3f346d9 --- /dev/null +++ b/tests/test_file_store.py @@ -0,0 +1,67 @@ +from unittest.mock import MagicMock +import unittest +from jigsawstack.exceptions import JigsawStackError +from jigsawstack import JigsawStack + +import pytest + +# flake8: noqa + +client = JigsawStack() + + +@pytest.mark.skip(reason="Skipping TestStoreAPI class for now") +class TestStoreAPI(unittest.TestCase): + def test_upload_success_response(self) -> None: + # Sample file content as bytes + file_content = b"This is a test file content" + options = { + "key": "test-file.txt", + "content_type": "text/plain", + "overwrite": True, + "temp_public_url": True + } + try: + result = client.store.upload(file_content, options) + assert result["success"] == True + except JigsawStackError as e: + assert e.message == "Failed to parse API response. Please try again." + + def test_get_success_response(self) -> None: + key = "test-file.txt" + try: + result = client.store.get(key) + # For file retrieval, we expect the actual file content + assert result is not None + except JigsawStackError as e: + assert e.message == "Failed to parse API response. Please try again." + + def test_delete_success_response(self) -> None: + key = "test-file.txt" + try: + result = client.store.delete(key) + assert result["success"] == True + except JigsawStackError as e: + assert e.message == "Failed to parse API response. Please try again." + + def test_upload_without_options_success_response(self) -> None: + # Test upload without optional parameters + file_content = b"This is another test file content" + try: + result = client.store.upload(file_content) + assert result["success"] == True + except JigsawStackError as e: + assert e.message == "Failed to parse API response. Please try again." + + def test_upload_with_partial_options_success_response(self) -> None: + # Test upload with partial options + file_content = b"This is a test file with partial options" + options = { + "key": "partial-test-file.txt", + "overwrite": False + } + try: + result = client.store.upload(file_content, options) + assert result["success"] == True + except JigsawStackError as e: + assert e.message == "Failed to parse API response. Please try again." From 17424fc50149130a9196773630dc6b979c0769f7 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 20 Aug 2025 11:49:00 -0700 Subject: [PATCH 2/4] test --- tests/test_classification.py | 117 ++++++++--------------------------- 1 file changed, 25 insertions(+), 92 deletions(-) diff --git a/tests/test_classification.py b/tests/test_classification.py index 4908fe0..e8a5235 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -1,129 +1,62 @@ from unittest.mock import MagicMock import unittest from jigsawstack.exceptions import JigsawStackError -from jigsawstack import JigsawStack, AsyncJigsawStack -import asyncio -import logging +from jigsawstack import JigsawStack -import pytest +import pytest -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +# flake8: noqa client = JigsawStack() -async_client = AsyncJigsawStack() -class TestClassificationAPI(unittest.TestCase): - def test_classification_text_success_response(self): +@pytest.mark.skip(reason="Skipping TestWebAPI class for now") +class TestClassification(unittest.TestCase): + def test_classification_text_success_response(self) -> None: params = { "dataset": [ - {"type": "text", "value": "Hello"}, - {"type": "text", "value": "World"} + {"type": "text", "value": "I love programming"}, + {"type": "text", "value": "I love reading books"}, + {"type": "text", "value": "I love watching movies"}, + {"type": "text", "value": "I love playing games"}, ], "labels": [ - {"type": "text", "value": "Greeting"}, - {"type": "text", "value": "Object"} - ] - } - try: - result = client.classification.text(params) - assert result["success"] == True - except JigsawStackError as e: - pytest.fail(f"Unexpected JigsawStackError: {e}") - - def test_classification_text_async_success_response(self): - async def _test(): - params = { - "dataset": [ - {"type": "text", "value": "Hello"}, - {"type": "text", "value": "World"} - ], - "labels": [ - {"type": "text", "value": "Greeting"}, - {"type": "text", "value": "Object"} - ] - } - try: - result = await async_client.classification.text(params) - assert result["success"] == True - except JigsawStackError as e: - pytest.fail(f"Unexpected JigsawStackError: {e}") - - asyncio.run(_test()) - - def test_classification_text_with_multiple_labels(self): - params = { - "dataset": [ - {"type": "text", "value": "This is a positive and happy message"} + {"type": "text", "value": "programming"}, + {"type": "text", "value": "reading"}, + {"type": "text", "value": "watching"}, + {"type": "text", "value": "playing"}, ], - "labels": [ - {"type": "text", "value": "positive"}, - {"type": "text", "value": "negative"}, - {"type": "text", "value": "happy"}, - {"type": "text", "value": "sad"} - ], - "multiple_labels": True } try: result = client.classification.text(params) assert result["success"] == True except JigsawStackError as e: - pytest.fail(f"Unexpected JigsawStackError: {e}") + assert e.message == "Failed to parse API response. Please try again." - def test_classification_image_success_response(self): + def test_classification_image_success_response(self) -> None: params = { "dataset": [ - {"type": "image", "value": "https://example.com/image1.jpg"}, - {"type": "image", "value": "https://example.com/image2.jpg"} + {"type": "image", "value": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ4aBhloyMLx5qA6G6wSEi0s9AvDu1r7utrbQ&s"}, + {"type": "image", "value": "https://cdn.britannica.com/79/232779-050-6B0411D7/German-Shepherd-dog-Alsatian.jpg"}, ], "labels": [ {"type": "text", "value": "Cat"}, - {"type": "text", "value": "Dog"} - ] + {"type": "text", "value": "Dog"}, + ], } try: result = client.classification.image(params) assert result["success"] == True except JigsawStackError as e: - pytest.fail(f"Unexpected JigsawStackError: {e}") + assert e.message == "Failed to parse API response. Please try again." - def test_classification_image_async_success_response(self): - async def _test(): - params = { - "dataset": [ - {"type": "image", "value": "https://example.com/image1.jpg"}, - {"type": "image", "value": "https://example.com/image2.jpg"} - ], - "labels": [ - {"type": "text", "value": "Cat"}, - {"type": "text", "value": "Dog"} - ] - } - try: - result = await async_client.classification.image(params) - assert result["success"] == True - except JigsawStackError as e: - pytest.fail(f"Unexpected JigsawStackError: {e}") + def test_dns_success_response(self) -> None: - asyncio.run(_test()) - - def test_classification_image_with_multiple_labels(self): params = { - "dataset": [ - {"type": "image", "value": "https://example.com/pet_image.jpg"} - ], - "labels": [ - {"type": "text", "value": "cute"}, - {"type": "text", "value": "fluffy"}, - {"type": "text", "value": "animal"}, - {"type": "text", "value": "pet"} - ], - "multiple_labels": True + "url": "https://supabase.com/pricing", } try: - result = client.classification.image(params) + result = client.web.dns(params) assert result["success"] == True except JigsawStackError as e: - pytest.fail(f"Unexpected JigsawStackError: {e}") - + assert e.message == "Failed to parse API response. Please try again." \ No newline at end of file From 5205cc161c2ca23a0c8c27f2e686a9f3cbfcc521 Mon Sep 17 00:00:00 2001 From: Win Cheng Date: Wed, 20 Aug 2025 12:43:29 -0700 Subject: [PATCH 3/4] updated test --- .gitignore | 7 +- tests/test_classification.py | 128 +++++++++++++++++++++-------------- 2 files changed, 84 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index bd39df3..356d525 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,9 @@ test.py test_web.py .eggs/ -.conda/ \ No newline at end of file +.conda/ + +main.py +.python-version +pyproject.toml +uv.lock \ No newline at end of file diff --git a/tests/test_classification.py b/tests/test_classification.py index e8a5235..1f247c5 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -1,5 +1,3 @@ -from unittest.mock import MagicMock -import unittest from jigsawstack.exceptions import JigsawStackError from jigsawstack import JigsawStack @@ -10,53 +8,83 @@ client = JigsawStack() -@pytest.mark.skip(reason="Skipping TestWebAPI class for now") -class TestClassification(unittest.TestCase): - def test_classification_text_success_response(self) -> None: - params = { - "dataset": [ - {"type": "text", "value": "I love programming"}, - {"type": "text", "value": "I love reading books"}, - {"type": "text", "value": "I love watching movies"}, - {"type": "text", "value": "I love playing games"}, - ], - "labels": [ - {"type": "text", "value": "programming"}, - {"type": "text", "value": "reading"}, - {"type": "text", "value": "watching"}, - {"type": "text", "value": "playing"}, - ], - } - try: - result = client.classification.text(params) - assert result["success"] == True - except JigsawStackError as e: - assert e.message == "Failed to parse API response. Please try again." +@pytest.mark.parametrize("dataset,labels", [ + ( + [ + {"type": "text", "value": "I love programming"}, + {"type": "text", "value": "I love reading books"}, + {"type": "text", "value": "I love watching movies"}, + {"type": "text", "value": "I love playing games"}, + ], + [ + {"type": "text", "value": "programming"}, + {"type": "text", "value": "reading"}, + {"type": "text", "value": "watching"}, + {"type": "text", "value": "playing"}, + ] + ), + ( + [ + {"type": "text", "value": "This is awesome!"}, + {"type": "text", "value": "I hate this product"}, + {"type": "text", "value": "It's okay, nothing special"}, + ], + [ + {"type": "text", "value": "positive"}, + {"type": "text", "value": "negative"}, + {"type": "text", "value": "neutral"}, + ] + ), + ( + [ + {"type": "text", "value": "The weather is sunny today"}, + {"type": "text", "value": "It's raining heavily outside"}, + {"type": "text", "value": "Snow is falling gently"}, + ], + [ + {"type": "text", "value": "sunny"}, + {"type": "text", "value": "rainy"}, + {"type": "text", "value": "snowy"}, + ] + ), +]) +def test_classification_text_success_response(dataset, labels) -> None: + params = { + "dataset": dataset, + "labels": labels, + } + try: + result = client.classification.text(params) + print(result) + assert result["success"] == True + except JigsawStackError as e: + print(str(e)) + assert e.message == "Failed to parse API response. Please try again." - def test_classification_image_success_response(self) -> None: - params = { - "dataset": [ - {"type": "image", "value": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ4aBhloyMLx5qA6G6wSEi0s9AvDu1r7utrbQ&s"}, - {"type": "image", "value": "https://cdn.britannica.com/79/232779-050-6B0411D7/German-Shepherd-dog-Alsatian.jpg"}, - ], - "labels": [ - {"type": "text", "value": "Cat"}, - {"type": "text", "value": "Dog"}, - ], - } - try: - result = client.classification.image(params) - assert result["success"] == True - except JigsawStackError as e: - assert e.message == "Failed to parse API response. Please try again." - def test_dns_success_response(self) -> None: - - params = { - "url": "https://supabase.com/pricing", - } - try: - result = client.web.dns(params) - assert result["success"] == True - except JigsawStackError as e: - assert e.message == "Failed to parse API response. Please try again." \ No newline at end of file +@pytest.mark.parametrize("dataset,labels", [ + ( + [ + {"type": "image", "value": "https://as2.ftcdn.net/v2/jpg/02/24/11/57/1000_F_224115780_2ssvcCoTfQrx68Qsl5NxtVIDFWKtAgq2.jpg"}, + {"type": "image", "value": "https://t3.ftcdn.net/jpg/02/95/44/22/240_F_295442295_OXsXOmLmqBUfZreTnGo9PREuAPSLQhff.jpg"}, + {"type": "image", "value": "https://as1.ftcdn.net/v2/jpg/05/54/94/46/1000_F_554944613_okdr3fBwcE9kTOgbLp4BrtVi8zcKFWdP.jpg"}, + ], + [ + {"type": "text", "value": "banana"}, + {"type": "image", "value": "https://upload.wikimedia.org/wikipedia/commons/8/8a/Banana-Single.jpg"}, + {"type": "text", "value": "kisses"}, + ] + ), +]) +def test_classification_image_success_response(dataset, labels) -> None: + params = { + "dataset": dataset, + "labels": labels, + } + try: + result = client.classification.image(params) + print(result) + assert result["success"] == True + except JigsawStackError as e: + print(str(e)) + assert e.message == "Failed to parse API response. Please try again." From f84591d4ac13b27be37d55fa4ef5234f188cea06 Mon Sep 17 00:00:00 2001 From: Khurdhula-Harshavardhan Date: Wed, 20 Aug 2025 21:30:14 -0700 Subject: [PATCH 4/4] feat: update .gitignore to ignore ruff cache. --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 356d525..7e9271c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,6 @@ test_web.py main.py .python-version pyproject.toml -uv.lock \ No newline at end of file +uv.lock + +.ruff_cache/ \ No newline at end of file