Skip to content

Commit 978f983

Browse files
author
Matt Sokoloff
committed
webhook validation. Update tests
1 parent 8a8b7a8 commit 978f983

File tree

9 files changed

+152
-84
lines changed

9 files changed

+152
-84
lines changed

labelbox/schema/asset_metadata.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from enum import Enum
2+
13
from labelbox.orm.db_object import DbObject
24
from labelbox.orm.model import Field
35

@@ -10,12 +12,15 @@ class AssetMetadata(DbObject):
1012
meta_value (str): URL to an external file or a string of text
1113
"""
1214

13-
VIDEO = "VIDEO"
14-
IMAGE = "IMAGE"
15-
TEXT = "TEXT"
16-
IMAGE_OVERLAY = "IMAGE_OVERLAY"
15+
class MetaType(Enum):
16+
VIDEO = "VIDEO"
17+
IMAGE = "IMAGE"
18+
TEXT = "TEXT"
19+
IMAGE_OVERLAY = "IMAGE_OVERLAY"
1720

18-
SUPPORTED_TYPES = {VIDEO, IMAGE, TEXT, IMAGE_OVERLAY}
21+
#For backwards compatibility
22+
for topic in MetaType:
23+
vars()[topic.name] = topic.value
1924

2025
meta_type = Field.String("meta_type")
2126
meta_value = Field.String("meta_value")

labelbox/schema/data_row.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,17 @@ def create_metadata(self, meta_type, meta_value):
5555
5656
Args:
5757
meta_type (str): Asset metadata type, must be one of:
58-
VIDEO, IMAGE, TEXT, IMAGE_OVERLAY (AssetMetadata.SUPPORTED_TYPES)
58+
VIDEO, IMAGE, TEXT, IMAGE_OVERLAY (AssetMetadata.MetaType)
5959
meta_value (str): Asset metadata value.
6060
Returns:
6161
`AssetMetadata` DB object.
62+
Raises:
63+
ValueError: meta_type must be one of the supported types.
6264
"""
63-
if meta_type not in AssetMetadata.SUPPORTED_TYPES:
65+
supported_meta_types = [x.value for x in AssetMetadata.MetaType]
66+
if meta_type not in supported_meta_types:
6467
raise ValueError(
65-
f"metadata type must be one of {AssetMetadata.SUPPORTED_TYPES}. Found {meta_type}"
68+
f"metadata type must be one of {supported_meta_types}. Found {meta_type}"
6669
)
6770

6871
meta_type_param = "metaType"

labelbox/schema/dataset.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def data_rows_for_external_id(self, external_id, limit=10):
170170
171171
Args:
172172
external_id (str): External ID of the sought `DataRow`.
173+
limit (int): The maximum number of data rows to return for the given external_id
173174
174175
Returns:
175176
A single `DataRow` with the given ID.

labelbox/schema/project.py

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
from collections import namedtuple
2-
from datetime import datetime, timezone
31
import json
4-
from labelbox.schema.data_row import DataRow
2+
import time
53
import logging
4+
from collections import namedtuple
5+
from datetime import datetime, timezone
66
from pathlib import Path
7-
import time
87
from typing import Dict, List, Union, Iterable
98
from urllib.parse import urlparse
109

1110
from labelbox import utils
1211
from labelbox.schema.bulk_import_request import BulkImportRequest
12+
from labelbox.schema.data_row import DataRow
1313
from labelbox.exceptions import InvalidQueryError
1414
from labelbox.orm import query
1515
from labelbox.orm.db_object import DbObject, Updateable, Deletable
@@ -320,50 +320,28 @@ def validate_labeling_parameter_overrides(self, data):
320320
for idx, row in enumerate(data):
321321
if len(row) != 3:
322322
raise TypeError(
323-
f"Data must be a list of tuples containing a DataRow, priority (int), num_labels (int). Found {len(row)} items"
323+
f"Data must be a list of tuples containing a DataRow, priority (int), num_labels (int). Found {len(row)} items. Index: {idx}"
324324
)
325325
data_row, priority, num_labels = row
326326
if not isinstance(data_row, DataRow):
327327
raise TypeError(
328-
f"Datarow should be be of type DataRow. Found {data_row}")
328+
f"Datarow should be be of type DataRow. Found {type(data_row)}. Index: {idx}"
329+
)
329330

