Skip to content

Commit d57081c

Browse files
author
Matt Sokoloff
committed
recommended changes
1 parent 432d628 commit d57081c

File tree

8 files changed

+333
-344
lines changed

8 files changed

+333
-344
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FROM python:3.7
22

3-
RUN pip install pytest
3+
RUN pip install pytest pytest-cases
44

55

66
WORKDIR /usr/src/labelbox

labelbox/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,6 @@ class UuidError(LabelboxError):
106106
pass
107107

108108

109-
class NDJsonError(LabelboxError):
109+
class ValidationError(LabelboxError):
110110
"""Raised when an ndjson line is invalid."""
111111
...

labelbox/schema/bulk_import_request.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
import json
2-
import logging
32
import time
4-
from pathlib import Path
53
from uuid import UUID, uuid4
6-
from pydantic import BaseModel, validator
4+
5+
import logging
6+
from pathlib import Path
77
import pydantic
88
import backoff
99
import ndjson
10-
import labelbox
1110
import requests
11+
from pydantic import BaseModel, validator
12+
from pydantic import ValidationError
13+
from typing_extensions import TypedDict, Literal
14+
from typing import (Any, List, Optional, BinaryIO, Dict, Iterable, Tuple, Union,
15+
Type, Set)
16+
17+
import labelbox
1218
from labelbox import utils
1319
from labelbox.orm import query
1420
from labelbox.orm.db_object import DbObject
15-
from labelbox.orm.model import Field
16-
from labelbox.orm.model import Relationship
21+
from labelbox.orm.model import Field, Relationship
1722
from labelbox.schema.enums import BulkImportRequestState
18-
from pydantic import ValidationError
19-
from typing import Any, List, Optional, BinaryIO, Dict, Iterable, Tuple, Union, Type, Set
20-
from typing_extensions import TypedDict, Literal
2123

