Skip to content

Commit 4f973fd

Browse files
Add Review and review metrics.
1 parent 3521ac5 commit 4f973fd

File tree

4 files changed

+140
-2
lines changed

4 files changed

+140
-2
lines changed

labelbox/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
from labelbox.client import Client
44
from labelbox.schema import (
5-
Project, Dataset, DataRow, Label, User, Organization, Task, LabelingFrontend,
6-
AssetMetadata, Webhook)
5+
Project, Dataset, DataRow, Label, Review, User, Organization, Task,
6+
LabelingFrontend, AssetMetadata, Webhook)

labelbox/orm/query.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,3 +592,14 @@ def edit_webhook(webhook, topics, url, status):
592592
results_query_part(type(webhook)))
593593

594594
return query_str, {}
595+
596+
597+
def project_review_metrics(project, net_score):
598+
project_id_param = "project_id"
599+
net_score_literal = "None" if net_score is None else net_score.name
600+
query_str = """query ProjectReviewMetricsPyApi($%s: ID!){
601+
project(where: {id:$%s})
602+
{reviewMetrics {labelAggregate(netScore: %s) {count}}}
603+
}""" % (project_id_param, project_id_param, net_score_literal)
604+
605+
return query_str, {project_id_param: project.uid}

labelbox/schema.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime, timezone
2+
from enum import Enum, auto
23
import json
34
import logging
45
from multiprocessing.dummy import Pool as ThreadPool
@@ -33,6 +34,7 @@ class Project(DbObject, Updateable, Deletable):
3334
datasets = Relationship.ToMany("Dataset", True)
3435
created_by = Relationship.ToOne("User", False, "created_by")
3536
organization = Relationship.ToOne("Organization", False)
37+
reviews = Relationship.ToMany("Review", True)
3638
labeling_frontend = Relationship.ToOne("LabelingFrontend")
3739
labeling_frontend_options = Relationship.ToMany(
3840
"LabelingFrontendOptions", False, "labeling_frontend_options")
@@ -104,6 +106,20 @@ def export_labels(self, timeout_seconds=60):
104106
# TODO Mutable (fetched) attributes
105107
# ...many, define which are required for v0.1
106108

