From 0841253f9dda2fda76be2cbcfa949170175fb265 Mon Sep 17 00:00:00 2001 From: Simon Li Date: Wed, 16 Dec 2020 11:46:17 +0000 Subject: [PATCH] Single renderdef file for multiple named projects/datasets --- src/omero_cli_render.py | 137 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 7 deletions(-) diff --git a/src/omero_cli_render.py b/src/omero_cli_render.py index 507694ae..4713f153 100755 --- a/src/omero_cli_render.py +++ b/src/omero_cli_render.py @@ -128,6 +128,51 @@ # name (label) is still taken into account. """ +BATCHSET_HELP = """Set rendering settings from a hierachy of rendering + definitions corresponding to an OMERO container hierarchy. + + 'projects' and 'datasets' are supported as top level containers, 'name' + must be provided. + If multiple containers have the same name they will all be processed. + Top level keys beginning with '_' are ignored, so they could for example + be used as YAML anchors. + + Definitions can be applied to projects, datasets, or datasets in projects. + + 'renderdef' is a rendering definition as passed to 'set', except that + 'version' is omitted. + + Example: + + projects: + # Apply renderdef to all Images in Project A + - name: Project A + renderdef: + channels: + ... + + # Apply renderdef to all Images in Dataset 1 in Project B + - name: Project B + datasets: + - name: Dataset 1 + renderdef: + channels: + ... + + datasets: + # Apply the channel_group1 renderdef anchor to all Images in Dataset 2 + - name: Dataset 2 + renderdef: + channels: + *channel_group1 + + _anchors: &channel_group1 + 1: + active: true + label: Plk1 + +""" + TEST_HELP = """Test that underlying pixel data is available The syntax for specifying objects is: : @@ -366,6 +411,7 @@ def _configure(self, parser): info = parser.add(sub, self.info, INFO_HELP) copy = parser.add(sub, self.copy, COPY_HELP) set_cmd = parser.add(sub, self.set, SET_HELP) + batchset = parser.add(sub, self.batchset, BATCHSET_HELP) edit = parser.add(sub, self.edit, EDIT_HELP) test = parser.add(sub, self.test, TEST_HELP) @@ -380,7 +426,10 @@ def _configure(self, parser): x.add_argument("object", type=render_type, help=tgt_help, nargs="+") - for x in (copy, set_cmd, edit): + batchset.add_argument("path", help=( + "Directory containing subdirectories with renderdefs.")) + + for x in (copy, set_cmd, edit, batchset): x.add_argument( "--skipthumbs", help="Do not regenerate thumbnails " "immediately", action="store_true") @@ -394,13 +443,14 @@ def _configure(self, parser): copy.add_argument("target", type=render_type, help=tgt_help, nargs="+") - for x in (set_cmd, edit): + for x in (set_cmd, edit, batchset): x.add_argument( "--disable", help="Disable non specified channels ", action="store_true") x.add_argument( "--ignore-errors", help="Do not error on mismatching" " rendering settings", action="store_true") + for x in (set_cmd, edit): x.add_argument( "channels", help="Local file or OriginalFile:ID which specifies the " @@ -424,6 +474,11 @@ def _lookup(self, gateway, type, oid): self.ctx.die(110, "No such %s: %s" % (type, oid)) return obj + def _lookup_name(self, gateway, type, name): + gateway.SERVICE_OPTS.setOmeroGroup('-1') + objs = gateway.getObjects(type, attributes={'name': name}) + return list(objs) + def render_images(self, gateway, object, batch=100): """ Get the images. @@ -658,6 +713,74 @@ def set(self, args): """ Implements the 'set' command """ data = self._load_rendering_settings( args.channels, session=self.client.getSession()) + self._set(data, args.object, + args.ignore_errors, args.disable, args.skipthumbs) + + def _get_batchset_targets(self, otype, descriptor): + """ Gets targets and renderdefs for the 'batchset' command """ + def require_one(required, keys): + if sum((r in keys) for r in required) != 1: + raise ValueError( + 'Exactly one of {} required'.format(required)) + + descriptor_keys = list(descriptor.keys()) + + require_one(['name'], descriptor_keys) + if otype == 'Project': + require_one(['datasets', 'renderdef'], descriptor_keys) + else: + require_one(['renderdef'], descriptor_keys) + + parents = self._lookup_name(self.gateway, otype, descriptor['name']) + if not parents: + raise Exception(f"No match for Project: {descriptor['name']}") + for parent in parents: + if 'renderdef' in descriptor_keys: + yield parent, descriptor['renderdef'] + else: + dataset_renderdefs = dict( + (d['name'], d) for d in descriptor['datasets']) + expected_names = set(dataset_renderdefs.keys()) + for dataset in parent.listChildren(): + if dataset.name in dataset_renderdefs: + for target in self._get_batchset_targets( + 'Dataset', dataset_renderdefs[dataset.name]): + yield target + try: + expected_names.remove(dataset.name) + except KeyError: + # Multiple containers have the same name + pass + if expected_names: + raise Exception( + 'No match for datasets: {}'.format(expected_names)) + + @gateway_required + def batchset(self, args): + """ Implements the 'batchset' command """ + try: + batch_data = pydict_text_io.load( + args.path, session=self.client.getSession()) + except Exception as e: + self.ctx.dbg(e) + self.ctx.die(103, "Could not read %s" % args.path) + for key, containers in batch_data.items(): + if key.startswith('_'): + continue + if key not in {'projects', 'datasets'}: + raise NotImplementedError( + 'Invalid batchset container: {}'.format(key)) + for container in containers: + for c, renderdef in self._get_batchset_targets( + f'{key[0].upper()}{key[1:-1]}', container): + self.ctx.out('Applying settings to ' + f'{c.OMERO_CLASS}:{c.id} {c.name}') + self._set( + renderdef, c._obj, + args.ignore_errors, args.disable, args.skipthumbs) + + def _set(self, data, target, ignore_errors, disable, skipthumbs): + """ Utility method to apply rendering settings """ (namedict, cindices, rangelist, colourlist) = self._read_channels( data) greyscale = data.get('greyscale', None) @@ -665,14 +788,14 @@ def set(self, args): self.ctx.dbg('greyscale=%s' % greyscale) iids = [] - for img in self.render_images(self.gateway, args.object, batch=1): + for img in self.render_images(self.gateway, target, batch=1): iids.append(img.id) (def_z, def_t) = self._read_default_planes( - img, data, ignore_errors=args.ignore_errors) + img, data, ignore_errors=ignore_errors) active_channels = [] - if not args.disable: + if not disable: # Calling set_active_channels will disable channels which # are not specified. # Need to reset ALL active channels after set_active_channels() @@ -705,7 +828,7 @@ def set(self, args): img.saveDefaults() self.ctx.dbg( "Updated rendering settings for Image:%s" % img.id) - if not args.skipthumbs: + if not skipthumbs: self._generate_thumbs([img]) except Exception as e: self.ctx.err('ERROR: %s' % e) @@ -714,7 +837,7 @@ def set(self, args): if not iids: self.ctx.die(113, "ERROR: No images found for %s %d" % - (args.object.__class__.__name__, args.object.id._val)) + (target.__class__.__name__, target.id._val)) if namedict: self._update_channel_names(self.gateway, iids, namedict)