Skip to content

Commit

Permalink
Merge pull request #5241 from aleksandra-tarkowska/rebased/develop/me…
Browse files Browse the repository at this point in the history
…rge_populate

Merge populate_metadata PRs (rebased onto develop)
  • Loading branch information
joshmoore committed May 9, 2017
2 parents ed1c3dd + 07e9010 commit dfa9f72
Show file tree
Hide file tree
Showing 18 changed files with 3,561 additions and 559 deletions.
4 changes: 4 additions & 0 deletions components/blitz/resources/omero/Tables.ice
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ module omero {
omero::api::LongArray values;
};

class DatasetColumn extends Column {
omero::api::LongArray values;
};

class RoiColumn extends Column {
omero::api::LongArray values;
};
Expand Down
16 changes: 8 additions & 8 deletions components/tools/OmeroPy/src/omero/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,12 @@ def _ask_for_password(self, reason="", root_pass=None, strict=True):
break
return root_pass

def _add_wait(self, parser, default=-1):
parser.add_argument(
"--wait", type=long,
help="Number of seconds to wait for the processing to complete "
"(Indefinite < 0; No wait=0).", default=default)

def get_subcommands(self):
"""Return a list of subcommands"""
parser = Parser()
Expand Down Expand Up @@ -1618,10 +1624,7 @@ def cmd_type(self):

def _configure(self, parser):
parser.set_defaults(func=self.main_method)
parser.add_argument(
"--wait", type=long,
help="Number of seconds to wait for the processing to complete "
"(Indefinite < 0; No wait=0).", default=-1)
self._add_wait(parser, default=-1)

def main_method(self, args):
client = self.ctx.conn(args)
Expand Down Expand Up @@ -1736,10 +1739,7 @@ def cmd_type(self):

def _configure(self, parser):
parser.set_defaults(func=self.main_method)
parser.add_argument(
"--wait", type=long,
help="Number of seconds to wait for the processing to complete "
"(Indefinite < 0; No wait=0).", default=-1)
self._add_wait(parser, default=-1)
parser.add_argument(
"--include",
help="Modifies the given option by including a list of objects")
Expand Down
29 changes: 26 additions & 3 deletions components/tools/OmeroPy/src/omero/plugins/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from omero.cli import ProxyStringType
from omero.constants import namespaces
from omero.gateway import BlitzGateway
from omero.util import populate_metadata, populate_roi
from omero.util import populate_metadata, populate_roi, pydict_text_io
from omero.util.metadata_utils import NSBULKANNOTATIONSCONFIG
from omero.util.metadata_utils import NSBULKANNOTATIONSRAW

Expand Down Expand Up @@ -147,6 +147,12 @@ def _configure(self, parser):
populate = parser.add(sub, self.populate)
populateroi = parser.add(sub, self.populateroi)

populate.add_argument("--batch",
type=long,
default=1000,
help="Number of objects to process at once")
self._add_wait(populate)

for x in (summary, original, bulkanns, measures, mapanns, allanns,
rois, populate, populateroi):
x.add_argument("obj",
Expand Down Expand Up @@ -196,6 +202,9 @@ def _configure(self, parser):
populate.add_argument("--attach", action="store_true", help=(
"Upload input or configuration files and attach to parent object"))

populate.add_argument("--localcfg", help=(
"Local configuration file or a JSON object string"))

populateroi.add_argument(
"--measurement", type=int, default=None,
help="Index of the measurement to populate. By default, all")
Expand Down Expand Up @@ -400,6 +409,12 @@ def populate(self, args):

context_class = dict(self.POPULATE_CONTEXTS)[args.context]

if args.localcfg:
localcfg = pydict_text_io.load(
args.localcfg, session=client.getSession())
else:
localcfg = {}

fileid = args.fileid
cfgid = args.cfgid

Expand All @@ -420,10 +435,18 @@ def populate(self, args):

# Note some contexts only support a subset of these args
ctx = context_class(client, args.obj, file=args.file, fileid=fileid,
cfg=args.cfg, cfgid=cfgid, attach=args.attach)
cfg=args.cfg, cfgid=cfgid, attach=args.attach,
options=localcfg)
ctx.parse()
if not args.dry_run:
ctx.write_to_omero()
wait = args.wait
if not wait:
loops = 0
ms = 0
else:
ms = 5000
loops = int((wait * 1000) / ms) + 1
ctx.write_to_omero(batch_size=args.batch, loops=loops, ms=ms)

