#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2008 Glencoe Software, Inc. All rights reserved.
# Use is subject to license terms supplied in LICENSE.txt
#
"""
Scripting types
- Classes:
- Type -- Top of parameter type hierarchy
- Long --
- String --
- Bool --
- List
- Map
- Set
- Functions:
- client -- Produces an omero.client object with given
input/output constraints.
"""
import os
import logging
import omero
import omero.callbacks
import omero.util.concurrency
import omero.util.temp_files
from omero.rtypes import rint, rfloat, rstring, rinternal, rbool, rmap
from omero.rtypes import robject, rlist, rset, rtype, rlong, rdouble
from omero.rtypes import wrap, unwrap
from collections import defaultdict
BASE_LOG = logging.getLogger("omero.scripts")
PROC_LOG = logging.getLogger("omero.scripts.ProcessCallback")
TYPE_LOG = logging.getLogger("omero.scripts.Type")
[docs]
class Type(omero.grid.Param):
"""
omero.grid.Param subclass which provides convenience methods for
input/output specification.
Further subclasses are responsible for creating proper prototypes.
kwargs
"""
PROTOTYPE_FUNCTION = None
PROTOTYPE_DEFAULT = None
PROTOTYPE_MIN = None
PROTOTYPE_MAX = None
PROTOTYPE_VALUES = None
def __init__(self, name, optional=True, out=False, description=None,
default=None, **kwargs):
# Non-Param attributes
omero.grid.Param.__init__(self)
# Non-Param attributes
self._name = name
self._in = True
self._out = out
# Other values will be filled in by the kwargs
# Mostly leaving these for backwards compatibility
self.description = description
self.optional = optional
# Assign all the kwargs
for k, v in list(kwargs.items()):
if not hasattr(self, k):
TYPE_LOG.warn("Unknown property: %s", k)
setattr(self, k, v)
_DEF = self.__get(self.PROTOTYPE_DEFAULT, False)
_FUN = self.__get(self.PROTOTYPE_FUNCTION)
_MAX = self.__get(self.PROTOTYPE_MAX)
_MIN = self.__get(self.PROTOTYPE_MIN)
_VAL = self.__get(self.PROTOTYPE_VALUES)
# Someone specifically set the prototype, then
# we assume that useDefault should be True
# For whatever reason, inheritance isn't working.
if default is not None:
newfunc = _FUN
newdefault = default
if isinstance(self, List):
if isinstance(default, (list, tuple)):
newdefault = wrap(default).val
elif isinstance(default, omero.RCollection):
newfunc = lambda x: x
elif isinstance(default, omero.RType):
default = [default]
else:
newfunc = lambda x: x
newdefault = rlist([rtype(default)])
self.useDefault = True
self.prototype = newfunc(newdefault)
else:
if not callable(_FUN):
raise ValueError("Bad prototype function: %s" % _FUN)
# To prevent weirdness, if the class default is
# callable, we'll assume its a constructor and
# create a new one to prevent modification.
try:
_def = _DEF()
except TypeError:
_def = _DEF
self.prototype = _FUN(_def)
# The following use wrap to guarantee that an rtype is present
if self.min is not None:
if _MIN is None:
self.min = _FUN(self.min)
else:
self.min = _MIN(self.min)
if self.max is not None:
if _MAX is None:
self.max = _FUN(self.max)
else:
self.max = _MAX(self.max)
if self.values is not None:
if _VAL is None:
self.values = wrap(self.values)
else:
self.values = _VAL(self.values)
# Now if useDefault has been set, either manually, or
# via setting default="..." we check that the default
# value matches a value if present
if self.values is not None and self.values and self.useDefault:
if isinstance(self.prototype, omero.RCollection):
test = unwrap(self.prototype.val[0])
else:
test = unwrap(self.prototype)
values = unwrap(self.values)
if test not in values:
raise ValueError("%s is not in %s" % (test, values))
[docs]
def out(self):
self._in = False
self._out = True
return self
[docs]
def inout(self):
self._in = True
self._out = True
return self
[docs]
def type(self, *arg):
self.prototype = wrap(arg)
return self
[docs]
def name(self):
return self._name
def __get(self, val, func=True):
if val is not None:
if func:
return val.__func__
else:
return val
[docs]
class Object(Type):
"""
Wraps an robject
"""
PROTOTYPE_FUNCTION = robject
PROTOTYPE_DEFAULT = None
[docs]
class Long(Type):
"""
Wraps an rlong
"""
PROTOTYPE_FUNCTION = rlong
PROTOTYPE_DEFAULT = 0
[docs]
class Int(Type):
"""
Wraps an rint
"""
PROTOTYPE_FUNCTION = rint
PROTOTYPE_DEFAULT = 0
[docs]
class Double(Type):
"""
Wraps an rdouble
"""
PROTOTYPE_FUNCTION = rdouble
PROTOTYPE_DEFAULT = 0.0
[docs]
class Float(Type):
"""
Wraps an rfloat
"""
PROTOTYPE_FUNCTION = rfloat
PROTOTYPE_DEFAULT = 0.0
[docs]
class String(Type):
"""
Wraps an rstring
"""
PROTOTYPE_FUNCTION = rstring
PROTOTYPE_DEFAULT = ""
[docs]
class Bool(Type):
"""
Wraps an rbool
"""
PROTOTYPE_FUNCTION = rbool
PROTOTYPE_DEFAULT = False
[docs]
class Color(Type):
"""
Wraps an rinternal(Color)
"""
PROTOTYPE_FUNCTION = rinternal
PROTOTYPE_DEFAULT = omero.Color
[docs]
class Point(Type):
"""
Wraps an rinternal(Point)
"""
PROTOTYPE_FUNCTION = rinternal
PROTOTYPE_DEFAULT = omero.Point
[docs]
class Plane(Type):
"""
Wraps an rinternal(Plane)
"""
PROTOTYPE_FUNCTION = rinternal
PROTOTYPE_DEFAULT = omero.Plane
class __Coll(Type):
"""
Base type providing the append and extend functionality.
Not for user use.
"""
PROTOTYPE_DEFAULT = list
def append(self, *arg):
self.prototype.val.append(*arg)
return self
def extend(self, *arg):
self.prototype.val.extend(*arg)
return self
def ofType(self, obj):
if callable(obj):
obj = obj() # Ctors, etc.
# If someone used default=, then "ofType()" is not necessary
# and so we check for their correspondence.
if self.useDefault and self.prototype.val:
if not isinstance(obj, self.prototype.val[0].__class__):
raise ValueError(
"ofType values doesn't match default value: %s <> %s"
% (unwrap(obj), unwrap(self.prototype.val[0])))
else:
self.prototype.val.append(wrap(obj))
return self
[docs]
class Set(__Coll):
"""
Wraps an rset. To add values to the contents of the set,
use "append" or "extend" since set.val is of type list.
"""
PROTOTYPE_FUNCTION = rset
[docs]
class List(__Coll):
"""
Wraps an rlist. To add values to the contents of the list,
use "append" or "extend" since set.val is of type list.
"""
PROTOTYPE_FUNCTION = rlist
[docs]
class Map(Type):
"""
Wraps an rmap. To add values to the contents of the map,
use "update" since map.val is of type dict.
"""
PROTOTYPE_FUNCTION = rmap
PROTOTYPE_DEFAULT = dict
[docs]
def update(self, *args, **kwargs):
self.prototype.val.update(*args, **kwargs)
return self
[docs]
class ParseExit(Exception):
"""
Raised when this script should just parse parameters and return.
"""
def __init__(self, params):
Exception.__init__(self)
self.params = params
[docs]
def client(*args, **kwargs):
"""
Entry point for all script engine scripts.
Typical usage consists of::
client = omero.scripts.client("name","description", \
omero.scripts.Long("name"),...)
where the returned client is created via the empty constructor to
omero.client using only --Ice.Config or ICE_CONFIG, and the function
arguments are taken as metadata about the current script. With this
information, all script consumers should be able to determine the required
types for execution.
Possible types are all subclasses of omero.scripts.Type
To change the omero.model.Format of the stdout and stderr produced by
this script, use the constructor arguments::
client = omero.scripts.client(..., \
stdoutFormat = "text/plain",
stderrFormat = "text/plain")
If you would like to prevent stdout and/or stderr from being
uploaded, set the corresponding value to None. If you would like
to overwrite the value with another file, use
client.setOutput(). Though it is possible to attach any RType to
"stdout" or "stderr", there is an assumption that the value will
be an robject(OriginalFileI())
Providing your own client is possible via the kwarg "client = ...",
but be careful since this may break usage with the rest of the
scripting framework. The client should not have a session, and
must be configured for the argumentless version of createSession()
"""
args = list(args)
if len(args) >= 1:
if isinstance(args[0], str):
kwargs["name"] = args.pop(0)
if len(args) >= 1:
if isinstance(args[0], str):
kwargs["description"] = args.pop(0)
if "client" not in kwargs:
c = omero.client()
router = c.getProperty("Ice.Default.Router")
router = c.getCommunicator().stringToProxy(router)
for endpoint in router.ice_getEndpoints():
host = endpoint.getInfo().host
c.ic.getProperties().setProperty("omero.host", host)
break
else:
BASE_LOG.warn("no host configuration found for script client")
kwargs["client"] = c
else:
c = kwargs["client"]
c.setAgent("OMERO.scripts")
if args and isinstance(args[0], omero.grid.JobParams):
c.params = args.pop(0)
else:
c.params = omero.grid.JobParams()
c.params.inputs = {}
c.params.outputs = {}
for k, v in list(kwargs.items()):
if hasattr(c.params, k):
setattr(c.params, k, v)
if not c.params.stdoutFormat:
c.params.stdoutFormat = "text/plain"
if not c.params.stderrFormat:
c.params.stderrFormat = "text/plain"
for p in args:
if isinstance(p, Type):
if p._in:
c.params.inputs[p._name] = p
if p._out:
c.params.outputs[p._name] = p
else:
raise ValueError("Not Type: %s" % type(p))
handleParse(c) # May throw
c.createSession().detachOnDestroy()
return c
[docs]
def handleParse(c):
"""
Raises ParseExit if the client has the configuration property
"omero.scripts.parse". If the value is anything other than "only",
then the parameters will also be sent to the server.
"""
parse = c.getProperty("omero.scripts.parse")
if len(parse) > 0: # TODO Add to omero/Constants.ice
if parse != "only":
c.createSession().detachOnDestroy()
c.setOutput("omero.scripts.parse", rinternal(c.params))
raise ParseExit(c.params)
[docs]
def parse_text(scriptText):
"""
Parses the given script text with "omero.scripts.parse" set
and catches the exception. The parameters are returned.
WARNING: This method calls "exec" on the given text.
Do NOT use this on data you don't trust.
"""
try:
cfg = omero.util.temp_files.create_path()
cfg.write_lines(["omero.scripts.parse=only", "omero.host=localhost"])
old = os.environ.get("ICE_CONFIG")
try:
os.environ["ICE_CONFIG"] = cfg.abspath()
exec(scriptText, {"__name__": "__main__"})
raise Exception("Did not throw ParseExit: %s" % scriptText)
finally:
if old:
os.environ["ICE_CONFIG"] = old
except ParseExit as exit:
return exit.params
[docs]
def parse_file(filename):
"""
Parses the given script file with "omero.scripts.parse" set
and catches the exception. The parameters are returned.
WARNING: This method calls "exec" on the given file's contents.
Do NOT use this on data you don't trust.
"""
from omero_ext.path import path
scriptText = path(filename).text()
return parse_text(scriptText)
[docs]
def group_params(params):
"""
Walks through the inputs of the given JobParams
and returns a map-of-maps with Param names as
the leaf nodes.
For example, for the following:
Params("1", grouping = "A") # "A." is equivalent
Params("2", grouping = "A.B")
Params("3", grouping = "A.C")
this function returns:
{"A" : {"": "1", "B" : "2", "C" : "3"} }
while:
Params("1", grouping = "A")
returns:
{"A" : "1"}
"""
nested_dict = lambda: defaultdict(nested_dict)
groupings = nested_dict()
ordered_params = sorted(list(params.inputs.items()))
for param_name, v in ordered_params:
val = v.grouping
if val.endswith("."):
val = val[:-1]
parts = val.split(".")
previous = None
current = groupings
for idx, key in enumerate(parts):
if (idx+1) < len(parts):
# We need to descend further
previous = current
current = current[key]
else:
# No further keys remaining, assign
if isinstance(current, dict):
if key in current:
current[key][""] = param_name
else:
current[key] = param_name
elif isinstance(current, str):
# Here we assume the value is a node key
replacement = dict()
replacement[""] = current
replacement[key] = param_name
assert previous[parts[idx-1]] == current
previous[parts[idx-1]] = replacement
else:
raise Exception(current, type(current))
return groupings
[docs]
def error_msg(category, key, format_string, *args):
c = "%s" % (category.upper())
s = """%s for "%s": %s\n""" % (c, key, format_string)
return s % args
[docs]
def compare_proto(key, proto, input, cache=None):
if cache is None:
cache = {}
if id(proto) in cache and id(input) in cache:
return "" # Prevent StackOverflow
else:
cache[id(proto)] = True
cache[id(input)] = True
itype = None
ptype = None
both_collection = False
if input is not None:
itype = input.__class__
both_collection = isinstance(input, omero.RCollection)
if proto is not None:
ptype = proto.__class__
both_collection &= isinstance(proto, omero.RCollection)
# see https://github.com/openmicroscopy/openmicroscopy/issues/5788
# accept RSets as RLists and vice versa
if not (both_collection or isinstance(input, ptype)):
return error_msg("Wrong type", key, "%s != %s", itype, ptype)
# Now recurse if a collection type
errors = ""
if isinstance(proto, omero.RMap) and len(proto.val) > 0:
for x in list(input.val.values()):
errors += compare_proto(key, list(proto.val.values())[0], x, cache)
elif isinstance(proto, omero.RCollection) and len(proto.val) > 0:
for x in input.val:
errors += compare_proto(key, proto.val[0], x, cache)
return errors
[docs]
def expand(input):
if input is None:
items = []
elif isinstance(input, (list, tuple)):
items = list(input)
elif isinstance(input, dict):
items = list(input.values())
else:
items = [input]
return items
[docs]
def check_boundaries(key, min, max, input):
errors = ""
# Unwrap
min = unwrap(min)
max = unwrap(max)
input = unwrap(input)
items = expand(input)
# Check
for x in items:
if min is not None and min > x:
errors += error_msg("Out of bounds", key, "%s is below min %s", x,
min)
if max is not None and max < x:
errors += error_msg("Out of bounds", key, "%s is above max %s", x,
max)
return errors
[docs]
def check_values(key, values, input):
errors = ""
# Unwrap
values = unwrap(values)
input = unwrap(input)
items = expand(input)
values = expand(values)
if not values:
return errors
for x in items:
if x not in values:
errors += error_msg("Value list", key, "%s not in %s", x, values)
return errors
#
# Importing into omero.scripts namespace
#
ProcessCallbackI = omero.callbacks.ProcessCallbackI
[docs]
def wait(client, process, ms=500):
"""
Wrapper around the use of ProcessCallbackI
"""
cb = ProcessCallbackI(client, process)
while cb.block(500) is None:
process.poll()