Skip to content

Commit 8d00d00

Browse files
Merge pull request #778 from Labelbox/develop
Release 3.31.0
2 parents e2b65e0 + 9682825 commit 8d00d00

35 files changed

+2277
-336
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Changelog
22

3+
# Version 3.31.0 (2022-11-28)
4+
### Added
5+
* Added `client.clear_global_keys()` to remove global keys from their associated data rows
6+
* Added a new attribute `confidence` to `AnnotationObject` and `ClassificationAnswer` for Model Error Analysis
7+
8+
### Fixed
9+
* Fixed `project.create_batch()` to work with both data_row_ids and data_row objects
10+
311
# Version 3.30.1 (2022-11-16)
12+
### Added
13+
* Added step to `project.create_batch()` to wait for data rows to finish processing
414
### Fixed
515
* Running `project.setup_editor()` multiple times no longer resets the ontology, and instead raises an error if the editor is already set up for the project
616

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
copyright = '2021, Labelbox'
2222
author = 'Labelbox'
2323

24-
release = '3.30.1'
24+
release = '3.31.0'
2525

2626
# -- General configuration ---------------------------------------------------
2727

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ Learn more about annotation types in the [docs](https://docs.labelbox.com/docs/a
6262
| Text Annotation Import | [Github](annotation_import/text.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/text.ipynb) |
6363
| Tiled Imagery Annotation Import | [Github](annotation_import/tiled.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/tiled.ipynb) |
6464
| Video Model-Assisted Labeling | [Github](annotation_import/video.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/video.ipynb) |
65+
| PDF Annotation Import | [Github](annotation_import/pdf.ipynb) | [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/pdf.ipynb) |
6566
------
6667

6768
## [Project Configuration](project_configuration)

examples/annotation_import/pdf.ipynb

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
"metadata": {},
1717
"source": [
1818
"<td>\n",
19-
"<a href=\"https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/pdf_mal.ipynb\" target=\"_blank\"><img\n",
19+
"<a href=\"https://colab.research.google.com/github/Labelbox/labelbox-python/blob/develop/examples/annotation_import/pdf.ipynb\" target=\"_blank\"><img\n",
2020
"src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"></a>\n",
2121
"</td>\n",
2222
"\n",
2323
"<td>\n",
24-
"<a href=\"https://github.com/Labelbox/labelbox-python/tree/develop/examples/annotation_import/pdf_mal.ipynb\" target=\"_blank\"><img\n",
24+
"<a href=\"https://github.com/Labelbox/labelbox-python/tree/develop/examples/annotation_import/pdf.ipynb\" target=\"_blank\"><img\n",
2525
"src=\"https://img.shields.io/badge/GitHub-100000?logo=github&logoColor=white\" alt=\"GitHub\"></a>\n",
2626
"</td>"
2727
]
@@ -447,14 +447,6 @@
447447
"# This will provide information only after the upload_job is complete, so we do not need to worry about having to rerun\n",
448448
"print(\"Errors:\", upload_job.errors)"
449449
]
450-
},
451-
{
452-
"cell_type": "code",
453-
"execution_count": null,
454-
"id": "ba9dc45a",
455-
"metadata": {},
456-
"outputs": [],
457-
"source": []
458450
}
459451
],
460452
"metadata": {

labelbox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name = "labelbox"
2-
__version__ = "3.30.1"
2+
__version__ = "3.31.0"
33

44
from labelbox.client import Client
55
from labelbox.schema.project import Project

labelbox/client.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,102 @@ def _format_failed_rows(rows: List[str],
12291229
)
12301230
time.sleep(sleep_time)
12311231

1232+
def clear_global_keys(
1233+
self,
1234+
global_keys: List[str],
1235+
timeout_seconds=60) -> Dict[str, Union[str, List[Any]]]:
1236+
"""
1237+
Clears global keys for the data rows tha correspond to the global keys provided.
1238+
1239+
Args:
1240+
A list of global keys
1241+
Returns:
1242+
Dictionary containing 'status', 'results' and 'errors'.
1243+
1244+
'Status' contains the outcome of this job. It can be one of
1245+
'Success', 'Partial Success', or 'Failure'.
1246+
1247+
'Results' contains a list global keys that were successfully cleared.
1248+
1249+
'Errors' contains a list of global_keys correspond to the data rows that could not be
1250+
modified, accessed by the user, or not found.
1251+
Examples:
1252+
>>> job_result = client.get_data_row_ids_for_global_keys(["key1","key2"])
1253+
>>> print(job_result['status'])
1254+
Partial Success
1255+
>>> print(job_result['results'])
1256+
['cl7tv9wry00hlka6gai588ozv', 'cl7tv9wxg00hpka6gf8sh81bj']
1257+
>>> print(job_result['errors'])
1258+
[{'global_key': 'asdf', 'error': 'Data Row not found'}]
1259+
"""
1260+
1261+
def _format_failed_rows(rows: List[str],
1262+
error_msg: str) -> List[Dict[str, str]]:
1263+
return [{'global_key': r, 'error': error_msg} for r in rows]
1264+
1265+
# Start get data rows for global keys job
1266+
query_str = """mutation clearGlobalKeysPyApi($globalKeys: [ID!]!) {
1267+
clearGlobalKeys(where: {ids: $globalKeys}) { jobId}}
1268+
"""
1269+
params = {"globalKeys": global_keys}
1270+
clear_global_keys_job = self.execute(query_str, params)
1271+
1272+
# Query string for retrieving job status and result, if job is done
1273+
result_query_str = """query clearGlobalKeysResultPyApi($jobId: ID!) {
1274+
clearGlobalKeysResult(jobId: {id: $jobId}) { data {
1275+
clearedGlobalKeys
1276+
failedToClearGlobalKeys
1277+
notFoundGlobalKeys
1278+
accessDeniedGlobalKeys
1279+
} jobStatus}}
1280+
"""
1281+
result_params = {
1282+
"jobId": clear_global_keys_job["clearGlobalKeys"]["jobId"]
1283+
}
1284+
# Poll job status until finished, then retrieve results
1285+
sleep_time = 2
1286+
start_time = time.time()
1287+
while True:
1288+
res = self.execute(result_query_str, result_params)
1289+
if res["clearGlobalKeysResult"]['jobStatus'] == "COMPLETE":
1290+
data = res["clearGlobalKeysResult"]['data']
1291+
results, errors = [], []
1292+
results.extend(data['clearedGlobalKeys'])
1293+
errors.extend(
1294+
_format_failed_rows(data['failedToClearGlobalKeys'],
1295+
"Clearing global key failed"))
1296+
errors.extend(
1297+
_format_failed_rows(
1298+
data['notFoundGlobalKeys'],
1299+
"Failed to find data row matching provided global key"))
1300+
errors.extend(
1301+
_format_failed_rows(
1302+
data['accessDeniedGlobalKeys'],
1303+
"Denied access to modify data row matching provided global key"
1304+
))
1305+
1306+
if not errors:
1307+
status = CollectionJobStatus.SUCCESS.value
1308+
elif errors and len(results) > 0:
1309+
status = CollectionJobStatus.PARTIAL_SUCCESS.value
1310+
else:
1311+
status = CollectionJobStatus.FAILURE.value
1312+
1313+
if errors:
1314+
logger.warning(
1315+
"There are errors present. Please look at 'errors' in the returned dict for more details"
1316+
)
1317+
1318+
return {"status": status, "results": results, "errors": errors}
1319+
elif res["clearGlobalKeysResult"]['jobStatus'] == "FAILED":
1320+
raise labelbox.exceptions.LabelboxError(
1321+
"Job clearGlobalKeys failed.")
1322+
current_time = time.time()
1323+
if current_time - start_time > timeout_seconds:
1324+
raise labelbox.exceptions.TimeoutError(
1325+
"Timed out waiting for clear_global_keys job to complete.")
1326+
time.sleep(sleep_time)
1327+
12321328
def get_catalog_slice(self, slice_id) -> CatalogSlice:
12331329
"""
12341330
Fetches a Catalog Slice by ID.

labelbox/data/annotation_types/annotation.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import abc
22
from typing import Any, Dict, List, Optional, Union
33

4+
from labelbox.data.mixins import ConfidenceNotSupportedMixin, ConfidenceMixin
5+
46
from .classification import Checklist, Dropdown, Radio, Text
57
from .feature import FeatureSchema
68
from .geometry import Geometry, Rectangle, Point
@@ -31,7 +33,7 @@ class ClassificationAnnotation(BaseAnnotation):
3133
value: Union[Text, Checklist, Radio, Dropdown]
3234

3335

34-
class ObjectAnnotation(BaseAnnotation):
36+
class ObjectAnnotation(BaseAnnotation, ConfidenceMixin):
3537
"""Generic localized annotation (non classifications)
3638
3739
>>> ObjectAnnotation(
@@ -53,7 +55,7 @@ class ObjectAnnotation(BaseAnnotation):
5355
classifications: List[ClassificationAnnotation] = []
5456

5557

56-
class VideoObjectAnnotation(ObjectAnnotation):
58+
class VideoObjectAnnotation(ObjectAnnotation, ConfidenceNotSupportedMixin):
5759
"""Video object annotation
5860
5961
>>> VideoObjectAnnotation(
@@ -76,6 +78,7 @@ class VideoObjectAnnotation(ObjectAnnotation):
7678
classifications (List[ClassificationAnnotation]) = []
7779
extra (Dict[str, Any])
7880
"""
81+
7982
frame: int
8083
keyframe: bool
8184
segment_index: Optional[int] = None

labelbox/data/annotation_types/classification/classification.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from typing import Any, Dict, List, Union, Optional
22
import warnings
33

4+
from labelbox.data.mixins import ConfidenceMixin
5+
46
try:
57
from typing import Literal
68
except:
@@ -20,7 +22,7 @@ def dict(self, *args, **kwargs):
2022
return res
2123

2224

23-
class ClassificationAnswer(FeatureSchema):
25+
class ClassificationAnswer(FeatureSchema, ConfidenceMixin):
2426
"""
2527
- Represents a classification option.
2628
- Because it inherits from FeatureSchema

labelbox/data/mixins.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from typing import Optional
2+
3+
from pydantic import BaseModel, validator
4+
5+
from labelbox.exceptions import ConfidenceNotSupportedException
6+
7+
8+
class ConfidenceMixin(BaseModel):
9+
confidence: Optional[float] = None
10+
11+
@validator('confidence')
12+
def confidence_valid_float(cls, value):
13+
if value is None:
14+
return value
15+
if not isinstance(value, (int, float)) or not 0 <= value <= 1:
16+
raise ValueError('must be a number within [0,1] range')
17+
return value
18+
19+
def dict(self, *args, **kwargs):
20+
res = super().dict(*args, **kwargs)
21+
if 'confidence' in res and res['confidence'] is None:
22+
res.pop('confidence')
23+
return res
24+
25+
26+
class ConfidenceNotSupportedMixin:
27+
28+
def __new__(cls, *args, **kwargs):
29+
if 'confidence' in kwargs:
30+
raise ConfidenceNotSupportedException(
31+
'Confidence is not supported for this annotaiton type yet')
32+
return super().__new__(cls)

labelbox/data/serialization/ndjson/classification.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any, Dict, List, Union, Optional
22

33
from pydantic import BaseModel, Field, root_validator
4+
from labelbox.data.mixins import ConfidenceMixin
45

56
from labelbox.utils import camel_case
67
from ...annotation_types.annotation import ClassificationAnnotation, VideoClassificationAnnotation
@@ -10,7 +11,7 @@
1011
from .base import NDAnnotation
1112

1213

13-
class NDFeature(BaseModel):
14+
class NDFeature(ConfidenceMixin):
1415
name: Optional[str] = None
1516
schema_id: Optional[Cuid] = None
1617

@@ -41,7 +42,7 @@ class FrameLocation(BaseModel):
4142

4243

4344
class VideoSupported(BaseModel):
44-
#Note that frames are only allowed as top level inferences for video
45+
# Note that frames are only allowed as top level inferences for video
4546
frames: Optional[List[FrameLocation]] = None
4647

4748
def dict(self, *args, **kwargs):
@@ -70,15 +71,18 @@ class NDChecklistSubclass(NDFeature):
7071
def to_common(self) -> Checklist:
7172
return Checklist(answer=[
7273
ClassificationAnswer(name=answer.name,
73-
feature_schema_id=answer.schema_id)
74+
feature_schema_id=answer.schema_id,
75+
confidence=answer.confidence)
7476
for answer in self.answer
7577
])
7678

7779
@classmethod
7880
def from_common(cls, checklist: Checklist, name: str,
7981
feature_schema_id: Cuid) -> "NDChecklistSubclass":
8082
return cls(answer=[
81-
NDFeature(name=answer.name, schema_id=answer.feature_schema_id)
83+
NDFeature(name=answer.name,
84+
schema_id=answer.feature_schema_id,
85+
confidence=answer.confidence)
8286
for answer in checklist.answer
8387
],
8488
name=name,
@@ -95,19 +99,22 @@ class NDRadioSubclass(NDFeature):
9599
answer: NDFeature
96100

97101
def to_common(self) -> Radio:
98-
return Radio(answer=ClassificationAnswer(
99-
name=self.answer.name, feature_schema_id=self.answer.schema_id))
102+
return Radio(
103+
answer=ClassificationAnswer(name=self.answer.name,
104+
feature_schema_id=self.answer.schema_id,
105+
confidence=self.answer.confidence))
100106

101107
@classmethod
102108
def from_common(cls, radio: Radio, name: str,
103109
feature_schema_id: Cuid) -> "NDRadioSubclass":
104110
return cls(answer=NDFeature(name=radio.answer.name,
105-
schema_id=radio.answer.feature_schema_id),
111+
schema_id=radio.answer.feature_schema_id,
112+
confidence=radio.answer.confidence),
106113
name=name,
107114
schema_id=feature_schema_id)
108115

109116

110-
### ====== End of subclasses
117+
# ====== End of subclasses
111118

112119

113120
class NDText(NDAnnotation, NDTextSubclass):
@@ -133,7 +140,9 @@ def from_common(
133140
extra: Dict[str, Any], data: Union[VideoData, TextData,
134141
ImageData]) -> "NDChecklist":
135142
return cls(answer=[
136-
NDFeature(name=answer.name, schema_id=answer.feature_schema_id)
143+
NDFeature(name=answer.name,
144+
schema_id=answer.feature_schema_id,
145+
confidence=answer.confidence)
137146
for answer in checklist.answer
138147
],
139148
data_row={'id': data.uid},
@@ -150,7 +159,8 @@ def from_common(cls, radio: Radio, name: str, feature_schema_id: Cuid,
150159
extra: Dict[str, Any], data: Union[VideoData, TextData,
151160
ImageData]) -> "NDRadio":
152161
return cls(answer=NDFeature(name=radio.answer.name,
153-
schema_id=radio.answer.feature_schema_id),
162+
schema_id=radio.answer.feature_schema_id,
163+
confidence=radio.answer.confidence),
154164
data_row={'id': data.uid},
155165
name=name,
156166
schema_id=feature_schema_id,
@@ -241,6 +251,11 @@ def lookup_classification(
241251
}.get(type(annotation.value))
242252

243253

244-
NDSubclassificationType = Union[NDRadioSubclass, NDChecklistSubclass,
254+
# Make sure to keep NDChecklistSubclass prior to NDRadioSubclass in the list,
255+
# otherwise list of answers gets parsed by NDRadio whereas NDChecklist must be used
256+
NDSubclassificationType = Union[NDChecklistSubclass, NDRadioSubclass,
245257
NDTextSubclass]
246-
NDClassificationType = Union[NDRadio, NDChecklist, NDText]
258+
259+
# Make sure to keep NDChecklist prior to NDRadio in the list,
260+
# otherwise list of answers gets parsed by NDRadio whereas NDChecklist must be used
261+
NDClassificationType = Union[NDChecklist, NDRadio, NDText]

0 commit comments

Comments
 (0)