def rois(self, args):
"Manage ROIs"
Expand Down
25 changes: 10 additions & 15 deletions components/tools/OmeroPy/src/omero/testlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
import Glacier2
import omero
import omero.gateway

from omero_version import omero_version
from collections import defaultdict

from omero.cmd import DoAll, State, ERR, OK, Chmod2, Chgrp2, Delete2
from omero.callbacks import CmdCallbackI
Expand Down Expand Up @@ -983,7 +985,7 @@ def make_file_annotation(self, name=None, binary=None, mimetype=None,
store = client.sf.createRawFileStore()
try:
store.setFileId(ofile.getId().getValue())
store.write(binary, 0, 0)
store.write(binary, 0, len(binary))
ofile = store.save() # See ticket:1501
finally:
store.close()
Expand Down Expand Up @@ -1033,26 +1035,19 @@ def link(self, obj1, obj2, client=None):
link.setChild(obj2.proxy())
return client.sf.getUpdateService().saveAndReturnObject(link)

def delete(self, obj):
def delete(self, objs):
"""
Deletes a list of model entities (ProjectI, DatasetI or ImageI)
Deletes model entities (ProjectI, DatasetI, ImageI, etc)
by creating Delete2 commands and calling
:func:`~test.ITest.do_submit`.
:param obj: a list of objects to be deleted
"""
if isinstance(obj[0], ProjectI):
t = "Project"
elif isinstance(obj[0], DatasetI):
t = "Dataset"
elif isinstance(obj[0], ImageI):
t = "Image"
else:
assert False, "Object type not supported."

ids = [i.id.val for i in obj]
command = Delete2(targetObjects={t: ids})

to_delete = defaultdict(list)
for obj in objs:
t = obj.__class__.__name__
to_delete[t].append(obj.id.val)
command = Delete2(targetObjects=to_delete)
self.do_submit(command, self.client)

def change_group(self, obj, target, client=None):
Expand Down
220 changes: 220 additions & 0 deletions components/tools/OmeroPy/src/omero/util/metadata_mapannotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# Copyright (C) 2016 University of Dundee & Open Microscopy Environment.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""
Utilities for manipulating map-annotations used as metadata
"""

import logging
from omero.model import NamedValue
from omero.rtypes import rstring, unwrap
# For complicated reasons `from omero.sys import ParametersI` doesn't work
from omero_sys_ParametersI import ParametersI

log = logging.getLogger("omero.util.metadata_mapannotations")


class MapAnnotationPrimaryKeyException(Exception):

def __init__(self, message):
super(MapAnnotationPrimaryKeyException, self).__init__(message)


class CanonicalMapAnnotation(object):
"""
A canonical representation of a map-annotation for metadata use
This is based around the idea of a primary key derived from the
combination of the namespace with 1+ keys-value pairs. A null
namespace is treated as an empty string (''), but still forms part
of the primary key.
ma: The omero.model.MapAnnotation object
primary_keys: Keys from key-value pairs that will be used to form the
primary key.
"""

def __init__(self, ma, primary_keys=None):
# TODO: should we consider data and description
self.ma = ma
ns = unwrap(ma.getNs())
self.ns = ns if ns else ''
try:
mapvalue = [(kv.name, kv.value) for kv in ma.getMapValue()]
except TypeError:
mapvalue = []
self.kvpairs, self.primary = self.process_keypairs(
mapvalue, primary_keys)
self.parents = set()

def process_keypairs(self, kvpairs, primary_keys):
if len(set(kvpairs)) != len(kvpairs):
raise ValueError('Duplicate key-value pairs found: %s' % kvpairs)

if primary_keys:
primary_keys = set(primary_keys)
missing = primary_keys.difference(kv[0] for kv in kvpairs)
if missing:
raise MapAnnotationPrimaryKeyException(
'Missing primary key fields: %s' % missing)
# ns is always part of the primary key
primary = (
self.ns,
frozenset((k, v) for (k, v) in kvpairs if k in primary_keys))
else:
primary = None

return kvpairs, primary

def merge(self, other):
"""
Adds any key/value pairs from other that aren't in self
Adds parents from other
Does not update primary key
"""
if self.kvpairs != other.kvpairs:
kvpairsset = set(self.kvpairs)
for okv in other.kvpairs:
if okv not in kvpairsset:
self.kvpairs.append(okv)
self.merge_parents(other)

def merge_parents(self, other):
self.parents.update(other.parents)

def add_parent(self, parenttype, parentid):
"""
Add a parent descriptor
Parameter types are important because they are used in a set
parenttype: An OMERO type string
parentid: An OMERO object ID (integer)
"""
if not isinstance(parenttype, str) or not isinstance(
parentid, (int, long)):
raise ValueError('Expected parenttype:str parentid:integer')
self.parents.add((parenttype, parentid))

def get_mapann(self):
"""
Update and return an omero.model.MapAnnotation with merged/combined
fields
"""
mv = [NamedValue(*kv) for kv in self.kvpairs]
self.ma.setMapValue(mv)
self.ma.setNs(rstring(self.ns))
return self.ma

def get_parents(self):
return self.parents

def __str__(self):
return 'ns:%s primary:%s keyvalues:%s parents:%s id:%s' % (
self.ns, self.primary, self.kvpairs, self.parents,
unwrap(self.ma.getId()))


class MapAnnotationManager(object):
"""
Handles creation and de-duplication of MapAnnotations
"""
# Policies for combining/replacing MapAnnotations
MA_APPEND, MA_OLD, MA_NEW = range(3)

def __init__(self, combine=MA_APPEND):
"""
Ensure you understand the doc string for init_from_namespace_query
if not using MA_APPEND
"""
self.mapanns = {}
self.nokey = []
self.combine = combine

def add(self, cma):
"""
Adds a CanonicalMapAnnotation to the managed list.
Returns any CanonicalMapAnnotation that are no longer required,
this may be cma or it may be a previously added annotation.
The idea is that this can be used to de-duplicate existing OMERO
MapAnnotations by calling add() on all MapAnnotations and deleting
those which are returned
If MapAnnotations are combined the parents of the unwanted
MapAnnotations are appended to the one that is kept by the manager.
:param cma: A CanonicalMapAnnotation
"""

if cma.primary is None:
self.nokey.append(cma)
return

try:
current = self.mapanns[cma.primary]
if current.ma is cma.ma:
# Don't re-add an identical object
return
if self.combine == self.MA_APPEND:
current.merge(cma)
return cma
if self.combine == self.MA_NEW:
self.mapanns[cma.primary] = cma
cma.merge_parents(current)
return current
if self.combine == self.MA_OLD:
current.merge_parents(cma)
return cma
raise ValueError('Invalid combine policy')
except KeyError:
self.mapanns[cma.primary] = cma

def get_map_annotations(self):
return self.mapanns.values() + self.nokey

def add_from_namespace_query(self, session, ns, primary_keys):
"""
Fetches all map-annotations with the given namespace
This will only work if there are no duplicates, otherwise an
exception will be thrown
WARNING: You should probably only use this in MA_APPEND mode since
the parents of existing annotations aren't fetched (requires a query
for each parent type)
WARNING: This may be resource intensive
TODO: Use omero.utils.populate_metadata._QueryContext for batch queries
:param session: An OMERO session
:param ns: The namespace
:param primary_keys: Primary keys
"""
qs = session.getQueryService()
q = 'FROM MapAnnotation WHERE ns=:ns ORDER BY id DESC'
p = ParametersI()
p.addString('ns', ns)
results = qs.findAllByQuery(q, p)
log.debug('Found %d MapAnnotations in ns:%s', len(results), ns)
for ma in results:
cma = CanonicalMapAnnotation(ma, primary_keys)
r = self.add(cma)
if r:
raise Exception(
'Duplicate MapAnnotation primary key: id:%s %s' % (
unwrap(ma.getId()), str(r)))

0 comments on commit dfa9f72

Please sign in to comment.