@@ -258,6 +258,82 @@ def _create_annotations(label_id: UUID) -> list[DatasetItemAnnotation]:
258258 return _create_annotations
259259
260260
261+ @pytest .fixture
262+ def fxt_project_with_labeled_dataset_items (
263+ fxt_project_with_pipeline , db_session
264+ ) -> tuple [ProjectView , list [DatasetItemDB ]]:
265+ """Fixture to create a project with multiple labeled dataset items for testing label filtering."""
266+ project , _ = fxt_project_with_pipeline
267+
268+ # Ensure we have at least 2 labels
269+ assert len (project .task .labels ) >= 2 , "Project must have at least 2 labels for this fixture"
270+
271+ label_0_id = str (project .task .labels [0 ].id )
272+ label_1_id = str (project .task .labels [1 ].id )
273+
274+ configs = [
275+ # Item 0: No annotations
276+ {"name" : "item_no_labels" , "format" : "jpg" , "size" : 1024 , "width" : 1024 , "height" : 768 , "subset" : "unassigned" },
277+ # Item 1: Has label_0
278+ {
279+ "name" : "item_label_0" ,
280+ "format" : "jpg" ,
281+ "size" : 1024 ,
282+ "width" : 1024 ,
283+ "height" : 768 ,
284+ "subset" : "unassigned" ,
285+ "annotation_data" : [{"labels" : [{"id" : label_0_id }], "shape" : {"type" : "full_image" }}],
286+ },
287+ # Item 2: Has label_1
288+ {
289+ "name" : "item_label_1" ,
290+ "format" : "jpg" ,
291+ "size" : 1024 ,
292+ "width" : 1024 ,
293+ "height" : 768 ,
294+ "subset" : "unassigned" ,
295+ "annotation_data" : [{"labels" : [{"id" : label_1_id }], "shape" : {"type" : "full_image" }}],
296+ },
297+ # Item 3: Has both label_0 and label_1
298+ {
299+ "name" : "item_both_labels" ,
300+ "format" : "jpg" ,
301+ "size" : 1024 ,
302+ "width" : 1024 ,
303+ "height" : 768 ,
304+ "subset" : "unassigned" ,
305+ "annotation_data" : [
306+ {
307+ "labels" : [{"id" : label_0_id }],
308+ "shape" : {"type" : "rectangle" , "x" : 0 , "y" : 0 , "width" : 10 , "height" : 10 },
309+ },
310+ {
311+ "labels" : [{"id" : label_1_id }],
312+ "shape" : {"type" : "rectangle" , "x" : 20 , "y" : 20 , "width" : 10 , "height" : 10 },
313+ },
314+ ],
315+ },
316+ ]
317+
318+ db_dataset_items = []
319+ for config in configs :
320+ dataset_item = DatasetItemDB (** config )
321+ dataset_item .project_id = str (project .id )
322+ dataset_item .created_at = datetime .fromisoformat ("2025-02-01T00:00:00Z" )
323+ db_dataset_items .append (dataset_item )
324+ db_session .add_all (db_dataset_items )
325+ db_session .flush ()
326+
327+ # Link labels to dataset items
328+ db_session .add (DatasetItemLabelDB (dataset_item_id = db_dataset_items [1 ].id , label_id = label_0_id ))
329+ db_session .add (DatasetItemLabelDB (dataset_item_id = db_dataset_items [2 ].id , label_id = label_1_id ))
330+ db_session .add (DatasetItemLabelDB (dataset_item_id = db_dataset_items [3 ].id , label_id = label_0_id ))
331+ db_session .add (DatasetItemLabelDB (dataset_item_id = db_dataset_items [3 ].id , label_id = label_1_id ))
332+ db_session .flush ()
333+
334+ return project , db_dataset_items
335+
336+
261337class TestDatasetServiceIntegration :
262338 """Integration tests for DatasetService."""
263339
@@ -891,3 +967,109 @@ def test_annotation_status_filter_verifies_data_correctness(
891967 for item in to_review_items :
892968 assert item .annotation_data is not None
893969 assert item .user_reviewed is False
970+
971+ def test_list_dataset_items_filter_by_single_label (
972+ self ,
973+ fxt_dataset_service : DatasetService ,
974+ fxt_project_with_labeled_dataset_items : tuple [ProjectView , list [DatasetItemDB ]],
975+ ):
976+ """Test listing dataset items filtered by a single label."""
977+ project , db_dataset_items = fxt_project_with_labeled_dataset_items
978+ label_0_id = project .task .labels [0 ].id
979+
980+ # Filter by label_0 - should return items 1 and 3 (item_label_0 and item_both_labels)
981+ dataset_items = fxt_dataset_service .list_dataset_items (
982+ project = project ,
983+ label_ids = [label_0_id ],
984+ )
985+
986+ assert len (dataset_items ) == 2
987+ item_names = {item .name for item in dataset_items }
988+ assert item_names == {"item_label_0" , "item_both_labels" }
989+
990+ def test_list_dataset_items_filter_by_multiple_labels (
991+ self ,
992+ fxt_dataset_service : DatasetService ,
993+ fxt_project_with_labeled_dataset_items : tuple [ProjectView , list [DatasetItemDB ]],
994+ ):
995+ """Test listing dataset items filtered by multiple labels (OR logic)."""
996+ project , db_dataset_items = fxt_project_with_labeled_dataset_items
997+ label_0_id = project .task .labels [0 ].id
998+ label_1_id = project .task .labels [1 ].id
999+
1000+ # Filter by label_0 OR label_1 - should return items 1, 2, and 3
1001+ dataset_items = fxt_dataset_service .list_dataset_items (
1002+ project = project ,
1003+ label_ids = [label_0_id , label_1_id ],
1004+ )
1005+
1006+ assert len (dataset_items ) == 3
1007+ item_names = {item .name for item in dataset_items }
1008+ assert item_names == {"item_label_0" , "item_label_1" , "item_both_labels" }
1009+
1010+ def test_list_dataset_items_filter_by_nonexistent_label (
1011+ self ,
1012+ fxt_dataset_service : DatasetService ,
1013+ fxt_project_with_labeled_dataset_items : tuple [ProjectView , list [DatasetItemDB ]],
1014+ ):
1015+ """Test listing dataset items filtered by a nonexistent label."""
1016+ project , db_dataset_items = fxt_project_with_labeled_dataset_items
1017+ nonexistent_label_id = uuid4 ()
1018+
1019+ # Filter by nonexistent label - should return empty list
1020+ dataset_items = fxt_dataset_service .list_dataset_items (
1021+ project = project ,
1022+ label_ids = [nonexistent_label_id ],
1023+ )
1024+
1025+ assert len (dataset_items ) == 0
1026+
1027+ def test_count_dataset_items_filter_by_single_label (
1028+ self ,
1029+ fxt_dataset_service : DatasetService ,
1030+ fxt_project_with_labeled_dataset_items : tuple [ProjectView , list [DatasetItemDB ]],
1031+ ):
1032+ """Test counting dataset items filtered by a single label."""
1033+ project , db_dataset_items = fxt_project_with_labeled_dataset_items
1034+ label_0_id = project .task .labels [0 ].id
1035+
1036+ # Count items with label_0 - should return 2
1037+ count = fxt_dataset_service .count_dataset_items (
1038+ project = project ,
1039+ label_ids = [label_0_id ],
1040+ )
1041+
1042+ assert count == 2
1043+
1044+ def test_count_dataset_items_filter_by_multiple_labels (
1045+ self ,
1046+ fxt_dataset_service : DatasetService ,
1047+ fxt_project_with_labeled_dataset_items : tuple [ProjectView , list [DatasetItemDB ]],
1048+ ):
1049+ """Test counting dataset items filtered by multiple labels (OR logic)."""
1050+ project , db_dataset_items = fxt_project_with_labeled_dataset_items
1051+ label_0_id = project .task .labels [0 ].id
1052+ label_1_id = project .task .labels [1 ].id
1053+
1054+ # Count items with label_0 OR label_1 - should return 3
1055+ count = fxt_dataset_service .count_dataset_items (
1056+ project = project ,
1057+ label_ids = [label_0_id , label_1_id ],
1058+ )
1059+
1060+ assert count == 3
1061+
1062+ def test_list_dataset_items_no_label_filter (
1063+ self ,
1064+ fxt_dataset_service : DatasetService ,
1065+ fxt_project_with_labeled_dataset_items : tuple [ProjectView , list [DatasetItemDB ]],
1066+ ):
1067+ """Test listing dataset items without label filter returns all items."""
1068+ project , db_dataset_items = fxt_project_with_labeled_dataset_items
1069+
1070+ # No filter - should return all 4 items
1071+ dataset_items = fxt_dataset_service .list_dataset_items (project = project )
1072+
1073+ assert len (dataset_items ) == 4
1074+ item_names = {item .name for item in dataset_items }
1075+ assert item_names == {"item_no_labels" , "item_label_0" , "item_label_1" , "item_both_labels" }
0 commit comments