Skip to content

Add more opts support for getObjects('Annotation') - #489

Open
will-moore wants to merge 23 commits into
ome:masterfrom
will-moore:getObjects_Annotations
Open

Add more opts support for getObjects('Annotation')#489
will-moore wants to merge 23 commits into
ome:masterfrom
will-moore:getObjects_Annotations

Conversation

@will-moore

@will-moore will-moore commented Nov 24, 2025

Copy link
Copy Markdown
Member

conn.getObjects("Annotation")

This adds support for various options for conn.getObjects("Annotation", opts), in preparation for JSON api:
The OMERO_CLASS is now set on AnnotationWrapper objects.
Also Fixes #433.

This is used by ome/omero-web#682

Tests added in ome/openmicroscopy#6458

Supported opts - All are optional (but parent_ids needs parent_type)

  • "parent_type": "dataset"
  • "parent_ids": [1, 2]
  • "ns": "my.namespace"

E.g. /api/tagannotations/?project=1&project=2 would be backed by:

conn.getObjects("TagAnnotation", opts={
  "parent_type": "project",
  "parent_ids": [1,2]
})

%sAnnotationLink

This also improves the formulation of %sAnnotationLink (E.g. ImageAnnotationLink, ExperimenterGroupAnnotationLink, AnnotationAnnotationLink) in multiple places:

  • conn.getObjectsByMapAnnotations("Dataset", key="foo")
  • obj._loadAnnotationLinks() / _getAnnotationLinks()
    • unlinkAnnotations()
    • removeAnnotations()
    • getAnnotation()
    • listAnnotations()
  • obj._linkAnnotation() / _linkObject unused?
  • conn.getAnnotationLinks()
  • conn.countAnnotations()
  • conn.listOrphanedAnnotations()
  • AnnotationWrapper._getQueryString()

We can now do getObject() and listAnnotations() etc with many more object types:

E.g:

tag = omero.gateway.TagAnnotationWrapper(conn)
tag.setValue("Test Tag on Group")

group = conn.getGroupFromContext()
group.linkAnnotation(tag)

group.listAnnotations()

Known issue: Loading Parents

For webclient/api/annotations/ we get the annotations along with links to the parents and the parent objects too.
E.g.

  "link": {
    "id": 39752336,
    "owner": {
      "id": 2
    },
    "parent": {
      "id": 3414011,
      "class": "ImageI",
      "name": "10percent-Wt1-GFP-spheroid-MV.czi [0]"
    },
    "date": "2017-11-27T14:13:19+00:00",
    "permissions": {
      "canDelete": false,
      "canAnnotate": false,
      "canLink": false,
      "canEdit": false
    }
  }

This is useful to have permissions, timestamp, owner etc on the link but it is particularly essential when we are loading annotations on multiple objects. E.g. /api/annotations/?type=tag&project=1&project=2 would load annotations for 2 projects, BUT we without links, we don't know which Annotations are linked to which Project.