109+
def review_metrics(self, net_score):
110+
""" Returns this Project's review metrics.
111+
Args:
112+
net_score (None or Review.NetScore): Indicates desired metric.
113+
Return:
114+
int, aggregation count of reviews for given net_score.
115+
"""
116+
if net_score not in (None,) + tuple(Review.NetScore):
117+
raise InvalidQueryError("Review metrics net score must be either None "
118+
"or one of Review.NetScore values")
119+
query_str, params = query.project_review_metrics(self, net_score)
120+
res = self.client.execute(query_str, params)
121+
return res["data"]["project"]["reviewMetrics"]["labelAggregate"]["count"]
122+
107123
def setup(self, labeling_frontend, labeling_frontend_options):
108124
""" Finalizes the Project setup.
109125
Args:
@@ -344,6 +360,11 @@ def create_metadata(self, meta_type, meta_value):
344360

345361

346362
class Label(DbObject, Updateable, BulkDeletable):
363+
364+
def __init__(self, *args, **kwargs):
365+
super().__init__(*args, **kwargs)
366+
self.reviews.supports_filtering = False
367+
347368
label = Field.String("label")
348369
seconds_to_label = Field.Float("seconds_to_label")
349370
agreement = Field.Float("agreement")
@@ -352,11 +373,39 @@ class Label(DbObject, Updateable, BulkDeletable):
352373

353374
project = Relationship.ToOne("Project")
354375
data_row = Relationship.ToOne("DataRow")
376+
reviews = Relationship.ToMany("Review", False)
355377

356378
@staticmethod
357379
def bulk_delete(objects):
358380
BulkDeletable.bulk_delete(objects, False)
359381

382+
def create_review(self, **kwargs):
383+
""" Creates a Review for this label.
384+
Kwargs:
385+
Review attributes. At a minimum a `Review.score` field
386+
value must be provided.
387+
"""
388+
kwargs[Review.label.name] = self
389+
kwargs[Review.project.name] = self.project()
390+
return self.client._create(Review, kwargs)
391+
392+
393+
class Review(DbObject, Deletable, Updateable):
394+
395+
class NetScore(Enum):
396+
Negative = auto()
397+
Zero = auto()
398+
Positive = auto()
399+
400+
updated_at = Field.DateTime("updated_at")
401+
created_at = Field.DateTime("created_at")
402+
score = Field.Float("score")
403+
404+
created_by = Relationship.ToOne("User", False, "created_by")
405+
organization = Relationship.ToOne("Organization", False)
406+
project = Relationship.ToOne("Project", False)
407+
label = Relationship.ToOne("Label", False)
408+
360409

361410
class AssetMetadata(DbObject):
362411
VIDEO = "VIDEO"

tests/integration/test_review.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import pytest
2+
3+
from labelbox import Review
4+
from labelbox.exceptions import InvalidQueryError
5+
6+
7+
IMG_URL = "https://picsum.photos/200/300"
8+
9+
10+
def test_reviews(client, rand_gen):
11+
project = client.create_project(name=rand_gen(str))
12+
dataset = client.create_dataset(name=rand_gen(str), projects=project)
13+
data_row = dataset.create_data_row(row_data=IMG_URL)
14+
label = project.create_label(data_row=data_row, label="test",
15+
seconds_to_label=0.0)
16+
17+
assert set(label.reviews()) == set()
18+
assert set(project.reviews()) == set()
19+
20+
r1 = label.create_review(score=-1.0)
21+
assert r1.project() == project
22+
assert r1.label() == label
23+
assert r1.score == -1.0
24+
assert set(label.reviews()) == {r1}
25+
assert set(project.reviews()) == {r1}
26+
27+
r2 = label.create_review(score=1.0)
28+
29+
assert set(label.reviews()) == {r1, r2}
30+
assert set(project.reviews()) == {r1, r2}
31+
32+
# Project.reviews supports filtering
33+
assert set(project.reviews(where=Review.score > 0.0)) == {r2}
34+
assert set(project.reviews(where=Review.score < 0.0)) == {r1}
35+
36+
# Label.reviews doesn't support filtering
37+
with pytest.raises(InvalidQueryError) as exc_info:
38+
assert set(label.reviews(where=Review.score > 0.0)) == {r2}
39+
assert exc_info.value.message == \
40+
"Relationship Label.reviews doesn't support filtering"
41+
42+
r1.delete()
43+
44+
assert set(label.reviews()) == {r2}
45+
assert set(project.reviews()) == {r2}
46+
47+
dataset.delete()
48+
project.delete()
49+
50+
51+
def test_review_metrics(client, rand_gen):
52+
project = client.create_project(name=rand_gen(str))
53+
dataset = client.create_dataset(name=rand_gen(str), projects=project)
54+
data_row = dataset.create_data_row(row_data=IMG_URL)
55+
label = project.create_label(data_row=data_row, label="test",
56+
seconds_to_label=0.0)
57+
58+
assert project.review_metrics(None) == 1
59+
assert project.review_metrics(Review.NetScore.Negative) == 0
60+
assert project.review_metrics(Review.NetScore.Zero) == 0
61+
assert project.review_metrics(Review.NetScore.Positive) == 0
62+
63+
for count, score in ((4, 0), (2, 1), (3, -1)):
64+
for _ in range(count):
65+
l = project.create_label(data_row=data_row, label="l",
66+
seconds_to_label=0.0)
67+
l.create_review(score=score)
68+
69+
assert project.review_metrics(None) == 1
70+
assert project.review_metrics(Review.NetScore.Negative) == 3
71+
assert project.review_metrics(Review.NetScore.Zero) == 4
72+
assert project.review_metrics(Review.NetScore.Positive) == 2
73+
74+
with pytest.raises(InvalidQueryError):
75+
project.review_metrics(12)
76+
77+
dataset.delete()
78+
project.delete()

0 commit comments

Comments
 (0)