def test(*params): parser = ArgumentParser("Perform all the tests defined within the" " given descriptor") parser.add_argument("descriptor", action="store", help="The Boutiques descriptor as a JSON file, JSON " "string or Zenodo ID (prefixed by 'zenodo.').") result = parser.parse_args(params) # Generation of the invocation schema (and descriptor validation). invocation(result.descriptor) # Extraction of all the invocations defined for the test-cases. descriptor = loadJson(result.descriptor) if (not descriptor.get("tests")): # If no tests have been specified, we consider testing successful. return 0 for test in descriptor["tests"]: invocation_JSON = test["invocation"] # Check if the invocation is valid. invocation(result.descriptor, "--invocation", json.dumps(invocation_JSON)) # Invocations have been properly validated. We can launch the actual tests. test_path = op.join(op.dirname(op.realpath(__file__)), "test.py") return pytest.main([test_path, "--descriptor", result.descriptor])
def retrieve_data_record(): data_collect_dict = {} cache_dir = os.path.join(os.path.expanduser('~'), ".cache", "boutiques", "data") if os.path.exists(cache_dir): cache_fls = glob.glob(cache_dir + "/*") if cache_fls: latest_file = max(cache_fls, key=os.path.getctime) path = os.path.join(cache_dir, latest_file) data_collect_dict = loadJson(path) return data_collect_dict
def prettyprint(*params): parser = ArgumentParser("Boutiques pretty-print for generating help text") parser.add_argument("descriptor", action="store", help="The Boutiques descriptor.") results = parser.parse_args(params) from boutiques.prettyprint import PrettyPrinter desc = loadJson(results.descriptor) prettyclass = PrettyPrinter(desc) return prettyclass.docstring
def invocation(*params): parser = ArgumentParser("Creates invocation schema and validates" " invocations. Uses descriptor's invocation" " schema if it exists, otherwise creates one.") parser.add_argument("descriptor", action="store", help="The Boutiques descriptor as a JSON file, JSON " "string or Zenodo ID (prefixed by 'zenodo.').") parser.add_argument("-i", "--invocation", action="store", help="Input values in a JSON file or as a JSON " "object to be validated against " "the invocation schema.") parser.add_argument("-w", "--write-schema", action="store_true", help="If descriptor doesn't have an invocation " "schema, creates one and writes it to the descriptor" " file ") result = parser.parse_args(params) validate(result.descriptor) if result.invocation: data = loadJson(result.invocation) descriptor = loadJson(result.descriptor) if descriptor.get("invocation-schema"): invSchema = descriptor.get("invocation-schema") else: from boutiques.invocationSchemaHandler import generateInvocationSchema invSchema = generateInvocationSchema(descriptor) if result.write_schema: descriptor["invocation-schema"] = invSchema with open(result.descriptor, "w") as f: f.write(json.dumps(descriptor, indent=4, sort_keys=True)) if result.invocation: from boutiques.invocationSchemaHandler import validateSchema validateSchema(invSchema, data)
def fetch_tests(descriptor_input): descriptor = loadJson(descriptor_input) tests = [] # For each test present in the descriptor: for test in descriptor["tests"]: # We first extract the invocation and put it inside a temporary file. invocation_JSON = json.dumps(test["invocation"]) temp_invocation_JSON = tempfile.NamedTemporaryFile(suffix=".json", delete=False) temp_invocation_JSON.write(invocation_JSON.encode()) temp_invocation_JSON.seek(0) # Now we setup the necessary elements for the testing function. tests.append([descriptor_input, test, temp_invocation_JSON]) return (descriptor["name"], tests)
def carmin(self, output_file): carmin_desc = {} descriptor = loadJson(self.descriptor) if descriptor.get('doi'): self.identifier = descriptor.get('doi') if self.identifier is None: raise_error( ExportError, 'Descriptor must have a DOI, or ' 'identifier must be specified with --identifier.') carmin_desc['identifier'] = self.identifier carmin_desc['name'] = descriptor.get('name') carmin_desc['version'] = descriptor.get('tool-version') carmin_desc['description'] = descriptor.get('description') carmin_desc['canExecute'] = True carmin_desc['parameters'] = [] for inp in descriptor.get('inputs'): carmin_desc['parameters'].append( self.convert_input_or_output(inp, False)) for output in descriptor.get('output-files'): carmin_desc['parameters'].append( self.convert_input_or_output(output, True)) carmin_desc['properties'] = {} carmin_desc['properties']['boutiques'] = True if descriptor.get('tags'): for prop in descriptor.get('tags').keys(): carmin_desc['properties'][prop] = descriptor['tags'][prop] carmin_desc['errorCodesAndMessages'] = [] for errors in descriptor.get('error-codes'): obj = {} obj['errorCode'] = errors['code'] obj['errorMessage'] = errors['description'] carmin_desc['errorCodesAndMessages'].append(obj) with open(output_file, 'w') as fhandle: fhandle.write(json.dumps(carmin_desc, indent=4, sort_keys=True))
def validate_descriptor(json_file, **kwargs): """ Validates the Boutiques descriptor against the schema. """ path, fil = op.split(bfile) schema_file = op.join(path, "schema", "descriptor.schema.json") # Load schema with open(schema_file) as fhandle: schema = simplejson.load(fhandle) # Load descriptor descriptor = loadJson(json_file) # Validate basic JSON schema compliance for descriptor # Note: if it fails basic schema compliance we don"t do more checks try: validate(descriptor, schema) except ValidationError as e: raise_error(DescriptorValidationError, (str(e))) # Helper get functions def safeGet(desc, sec, targ): if desc.get(sec): return [ item.get(targ) for item in desc[sec] if list(item.keys()).count(targ) ] return [] def inputGet(s): return safeGet(descriptor, "inputs", s) def outputGet(s): return safeGet(descriptor, "output-files", s) def groupGet(s): return safeGet(descriptor, "groups", s) def inById(i): if i in inputGet("id"): return descriptor["inputs"][inputGet("id").index(i)] return {} # Begin looking at Boutiques-specific failures errors = [] clkeys = inputGet("value-key") + outputGet("value-key") flattenedTemplates = [y for x in outputGet("file-template") for y in x] configFileTemplates = flattenedTemplates + outputGet("path-template") cmdline = descriptor["command-line"] # Verify that all command-line key appear in the command-line, in # a file template or in an environment variable value msg_template = (" KeyError: \"{0}\" not in command-line or file template" " or environment variables") envValues = "" if descriptor.get('environment-variables'): for env in descriptor.get('environment-variables'): envValues += "||" + env['value'] errors += [ msg_template.format(k) for k in clkeys if ((cmdline + ".".join(configFileTemplates) + envValues).count(k)) < 1 ] # Verify that no key contains another key msg_template = " KeyError: \"{0}\" contains \"{1}\"" errors += [ msg_template.format(key, clkeys[jdx]) for idx, key in enumerate(clkeys) for jdx in range(0, len(clkeys)) if clkeys[jdx] in key and key != clkeys[jdx] ] # Verify that all Ids are unique inIds, outIds = inputGet("id"), outputGet("id") grpIds = groupGet("id") if "groups" in descriptor.keys() else [] allIds = inIds + outIds + grpIds msg_template = " IdError: \"{0}\" is non-unique" for idx, s1 in enumerate(allIds): for jdx, s2 in enumerate(allIds): if s1 == s2 and idx < jdx: errors += [msg_template.format(s1)] else: errors += [] # Verify that identical keys only exist if they are both in mutex groups msg_template = " MutExError: \"{0}\" belongs to 2+ non exclusive IDs" for idx, key in enumerate(clkeys): for jdx in range(idx + 1, len(clkeys)): if clkeys[jdx] == key: mids = [ inById(mid)["id"] for mid in inIds if inById(mid)["value-key"] == key ] for idx, grp in enumerate(descriptor.get("groups")): mutex = grp.get("mutually-exclusive") if set(grp["members"]) == set(mids) and not mutex: errors += [msg_template.format(key)] # Verify that output files have unique path-templates msg_template = ("OutputError: \"{0}\" and \"{1}\" have the same " "path-template") for ix, o1 in zip(outputGet("id"), outputGet("path-template")): for jx, o2 in zip(outputGet("id"), outputGet("path-template")): if o1 == o2 and jx != ix: errors += [msg_template.format(ix, jx)] else: errors += [] # Verify inputs for inp in descriptor["inputs"]: # Add optional property in case it's not # there (default to false as in JSON) if "optional" not in inp.keys(): inp["optional"] = False # Verify flag-type inputs (have flags, not required, cannot be lists) if inp["type"] == "Flag": msg_template = " InputError: \"{0}\" must have a command-line flag" if "command-line-flag" not in inp.keys(): errors += [msg_template.format(inp["id"])] else: errors += [] msg_template = " InputError: \"{0}\" is of type Flag,"\ " it has to be optional" if inp["optional"] is False: errors += [msg_template.format(inp["id"])] else: errors += [] # Verify number-type inputs min/max are sensible elif inp["type"] == "Number": msg_template = (" InputError: \"{0}\" cannot have greater" " min ({1}) than max ({2})") minn = inp["minimum"] if "minimum" in inp.keys() else -float("Inf") maxx = inp["maximum"] if "maximum" in inp.keys() else float("Inf") if minn > maxx: errors += [msg_template.format(inp["id"], minn, maxx)] else: errors += [] # Verify enum-type inputs (at least 1 option, default in set) elif "value-choices" in inp.keys(): msg_template = (" InputError: \"{0}\" must have at least" " one value choice") if len(inp["value-choices"]) < 1: errors += [msg_template.format(inp["id"])] else: errors += [] msg_template = " InputError: \"{0}\" cannot have default"\ " value outside its choices" if "default-value" in inp.keys(): if not isinstance(inp["default-value"], list): if inp["default-value"] not in inp["value-choices"]: errors += [msg_template.format(inp["id"])] else: for dv in inp["default-value"]: if dv not in inp["value-choices"]: errors += [msg_template.format(inp["id"])] else: errors += [] # Verify list-type inputs (min entries less than max, # no negative entries (both on min and max) if "list" in inp.keys(): msg_template = (" InputError: \"{0}\" cannot have greater min" " entries ({1}) than max entries ({2})") minn = inp.get("min-list-entries") or 0 maxx = inp.get("max-list-entries") or float("Inf") if minn > maxx: errors += [msg_template.format(inp["id"], minn, maxx)] else: errors += [] msg_template = (" InputError: \"{0}\" cannot have negative min" " entries ({1})") errors += [msg_template.format(inp["id"], minn) ] if minn < 0 else [] msg_template = (" InputError: \"{0}\" cannot have non-positive" " max entries ({1})") if maxx <= 0: errors += [msg_template.format(inp["id"], maxx)] else: errors += [] # Verify requires- and disables-inputs (present ids, non-overlapping) msg_template = " InputError: \"{0}\" {1}d id \"{2}\" not found" for param in ["require", "disable"]: if param + "s-inputs" in inp.keys(): errors += [ msg_template.format(inp["id"], param, ids) for ids in inp[param + "s-inputs"] if ids not in inIds ] if "requires-inputs" in inp.keys() and "disables-inputs" in inp.keys(): msg_template = " InputError: \"{0}\" requires and disables \"{1}\"" errors += [ msg_template.format(inp["id"], ids1) for ids1 in inp["requires-inputs"] for ids2 in inp["disables-inputs"] if ids1 == ids2 ] # Verify required inputs cannot require or disable other parameters if "requires-inputs" in inp.keys() or "disables-inputs" in inp.keys(): msg_template = (" InputError: \"{0}\" cannot require or" " disable other inputs") if not inp["optional"]: errors += [msg_template.format(inp["id"])] # Verify value-disables/requires fields accompany value-choices if (("value-disables" in inp.keys() or "value-requires" in inp.keys()) and "value-choices" not in inp.keys()): msg_template = (" InputError: \"{0}\" cannot have have value-opts" " without value-choices defined.") errors += [msg_template.format(inp["id"])] if "value-choices" in inp.keys(): # Verify not value not requiring and disabling input if ("value-requires" in inp.keys() and "value-disables" in inp.keys()): msg_template = (" InputError: \"{0}\" choice \"{1}\" requires" " and disables \"{2}\"") errors += [ msg_template.format(inp["id"], choice, ids1) for choice in inp["value-choices"] for ids1 in inp["value-disables"][choice] if ids1 in inp["value-requires"][choice] ] for param in ["value-requires", "value-disables"]: if param in inp.keys(): # Verify disables/requires keys are the same as choices msg_template = (" InputError: \"{0}\" {1} list is not the" " same as the value-choices") if set(inp[param].keys()) != set(inp["value-choices"]): errors += [msg_template.format(inp["id"], param)] # Verify all required or disabled IDs are valid msg_template = (" InputError: \"{0}\" {1} id \"{2}\" not" " found") errors += [ msg_template.format(inp["id"], param, ids) for ids in inp[param].values() for item in ids if item not in inIds ] # Verify not requiring or disabling required inputs msg_template = (" InputError: \"{0}\" {1} cannot be used " "with required input \"{2}\"") errors += [ msg_template.format(inp["id"], param, member) for ids in inp[param].keys() for member in inp[param][ids] if not inById(member).get("optional") ] # Verify groups for idx, grpid in enumerate(grpIds): grp = descriptor['groups'][idx] # Verify group members must (exist in inputs, show up # once, only belong to single group) msg_template = " GroupError: \"{0}\" member \"{1}\" does not exist" errors += [ msg_template.format(grp["id"], member) for member in grp["members"] if member not in inIds ] msg_template = " GroupError: \"{0}\" member \"{1}\" appears twice" errors += [ msg_template.format(grp["id"], member) for member in set(grp["members"]) if grp["members"].count(member) > 1 ] # Verify mutually exclusive groups cannot have required members # nor requiring members, and that pairs of inputs cannot both be # in an all-or-none group if grp.get("mutually-exclusive"): msg_template = (" GroupError: \"{0}\" is mutually-exclusive" " and cannot have required members, " "such as \"{1}\"") errors += [ msg_template.format(grp["id"], member) for member in set(grp["members"]) if not inById(member)["optional"] ] msg_template = (" GroupError: \"{0}\" is mutually-exclusive" " and cannot have members require one another," " such as \"{1}\" and \"{2}\"") for member in set(grp["members"]): if "requires-inputs" in inById(member).keys(): errors += [ msg_template.format(grp["id"], member, req) for req in inById(member)["requires-inputs"] if req in set(grp["members"]) ] for jdx, grp2 in enumerate(descriptor["groups"]): if grp2.get("all-or-none"): msg_template = (" GroupError: mutually-exclusive group" " \"{0}\" and all-or-none group \"{1}\"" " cannot both contain input pairs \"{2}\"" " and \"{3}\"") errors += [ msg_template.format(grp["id"], grp2["id"], m1, m2) for m1 in grp["members"] for m2 in grp["members"] if m1 != m2 and m1 in grp2["members"] and m2 in grp2["members"] and idx != jdx ] # Verify one-is-required groups should never have required members # and that the group is not a subset of an all-or-none group if grp.get("one-is-required"): msg_template = (" GroupError: \"{0}\" is a one-is-required" " group and contains a required member, \"{1}\"") errors += [ msg_template.format(grp["id"], member) for member in set(grp["members"]) if member in inIds and not inById(member)["optional"] ] for jdx, grp2 in enumerate(descriptor["groups"]): if grp2.get("all-or-none"): msg_template = ( " GroupError: \"{0}\" is one-is-required" " and cannot be a subset of the all-or-none" " group \"{1}\"") if (set(grp["members"]).issubset(set(grp2["members"])) and idx != jdx): errors += [msg_template.format(grp["id"], grp2["id"])] # Verify all-or-none groups should never have required members if grp.get("all-or-none"): msg_template = (" GroupError: \"{0}\" is an all-or-none group" " and cannot be paired with one-is-required" " or mutually-exclusive groups") if grp.get("one-is-required") or grp.get("mutually-exclusive"): errors += [msg_template.format(grp["id"])] msg_template = (" GroupError: \"{0}\" is an all-or-none" " group and contains a required member, \"{1}\"") errors += [ msg_template.format(grp["id"], member) for member in set(grp["members"]) if member in inIds and not inById(member)["optional"] ] # Verify tests if "tests" in descriptor.keys(): tests_names = [] for test in descriptor["tests"]: tests_names.append(test["name"]) if "output-files" in test["assertions"].keys(): test_output_ids = safeGet(test["assertions"], "output-files", "id") # Verify if output reference ids are valid msg_template = ("TestError: \"{0}\" output id" " not found, in test \"{1}\"") errors += [ msg_template.format(output_id, test["name"]) for output_id in test_output_ids if (output_id not in outIds) ] # Verify that we do not have multiple output # references referring to the same id msg_template = ("TestError: \"{0}\" output id" " cannot appear more than once within" " same test, in test \"{1}\"") errors += [ msg_template.format(output_id, test["name"]) for output_id in set(test_output_ids) if (test_output_ids.count(output_id) > 1) ] # Verify that all the defined tests have unique names msg_template = "TestError: \"{0}\" test name is non-unique" errors += [ msg_template.format(test_name) for test_name in set(tests_names) if (tests_names.count(test_name) > 1) ] errors = None if errors == [] else errors if errors is None: if kwargs.get('format_output'): with open(json_file, 'w') as fhandle: fhandle.write(json.dumps(descriptor, indent=4, sort_keys=True)) return descriptor else: raise DescriptorValidationError("\n".join(errors))
def upgrade_04(self): """ Differences between 0.4 and current (0.5): -schema version (obv) -singularity should now be represented same as docker -walltime should be part of suggested_resources structure I.e. "schema-version": "0.4", ...... becomes..... "schema-version": "0.5", I.e. "container-image": { "type": "singularity", "url": "shub://gkiar/ndmg-cbrain:master" }, ...... becomes..... "container-image": { "type": "singularity", "image": "gkiar/ndmg-cbrain:master", "index": "shub://", }, I.e. "walltime-estimate": 3600, ...... becomes..... "suggested-resources": { "walltime-estimate": 3600 }, """ descriptor = loadJson(self.input_descriptor) if descriptor["schema-version"] != "0.4": raise_error( ImportError, "The input descriptor must have " "'schema-version'=0.4") descriptor["schema-version"] = "0.5" if "container-image" in descriptor.keys(): if "singularity" == descriptor["container-image"]["type"]: url = descriptor["container-image"]["url"] img = url.split("://") if len(img) == 1: descriptor["container-image"]["image"] = img[0] elif len(img) == 2: descriptor["container-image"]["image"] = img[1] descriptor["container-image"]["index"] = img[0] + "://" del descriptor["container-image"]["url"] elif ("docker" == descriptor["container-image"]["type"] and descriptor["container-image"].get("index")): url = descriptor["container-image"]["index"].split("://")[-1] descriptor["container-image"]["index"] = url if "walltime-estimate" in descriptor.keys(): descriptor["suggested-resources"] =\ {"walltime-estimate": descriptor["walltime-estimate"]} del descriptor["walltime-estimate"] with open(self.output_descriptor, 'w') as fhandle: fhandle.write(json.dumps(descriptor, indent=4, sort_keys=True)) validate_descriptor(self.output_descriptor)