From 8734008033c642ea3cc82b982e9f0a4f8c59e6ec Mon Sep 17 00:00:00 2001 From: Tom Boissonnet Date: Wed, 14 Aug 2024 16:22:01 +0200 Subject: [PATCH] Implementation of annotation ID option in objects getters (#108) * get by annotation id image/dataset/project * tests get from annotation * get hcs objects by annotation * test of getting objects by annotation * remove unused user_group --- ezomero/_gets.py | 176 +++++++++++++++++++++++++++++++++++++++------ tests/test_gets.py | 139 +++++++++++++++++++++++++++++++++++ 2 files changed, 295 insertions(+), 20 deletions(-) diff --git a/ezomero/_gets.py b/ezomero/_gets.py index f95e8d7..779211e 100644 --- a/ezomero/_gets.py +++ b/ezomero/_gets.py @@ -333,6 +333,7 @@ def get_image_ids(conn: BlitzGateway, project: Optional[int] = None, plate: Optional[int] = None, well: Optional[int] = None, plate_acquisition: Optional[int] = None, + annotation: Optional[int] = None, across_groups: Optional[bool] = True) -> List[int]: """Return a list of image ids based on image container @@ -354,6 +355,9 @@ def get_image_ids(conn: BlitzGateway, project: Optional[int] = None, ID of Well from which to return image IDs. plate_acquisition : int, optional ID of Plate acquisition from which to return image IDs. + annotation : int, optional + ID of Annotation from which to return image IDs. This will return IDs + of all images linked to the specified annotation. across_groups : bool, optional Defines cross-group behavior of function - set to ``False`` to disable it. @@ -369,8 +373,9 @@ def get_image_ids(conn: BlitzGateway, project: Optional[int] = None, ``ezomero.set_group`` to specify group prior to passing the `conn` object to this function. - Only one of Project, Dataset, Plate, Well or Plate acquisition can be - specified. If none of those are specified, orphaned images are returned. + Only one of Project, Dataset, Plate, Well, Plate acquisition or Annotation + can be specified. If none of those are specified, orphaned images are + returned. Examples -------- @@ -381,14 +386,19 @@ def get_image_ids(conn: BlitzGateway, project: Optional[int] = None, # Return IDs of all images from Dataset with ID 448: >>> ds_ims = get_image_ids(conn, dataset=448) + + # Return IDs of all images annotated with Tag ID 876: + + >>> tag_ims = get_image_ids(conn, annotation=876) """ arg_counter = 0 - for arg in [project, dataset, plate, well, plate_acquisition]: + + for arg in [project, dataset, plate, well, plate_acquisition, annotation]: if arg is not None: arg_counter += 1 if arg_counter > 1: raise ValueError('Only one of Project/Dataset/Plate/Well' - '/PlateAcquisition can be specified') + '/PlateAcquisition/Annotation can be specified') q = conn.getQueryService() params = Parameters() @@ -456,6 +466,16 @@ def get_image_ids(conn: BlitzGateway, project: Optional[int] = None, params, conn.SERVICE_OPTS ) + elif annotation is not None: + if not isinstance(annotation, int): + raise TypeError('Annotation ID must be integer') + params.map = {"annotation": rlong(annotation)} + results = q.projection( + "SELECT l.parent.id FROM ImageAnnotationLink l" + " WHERE l.child.id=:annotation", + params, + conn.SERVICE_OPTS + ) else: results = q.projection( "SELECT i.id FROM Image i" @@ -476,6 +496,7 @@ def get_image_ids(conn: BlitzGateway, project: Optional[int] = None, @do_across_groups def get_project_ids(conn: BlitzGateway, + annotation: Optional[int] = None, across_groups: Optional[bool] = True) -> List[int]: """Return a list with IDs for all available Projects. @@ -483,6 +504,9 @@ def get_project_ids(conn: BlitzGateway, ---------- conn : ``omero.gateway.BlitzGateway`` object OMERO connection. + annotation : int, optional + ID of Annotation from which to return project IDs. This will return IDs + of all projects linked to the specified annotation. across_groups : bool, optional Defines cross-group behavior of function - set to ``False`` to disable it. @@ -497,19 +521,41 @@ def get_project_ids(conn: BlitzGateway, # Return IDs of all projects accessible by current user: >>> proj_ids = get_project_ids(conn) + + # Return IDs of all projects annotated with tag id 576: + + >>> proj_ids = get_project_ids(conn, annotation=576) """ - proj_ids = [] - for p in conn.listProjects(): - proj_ids.append(p.getId()) + + q = conn.getQueryService() + params = Parameters() + + if annotation is not None: + if not isinstance(annotation, int): + raise TypeError('Annotation ID must be integer') + params.map = {"annotation": rlong(annotation)} + results = q.projection( + "SELECT l.parent.id FROM ProjectAnnotationLink l" + " WHERE l.child.id=:annotation", + params, + conn.SERVICE_OPTS + ) + proj_ids = [r[0].val for r in results] + else: + proj_ids = [] + for p in conn.listProjects(): + proj_ids.append(p.getId()) return proj_ids @do_across_groups def get_dataset_ids(conn: BlitzGateway, project: Optional[int] = None, + annotation: Optional[int] = None, across_groups: Optional[bool] = True) -> List[int]: """Return a list of dataset ids based on project ID. - If no project is specified, function will return orphan datasets. + If no project or annotation is specified, function will return + orphan datasets. Parameters ---------- @@ -518,6 +564,9 @@ def get_dataset_ids(conn: BlitzGateway, project: Optional[int] = None, project : int, optional ID of Project from which to return dataset IDs. This will return IDs of all datasets contained in the specified Project. + annotation : int, optional + ID of Annotation from which to return dataset IDs. This will return IDs + of all datasets linked to the specified annotation. across_groups : bool, optional Defines cross-group behavior of function - set to ``False`` to disable it. @@ -537,6 +586,14 @@ def get_dataset_ids(conn: BlitzGateway, project: Optional[int] = None, >>> ds_ids = get_dataset_ids(conn, project=224) """ + arg_counter = 0 + for arg in [project, annotation]: + if arg is not None: + arg_counter += 1 + if arg_counter > 1: + raise ValueError('Only one of Project/Annotation' + ' can be specified') + q = conn.getQueryService() params = Parameters() @@ -552,6 +609,16 @@ def get_dataset_ids(conn: BlitzGateway, project: Optional[int] = None, params, conn.SERVICE_OPTS ) + elif annotation is not None: + if not isinstance(annotation, int): + raise TypeError('Annotation ID must be integer') + params.map = {"annotation": rlong(annotation)} + results = q.projection( + "SELECT l.parent.id FROM DatasetAnnotationLink l" + " WHERE l.child.id=:annotation", + params, + conn.SERVICE_OPTS + ) else: results = q.projection( "SELECT d.id FROM Dataset d" @@ -566,7 +633,7 @@ def get_dataset_ids(conn: BlitzGateway, project: Optional[int] = None, @do_across_groups -def get_screen_ids(conn: BlitzGateway, +def get_screen_ids(conn: BlitzGateway, annotation: Optional[int] = None, across_groups: Optional[bool] = True) -> List[int]: """Return a list with IDs for all available Screens. @@ -574,6 +641,9 @@ def get_screen_ids(conn: BlitzGateway, ---------- conn : ``omero.gateway.BlitzGateway`` object OMERO connection. + annotation : int, optional + ID of Annotation from which to return screen IDs. This will return IDs + of all screens linked to the specified annotation. across_groups : bool, optional Defines cross-group behavior of function - set to ``False`` to disable it. @@ -588,16 +658,36 @@ def get_screen_ids(conn: BlitzGateway, # Return IDs of all screens accessible by current user: >>> scrn_ids = get_screen_ids(conn) + + # Return IDs of all screens annotated with tag id 913: + + >>> proj_ids = get_screen_ids(conn, annotation=913) """ - scrn_ids = [] - for s in conn.listScreens(): - scrn_ids.append(s.getId()) + q = conn.getQueryService() + params = Parameters() + + if annotation is not None: + if not isinstance(annotation, int): + raise TypeError('Annotation ID must be integer') + params.map = {"annotation": rlong(annotation)} + results = q.projection( + "SELECT l.parent.id FROM ScreenAnnotationLink l" + " WHERE l.child.id=:annotation", + params, + conn.SERVICE_OPTS + ) + scrn_ids = [r[0].val for r in results] + else: + scrn_ids = [] + for s in conn.listScreens(): + scrn_ids.append(s.getId()) return scrn_ids @do_across_groups def get_plate_ids(conn: BlitzGateway, screen: Optional[int] = None, + annotation: Optional[int] = None, across_groups: Optional[bool] = True) -> List[int]: """Return a list of plate ids based on screen ID. @@ -610,6 +700,9 @@ def get_plate_ids(conn: BlitzGateway, screen: Optional[int] = None, screen : int, optional ID of Screen from which to return plate IDs. This will return IDs of all plates contained in the specified Screen. + annotation : int, optional + ID of Annotation from which to return plate IDs. This will return IDs + of all plates linked to the specified annotation. across_groups : bool, optional Defines cross-group behavior of function - set to ``False`` to disable it. @@ -629,6 +722,13 @@ def get_plate_ids(conn: BlitzGateway, screen: Optional[int] = None, >>> pl_ids = get_plate_ids(conn, screen=224) """ + arg_counter = 0 + for arg in [screen, annotation]: + if arg is not None: + arg_counter += 1 + if arg_counter > 1: + raise ValueError('Only one of Screen/Annotation' + ' can be specified') q = conn.getQueryService() params = Parameters() @@ -645,6 +745,16 @@ def get_plate_ids(conn: BlitzGateway, screen: Optional[int] = None, params, conn.SERVICE_OPTS ) + elif annotation is not None: + if not isinstance(annotation, int): + raise TypeError('Annotation ID must be integer') + params.map = {"annotation": rlong(annotation)} + results = q.projection( + "SELECT l.parent.id FROM PlateAnnotationLink l" + " WHERE l.child.id=:annotation", + params, + conn.SERVICE_OPTS + ) else: results = q.projection( "SELECT p.id FROM Plate p" @@ -660,7 +770,7 @@ def get_plate_ids(conn: BlitzGateway, screen: Optional[int] = None, @do_across_groups def get_well_ids(conn: BlitzGateway, screen: Optional[int] = None, - plate: Optional[int] = None, + plate: Optional[int] = None, annotation: Optional[int] = None, across_groups: Optional[bool] = True) -> List[int]: """Return a list of well ids based on a container @@ -674,6 +784,9 @@ def get_well_ids(conn: BlitzGateway, screen: Optional[int] = None, plate : int, optional ID of Plate from which to return well IDs. This will return IDs of all wells belonging to the specified Plate. + annotation : int, optional + ID of Annotation from which to return well IDs. This will return IDs + of all wells linked to the specified annotation. across_groups : bool, optional Defines cross-group behavior of function - set to ``False`` to disable it. @@ -690,14 +803,14 @@ def get_well_ids(conn: BlitzGateway, screen: Optional[int] = None, >>> wl_ids = get_well_ids(conn, screen=224) """ arg_counter = 0 - for arg in [screen, plate]: + for arg in [screen, plate, annotation]: if arg is not None: arg_counter += 1 if arg_counter > 1: - raise ValueError('Only one of Screen/Plate' + raise ValueError('Only one of Screen/Plate/Annotation' ' can be specified') elif arg_counter == 0: - raise ValueError('One of Screen/Plate' + raise ValueError('One of Screen/Plate/Annotation' ' must be specified') q = conn.getQueryService() @@ -727,13 +840,23 @@ def get_well_ids(conn: BlitzGateway, screen: Optional[int] = None, params, conn.SERVICE_OPTS ) + elif annotation is not None: + if not isinstance(annotation, int): + raise TypeError('Annotation ID must be integer') + params.map = {"annotation": rlong(annotation)} + results = q.projection( + "SELECT l.parent.id FROM WellAnnotationLink l" + " WHERE l.child.id=:annotation", + params, + conn.SERVICE_OPTS + ) return [r[0].val for r in results] @do_across_groups def get_plate_acquisition_ids( conn: BlitzGateway, screen: Optional[int] = None, - plate: Optional[int] = None, + plate: Optional[int] = None, annotation: Optional[int] = None, across_groups: Optional[bool] = True ) -> List[int]: """Return a list of plate acquisition ids based on a container @@ -750,6 +873,9 @@ def get_plate_acquisition_ids( ID of Plate from which to return plate acquisition IDs. This will return IDs of all plate acquisitions belonging to the specified Plate. + annotation : int, optional + ID of Annotation from which to return run IDs. This will return IDs + of all runs linked to the specified annotation. across_groups : bool, optional Defines cross-group behavior of function - set to ``False`` to disable it. @@ -766,14 +892,14 @@ def get_plate_acquisition_ids( >>> plate_acquisition_ids = get_plate_acquisition_ids(conn, screen=224) """ arg_counter = 0 - for arg in [screen, plate]: + for arg in [screen, plate, annotation]: if arg is not None: arg_counter += 1 if arg_counter > 1: - raise ValueError('Only one of Screen/Plate' + raise ValueError('Only one of Screen/Plate/Annotation' ' can be specified') elif arg_counter == 0: - raise ValueError('One of Screen/Plate' + raise ValueError('One of Screen/Plate/Annotation' ' must be specified') q = conn.getQueryService() @@ -803,6 +929,16 @@ def get_plate_acquisition_ids( params, conn.SERVICE_OPTS ) + elif annotation is not None: + if not isinstance(annotation, int): + raise TypeError('Annotation ID must be integer') + params.map = {"annotation": rlong(annotation)} + results = q.projection( + "SELECT l.parent.id FROM PlateAcquisitionAnnotationLink l" + " WHERE l.child.id=:annotation", + params, + conn.SERVICE_OPTS + ) return [r[0].val for r in results] diff --git a/tests/test_gets.py b/tests/test_gets.py index 2cf44d2..8fdef17 100644 --- a/tests/test_gets.py +++ b/tests/test_gets.py @@ -292,11 +292,35 @@ def test_get_image_ids(conn, project_structure, screen_structure, with pytest.raises(TypeError): _ = ezomero.get_image_ids(conn, plate_acquisition='test') + # Test get from tag annotation + username = users_groups[1][1][0] # test_user2 + groupname = users_groups[0][1][0] # test_group_2 + current_conn = conn.suConn(username, groupname) + tag_ann = TagAnnotationWrapper(current_conn) + tag_ann.setValue('test_tag') + tag_ann.save() + tag_id = tag_ann.getId() + im_o = current_conn.getObject('Image', im6_id) + im_o.linkAnnotation(tag_ann) + im_o = current_conn.getObject('Image', im7_id) + im_o.linkAnnotation(tag_ann) + tag_im_ids = ezomero.get_image_ids(current_conn, annotation=tag_id) + assert set(tag_im_ids) == set([im6_id, im7_id]) + current_conn.deleteObjects("Annotation", + [tag_id], + deleteAnns=True, + deleteChildren=True, + wait=True) + current_conn.close() + def test_get_project_ids(conn, project_structure, users_groups): project_info = project_structure[0] + with pytest.raises(TypeError): + _ = ezomero.get_project_ids(conn, annotation="test") + proj_ids = ezomero.get_project_ids(conn) assert len(proj_ids) == len(project_info) @@ -308,6 +332,29 @@ def test_get_project_ids(conn, project_structure, users_groups): assert len(pj_ids) == len(project_info) - 1 current_conn.close() + # Test get from tag annotation + username = users_groups[1][1][0] # test_user2 + groupname = users_groups[0][0][0] # test_group_1 + current_conn = conn.suConn(username, groupname) + proj4_id = project_info[4][1] + proj5_id = project_info[5][1] + tag_ann = TagAnnotationWrapper(current_conn) + tag_ann.setValue('test_tag') + tag_ann.save() + tag_id = tag_ann.getId() + pr_o = current_conn.getObject('Project', proj4_id) + pr_o.linkAnnotation(tag_ann) + pr_o = current_conn.getObject('Project', proj5_id) + pr_o.linkAnnotation(tag_ann) + tag_pr_ids = ezomero.get_project_ids(current_conn, annotation=tag_id) + assert set(tag_pr_ids) == set([proj4_id, proj5_id]) + current_conn.deleteObjects("Annotation", + [tag_id], + deleteAnns=True, + deleteChildren=True, + wait=True) + current_conn.close() + def test_get_dataset_ids(conn, project_structure, users_groups): @@ -316,6 +363,8 @@ def test_get_dataset_ids(conn, project_structure, users_groups): with pytest.raises(TypeError): _ = ezomero.get_dataset_ids(conn, project='test') + with pytest.raises(TypeError): + _ = ezomero.get_dataset_ids(conn, annotation="test") # Test orphans orphan_ids = ezomero.get_dataset_ids(conn) @@ -332,6 +381,29 @@ def test_get_dataset_ids(conn, project_structure, users_groups): bad_im_ids = ezomero.get_dataset_ids(conn, project=999999) assert not bad_im_ids + # Test get from tag annotation + username = users_groups[1][0][0] # test_user1 + groupname = users_groups[0][0][0] # test_group_1 + current_conn = conn.suConn(username, groupname) + ds4_id = dataset_info[4][1] + ds5_id = dataset_info[5][1] + tag_ann = TagAnnotationWrapper(current_conn) + tag_ann.setValue('test_tag') + tag_ann.save() + tag_id = tag_ann.getId() + ds_o = current_conn.getObject('Dataset', ds4_id) + ds_o.linkAnnotation(tag_ann) + ds_o = current_conn.getObject('Dataset', ds5_id) + ds_o.linkAnnotation(tag_ann) + tag_ds_ids = ezomero.get_dataset_ids(current_conn, annotation=tag_id) + assert set(tag_ds_ids) == set([ds4_id, ds5_id]) + current_conn.deleteObjects("Annotation", + [tag_id], + deleteAnns=True, + deleteChildren=True, + wait=True) + current_conn.close() + def test_get_screen_ids(conn, screen_structure): @@ -340,6 +412,21 @@ def test_get_screen_ids(conn, screen_structure): screen_ids = ezomero.get_screen_ids(conn) assert set(screen_ids) == set([screen_id]) + # Test get from tag annotation + tag_ann = TagAnnotationWrapper(conn) + tag_ann.setValue('test_tag') + tag_ann.save() + tag_id = tag_ann.getId() + scr_o = conn.getObject('Screen', screen_id) + scr_o.linkAnnotation(tag_ann) + tag_scr_ids = ezomero.get_screen_ids(conn, annotation=tag_id) + assert set(tag_scr_ids) == set([screen_id]) + conn.deleteObjects("Annotation", + [tag_id], + deleteAnns=True, + deleteChildren=True, + wait=True) + def test_get_plate_ids(conn, screen_structure): @@ -363,6 +450,23 @@ def test_get_plate_ids(conn, screen_structure): bad_pl_ids = ezomero.get_plate_ids(conn, screen=999999) assert not bad_pl_ids + # Test get from tag annotation + tag_ann = TagAnnotationWrapper(conn) + tag_ann.setValue('test_tag') + tag_ann.save() + tag_id = tag_ann.getId() + pl_o = conn.getObject('Plate', plate_id) + pl_o.linkAnnotation(tag_ann) + pl_o = conn.getObject('Plate', orphan_plate_id) + pl_o.linkAnnotation(tag_ann) + tag_pl_ids = ezomero.get_plate_ids(conn, annotation=tag_id) + assert set(tag_pl_ids) == set([plate_id, orphan_plate_id]) + conn.deleteObjects("Annotation", + [tag_id], + deleteAnns=True, + deleteChildren=True, + wait=True) + def test_get_well_ids(conn, screen_structure): @@ -396,6 +500,23 @@ def test_get_well_ids(conn, screen_structure): bad_ids = ezomero.get_well_ids(conn, screen=999999) assert not bad_ids + # Test get from tag annotation + tag_ann = TagAnnotationWrapper(conn) + tag_ann.setValue('test_tag') + tag_ann.save() + tag_id = tag_ann.getId() + wl_o = conn.getObject('Well', well_id1) + wl_o.linkAnnotation(tag_ann) + wl_o = conn.getObject('Well', well_id3) + wl_o.linkAnnotation(tag_ann) + tag_wl_ids = ezomero.get_well_ids(conn, annotation=tag_id) + assert set(tag_wl_ids) == set([well_id1, well_id3]) + conn.deleteObjects("Annotation", + [tag_id], + deleteAnns=True, + deleteChildren=True, + wait=True) + def test_get_plate_acquisition_ids(conn, screen_structure): @@ -425,6 +546,24 @@ def test_get_plate_acquisition_ids(conn, screen_structure): bad_ids = ezomero.get_plate_acquisition_ids(conn, screen=999999) assert not bad_ids + # Test get from tag annotation + tag_ann = TagAnnotationWrapper(conn) + tag_ann.setValue('test_tag') + tag_ann.save() + tag_id = tag_ann.getId() + paq_o = conn.getObject('PlateAcquisition', pacq_id1) + paq_o.linkAnnotation(tag_ann) + paq_o = conn.getObject('PlateAcquisition', pacq_id3) + paq_o.linkAnnotation(tag_ann) + tag_paq_ids = ezomero.get_plate_acquisition_ids(conn, + annotation=tag_id) + assert set(tag_paq_ids) == set([pacq_id1, pacq_id3]) + conn.deleteObjects("Annotation", + [tag_id], + deleteAnns=True, + deleteChildren=True, + wait=True) + def test_get_image_ids_params(conn): with pytest.raises(ValueError):