Skip to content

Maintains compatibility with CLI return, broken in PR #266 #309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
42 changes: 42 additions & 0 deletions roboflow/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 15 additions & 3 deletions roboflow/core/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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]
Comment on lines -572 to +584
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the bug reported here:

#266 (comment)

dict_part = dict_part.replace("True", "true")
dict_part = dict_part.replace("False", "false")
dict_part = dict_part.replace("None", "null")
Expand Down
28 changes: 22 additions & 6 deletions roboflow/core/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -313,32 +314,45 @@ 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 = (
f" (with {uploadres['upload_retry_attempts']} retries)"
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']}"
Expand All @@ -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)

Expand Down
8 changes: 0 additions & 8 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
8 changes: 8 additions & 0 deletions tests/annotations/invalid_annotation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"it is": [
{
"a": 0,
"invalid annotation": true
}
]
}
64 changes: 64 additions & 0 deletions tests/annotations/valid_annotation.json
Original file line number Diff line number Diff line change
@@ -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
}
]
}
93 changes: 92 additions & 1 deletion tests/test_project.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -14,10 +27,88 @@ 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",
]

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.")