330-
for name, value in [["priority", priority],
331+
for name, value in [["Priority", priority],
331332
["Number of labels", num_labels]]:
332333
if not isinstance(value, int):
333334
raise TypeError(
334-
f"{name} must be an int. Found {type(value)} for data_row {data_row}"
335+
f"{name} must be an int. Found {type(value)} for data_row {data_row}. Index: {idx}"
335336
)
336337
if value < 1:
337338
raise ValueError(
338-
f"{name} must be greater than 0 for data_row {data_row}"
339+
f"{name} must be greater than 0 for data_row {data_row}. Index: {idx}"
339340
)
340341

341342
def set_labeling_parameter_overrides(self, data):
342343
""" Adds labeling parameter overrides to this project.
343-
344-
Priority:
345-
* data will be labeled in priority order
346-
- lower numbers labeled first
347-
- Minimum priority is 1.
348-
* Priority is not the queue position.
349-
- The position is determined by the relative priority.
350-
- Eg. [(data_row_1, 5,1), (data_row_2, 2,1), (data_row_3, 10,1)]
351-
will be assigned in the following order: [data_row_2, data_row_1, data_row_3]
352-
* datarows with parameter overrides will appear before datarows without overrides
353-
* The priority only effects items in the queue
354-
- Assigning a priority will not automatically add the item back into the queue
355-
356-
Number of labels:
357-
* The number times a data row should be labeled
358-
* This will create duplicates in a project
359-
* The queue will never assign the same datarow to a labeler more than once
360-
- if the number of labels is greater than the number of labelers working on a project then
361-
the extra items will get stuck in the queue (thsi can be fixed by removing the override at any time).
362-
* This can add items to the queue (even if they have already been labeled)
363-
- New copies will only be assigned to members who have not labeled that same datarow before.
364-
* Setting this to 1 will result in the default behavior (no duplicates)
365-
366-
344+
367345
See information on priority here:
368346
https://docs.labelbox.com/en/configure-editor/queue-system#reservation-system
369347
@@ -372,7 +350,29 @@ def set_labeling_parameter_overrides(self, data):
372350
373351
Args:
374352
data (iterable): An iterable of tuples. Each tuple must contain
375-
(Union[DataRow, datarow_id], priority, numberOfLabels) for the new override.
353+
(DataRow, priority<int>, number_of_labels<int>) for the new override.
354+
Priority:
355+
* Data will be labeled in priority order.
356+
- Lower number priority is labeled first.
357+
- Minimum priority is 1.
358+
* Priority is not the queue position.
359+
- The position is determined by the relative priority.
360+
- Eg. [(data_row_1, 5,1), (data_row_2, 2,1), (data_row_3, 10,1)]
361+
will be assigned in the following order: [data_row_2, data_row_1, data_row_3]
362+
* Datarows with parameter overrides will appear before datarows without overrides.
363+
* The priority only effects items in the queue.
364+
- Assigning a priority will not automatically add the item back into the queue.
365+
Number of labels:
366+
* The number times a data row should be labeled.
367+
* This will create duplicate data rows in a project (one for each number of labels).
368+
* Data rows will be sent to the queue (even if they have already been labeled).
369+
- New copies will only be assigned to members who have not labeled that same datarow before.
370+
- Already labeled duplicates will not be sent back to the queue.
371+
* The queue will never assign the same datarow to a single labeler more than once.
372+
- If the number of labels is greater than the number of labelers working on a project then
373+
the extra items will remain in the queue (this can be fixed by removing the override at any time).
374+
375+
* Setting this to 1 will result in the default behavior (no duplicates).
376376
Returns:
377377
bool, indicates if the operation was a success.
378378
"""

labelbox/schema/webhook.py

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import logging
2+
from enum import Enum
3+
from typing import Iterable, List
4+
25
from labelbox.orm import query
36
from labelbox.orm.db_object import DbObject, Updateable
47
from labelbox.orm.model import Entity, Field, Relationship
@@ -16,35 +19,41 @@ class Webhook(DbObject, Updateable):
1619
created_at (datetime)
1720
url (str)
1821
topics (str): LABEL_CREATED, LABEL_UPDATED, LABEL_DELETED
22+
REVIEW_CREATED, REVIEW_UPDATED, REVIEW_DELETED
1923
status (str): ACTIVE, INACTIVE, REVOKED
2024
2125
"""
2226

23-
# Status
24-
ACTIVE = "ACTIVE"
25-
INACTIVE = "INACTIVE"
26-
REVOKED = "REVOKED"
27+
class WebhookStatus(Enum):
28+
ACTIVE = "ACTIVE"
29+
INACTIVE = "INACTIVE"
30+
REVOKED = "REVOKED"
2731

28-
# Topic
29-
LABEL_CREATED = "LABEL_CREATED"
30-
LABEL_UPDATED = "LABEL_UPDATED"
31-
LABEL_DELETED = "LABEL_DELETED"
32+
class WebhookTopic(Enum):
33+
LABEL_CREATED = "LABEL_CREATED"
34+
LABEL_UPDATED = "LABEL_UPDATED"
35+
LABEL_DELETED = "LABEL_DELETED"
36+
REVIEW_CREATED = "REVIEW_CREATED"
37+
REVIEW_UPDATED = "REVIEW_UPDATED"
38+
REVIEW_DELETED = "REVIEW_DELETED"
3239

33-
REVIEW_CREATED = "REVIEW_CREATED"
34-
REVIEW_UPDATED = "REVIEW_UPDATED"
35-
REVIEW_DELETED = "REVIEW_DELETED"
40+
#For backwards compatibility
41+
for topic in WebhookStatus:
42+
vars()[topic.name] = topic.value
3643

37-
SUPPORTED_TOPICS = {
38-
LABEL_CREATED, LABEL_UPDATED, LABEL_DELETED, REVIEW_CREATED,
39-
REVIEW_UPDATED, REVIEW_DELETED
40-
}
44+
for status in WebhookTopic:
45+
vars()[status.name] = status.value
4146

4247
updated_at = Field.DateTime("updated_at")
4348
created_at = Field.DateTime("created_at")
4449
url = Field.String("url")
4550
topics = Field.String("topics")
4651
status = Field.String("status")
4752

53+
created_by = Relationship.ToOne("User", False, "created_by")
54+
organization = Relationship.ToOne("Organization")
55+
project = Relationship.ToOne("Project")
56+
4857
@staticmethod
4958
def create(client, topics, url, secret, project):
5059
""" Creates a Webhook.
@@ -53,7 +62,7 @@ def create(client, topics, url, secret, project):
5362
client (Client): The Labelbox client used to connect
5463
to the server.
5564
topics (list of str): A list of topics this Webhook should
56-
get notifications for. Must be one of Webhook.SUPPORTED_TOPICS
65+
get notifications for. Must be one of Webhook.WebhookTopic
5766
url (str): The URL to which notifications should be sent
5867
by the Labelbox server.
5968
secret (str): A secret key used for signing notifications.
@@ -63,10 +72,14 @@ def create(client, topics, url, secret, project):
6372
Returns:
6473
A newly created Webhook.
6574
75+
Raises:
76+
ValueError: If the topic is not one of WebhookTopic or status is not one of WebhookStatus
77+
6678
Information on configuring your server can be found here (this is where the url points to and the secret is set).
6779
https://docs.labelbox.com/en/configure-editor/webhooks-setup#setup-steps
68-
80+
6981
"""
82+
Webhook.validate_topics(topics)
7083

7184
project_str = "" if project is None \
7285
else ("project:{id:\"%s\"}," % project.uid)
@@ -78,30 +91,47 @@ def create(client, topics, url, secret, project):
7891

7992
return Webhook(client, client.execute(query_str)["createWebhook"])
8093

81-
created_by = Relationship.ToOne("User", False, "created_by")
82-
organization = Relationship.ToOne("Organization")
83-
project = Relationship.ToOne("Project")
94+
@staticmethod
95+
def validate_topics(topics: List["Webhook.WebhookTopic"]):
96+
if not isinstance(topics, list):
97+
raise TypeError(
98+
f"Topics must be List[Webhook.WebhookTopic]. Found `{topics}`")
99+
100+
for topic in topics:
101+
Webhook.validate_value(topic, Webhook.WebhookTopic)
102+
103+
@staticmethod
104+
def validate_value(value, enum):
105+
supported_values = [x.value for x in enum]
106+
if value not in supported_values:
107+
raise ValueError(
108+
f"Value `{value}` does not exist in supported values. Expected one of {supported_values}"
109+
)
84110

85111
def delete(self):
86-
self.update(status="INACTIVE")
112+
self.update(status=self.WebhookStatus.INACTIVE)
87113

88114
def update(self, topics=None, url=None, status=None):
89-
""" Updates this Webhook.
115+
""" Updates the Webhook.
90116
91117
Args:
92-
topics (list of str): The new topics value, optional.
93-
url (str): The new URL value, optional.
94-
status (str): The new status value, optional.
118+
topics (List[str]): The new topics.
119+
url (str): The new URL value.
120+
status (str): The new status.
95121
96122
If values are set to None then there are no updates made to that field.
97123
98-
The following code will delete the webhook.
99-
>>> self.update(status = Webhook.INACTIVE)
100-
101124
"""
102125

103126
# Webhook has a custom `update` function due to custom types
104127
# in `status` and `topics` fields.
128+
129+
if topics is not None:
130+
self.validate_topics(topics)
131+
132+
if status is not None:
133+
self.validate_value(status, self.WebhookStatus)
134+
105135
topics_str = "" if topics is None \
106136
else "topics: {set: [%s]}" % " ".join(topics)
107137
url_str = "" if url is None else "url: \"%s\"" % url

tests/integration/test_asset_metadata.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ def test_asset_metadata_crud(project, dataset, rand_gen):
1919
assert asset.meta_value == "Value"
2020
assert len(list(data_row.metadata())) == 1
2121

22-
with pytest.raises(ValueError):
22+
with pytest.raises(ValueError) as exc_info:
2323
data_row.create_metadata("NOT_SUPPORTED_TYPE", "Value")
24+
assert str(exc_info.value) == \
25+
f"metadata type must be one of {[x.value for x in AssetMetadata.MetaType]}. Found NOT_SUPPORTED_TYPE"
2426

2527
# Check that filtering and sorting is prettily disabled
2628
with pytest.raises(InvalidQueryError) as exc_info:

tests/integration/test_labeling_parameter_overrides.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,34 @@ def test_labeling_parameter_overrides(project, rand_gen):
3434
# currently this doesn't work so the count remains 3
3535
assert len(list(project.labeling_parameter_overrides())) == 1
3636

37-
with pytest.raises(TypeError):
38-
data = [(data_rows[12], "yoo", 3)]
37+
with pytest.raises(TypeError) as exc_info:
38+
data = [(data_rows[12], "a_string", 3)]
3939
project.set_labeling_parameter_overrides(data)
40+
assert str(exc_info.value) == \
41+
f"Priority must be an int. Found <class 'str'> for data_row {data_rows[12]}. Index: 0"
4042

41-
with pytest.raises(TypeError):
42-
data = [(data_rows[12], 3, "yoo")]
43+
with pytest.raises(TypeError) as exc_info:
44+
data = [(data_rows[12], 3, "a_string")]
4345
project.set_labeling_parameter_overrides(data)
46+
assert str(exc_info.value) == \
47+
f"Number of labels must be an int. Found <class 'str'> for data_row {data_rows[12]}. Index: 0"
4448

45-
with pytest.raises(TypeError):
49+
with pytest.raises(TypeError) as exc_info:
4650
data = [(data_rows[12].uid, 1, 3)]
4751
project.set_labeling_parameter_overrides(data)
52+
assert str(exc_info.value) == \
53+
"Datarow should be be of type DataRow. Found <class 'str'>. Index: 0"
4854

49-
with pytest.raises(TypeError):
50-
data = [(data_rows[12].uid, 0, 3)]
55+
with pytest.raises(ValueError) as exc_info:
56+
data = [(data_rows[12], 0, 3)]
5157
project.set_labeling_parameter_overrides(data)
58+
assert str(exc_info.value) == \
59+
f"Priority must be greater than 0 for data_row {data_rows[12]}. Index: 0"
5260

53-
with pytest.raises(TypeError):
54-
data = [(data_rows[12].uid, 1, 0)]
61+
with pytest.raises(ValueError) as exc_info:
62+
data = [(data_rows[12], 1, 0)]
5563
project.set_labeling_parameter_overrides(data)
64+
assert str(exc_info.value) == \
65+
f"Number of labels must be greater than 0 for data_row {data_rows[12]}. Index: 0"
5666

5767
dataset.delete()

tests/integration/test_project.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ def test_project_filtering(client, rand_gen):
6060
def test_upsert_review_queue(project):
6161
project.upsert_review_queue(0.6)
6262

63-
with pytest.raises(ValueError):
63+
with pytest.raises(ValueError) as exc_info:
6464
project.upsert_review_queue(1.001)
65+
assert str(exc_info.value) == "Quota factor must be in the range of [0,1]"
6566

66-
with pytest.raises(ValueError):
67+
with pytest.raises(ValueError) as exc_info:
6768
project.upsert_review_queue(-0.001)
69+
assert str(exc_info.value) == "Quota factor must be in the range of [0,1]"
6870

6971

7072
def test_extend_reservations(project):
@@ -95,6 +97,6 @@ def test_attach_instructions(client, project):
9597
list(project.labeling_frontend_options())
9698
[-1].customization_options).get('projectInstructions') is not None
9799

98-
with pytest.raises(ValueError) as execinfo:
100+
with pytest.raises(ValueError) as exc_info:
99101
project.upsert_instructions('/tmp/file.invalid_file_extension')
100-
assert "instructions_file must end with one of" in str(execinfo.value)
102+
assert "instructions_file must end with one of" in str(exc_info.value)

0 commit comments

Comments
 (0)