2224
NDJSON_MIME_TYPE = "application/x-ndjson"
2325
logger = logging.getLogger(__name__)
@@ -326,7 +328,7 @@ def _validate_ndjson(lines: Iterable[Dict[str, Any]], project) -> None:
326328
project (Project): id of project for which predictions will be imported
327329
328330
Raises:
329-
NDJsonError: Raise for invalid NDJson
331+
ValidationError: Raise for invalid NDJson
330332
UuidError: Duplicate UUID in upload
331333
"""
332334
data_row_ids = {
@@ -346,7 +348,7 @@ def _validate_ndjson(lines: Iterable[Dict[str, Any]], project) -> None:
346348
'must be unique for the project.')
347349
uids.add(uuid)
348350
except (ValidationError, ValueError, TypeError, KeyError) as e:
349-
raise labelbox.exceptions.NDJsonError(
351+
raise labelbox.exceptions.ValidationError(
350352
f"Invalid NDJson on line {idx}") from e
351353

352354

labelbox/schema/project.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,8 @@ def enable_model_assisted_labeling(self, toggle: bool = True) -> bool:
416416
def upload_annotations(
417417
self,
418418
name: str,
419-
annotations: Union[str, Union[str, Path], Iterable[dict]],
420-
validate=True) -> 'BulkImportRequest': # type: ignore
419+
annotations: Union[str, Path, Iterable[dict]],
420+
validate: bool = True) -> 'BulkImportRequest': # type: ignore
421421
""" Uploads annotations to a new Editor project.
422422
423423
Args:
@@ -427,7 +427,7 @@ def upload_annotations(
427427
ndjson file
428428
OR local path to an ndjson file
429429
OR iterable of annotation rows
430-
validate (str):
430+
validate (bool):
431431
Whether or not to validate the payload before uploading.
432432
Returns:
433433
BulkImportRequest
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import uuid
2+
import pytest
3+
4+
from labelbox.schema.labeling_frontend import LabelingFrontend
5+
6+
IMG_URL = "https://picsum.photos/200/300"
7+
8+
9+
@pytest.fixture
10+
def ontology():
11+
bbox_tool = {
12+
'required':
13+
False,
14+
'name':
15+
'bbox',
16+
'tool':
17+
'rectangle',
18+
'color':
19+
'#a23030',
20+
'classifications': [{
21+
'required': False,
22+
'instructions': 'nested',
23+
'name': 'nested',
24+
'type': 'radio',
25+
'options': [{
26+
'label': 'radio_option_1',
27+
'value': 'radio_value_1'
28+
}]
29+
}]
30+
}
31+
polygon_tool = {
32+
'required': False,
33+
'name': 'polygon',
34+
'tool': 'polygon',
35+
'color': '#FF34FF',
36+
'classifications': []
37+
}
38+
polyline_tool = {
39+
'required': False,
40+
'name': 'polyline',
41+
'tool': 'line',
42+
'color': '#FF4A46',
43+
'classifications': []
44+
}
45+
point_tool = {
46+
'required': False,
47+
'name': 'point--',
48+
'tool': 'point',
49+
'color': '#008941',
50+
'classifications': []
51+
}
52+
entity_tool = {
53+
'required': False,
54+
'name': 'entity--',
55+
'tool': 'named-entity',
56+
'color': '#006FA6',
57+
'classifications': []
58+
}
59+
segmentation_tool = {
60+
'required': False,
61+
'name': 'segmentation--',
62+
'tool': 'superpixel',
63+
'color': '#A30059',
64+
'classifications': []
65+
}
66+
checklist = {
67+
'required':
68+
False,
69+
'instructions':
70+
'checklist',
71+
'name':
72+
'checklist',
73+
'type':
74+
'checklist',
75+
'options': [{
76+
'label': 'option1',
77+
'value': 'option1'
78+
}, {
79+
'label': 'option2',
80+
'value': 'option2'
81+
}, {
82+
'label': 'optionN',
83+
'value': 'optionn'
84+
}]
85+
}
86+
free_form_text = {
87+
'required': False,
88+
'instructions': 'text',
89+
'name': 'text',
90+
'type': 'text',
91+
'options': []
92+
}
93+
94+
tools = [
95+
bbox_tool, polygon_tool, polyline_tool, point_tool, entity_tool,
96+
segmentation_tool
97+
]
98+
classifications = [checklist, free_form_text]
99+
return {"tools": tools, "classifications": classifications}
100+
101+
102+
@pytest.fixture
103+
def configured_project(client, project, ontology, dataset):
104+
editor = list(
105+
client.get_labeling_frontends(
106+
where=LabelingFrontend.name == "editor"))[0]
107+
project.setup(editor, ontology)
108+
for _ in range(len(ontology['tools']) + len(ontology['classifications'])):
109+
dataset.create_data_row(row_data=IMG_URL)
110+
project.datasets.connect(dataset)
111+
yield project
112+
113+
114+
@pytest.fixture
115+
def prediction_id_mapping(configured_project):
116+
#Maps tool types to feature schema ids
117+
ontology = configured_project.ontology().normalized
118+
inferences = []
119+
datarows = [d for d in list(configured_project.datasets())[0].data_rows()]
120+
result = {}
121+
122+
for idx, tool in enumerate(ontology['tools'] + ontology['classifications']):
123+
if 'tool' in tool:
124+
tool_type = tool['tool']
125+
else:
126+
tool_type = tool['type']
127+
result[tool_type] = {
128+
"uuid": str(uuid.uuid4()),
129+
"schemaId": tool['featureSchemaId'],
130+
"dataRow": {
131+
"id": datarows[idx].uid,
132+
},
133+
'tool': tool
134+
}
135+
return result
136+
137+
138+
@pytest.fixture
139+
def polygon_inference(prediction_id_mapping):
140+
polygon = prediction_id_mapping['polygon'].copy()
141+
polygon.update({
142+
"polygon": [{
143+
"x": 147.692,
144+
"y": 118.154
145+
}, {
146+
"x": 142.769,
147+
"y": 404.923
148+
}, {
149+
"x": 57.846,
150+
"y": 318.769
151+
}, {
152+
"x": 28.308,
153+
"y": 169.846
154+
}]
155+
})
156+
del polygon['tool']
157+
return polygon
158+
159+
160+
@pytest.fixture
161+
def rectangle_inference(prediction_id_mapping):
162+
rectangle = prediction_id_mapping['rectangle'].copy()
163+
rectangle.update({
164+
"bbox": {
165+
"top": 48,
166+
"left": 58,
167+
"height": 865,
168+
"width": 1512
169+
},
170+
'classifications': [{
171+
"schemaId":
172+
rectangle['tool']['classifications'][0]['featureSchemaId'],
173+
"answer": {
174+
"schemaId":
175+
rectangle['tool']['classifications'][0]['options'][0]
176+
['featureSchemaId']
177+
}
178+
}]
179+
})
180+
del rectangle['tool']
181+
return rectangle
182+
183+
184+
@pytest.fixture
185+
def line_inference(prediction_id_mapping):
186+
line = prediction_id_mapping['line'].copy()
187+
line.update(
188+
{"line": [{
189+
"x": 147.692,
190+
"y": 118.154
191+
}, {
192+
"x": 150.692,
193+
"y": 160.154
194+
}]})
195+
del line['tool']
196+
return line
197+
198+
199+
@pytest.fixture
200+
def point_inference(prediction_id_mapping):
201+
point = prediction_id_mapping['point'].copy()
202+
point.update({"point": {"x": 147.692, "y": 118.154}})
203+
del point['tool']
204+
return point
205+
206+
207+
@pytest.fixture
208+
def entity_inference(prediction_id_mapping):
209+
entity = prediction_id_mapping['named-entity'].copy()
210+
entity.update({"location": {"start": 67, "end": 128}})
211+
del entity['tool']
212+
return entity
213+
214+
215+
@pytest.fixture
216+
def segmentation_inference(prediction_id_mapping):
217+
segmentation = prediction_id_mapping['superpixel'].copy()
218+
segmentation.update(
219+
{'mask': {
220+
'instanceURI': "sampleuri",
221+
'colorRGB': [0, 0, 0]
222+
}})
223+
del segmentation['tool']
224+
return segmentation
225+
226+
227+
@pytest.fixture
228+
def checklist_inference(prediction_id_mapping):
229+
checklist = prediction_id_mapping['checklist'].copy()
230+
checklist.update({
231+
'answers': [{
232+
'schemaId': checklist['tool']['options'][0]['featureSchemaId']
233+
}]
234+
})
235+
del checklist['tool']
236+
return checklist
237+
238+
239+
@pytest.fixture
240+
def text_inference(prediction_id_mapping):
241+
text = prediction_id_mapping['text'].copy()
242+
text.update({'answer': "free form text..."})
243+
del text['tool']
244+
return text
245+
246+
247+
@pytest.fixture
248+
def video_checklist_inference(prediction_id_mapping):
249+
checklist = prediction_id_mapping['checklist'].copy()
250+
checklist.update({
251+
'answers': [{
252+
'schemaId': checklist['tool']['options'][0]['featureSchemaId']
253+
}]
254+
})
255+
256+
checklist.update(
257+
{"frames": [{
258+
"start": 7,
259+
"end": 13,
260+
}, {
261+
"start": 18,
262+
"end": 19,
263+
}]})
264+
del checklist['tool']
265+
return checklist
266+
267+
268+
@pytest.fixture
269+
def predictions(polygon_inference, rectangle_inference, line_inference,
270+
entity_inference, segmentation_inference, checklist_inference,
271+
text_inference):
272+
return [
273+
polygon_inference, rectangle_inference, line_inference,
274+
entity_inference, segmentation_inference, checklist_inference,
275+
text_inference
276+
]

tests/integration/test_bulk_import_request.py renamed to tests/integration/bulk_import/test_bulk_import_request.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import uuid
2-
from attr import validate
3-
42
import ndjson
53
import pytest
64
import requests

0 commit comments

Comments
 (0)