Skip to content

Commit cfbae96

Browse files
committed
Add operator for labeled_by, dataset, issue_category, annotation
1 parent 549e0d3 commit cfbae96

File tree

3 files changed

+33
-121
lines changed

3 files changed

+33
-121
lines changed

libs/labelbox/src/labelbox/schema/workflow/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
from labelbox.schema.workflow.project_filter import (
4646
ProjectWorkflowFilter,
4747
labeled_by,
48-
created_by, # Deprecated, use labeled_by instead
4948
annotation,
5049
dataset,
5150
issue_category,
@@ -94,13 +93,13 @@
9493
"NodeType",
9594
"ProjectWorkflowGraph",
9695
"ProjectWorkflowFilter",
97-
# Filter construction functions
96+
# Filter field objects
9897
"labeled_by",
99-
"created_by", # Deprecated, use labeled_by instead
10098
"annotation",
101-
"sample",
10299
"dataset",
103100
"issue_category",
101+
# Filter construction functions
102+
"sample",
104103
"model_prediction",
105104
"natural_language",
106105
"labeled_at",

libs/labelbox/src/labelbox/schema/workflow/project_filter.py

Lines changed: 24 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def is_one_of(self, values: List[str]) -> Dict[str, Any]:
101101
Args:
102102
values: List of IDs to match
103103
"""
104-
return {self._field_name: values, "__operator": "is"}
104+
return {self._field_name: values}
105105

106106
def is_not_one_of(self, values: List[str]) -> Dict[str, Any]:
107107
"""Filter for items that are NOT one of the specified values.
@@ -111,6 +111,17 @@ def is_not_one_of(self, values: List[str]) -> Dict[str, Any]:
111111
"""
112112
return {self._field_name: values, "__operator": "is_not"}
113113

114+
def has_any_of(self, values: List[str]) -> Dict[str, Any]:
115+
"""Filter for items that have any of the specified values.
116+
117+
This is semantically equivalent to is_one_of but provides clearer intent
118+
for certain filter types like categories or annotations.
119+
120+
Args:
121+
values: List of IDs to match
122+
"""
123+
return {self._field_name: values}
124+
114125

115126
class RangeField:
116127
"""Field class for range-based filters like consensus_average."""
@@ -171,30 +182,12 @@ def __call__(
171182
batch = ListField("Batch")
172183
consensus_average = RangeField("ConsensusAverage")
173184
feature_consensus_average = FeatureRangeField("FeatureConsensusAverage")
174-
# Note: dataset is a function, not a field object
175-
176-
177-
# Function versions for filter functions
178-
def dataset(
179-
dataset_ids: List[str], label: Optional[str] = None
180-
) -> Dict[str, Any]:
181-
"""Filter by dataset IDs.
182185

183-
Args:
184-
dataset_ids: List of dataset IDs to filter by
185-
label: Optional custom label to display in the UI instead of the default "DS-0" format
186-
187-
Returns:
188-
Dict representing the filter rule
189-
190-
Examples:
191-
dataset(["dataset-123", "dataset-456"])
192-
dataset(["dataset-123"], label="My Custom Dataset")
193-
"""
194-
result: Dict[str, Any] = {"Dataset": dataset_ids}
195-
if label is not None:
196-
result["__label"] = label
197-
return result
186+
# List-based filter field instances
187+
labeled_by = ListField("CreatedBy") # Maps to backend CreatedBy field
188+
dataset = ListField("Dataset")
189+
issue_category = ListField("IssueCategory")
190+
annotation = ListField("Annotation")
198191

199192

200193
class MetadataCondition:
@@ -302,68 +295,6 @@ def metadata(
302295
return result
303296

304297

305-
def labeled_by(
306-
user_ids: List[str], label: Optional[str] = None
307-
) -> Dict[str, Any]:
308-
"""Filter by users who labeled the data.
309-
310-
Args:
311-
user_ids: List of user IDs
312-
label: Optional custom label to display in the UI
313-
314-
Returns:
315-
Dict representing the filter rule
316-
"""
317-
result: Dict[str, Any] = {"CreatedBy": user_ids}
318-
if label is not None:
319-
result["__label"] = label
320-
return result
321-
322-
323-
def created_by(
324-
user_ids: List[str], label: Optional[str] = None
325-
) -> Dict[str, Any]:
326-
"""Filter by users who created the labels.
327-
328-
.. deprecated:: 2.1.0
329-
Use `labeled_by()` instead. This function will be removed in a future version.
330-
331-
Args:
332-
user_ids: List of user IDs
333-
label: Optional custom label to display in the UI
334-
335-
Returns:
336-
Dict representing the filter rule
337-
"""
338-
import warnings
339-
340-
warnings.warn(
341-
"created_by() is deprecated and will be removed in a future version. "
342-
"Use labeled_by() instead.",
343-
DeprecationWarning,
344-
stacklevel=2,
345-
)
346-
return labeled_by(user_ids, label)
347-
348-
349-
def annotation(
350-
schema_node_ids: List[str], label: Optional[str] = None
351-
) -> Dict[str, Any]:
352-
"""Filter by annotation schema node IDs.
353-
354-
Args:
355-
schema_node_ids: List of annotation schema node IDs
356-
label: Optional custom label to display in the UI
357-
358-
Returns:
359-
Dict representing the filter rule
360-
"""
361-
result: Dict[str, Any] = {"Annotation": schema_node_ids}
362-
if label is not None:
363-
result["__label"] = label
364-
return result
365-
366-
367298
def sample(percentage: int, label: Optional[str] = None) -> Dict[str, Any]:
368299
"""Filter by random sample percentage.
369300
@@ -391,24 +322,6 @@ def sample(percentage: int, label: Optional[str] = None) -> Dict[str, Any]:
391322
return result
392323

393324

394-
def issue_category(
395-
category_ids: List[str], label: Optional[str] = None
396-
) -> Dict[str, Any]:
397-
"""Filter by issue category IDs.
398-
399-
Args:
400-
category_ids: List of issue category IDs
401-
label: Optional custom label to display in the UI
402-
403-
Returns:
404-
Dict representing the filter rule
405-
"""
406-
result: Dict[str, Any] = {"IssueCategory": category_ids}
407-
if label is not None:
408-
result["__label"] = label
409-
return result
410-
411-
412325
def model_prediction(
413326
conditions: List[Dict[str, Any]], label: Optional[str] = None
414327
) -> Dict[str, Any]:
@@ -576,14 +489,17 @@ def convert_to_api_format(filter_rule: Dict[str, Any]) -> Dict[str, Any]:
576489

577490
class ProjectWorkflowFilter(BaseModel):
578491
"""
579-
Project workflow filter collection that enforces filter function syntax.
492+
Project workflow filter collection that enforces filter syntax.
580493
581-
Only accepts filters created using filter functions in this module.
494+
Only accepts filters created using filter field objects and functions in this module.
582495
This ensures type safety, IDE support, and eliminates manual string construction errors.
583496
584497
Example Usage:
585498
filters = ProjectWorkflowFilter([
586-
labeled_by(["user-123"]),
499+
labeled_by.is_one_of(["user-123"]),
500+
dataset.is_one_of(["dataset-456"]),
501+
issue_category.has_any_of(["cat1", "cat2"]),
502+
annotation.has_any_of(["bbox", "segmentation"]),
587503
sample(20),
588504
labeled_at.between("2024-01-01", "2024-12-31"),
589505
metadata([condition.contains("tag", "test")]),
@@ -594,7 +510,7 @@ class ProjectWorkflowFilter(BaseModel):
594510
logic.set_filters(filters)
595511
596512
# Or add individual filters
597-
logic.add_filter(labeled_by(["user-123"]))
513+
logic.add_filter(labeled_by.is_one_of(["user-123"]))
598514
"""
599515

600516
rules: List[Dict[str, Any]] = Field(default_factory=lambda: [])

libs/labelbox/tests/integration/test_workflow.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
FilterField,
2323
# Import filter functions
2424
labeled_by,
25-
created_by, # Still works for backward compatibility
2625
dataset,
2726
natural_language,
2827
labeling_time,
@@ -420,7 +419,7 @@ def test_workflow_copy(client, test_projects):
420419
logic = source_workflow.add_node(
421420
type=NodeType.Logic,
422421
name="Source Logic",
423-
filters=ProjectWorkflowFilter([labeled_by(["source-user"])]),
422+
filters=ProjectWorkflowFilter([labeled_by.is_one_of(["source-user"])]),
424423
)
425424
done = source_workflow.add_node(type=NodeType.Done, name="Source Done")
426425

@@ -469,7 +468,7 @@ def test_production_logic_node_with_comprehensive_filters(
469468
match_filters=MatchFilters.Any,
470469
filters=ProjectWorkflowFilter(
471470
[
472-
labeled_by(
471+
labeled_by.is_one_of(
473472
["cly7gzohg07zz07v5fqs63zmx", "cl7k7a9x1764808vk6bm1hf8e"]
474473
),
475474
metadata([m_condition.contains("tag", ["test"])]),
@@ -480,8 +479,8 @@ def test_production_logic_node_with_comprehensive_filters(
480479
),
481480
labeling_time.greater_than(1000),
482481
review_time.less_than_or_equal(100),
483-
dataset(["cm37vyets000z072314wxgt0l"]),
484-
annotation(["cm37w0e0500lf0709ba7c42m9"]),
482+
dataset.is_one_of(["cm37vyets000z072314wxgt0l"]),
483+
annotation.has_any_of(["cm37w0e0500lf0709ba7c42m9"]),
485484
consensus_average(0.17, 0.61),
486485
model_prediction(
487486
[
@@ -558,9 +557,7 @@ def test_filter_operations_with_persistence(client, test_projects):
558557
name="Filter Test",
559558
filters=ProjectWorkflowFilter(
560559
[
561-
created_by(
562-
["user1", "user2"]
563-
), # Still works - backward compatibility
560+
labeled_by.is_one_of(["user1", "user2"]), # New syntax
564561
sample(30),
565562
labeling_time.greater_than(500),
566563
]
@@ -614,7 +611,7 @@ def test_filter_operations_with_persistence(client, test_projects):
614611
), "LabeledBy filter should be removed"
615612

616613
# Test adding filters with persistence
617-
logic_after_removal.add_filter(dataset(["new-dataset"]))
614+
logic_after_removal.add_filter(dataset.is_one_of(["new-dataset"]))
618615
logic_after_removal.add_filter(
619616
metadata([m_condition.starts_with("priority", "high")])
620617
)

0 commit comments

Comments
 (0)