Skip to content

Commit 75abcf7

Browse files
authored
Merge pull request #244 from roboflow/upload-add-retry
Reliability improvement for CLI upload
2 parents 14ff57e + 34d0a67 commit 75abcf7

File tree

8 files changed

+65
-48
lines changed

8 files changed

+65
-48
lines changed

roboflow/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from roboflow.models import CLIPModel, GazeModel # noqa: F401
1515
from roboflow.util.general import write_line
1616

17-
__version__ = "1.1.24"
17+
__version__ = "1.1.25"
1818

1919

2020
def check_key(api_key, model, notebook, num_retries=0):

roboflow/core/project.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from roboflow.adapters import rfapi
1212
from roboflow.config import API_URL, DEMO_KEYS
1313
from roboflow.core.version import Version
14-
from roboflow.util.general import retry
14+
from roboflow.util.general import Retry
1515
from roboflow.util.image_utils import load_labelmap
1616

1717
ACCEPTED_IMAGE_FORMATS = ["PNG", "JPEG"]
@@ -473,12 +473,12 @@ def single_upload(
473473
annotation_labelmap = load_labelmap(annotation_labelmap)
474474
uploaded_image, uploaded_annotation = None, None
475475
upload_time = None
476+
upload_retry_attempts = 0
476477
if image_path:
477478
t0 = time.time()
478479
try:
480+
retry = Retry(num_retry_uploads, Exception)
479481
uploaded_image = retry(
480-
num_retry_uploads,
481-
Exception,
482482
rfapi.upload_image,
483483
self.__api_key,
484484
project_url,
@@ -492,6 +492,7 @@ def single_upload(
492492
**kwargs,
493493
)
494494
image_id = uploaded_image["id"]
495+
upload_retry_attempts = retry.retries
495496
except BaseException as e:
496497
uploaded_image = {"error": e}
497498
finally:
@@ -522,6 +523,7 @@ def single_upload(
522523
"annotation": uploaded_annotation,
523524
"upload_time": upload_time,
524525
"annotation_time": annotation_time,
526+
"upload_retry_attempts": upload_retry_attempts,
525527
}
526528

527529
def _annotation_params(self, annotation_path):

roboflow/core/workspace.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ def upload_dataset(
275275
project_license: str = "MIT",
276276
project_type: str = "object-detection",
277277
batch_name=None,
278+
num_retries=0,
278279
):
279280
"""
280281
Upload a dataset to Roboflow.
@@ -309,12 +310,17 @@ def _log_img_upload(image_path, uploadres):
309310
image = uploadres.get("image")
310311
upload_time_str = f"[{uploadres['upload_time']:.1f}s]" if uploadres.get("upload_time") else ""
311312
annotation_time_str = f"[{uploadres['annotation_time']:.1f}s]" if uploadres.get("annotation_time") else ""
313+
retry_attempts = (
314+
f" (with {uploadres['upload_retry_attempts']} retries)"
315+
if uploadres.get("upload_retry_attempts", 0) > 0
316+
else ""
317+
)
312318
if img_duplicate:
313-
msg = f"[DUPLICATE] {image_path} ({image_id}) {upload_time_str}"
319+
msg = f"[DUPLICATE]{retry_attempts} {image_path} ({image_id}) {upload_time_str}"
314320
elif img_success:
315-
msg = f"[UPLOADED] {image_path} ({image_id}) {upload_time_str}"
321+
msg = f"[UPLOADED]{retry_attempts} {image_path} ({image_id}) {upload_time_str}"
316322
else:
317-
msg = f"[ERR] {image_path} ({image}) {upload_time_str}"
323+
msg = f"[ERR]{retry_attempts} {image_path} ({image}) {upload_time_str}"
318324
if annotation:
319325
if annotation.get("success"):
320326
msg += f" / annotations = OK {annotation_time_str}"
@@ -349,6 +355,7 @@ def _upload_image(imagedesc):
349355
sequence_number=imagedesc.get("index"),
350356
sequence_size=len(images),
351357
batch_name=batch_name,
358+
num_retry_uploads=num_retries,
352359
)
353360
_log_img_upload(image_path, uploadres)
354361
except Exception as e:

roboflow/roboflowpy.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ def import_dataset(args):
4848
rf = roboflow.Roboflow()
4949
workspace = rf.workspace(args.workspace)
5050
workspace.upload_dataset(
51-
dataset_path=args.folder, project_name=args.project, num_workers=args.concurrency, batch_name=args.batch_name
51+
dataset_path=args.folder,
52+
project_name=args.project,
53+
num_workers=args.concurrency,
54+
batch_name=args.batch_name,
55+
num_retries=args.num_retries,
5256
)
5357

5458

@@ -263,6 +267,9 @@ def _add_import_parser(subparsers):
263267
dest="batch_name",
264268
help="name of batch to upload to within project",
265269
)
270+
import_parser.add_argument(
271+
"-r", dest="num_retries", type=int, help="Retry failed uploads this many times (default=0)", default=0
272+
)
266273
import_parser.set_defaults(func=import_dataset)
267274

268275

roboflow/util/folderparser.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ def _filterIndividualAnnotations(image, annotation, format):
116116
if len(imgReferences) > 1:
117117
print(f"warning: found multiple image entries for image {image['file']} in {annotation['file']}")
118118
if imgReferences:
119+
# workaround to make Annotations.js correctly identify this as coco in the backend
120+
fake_annotation = {
121+
"id": 999999999,
122+
"image_id": 999999999,
123+
"category_id": 0,
124+
"area": 1,
125+
"segmentation": [],
126+
"iscrowd": 0,
127+
}
119128
imgReference = imgReferences[0]
120129
_annotation = {
121130
"name": "annotation.coco.json",
@@ -125,7 +134,8 @@ def _filterIndividualAnnotations(image, annotation, format):
125134
"licenses": parsed["licenses"],
126135
"categories": parsed["categories"],
127136
"images": [imgReference],
128-
"annotations": [a for a in parsed["annotations"] if a["image_id"] == imgReference["id"]],
137+
"annotations": [a for a in parsed["annotations"] if a["image_id"] == imgReference["id"]]
138+
or [fake_annotation],
129139
},
130140
}
131141
return _annotation

roboflow/util/general.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,25 @@ def write_line(line):
77
sys.stdout.flush()
88

99

10-
def retry(max_retries, retry_on, func, *args, **kwargs):
11-
if not retry_on:
12-
retry_on = (Exception,)
13-
retries = 0
14-
while retries <= max_retries:
15-
try:
16-
return func(*args, **kwargs)
17-
except BaseException as e:
18-
if isinstance(e, retry_on):
19-
retries += 1
20-
if retries > max_retries:
10+
class Retry:
11+
def __init__(self, max_retries, retry_on):
12+
self.max_retries = max_retries
13+
self.retry_on = retry_on
14+
self.retries = 0
15+
16+
def __call__(self, func, *args, **kwargs):
17+
self.retries = 0
18+
retry_on = self.retry_on
19+
if not retry_on:
20+
retry_on = (Exception,)
21+
self.retries = 0
22+
while self.retries <= self.max_retries:
23+
try:
24+
return func(*args, **kwargs)
25+
except BaseException as e:
26+
if isinstance(e, retry_on):
27+
self.retries += 1
28+
if self.retries > self.max_retries:
29+
raise
30+
else:
2131
raise
22-
else:
23-
raise

tests/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,7 @@ def setUp(self):
161161
# Upload image
162162
responses.add(
163163
responses.POST,
164-
f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}"
165-
f"&batch={DEFAULT_BATCH_NAME}",
164+
f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}",
166165
json={"duplicate": True, "id": "hbALkCFdNr9rssgOUXug"},
167166
status=200,
168167
)

tests/models/test_object_detection.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ def setUp(self):
4747
self.version_id = f"{self.workspace}/{self.dataset_id}/{self.version}"
4848

4949
def test_init_sets_attributes(self):
50-
instance = ObjectDetectionModel(
51-
self.api_key, self.version_id, version=self.version
52-
)
50+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
5351

5452
self.assertEqual(instance.id, self.version_id)
5553
# self.assertEqual(instance.api_url,
@@ -59,9 +57,7 @@ def test_init_sets_attributes(self):
5957
def test_predict_returns_prediction_group(self):
6058
print(self.api_url)
6159
image_path = "tests/images/rabbit.JPG"
62-
instance = ObjectDetectionModel(
63-
self.api_key, self.version_id, version=self.version
64-
)
60+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
6561

6662
responses.add(responses.POST, self.api_url, json=MOCK_RESPONSE)
6763

@@ -72,9 +68,7 @@ def test_predict_returns_prediction_group(self):
7268
@responses.activate
7369
def test_predict_with_local_image_request(self):
7470
image_path = "tests/images/rabbit.JPG"
75-
instance = ObjectDetectionModel(
76-
self.api_key, self.version_id, version=self.version
77-
)
71+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
7872

7973
responses.add(responses.POST, self.api_url, json=MOCK_RESPONSE)
8074

@@ -90,9 +84,7 @@ def test_predict_with_local_image_request(self):
9084
@responses.activate
9185
def test_predict_with_a_numpy_array_request(self):
9286
np_array = np.ones((100, 100, 1), dtype=np.uint8)
93-
instance = ObjectDetectionModel(
94-
self.api_key, self.version_id, version=self.version
95-
)
87+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
9688

9789
responses.add(responses.POST, self.api_url, json=MOCK_RESPONSE)
9890

@@ -107,9 +99,7 @@ def test_predict_with_a_numpy_array_request(self):
10799

108100
def test_predict_with_local_wrong_image_request(self):
109101
image_path = "tests/images/not_an_image.txt"
110-
instance = ObjectDetectionModel(
111-
self.api_key, self.version_id, version=self.version
112-
)
102+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
113103
self.assertRaises(UnidentifiedImageError, instance.predict, image_path)
114104

115105
@responses.activate
@@ -119,9 +109,7 @@ def test_predict_with_hosted_image_request(self):
119109
**self._default_params,
120110
"image": image_path,
121111
}
122-
instance = ObjectDetectionModel(
123-
self.api_key, self.version_id, version=self.version
124-
)
112+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
125113

126114
# Mock the library validating that the URL is valid before sending to the API
127115
responses.add(responses.POST, self.api_url, json=MOCK_RESPONSE)
@@ -140,9 +128,7 @@ def test_predict_with_confidence_request(self):
140128
confidence = "100"
141129
image_path = "tests/images/rabbit.JPG"
142130
expected_params = {**self._default_params, "confidence": confidence}
143-
instance = ObjectDetectionModel(
144-
self.api_key, self.version_id, version=self.version
145-
)
131+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
146132

147133
responses.add(responses.POST, self.api_url, json=MOCK_RESPONSE)
148134

@@ -160,9 +146,7 @@ def test_predict_with_non_200_response_raises_http_error(self):
160146
image_path = "tests/images/rabbit.JPG"
161147
responses.add(responses.POST, self.api_url, status=403)
162148

163-
instance = ObjectDetectionModel(
164-
self.api_key, self.version_id, version=self.version
165-
)
149+
instance = ObjectDetectionModel(self.api_key, self.version_id, version=self.version)
166150

167151
with self.assertRaises(HTTPError):
168152
instance.predict(image_path)

0 commit comments

Comments
 (0)