diff --git a/roboflow/core/exceptions.py b/roboflow/core/exceptions.py new file mode 100644 index 00000000..48bc69ed --- /dev/null +++ b/roboflow/core/exceptions.py @@ -0,0 +1,42 @@ +class UploadImageError(Exception): + """ + Exception raised for errors that occur during the image upload process. + + Attributes: + message (str): A description of the error. + retry_attempts (int): The number of retry attempts made before the error occurred. + """ + + def __init__( + self, + message="An error occurred during the image upload process.", + retry_attempts=0, + ): + self.message = message + self.retry_attempts = retry_attempts + super().__init__(self.message) + + +class UploadAnnotationError(Exception): + """ + Exception raised for errors that occur during the annotation upload process. + + Attributes: + message (str): A description of the error. + image_id (Optional[str]): The ID of the image associated with the error. + image_upload_time (Optional[datetime]): The timestamp when the image upload was attempted. + image_retry_attempts (Optional[int]): The number of retry attempts made for the image upload. + """ + + def __init__( + self, + message="An error occurred during the annotation upload process.", + image_id=None, + image_upload_time=None, + image_retry_attempts=None, + ): + self.message = message + self.image_id = image_id + self.image_upload_time = image_upload_time + self.image_retry_attempts = image_retry_attempts + super().__init__(self.message) diff --git a/roboflow/core/project.py b/roboflow/core/project.py index 35e1fe5b..76f9e5bb 100644 --- a/roboflow/core/project.py +++ b/roboflow/core/project.py @@ -13,6 +13,7 @@ from roboflow.adapters import rfapi from roboflow.config import API_URL, DEMO_KEYS +from roboflow.core.exceptions import UploadAnnotationError, UploadImageError from roboflow.core.version import Version from roboflow.util.general import Retry from roboflow.util.image_utils import load_labelmap @@ -512,7 +513,10 @@ def single_upload( image_id = uploaded_image["id"] # type: ignore[index] upload_retry_attempts = retry.retries except rfapi.UploadError as e: - raise RuntimeError(f"Error uploading image: {self._parse_upload_error(e)}") + raise UploadImageError( + f"Error uploading image: {self._parse_upload_error(e)}", + retry_attempts=upload_retry_attempts, + ) except BaseException as e: uploaded_image = {"error": e} finally: @@ -535,7 +539,12 @@ def single_upload( overwrite=annotation_overwrite, ) except rfapi.UploadError as e: - raise RuntimeError(f"Error uploading annotation: {self._parse_upload_error(e)}") + raise UploadAnnotationError( + f"Error uploading annotation: {self._parse_upload_error(e)}", + image_id=image_id, + image_upload_time=upload_time, + image_retry_attempts=upload_retry_attempts, + ) except BaseException as e: uploaded_annotation = {"error": e} finally: @@ -569,7 +578,10 @@ def _annotation_params(self, annotation_path): return annotation_name, annotation_string def _parse_upload_error(self, error: rfapi.UploadError) -> str: - dict_part = str(error).split(": ", 2)[2] + error_str = str(error) + start_idx = error_str.index("{") + end_idx = error_str.rindex("}") + 1 + dict_part = error_str[start_idx:end_idx] dict_part = dict_part.replace("True", "true") dict_part = dict_part.replace("False", "false") dict_part = dict_part.replace("None", "null") diff --git a/roboflow/core/workspace.py b/roboflow/core/workspace.py index b3179e7d..293185d7 100644 --- a/roboflow/core/workspace.py +++ b/roboflow/core/workspace.py @@ -11,6 +11,7 @@ from roboflow.adapters import rfapi from roboflow.adapters.rfapi import RoboflowError from roboflow.config import API_URL, CLIP_FEATURIZE_URL, DEMO_KEYS +from roboflow.core.exceptions import UploadAnnotationError, UploadImageError from roboflow.core.project import Project from roboflow.util import folderparser from roboflow.util.active_learning_utils import check_box_size, clip_encode, count_comparisons @@ -313,7 +314,6 @@ def _log_img_upload(image_path, uploadres): img_success = uploadres.get("image", {}).get("success") img_duplicate = uploadres.get("image", {}).get("duplicate") annotation = uploadres.get("annotation") - image = uploadres.get("image") upload_time_str = f"[{uploadres['upload_time']:.1f}s]" if uploadres.get("upload_time") else "" annotation_time_str = f"[{uploadres['annotation_time']:.1f}s]" if uploadres.get("annotation_time") else "" retry_attempts = ( @@ -321,24 +321,38 @@ def _log_img_upload(image_path, uploadres): if uploadres.get("upload_retry_attempts", 0) > 0 else "" ) + # TODO: Will this duplicate case still occurs? if img_duplicate: msg = f"[DUPLICATE]{retry_attempts} {image_path} ({image_id}) {upload_time_str}" elif img_success: msg = f"[UPLOADED]{retry_attempts} {image_path} ({image_id}) {upload_time_str}" else: - msg = f"[ERR]{retry_attempts} {image_path} ({image}) {upload_time_str}" + msg = f"[LOG ERROR]: Unrecognized image upload status ({image_id=})" if annotation: if annotation.get("success"): msg += f" / annotations = OK {annotation_time_str}" elif annotation.get("warn"): msg += f" / annotations = WARN: {annotation['warn']} {annotation_time_str}" - elif annotation.get("error"): - msg += f" / annotations = ERR: {annotation['error']} {annotation_time_str}" + else: + msg += " / annotations = ERR: Unrecognized annotation upload status" + print(msg) def _log_img_upload_err(image_path, e): - msg = f"[ERR] {image_path} ({e})" - print(msg) + if isinstance(e, UploadImageError): + retry_attempts = f" (with {e.retry_attempts} retries)" if e.retry_attempts > 0 else "" + print(f"[ERR]{retry_attempts} {image_path} ({e.message})") + return + + if isinstance(e, UploadAnnotationError): + upload_time_str = f"[{e.image_upload_time:.1f}s]" if e.image_upload_time else "" + retry_attempts = f" (with {e.image_retry_attempts} retries)" if e.image_retry_attempts > 0 else "" + image_msg = f"[UPLOADED]{retry_attempts} {image_path} ({e.image_id}) {upload_time_str}" + annotation_msg = f"annotations = ERR: {e.message}" + print(f"{image_msg} / {annotation_msg}") + return + + print(f"[ERR] {image_path} ({e})") def _upload_image(imagedesc): image_path = f"{location}{imagedesc['file']}" @@ -364,6 +378,8 @@ def _upload_image(imagedesc): num_retry_uploads=num_retries, ) _log_img_upload(image_path, uploadres) + except (UploadImageError, UploadAnnotationError) as e: + _log_img_upload_err(image_path, e) except Exception as e: _log_img_upload_err(image_path, e) diff --git a/tests/__init__.py b/tests/__init__.py index 371a3106..63a080c9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -210,14 +210,6 @@ def setUp(self): status=200, ) - # Upload image - responses.add( - responses.POST, - f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", - json={"duplicate": True, "id": "hbALkCFdNr9rssgOUXug"}, - status=200, - ) - self.connect_to_roboflow() def tearDown(self): diff --git a/tests/annotations/invalid_annotation.json b/tests/annotations/invalid_annotation.json new file mode 100644 index 00000000..fd8561a3 --- /dev/null +++ b/tests/annotations/invalid_annotation.json @@ -0,0 +1,8 @@ +{ + "it is": [ + { + "a": 0, + "invalid annotation": true + } + ] +} diff --git a/tests/annotations/valid_annotation.json b/tests/annotations/valid_annotation.json new file mode 100644 index 00000000..8a8284ff --- /dev/null +++ b/tests/annotations/valid_annotation.json @@ -0,0 +1,64 @@ +{ + "info": { + "year": "2020", + "version": "1", + "description": "None", + "contributor": "Linas", + "url": "https://app.roboflow.ai/datasets/hard-hat-sample/1", + "date_created": "2000-01-01T00:00:00+00:00" + }, + "licenses": [ + { + "id": 1, + "url": "https://creativecommons.org/publicdomain/zero/1.0/", + "name": "Public Domain" + } + ], + "categories": [ + { + "id": 0, + "name": "cat", + "supercategory": "animals" + } + ], + "images": [ + { + "id": 0, + "license": 1, + "file_name": "bla.JPG", + "height": 1024, + "width": 1792, + "date_captured": "2020-07-20T19:39:26+00:00" + } + ], + "annotations": [ + { + "id": 0, + "image_id": 0, + "category_id": 0, + "bbox": [ + 45, + 2, + 85, + 85 + ], + "area": 7225, + "segmentation": [], + "iscrowd": 0 + }, + { + "id": 1, + "image_id": 0, + "category_id": 0, + "bbox": [ + 324, + 29, + 72, + 81 + ], + "area": 5832, + "segmentation": [], + "iscrowd": 0 + } + ] +} diff --git a/tests/test_project.py b/tests/test_project.py index e43d9ec3..cf2eb810 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,8 +1,21 @@ -from tests import RoboflowTest +import responses + +from roboflow import API_URL +from roboflow.config import DEFAULT_BATCH_NAME +from roboflow.core.exceptions import UploadAnnotationError, UploadImageError +from tests import PROJECT_NAME, ROBOFLOW_API_KEY, RoboflowTest class TestProject(RoboflowTest): def test_check_valid_image_with_accepted_formats(self): + # Mock dataset upload + responses.add( + responses.POST, + f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", + json={"duplicate": True, "id": "hbALkCFdNr9rssgOUXug"}, + status=200, + ) + images_to_test = [ "rabbit.JPG", "rabbit2.jpg", @@ -14,6 +27,14 @@ def test_check_valid_image_with_accepted_formats(self): self.assertTrue(self.project.check_valid_image(f"tests/images/{image}")) def test_check_valid_image_with_unaccepted_formats(self): + # Mock dataset upload + responses.add( + responses.POST, + f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", + json={"duplicate": True, "id": "hbALkCFdNr9rssgOUXug"}, + status=200, + ) + images_to_test = [ "sky-rabbit.gif", "sky-rabbit.heic", @@ -21,3 +42,73 @@ def test_check_valid_image_with_unaccepted_formats(self): for image in images_to_test: self.assertFalse(self.project.check_valid_image(f"tests/images/{image}")) + + def test_upload_raises_upload_image_error_response_200(self): + responses.add( + responses.POST, + f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", + json={ + "message": "Invalid Image", + "type": "InvalidImageException", + }, + status=200, + ) + + with self.assertRaises(UploadImageError) as error: + self.project.upload( + "tests/images/rabbit.JPG", + annotation_path="tests/annotations/valid_annotation.json", + ) + + self.assertEqual(str(error.exception), "Error uploading image: Invalid Image") + + def test_upload_raises_upload_image_error_response_400(self): + responses.add( + responses.POST, + f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", + json={ + "message": "Invalid Image", + "type": "InvalidImageException", + }, + status=400, + ) + + with self.assertRaises(UploadImageError) as error: + self.project.upload( + "tests/images/rabbit.JPG", + annotation_path="tests/annotations/valid_annotation.json", + ) + + self.assertEqual(str(error.exception), "Error uploading image: Invalid Image") + + def test_upload_raises_upload_annotation_error(self): + image_id = "hbALkCFdNr9rssgOUXug" + image_name = "invalid_annotation.json" + + # Image upload + responses.add( + responses.POST, + f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", + json={"success": True, "id": image_id}, + status=200, + ) + + # Annotation + responses.add( + responses.POST, + f"{API_URL}/dataset/{PROJECT_NAME}/annotate/{image_id}?api_key={ROBOFLOW_API_KEY}" f"&name={image_name}", + json={ + "message": "Image was already annotated.", + "type": "InvalidImageException", + "hint": "This image was already annotated; to overwrite the annotation, pass overwrite=true...", + }, + status=400, + ) + + with self.assertRaises(UploadAnnotationError) as error: + self.project.upload( + "tests/images/rabbit.JPG", + annotation_path=f"tests/annotations/{image_name}", + ) + + self.assertEqual(str(error.exception), "Error uploading annotation: Image was already annotated.")