But, it doesn't appear possible to achieve this with omero_marshal, since for Annotations, there is no handling of links to Parent objects. Annotations are "Annotatable", so we check for annotations ON the object https://github.com/ome/omero-marshal/blob/f8ff1c2e439f0599d96423b7f64898864548ba9f/omero_marshal/encode/encoders/annotation.py#L68
but not whether the Object (annotation) is annotating a Parent.
In fact, I don't see that it's possible for e.g. an Annotation to have links loaded (in the same way that a Project or Dataset can have obj.copyAnnotationLinks(), since the possible links for an Annotation object are stored in a different Table for each parent, e.g. ProjectAnnotationLinks, DatasetAnnotationLinks etc.

@will-moore

Copy link
Copy Markdown
Member Author

Tests added in ome/openmicroscopy#6458

@will-moore

Copy link
Copy Markdown
Member Author

Comment on what changed after first commit....

The problem with the getObjects() is that we can't easily handle OR logic. E.g. "get Annotations with links to Project:1 OR Dataset:2". Currently, for each obj_type we add a clause, so we get AND logic:

                    clauses.append(
                        "exists (from %sAnnotationLink as link "
                        "where link.child.id = obj.id "
                        "and link.parent.id in (:%s_ids))" % (obj_type, plural))
                    params.add("%s_ids" % plural, rlist([rlong(i) for i in ids]))

Therefore,
conn.getObjects("Annotation", opts={"ann_type": "tag", "projects": [1,2], "datasets": [3,4]}) won't give you annotations on all objects, as you currently get with e.g. https://idr.openmicroscopy.org/webclient/api/annotations/?type=map&screen=3&image=3414011

Do we ever need to load annotations for more that 1 type of Object? E.g. webclient almost never allows you to select multiple different Object types, (e.g. Dataset and Image, except maybe in the search results)

The webclient/api endpoint does a series of queries select oal from %sAnnotationLink as oal for each obj_type requested. Each Database query only supports 1 object type. So it's probably OK for the getObjects("Annotation") to also support just ONE parent type. Same behaviour in E.g.

conn.getAnnotationLinks(...parent_type, parent_ids=None)

So, let's instead go for:

conn.getObjects("Annotation", opts={
  "ann_type": "tag",
  "parent_type": "project",
  "parent_ids": [1,2]
})

@will-moore will-moore changed the title Start to add more opts support for getObjects('Annotation') Add more opts support for getObjects('Annotation') Jun 30, 2026
We may be loading file annotations even without using ann_type of 'file'
@knabar

knabar commented Jul 2, 2026

Copy link
Copy Markdown
Member

Do we ever need to load annotations for more that 1 type of Object? E.g. webclient almost never allows you to select multiple different Object types, (e.g. Dataset and Image, except maybe in the search results)

The webclient/api endpoint does a series of queries select oal from %sAnnotationLink as oal for each obj_type requested. Each Database query only supports 1 object type. So it's probably OK for the getObjects("Annotation") to also support just ONE parent type.

It makes sense to me to limit the parent type to one per request, if a client requires annotations on different parent types, the client can make multiple requests.

I think it could even be justified to limit requests to a single parent object if that simplifies the requests and responses significantly.

Comment thread src/omero/gateway/__init__.py Outdated
@will-moore

will-moore commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

@knabar Anything else to address here?

I think it could even be justified to limit requests to a single parent object if that simplifies the requests and responses significantly.

There is no additional logic associated with supporting multiple parent objects (IDs) of the same type, and it could be useful so I'd like to leave this in.

@sbesson sbesson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few initial comments. I think the extensibility to support the filtering by annotation type, parent and namespace is useful but we will need additional changes to handle more generically all relevant objects.

Comment thread src/omero/gateway/__init__.py Outdated
Comment thread src/omero/gateway/__init__.py Outdated
Comment thread src/omero/gateway/__init__.py Outdated
:param opts: Dictionary of optional parameters.
NB: No options supported for this class.
ann_type: (optional) "tag", "file", "comment", "long", "map"
parent_type: (optional) "project", "dataset", "image" etc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a list of all objects that can be annotated?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually know where to find such a list or if it's possible to create one?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread src/omero/gateway/__init__.py Outdated
raise AttributeError(msg)

if 'parent_type' in opts:
obj_type = opts['parent_type'].title().replace("Plateacquisition", "PlateAcquisition")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, PlateAcquisition is not the only object that will need this special handling to convert from its lowercase version. Other examples include all annotation types (which can be annotated themselves), some instrument objects (LightPath, LightSource...) as well as OriginalFile.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using this list (from https://omero.readthedocs.io/en/stable/developers/Model/EveryObject.html#annotation)

AnnotationAnnotationLink
ChannelAnnotationLink
DatasetAnnotationLink
DetectorAnnotationLink
DichroicAnnotationLink
ExperimenterAnnotationLink
ExperimenterGroupAnnotationLink
FilesetAnnotationLink
FilterAnnotationLink
FolderAnnotationLink
ImageAnnotationLink
InstrumentAnnotationLink
LightPathAnnotationLink
LightSourceAnnotationLink
NamespaceAnnotationLink
NodeAnnotationLink
ObjectiveAnnotationLink
OriginalFileAnnotationLink
PlaneInfoAnnotationLink
PlateAcquisitionAnnotationLink
PlateAnnotationLink
ProjectAnnotationLink
ReagentAnnotationLink
RoiAnnotationLink
ScreenAnnotationLink
SessionAnnotationLink
ShapeAnnotationLink
WellAnnotationLink

I see only these cases:

ExperimenterGroup
LightPath
LightSource
OriginalFile
PlaneInfo
PlateAcquisition

If the parent_type is annotation then this will query for AnnotationAnnotationLink without any other handling needed.

@sbesson sbesson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting much better in particular with the removal of the duplicated _getQueryString to use OMERO_CLASS (with the additional clause for fetching OriginalFile).

Reviewing the list of structured annotations, the only concrete instantiation that misses a wrapper is ListAnnotation. Is that something we want to quickly implement as part of this PR to be feature complete?

if obj_type == otype.title():
obj_type = otype
ids_clause = ""
if 'parent_ids' in opts:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the real-world use case associated with specifying parent_type but no parent_ids?
I think it should be easy to enforce that parent_ids must be supplied if parent_type is specified. And that can always be relaxed later on if there is a real need.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbesson You may want to search for all omero.figure.json (ns) annotations that are on Datasets rather than on Images, or look for Tags on ROIs rather than on other objects etc. It's possible to imagine various use-cases.

Comment thread src/omero/gateway/__init__.py Outdated

# We want to make parent_type case-insensitive...
if 'parent_type' in opts:
# Title case works for most objects...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this utility to a separate utility method for transforming lower-case strings into case-sensitive object types with associated unit tests ?

I see a few places in the gateway that can use it. There might be use cases elsewhere in OMERO.py.

@pwalczysko

Copy link
Copy Markdown
Member

@will-moore excluding - this PR is causing the test_show.py errors in https://merge-ci.openmicroscopy.org/jenkins/job/OMERO-test-integration/43/testReport/

@sbesson

sbesson commented Aug 4, 2026

Copy link
Copy Markdown
Member

More specifically, the addition of OMERO_CLASS static variable to the various annotation wrapper classes is at odds with the implementation of ‎_BlitzGateway.getAnnotationLinks - see

class_string = wrapper().OMERO_CLASS
# E.g. AnnotationWrappers have no OMERO_CLASS
if class_string is None and "annotation" in parent_type.lower():
class_string = "Annotation"
which will need to be updated accordingly.

@will-moore

Copy link
Copy Markdown
Member Author

@sbesson Thanks for catching that. I realise (since no tests failed) that we don't have tests for this, so I added that to ome/openmicroscopy@2084d18
That also revealed that e.g. ann.linkAnnotation(tag) didn't work for linking annotations to annotations because ann is expected to have OMERO_CLASS so I fixed that too.

@sbesson

sbesson commented Aug 14, 2026

Copy link
Copy Markdown
Member

That also revealed that e.g. ann.linkAnnotation(tag) didn't work for linking annotations to annotations because ann is expected to have OMERO_CLASS so I fixed that too.

👍 does this mean this PR is incidentally fixing #433 as well?

@will-moore

will-moore commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

Updated the %sAnnotationLink logic in these methods:

  • conn.getObjectsByMapAnnotations("Dataset", key="foo")
  • obj._loadAnnotationLinks() / _getAnnotationLinks()
    • unlinkAnnotations()
    • removeAnnotations()
    • getAnnotation()
    • listAnnotations()
  • obj._linkAnnotation() / _linkObject unused?
  • conn.getAnnotationLinks()
  • conn.countAnnotations()
  • conn.listOrphanedAnnotations()
  • AnnotationWrapper._getQueryString()

@will-moore

Copy link
Copy Markdown
Member Author

Currently 1 test failure at https://merge-ci.openmicroscopy.org/jenkins/job/OMERO-test-integration/60/

def testGetAnnotations(self, gatewaywrapper, author_testimg_tiny):
...
# Check links to Tag on the Comment
        for obj_type in ["CommentAnnotation", "Annotation"]:
>           annLinks = list(gatewaywrapper.gateway.getAnnotationLinks(
                obj_type, parent_ids=[ann.getId()]))

        wrapper = KNOWN_WRAPPERS.get(parent_type.lower(), None)
>       link_name = wrapper.ann_link_name()
                    ^^^^^^^^^^^^^^^^^^^^^
E       AttributeError: 'function' object has no attribute 'ann_link_name'

../../../../.venv3/lib64/python3.11/site-packages/omero/gateway/__init__.py:3556: AttributeError

@pwalczysko

Copy link
Copy Markdown
Member

A new failing test - freshly from today is https://merge-ci.openmicroscopy.org/jenkins/job/OMERO-test-integration/62/testReport/OmeroPy.test.integration.gatewaytest.test_get_objects/TestGetObject/testGetObjectsAnnotation/

cls = <class 'omero.gateway.AnnotationWrapper'>
opts = {'parent_type': 'Dataset', 'parent_ids': [572]}

    @classmethod
    def _getQueryString(cls, opts=None):
        """
        Used for building queries in generic methods such as
        getObjects("Annotation")
        Returns a tuple of (query, clauses, params).
    
        :param opts:        Dictionary of optional parameters.
                            parent_type: (optional) "Project", "Dataset", "Image" etc
                            parent_ids: (optional) list of IDs for the parent type
                            ns: (optional) namespace string to filter by
        :return:            Tuple of string, list, ParametersI
        """
    
        query, clauses, params = super(
            AnnotationWrapper, cls)._getQueryString(opts)
        if opts is None:
            opts = {}
    
        fetch_file = ""
        if cls.OMERO_CLASS in ("Annotation", "FileAnnotation"):
            fetch_file = "left outer join fetch obj.file as file"
    
        query = (f"select obj from {cls.OMERO_CLASS} obj "
                 f"{fetch_file} "
                 "join fetch obj.details.owner as owner "
                 "join fetch obj.details.creationEvent")
    
        # We want to make parent_type case-insensitive...
        if 'parent_type' in opts:
>           ann_link_name = ann_link_name(opts['parent_type'])
                            ^^^^^^^^^^^^^
E           UnboundLocalError: cannot access local variable 'ann_link_name' where it is not associated with a value

@will-moore

Copy link
Copy Markdown
Member Author

@sbesson ready for another review, thanks.
I updated the %sAnnotationLink logic everywhere that uses it in the BlitzGateway.
So you can now do e.g:

        tag = omero.gateway.TagAnnotationWrapper(conn)
        tag.setValue("Test")
        group = conn.getGroupFromContext()
        group.linkAnnotation(tag)

        # also link annotations
        tag.linkAnnotation(anotherAnnotation)

        # listAnnotations works too
        group.listAnnotations()

and the ExperimenterGroupAnnotationLink and AnnotationAnnotationLink objects will be created.

Tests for both of these are in PR at ome/openmicroscopy#6458

@snoopycrimecop

Copy link
Copy Markdown
Member

Conflicting PR. Removed from build OMERO-python-superbuild-push#54. See the console output for more details.
Possible conflicts:

--conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BlitzGateway: Support linking annotations to other annotations

5 participants