#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2016 Glencoe Software, Inc. All Rights Reserved.
# Use is subject to license terms supplied in LICENSE.txt
#
# 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.
"""
Plugin for performing OMERO transactional changes (DML)
from a simple script. This macro like language is intended
to be used simply from the command-line as well as in scripts
and tests.
"""
import re
import sys
import shlex
import fileinput
from omero_ext.argparse import SUPPRESS
from omero.cli import BaseControl, CLI, ExceptionHandler
from omero.rtypes import rlong, unwrap
[docs]
class TxField(object):
ARG_RE = re.compile((r"(?P<FIELD>[a-zA-Z][a-zA-Z0-9]*)"
"(?P<OPER>[@])?="
"(?P<VALUE>.*)"), re.MULTILINE|re.DOTALL)
def __init__(self, tx_state, arg):
self.tx_state = tx_state
m = self.ARG_RE.match(arg)
if not m:
raise Exception("Unparseable argument: %s" % arg)
self.argname = m.group("FIELD")
capitalized = self.argname[0].upper() + self.argname[1:]
self.setter = "set%s" % capitalized
self.value = m.group("VALUE")
self.oper = m.group("OPER")
if self.oper == "@":
# Treat value like an array lookup
if re.match(r'\d+$', self.value):
self.value = tx_state.get_row(int(self.value))
elif re.match(TxCmd.VAR_NAME + '$', self.value):
self.value = tx_state.get_var(self.value)
else:
raise Exception("Invalid reference: %s" % self.value)
def __call__(self, obj):
return getattr(obj, self.setter)(self.value, wrap=True)
[docs]
class TxCmd(object):
VAR_NAME = r"(?P<DEST>[a-zA-Z][a-zA-Z0-9]*)"
VAR_RE = re.compile((r"^\s*%s"
r"\s*=\s"
r"(?P<REST>.*)$") % VAR_NAME)
def __init__(self, tx_state, arg_list=None, line=None):
"""
Command of the form:
(var = ) action type(:id) ( field(@)=value ... )
where parentheses denote optional values.
"""
self.tx_state = tx_state
self.arg_list = arg_list
self.orig_line = line
self.dest = None
self.action = None
self.type = None
self.fields = []
self._parse_early()
def _parse_early(self):
line = self.orig_line
if self.arg_list and line:
raise Exception("Both arg_list and line specified")
elif not self.arg_list and not line:
raise Exception("Neither arg_list nor line specified")
elif line:
m = re.match(self.VAR_RE, line)
if m:
self.dest = m.group("DEST")
line = m.group("REST")
self.arg_list = shlex.split(line)
self.action = self.arg_list[0]
if len(self.arg_list) > 1:
self.type = self.arg_list[1]
def _parse_late(self):
if len(self.arg_list) > 2:
for arg in self.arg_list[2:]:
self.fields.append(TxField(self.tx_state, arg))
[docs]
def setters(self):
self._parse_late()
for field in self.fields:
yield (field.argname, field)
def __str__(self):
return " ".join(self.arg_list)
[docs]
class TxAction(object):
"""
Parsed operation provided by the user chosen
based on the first non-variable field of the
command. Implementations can choose how they
will handle the fields parsed by TxCmd.
"""
def __init__(self, tx_state, tx_cmd):
self.tx_state = tx_state
self.tx_cmd = tx_cmd
[docs]
def go(self, ctx, args):
raise Exception("Unimplemented")
[docs]
def class_name(self, ctx):
try:
kls = self.tx_cmd.type.split(":")[0]
if not kls.endswith("I"):
kls = "%sI" % kls
return kls
except AttributeError:
ctx.die(102, "No object argument provided. Use e.g. 'Image:123'")
[docs]
def obj_id(self):
parts = self.tx_cmd.type.split(":")
try:
return int(parts[1])
except Exception:
return None
[docs]
def instance(self, ctx):
import omero
import omero.all
try:
kls = getattr(omero.model, self.class_name(ctx))
obj = kls()
oid = self.obj_id()
if oid is not None:
obj.setId(rlong(oid))
obj.unload()
kls = kls.__name__
if kls.endswith("I"):
kls = kls[0:-1]
return obj, kls
except AttributeError:
ctx.die(102, "No class named '%s'" % self.class_name(ctx))
[docs]
class NewObjectTxAction(TxAction):
[docs]
def check_requirements(self, ctx, obj, completed):
missing = []
total = dict(obj._field_info._asdict())
for arg in completed:
del total[arg]
for remaining, info in list(total.items()):
if info.nullable is False:
missing.append(remaining)
if missing:
ctx.die(103, "required arguments: %s" %
", ".join(missing))
[docs]
def go(self, ctx, args):
import omero
self.tx_state.add(self)
c = ctx.conn(args)
up = c.sf.getUpdateService()
obj, kls = self.instance(ctx)
completed = []
for field, setter in self.tx_cmd.setters():
try:
setter(obj)
completed.append(field)
except omero.ClientError as ce:
ctx.die(333, "%s" % ce)
self.check_requirements(ctx, obj, completed)
try:
out = up.saveAndReturnObject(obj)
except omero.ServerError as se:
ctx.die(336, "Failed to create %s - %s" %
(kls, se.message))
proxy = "%s:%s" % (kls, out.id.val)
self.tx_state.set_value(proxy, dest=self.tx_cmd.dest)
[docs]
class UpdateObjectTxAction(TxAction):
[docs]
def go(self, ctx, args):
import omero
self.tx_state.add(self)
c = ctx.conn(args)
q = c.sf.getQueryService()
up = c.sf.getUpdateService()
obj, kls = self.instance(ctx)
if obj.id is None:
ctx.die(334, "No id given for %s. Use e.g. '%s:123'"
% (kls, kls))
try:
obj = q.get(kls, obj.id.val, {"omero.group": "-1"})
except omero.ServerError:
ctx.die(334, "No object found: %s:%s" %
(kls, obj.id.val))
for field, setter in self.tx_cmd.setters():
try:
setter(obj)
except omero.ClientError as ce:
ctx.die(335, "%s" % ce)
try:
out = up.saveAndReturnObject(obj)
except omero.ServerError as se:
ctx.die(336, "Failed to update %s:%s - %s" %
(kls, obj.id.val, se.message))
proxy = "%s:%s" % (kls, out.id.val)
self.tx_state.set_value(proxy, dest=self.tx_cmd.dest)
[docs]
class NonFieldTxAction(TxAction):
"""
Base class for use with command actions which
don't take the standard a=b c=d fields.
"""
[docs]
def go(self, ctx, args):
import omero
self.tx_state.add(self)
self.client = ctx.conn(args)
self.query = self.client.sf.getQueryService()
self.update = self.client.sf.getUpdateService()
self.obj, self.kls = self.instance(ctx)
if self.obj.id is None:
ctx.die(334, "No id given for %s. Use e.g. '%s:123'"
% (self.kls, self.kls))
try:
self.obj = self.query.get(
self.kls, self.obj.id.val, {"omero.group": "-1"})
except omero.ServerError:
ctx.die(334, "No object found: %s:%s" %
(self.kls, self.obj.id.val))
self.on_go(ctx, args)
[docs]
def save_and_return(self, ctx):
import omero
try:
out = self.update.saveAndReturnObject(self.obj)
except omero.ServerError as se:
ctx.die(336, "Failed to update %s:%s - %s" % (
self.kls, self.obj.id.val, se.message))
proxy = "%s:%s" % (self.kls, out.id.val)
self.tx_state.set_value(proxy, dest=self.tx_cmd.dest)
[docs]
class ExtInfoSetTxAction(NonFieldTxAction):
[docs]
def on_go(self, ctx, args):
# ['ext-info-set', 'Project:302', 'lsid', 'entityType', 'entityId']
argc = len(self.tx_cmd.arg_list)
if argc not in [4, 5, 6]:
ctx.die(345,
"usage: ext-info-set OBJ entityId entityType [lsid] [uuid]")
try:
entity_id = int(self.tx_cmd.arg_list[2])
except ValueError:
ctx.die(347, "entityId must be an integer, got: %s" %
self.tx_cmd.arg_list[2])
entity_type = self.tx_cmd.arg_list[3]
details = self.obj.getDetails()
extinfo_id = None
# if externalInfo exists, delete affer replacing below...
if details and details._externalInfo:
extinfo_id = details._externalInfo._id.val
from omero.model import ExternalInfoI
from omero.rtypes import rstring, rlong
extinfo = ExternalInfoI()
# non-nullable properties
setattr(extinfo, "entityId", rlong(entity_id))
setattr(extinfo, "entityType", rstring(entity_type))
# nullable properties
if argc > 4:
lsid = self.tx_cmd.arg_list[4]
setattr(extinfo, "lsid", rstring(lsid))
if argc == 6:
uuid = self.tx_cmd.arg_list[5]
setattr(extinfo, "uuid", rstring(uuid))
self.obj.details.externalInfo = extinfo
self.save_and_return(ctx)
if extinfo_id is not None:
self.update.deleteObject(ExternalInfoI(extinfo_id, False))
[docs]
class ExtInfoGetTxAction(NonFieldTxAction):
[docs]
def on_go(self, ctx, args):
details = self.obj.getDetails()
extinfo = None
if details and details._externalInfo:
extinfo = self.query.get("ExternalInfo",
details._externalInfo._id.val, {"omero.group": "-1"})
if extinfo is None:
obj, kls = self.instance(ctx)
ctx.die(346, f"No ExternalInfo found: {kls}:{obj.id.val}")
attr_list = ["id", "entityId", "entityType", "lsid", "uuid"]
proxy = ""
if len(self.tx_cmd.arg_list) == 3:
field = self.tx_cmd.arg_list[2]
if field not in attr_list:
ctx.die(335, f"usage: ext-info-get OBJ [{'|'.join(attr_list)}]")
value = getattr(extinfo, field) or ""
proxy += f"{unwrap(value)}"
else:
for field in attr_list:
value = getattr(extinfo, field) or ""
proxy += f"{field}={unwrap(value)}\n"
self.tx_state.set_value(proxy, dest=self.tx_cmd.dest)
[docs]
class MapSetTxAction(NonFieldTxAction):
[docs]
def on_go(self, ctx, args):
from omero.model import NamedValue as NV
argc = len(self.tx_cmd.arg_list)
if argc not in [4, 5]:
ctx.die(335, "usage: map-set OBJ FIELD KEY [VALUE]")
field = self.tx_cmd.arg_list[2]
current = getattr(self.obj, field)
if current is None:
setattr(self.obj, field, [])
if argc == 4:
name = self.tx_cmd.arg_list[3]
current = [nv for nv in current if nv and nv.name != name]
setattr(self.obj, field, current)
else:
name, value = self.tx_cmd.arg_list[3:]
state = None
for nv in current:
if nv and nv.name == name:
nv.value = value
state = "SET"
break
if state != "SET":
current.append(NV(name, value))
self.save_and_return(ctx)
[docs]
class MapGetTxAction(NonFieldTxAction):
[docs]
def on_go(self, ctx, args):
if len(self.tx_cmd.arg_list) != 4:
ctx.die(335, "usage: map-get OBJ FIELD KEY")
field = self.tx_cmd.arg_list[2]
current = getattr(self.obj, field)
if current is None:
setattr(self.obj, field, [])
name = self.tx_cmd.arg_list[3]
value = None
for nv in current:
if nv and nv.name == name:
value = nv.value
self.tx_state.set_value(value, dest=self.tx_cmd.dest)
[docs]
class NullTxAction(NonFieldTxAction):
[docs]
def on_go(self, ctx, args):
if len(self.tx_cmd.arg_list) != 3:
ctx.die(335, "usage: null OBJ FIELD")
field = self.tx_cmd.arg_list[2]
setattr(self.obj, field, None)
self.save_and_return(ctx)
[docs]
class ObjGetTxAction(NonFieldTxAction):
[docs]
def on_go(self, ctx, args):
if len(self.tx_cmd.arg_list) not in (2, 3):
ctx.die(335, "usage: get OBJ [FIELD]")
if len(self.tx_cmd.arg_list) == 3:
field = self.tx_cmd.arg_list[2]
try:
proxy = self.get_field(field)
except AttributeError as ae:
message = ae.args
ctx.die(336, message)
else:
proxy = ""
for attr in dir(self.obj):
if (attr.startswith("_") and
not (attr.startswith("__")
or attr.startswith("_op_")
or attr.endswith("oaded"))):
field = attr.lstrip("_")
if hasattr(self.obj, field):
try:
proxy += (field + "="
+ str(self.get_field(field)) + "\n")
except AttributeError:
pass
self.tx_state.set_value(proxy, dest=self.tx_cmd.dest)
[docs]
def get_field(self, field):
from omero.model import NamedValue as NV
try:
current = getattr(self.obj, field)
except AttributeError:
raise AttributeError("Unknown field '%s' for %s:%s" % (
field, self.kls, self.obj.id.val))
if current is None:
proxy = ""
else:
try:
if hasattr(current, "val"):
proxy = current.val
elif hasattr(current, "id"):
objId = current.id.val
klass = current.__class__.__name__.rstrip("I")
proxy = klass + ":" + str(objId)
elif hasattr(current, "_value") and hasattr(current, "_unit"):
proxy = str(current._value) + " " + str(current._unit)
elif isinstance(current, list):
if len(current) == 0:
proxy = ""
else:
if isinstance(current[0], NV):
proxy = ",".join(
["(" + str(i.name) + "," + str(i.value) + ")"
for i in current])
else:
proxy = ",".join([str(i) for i in current])
else:
raise AttributeError(
"Error: field '%s' for %s:%s : no val, id or value" % (
field, self.kls, self.obj.id.val))
except AttributeError as ae:
message = ae.args
raise AttributeError("Error: field '%s' for %s:%s : %s" % (
field, self.kls, self.obj.id.val, message))
return proxy
[docs]
class ListGetTxAction(NonFieldTxAction):
[docs]
def on_go(self, ctx, args):
from omero.model import NamedValue as NV
if len(self.tx_cmd.arg_list) != 4:
ctx.die(335, "usage: list-get OBJ FIELD INDEX")
field = self.tx_cmd.arg_list[2]
try:
current = getattr(self.obj, field)
except AttributeError:
ctx.die(336, "Unknown field '%s' for %s:%s" % (
field, self.kls, self.obj.id.val))
index = int(self.tx_cmd.arg_list[3])
if current is None:
proxy = ""
else:
if isinstance(current, list):
try:
item = current[index]
if isinstance(item, NV):
proxy = ("(" + str(item.name) + ","
+ str(item.value) + ")")
else:
proxy = str(item)
except IndexError as ie:
message = ie.args
ctx.die(336, "Error: field '%s[%s]' for %s:%s, %s" % (
field, index, self.kls, self.obj.id.val, message))
else:
ctx.die(336, "Field '%s' for %s:%s is not a list" % (
field, self.kls, self.obj.id.val))
self.tx_state.set_value(proxy, dest=self.tx_cmd.dest)
[docs]
class TxState(object):
def __init__(self, ctx):
self.ctx = ctx
self._commands = []
self._vars = {}
self.is_stdin = sys.stdin.isatty()
[docs]
def add(self, command):
self._commands.append([command, None])
return len(self._commands)
[docs]
def set_value(self, proxy, dest=None):
idx = len(self._commands) - 1
self.ctx.out("%s" % proxy)
self._commands[idx][1] = proxy
if dest:
self._vars[dest] = proxy
[docs]
def get_row(self, i):
return self._commands[i][1]
[docs]
def get_var(self, key):
return self._vars[key]
def __len__(self):
return len(self._commands)
[docs]
class ObjControl(BaseControl):
"""Create, Update and Query OMERO objects
The obj command allows inserting any objects into the OMERO
database as well as updating and querying existing ones. This
is likely useful for preparing datasets for import and similar.
Examples:
$ omero obj new Dataset name=foo
Dataset:123
$ omero obj update Dataset:123 description=bar
Dataset:123
$ omero obj get Dataset:123 name
foo
$ omero obj get Dataset:123
description=bar
id=123
name=foo
version=
$ omero obj null Dataset:123 description
Dataset:123
$ omero obj get Dataset:123 description
$ omero obj new MapAnnotation ns=example.com
MapAnnotation:456
$ omero obj map-set MapAnnotation:456 mapValue foo bar
MapAnnotation:456
$ omero obj map-set MapAnnotation:456 mapValue foo
MapAnnotation:456
$ omero obj map-set MapAnnotation:456 mapValue fu baa
MapAnnotation:456
$ omero obj map-get MapAnnotation:456 mapValue foo
bar
$ omero obj get MapAnnotation:456 mapValue
(foo,bar),(fu,baa)
$ omero obj list-get MapAnnotation:456 mapValue 0
(foo,bar)
$ omero obj ext-info-set Image:12 3 myEntityType myLsid
$ omero obj ext-info-set Image:12 3 myEntityType myLsid myUuid
$ omero obj ext-info-get Image:12
$ omero obj ext-info-get Image:12 entityType
Bash examples:
$ project=$(omero obj new Project name='my Project')
$ dataset=$(omero obj new Dataset name='my Dataset')
$ omero obj new ProjectDatasetLink parent=$project child=$dataset
ProjectDatasetLink:456
$ omero import -d $dataset ...
"""
def _configure(self, parser):
self.exc = ExceptionHandler()
parser.add_login_arguments()
parser.add_argument(
"--file", help=SUPPRESS)
parser.add_argument(
"command", nargs="?",
choices=("new", "update", "null",
"map-get", "map-set",
"get", "list-get",
"ext-info-get", "ext-info-set"),
help="operation to be performed")
parser.add_argument(
"Class", nargs="?",
help="OMERO model object name, e.g. Project")
parser.add_argument(
"fields", nargs="*",
help="fields to be set, e.g. name=foo")
parser.set_defaults(func=self.process)
[docs]
def process(self, args):
state = TxState(self.ctx)
self.ctx.set("tx.state", state)
actions = []
if not args.command:
if args.file:
path = args.file
fi = None
try:
fi = fileinput.FileInput([path])
for line in fi:
line = line.strip()
if line and not line.startswith("#"):
actions.append(self.parse(state, line=line))
except IOError:
self.ctx.die(337, "Cannot read file")
finally:
if fi is not None:
fi.close()
else:
self.ctx.die(100, "No command provided")
else:
if args.file:
self.ctx.err("Ignoring %s" % args.file)
actions.append(
self.parse(state,
arg_list=[args.command, args.Class] +
args.fields))
for action in actions:
action.go(self.ctx, args)
return actions
[docs]
def parse(self, tx_state, arg_list=None, line=None):
"""
Takes a single command list and turns
it into a TxAction object
"""
tx_cmd = TxCmd(tx_state, arg_list=arg_list, line=line)
if tx_cmd.action == "new":
return NewObjectTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "update":
return UpdateObjectTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "map-set":
return MapSetTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "map-get":
return MapGetTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "ext-info-set":
return ExtInfoSetTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "ext-info-get":
return ExtInfoGetTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "null":
return NullTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "get":
return ObjGetTxAction(tx_state, tx_cmd)
elif tx_cmd.action == "list-get":
return ListGetTxAction(tx_state, tx_cmd)
else:
raise self.ctx.die(100, "Unknown command: %s" % tx_cmd)
try:
register("obj", ObjControl, ObjControl.__doc__)
except NameError:
if __name__ == "__main__":
cli = CLI()
cli.register("obj", ObjControl, ObjControl.__doc__)
cli.invoke(sys.argv[1:])