Codebase list ctdconverter / 250ebdf
Added extra functionality (generation of tool_conf.xml, support of several inputs, etc.) Luis de la Garza 9 years ago
1 changed file(s) with 146 addition(s) and 48 deletion(s). Raw diff Collapse all Expand all
77 import sys
88 import os
99 import traceback
10 import ntpath
1011
1112 from argparse import ArgumentParser
1213 from argparse import RawDescriptionHelpFormatter
3334 def __unicode__(self):
3435 return self.msg
3536
37 class ApplicationException(Exception):
38 def __init__(self, msg):
39 super(ApplicationException).__init__(type(self))
40 self.msg = msg
41 def __str__(self):
42 return self.msg
43 def __unicode__(self):
44 return self.msg
45
3646 class ExitCode:
3747 def __init__(self, code_range="", level="", description=""):
3848 self.range = code_range
8494 try:
8595 # Setup argument parser
8696 parser = ArgumentParser(prog="GalaxyConfigGenerator", description=program_license, formatter_class=RawDescriptionHelpFormatter, add_help=True)
87 parser.add_argument("-i", "--input-file", dest="input_file", help="provide a single input CTD file to convert", required=True)
88 parser.add_argument("-o", "--output-file", dest="output_file", help="provide a single output Galaxy wrapper file", required=True)
97 parser.add_argument("-i", "--input", dest="input_files", required=True, nargs="+", action="append",
98 help="list of CTD files to convert.")
99 parser.add_argument("-o", "--output-destination", dest="output_dest", required=True,
100 help="if multiple input files are given, then a folder in which all generated XMLs will generated is expected;"\
101 "if a single input file is given, then a destination file is expected.")
89102 parser.add_argument("-a", "--add-to-command-line", dest="add_to_command_line", help="adds content to the command line", default="", required=False)
90103 parser.add_argument("-w", "--whitespace-validation", dest="whitespace_validation", action="store_true", default=False,
91104 help="if true, each parameter in the generated command line will be "+
99112 parser.add_argument("-x", "--exit-code", dest="exit_codes", default=[], nargs="+", action="append",
100113 help="list of <stdio> galaxy exit codes, in the following format: range=<range>,level=<level>,description=<description>,\n" +
101114 "example: --exit-codes \"range=3:4,level=fatal,description=Out of memory\"")
115 parser.add_argument("-t", "--tool-conf-destination", dest="tool_conf_dest", default=None, required=False,
116 help="specify the destination file of a generated tool_conf.xml for all given input files; each category will be written in its own section.")
117 parser.add_argument("-g", "--galaxy-tool-path", dest="galaxy_tool_path", default=None, required=False,
118 help="the path that will be prepended to the file names when generating tool_conf.xml")
102119 # verbosity will be added later on, will not waste time on this now
103120 # parser.add_argument("-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %(default)s]")
104121 parser.add_argument("-V", "--version", action='version', version=program_version_message)
106123 # Process arguments
107124 args = parser.parse_args()
108125
109 # collect arguments
110 input_file = args.input_file
111 output_file = args.output_file
126 # validate and prepare the passed arguments
127 validate_and_prepare_args(args)
112128
113129 #if verbose > 0:
114130 # print("Verbose mode on")
115 convert(input_file,
116 output_file,
131 convert(args.input_files,
132 args.output_dest,
117133 add_to_command_line=args.add_to_command_line,
118134 whitespace_validation=args.whitespace_validation,
119135 quote_parameters=args.quote_parameters,
120136 # remember that blacklisted_parameters, package_requirements and exit_codes are lists of lists of strings
121 blacklisted_parameters=[item for sublist in args.blacklisted_parameters for item in sublist],
122 package_requirements=[item for sublist in args.package_requirements for item in sublist],
123 exit_codes=convert_exit_codes([item for sublist in args.exit_codes for item in sublist]))
137 blacklisted_parameters=args.blacklisted_parameters,
138 package_requirements=args.package_requirements,
139 exit_codes=args.exit_codes,
140 galaxy_tool_path=args.galaxy_tool_path,
141 tool_conf_dest=args.tool_conf_dest)
124142 return 0
125143
126144 except KeyboardInterrupt:
127145 ### handle keyboard interrupt ###
128146 return 0
129 except IOError, e:
130 indent = len(program_name) * " "
131 sys.stderr.write(program_name + ": " + repr(e) + "\n")
132 sys.stderr.write(indent + "Could not access input file [%(input)s] or output file [%(output)s]\n" % {"input":input_file, "output":output_file})
133 sys.stderr.write(indent + "For help use --help\n")
134 # #define EX_NOINPUT 66 /* cannot open input */
135 return 66
147 except ApplicationException, e:
148 sys.stderr.write("GalaxyConfigGenerator could not complete the requested operation.\n")
149 sys.stderr.write("Reason: " + e.msg)
150 return 1
136151 except ModelError, e:
137152 indent = len(program_name) * " "
138153 sys.stderr.write(program_name + ": " + repr(e) + "\n")
139 sys.stderr.write(indent + "There seems to be a problem with your input CTD [%s], please make sure that it is a valid CTD.\n" % input_file)
154 sys.stderr.write(indent + "There seems to be a problem with one your input CTD.\n")
140155 sys.stderr.write(indent + "For help use --help\n")
141156 return 1
142157 except Exception, e:
143158 traceback.print_exc()
144159 return 2
160
161 def validate_and_prepare_args(args):
162 # first, we convert all list of lists to flat lists
163 args.input_files = [item for sublist in args.input_files for item in sublist]
164 args.blacklisted_parameters=[item for sublist in args.blacklisted_parameters for item in sublist]
165 args.package_requirements=[item for sublist in args.package_requirements for item in sublist]
166 args.exit_codes=convert_exit_codes([item for sublist in args.exit_codes for item in sublist])
167
168 # if input is a single file, we expect output to be a file (and not a dir that already exists)
169 if len(args.input_files) == 1:
170 if os.path.isdir(args.output_dest):
171 raise ApplicationException("If a single input file is provided, output (%s) is expected to be a file and not a folder." % args.output_dest)
172
173 # if input is a list of files, we expect output to be a folder
174 if len(args.input_files) > 1:
175 if not os.path.isdir(args.output_dest):
176 raise ApplicationException("If several input files are provided, output (%s) is expected to be an existing directory." % args.output_dest)
145177
146178 def convert_exit_codes(exit_codes_raw):
147179 # input is in the format:
157189 exit_codes.append(exit_code)
158190 return exit_codes
159191
160 def convert(input_file, output_file, **kwargs):
192 def convert(input_files, output_dest, **kwargs):
161193 # first, generate a model
162 print("Parsing CTD from [%s]" % input_file)
163 model = CTDModel(from_file=input_file)
164
194 is_converting_multiple_ctds = len(input_files) > 1
195 parsed_models = []
196 try:
197 for input_file in input_files:
198 print("Parsing CTD from [%s]" % input_file)
199 model = CTDModel(from_file=input_file)
200
201 doc = Document()
202 tool = create_tool(doc, model)
203 doc.appendChild(tool)
204 create_description(doc, tool, model)
205 create_requirements(doc, tool, model, kwargs["package_requirements"])
206 create_command(doc, tool, model, **kwargs)
207 create_inputs(doc, tool, model, kwargs["blacklisted_parameters"])
208 create_outputs(doc, tool, model, kwargs["blacklisted_parameters"])
209 create_exit_codes(doc, tool, model, kwargs["exit_codes"])
210 create_help(doc, tool, model)
211
212 # finally, serialize the tool
213 output_file = output_dest
214 # if multiple inputs are being converted, then we need to generate a different output_file for each input
215 if is_converting_multiple_ctds:
216 if not output_file.endswith('/'):
217 output_file += "/"
218 output_file += get_filename(input_file) + ".xml"
219 doc.writexml(open(output_file, 'w'), indent=" ", addindent=" ", newl='\n')
220 # let's use model to hold the name of the outputfile
221 parsed_models.append([model, get_filename(output_file)])
222 print("Generated Galaxy wrapper in [%s]\n" % output_file)
223 # generation of galaxy stubs is ready... now, let's see if we need to generate a tool_conf.xml
224 if kwargs["tool_conf_dest"] is not None:
225 generate_tool_conf(parsed_models, kwargs["tool_conf_dest"], kwargs["galaxy_tool_path"])
226
227 except IOError, e:
228 raise ApplicationException("One of the provided input files or the destination file could not be accessed. Detailed information: " + str(e) + "\n")
229
230 def generate_tool_conf(parsed_models, tool_conf_dest, galaxy_tool_path):
231 # for each category, we keep a list of models corresponding to it
232 categories_to_tools = dict()
233 for model in parsed_models:
234 if "category" in model[0].opt_attribs:
235 category = model[0].opt_attribs["category"]
236 if category is not None and len(strip(category)) > 0:
237 category = strip(category)
238 if category not in categories_to_tools:
239 categories_to_tools[category] = []
240 categories_to_tools[category].append(model[1])
241
242 # at this point, we should have a map for all categories->tools
165243 doc = Document()
166 tool = create_tool(doc, model)
167 doc.appendChild(tool)
168 create_description(doc, tool, model)
169 create_requirements(doc, tool, model, kwargs["package_requirements"])
170 create_command(doc, tool, model, **kwargs)
171 create_inputs(doc, tool, model, kwargs["blacklisted_parameters"])
172 create_outputs(doc, tool, model, kwargs["blacklisted_parameters"])
173 create_exit_codes(doc, tool, model, kwargs["exit_codes"])
174 create_help(doc, tool, model)
175
176 # finally, serialize the tool
177 doc.writexml(open(output_file, 'w'), indent=" ", addindent=" ", newl='\n')
178 print("Generated Galaxy wrapper in [%s]\n" % output_file)
244 toolbox_node = doc.createElement("toolbox")
245
246 if galaxy_tool_path is not None and not galaxy_tool_path.strip().endswith("/"):
247 galaxy_tool_path = galaxy_tool_path.strip() + "/"
248 if galaxy_tool_path is None:
249 galaxy_tool_path = ""
250
251 for category, filenames in categories_to_tools.iteritems():
252 section_node = doc.createElement("section")
253 section_node.setAttribute("id", "section-id-" + "".join(category.split()))
254 section_node.setAttribute("name", category)
255
256 for filename in filenames:
257 tool_node = doc.createElement("tool")
258 tool_node.setAttribute("file", galaxy_tool_path + filename)
259 toolbox_node.appendChild(section_node)
260 section_node.appendChild(tool_node)
261 toolbox_node.appendChild(section_node)
262
263 doc.appendChild(toolbox_node)
264 doc.writexml(open(tool_conf_dest, 'w'), indent=" ", addindent=" ", newl='\n')
265 print("Generated Galaxy tool_conf.xml in [%s]\n" % tool_conf_dest)
266
267 # taken from
268 # http://stackoverflow.com/questions/8384737/python-extract-file-name-from-path-no-matter-what-the-os-path-format
269 def get_filename(path):
270 head, tail = ntpath.split(path)
271 return tail or ntpath.basename(head)
179272
180273 def create_tool(doc, model):
181274 tool = doc.createElement("tool")
352445 # we ASSUME that a list of parameters looks like:
353446 # $ tool -ignore He Ar Xe
354447 # meaning, that, for example, Helium, Argon and Xenon will be ignored
355 param_node.setAttribute("value", ' '.join(param.default))
448 param_node.setAttribute("value", ' '.join(map(str, param.default)))
356449 elif param_type != "boolean":
357450 # boolean parameters handle default values by using the "checked" attribute
358451 # there isn't much we can do... just stringify the value
362455 # galaxy requires "value" to be included for int/float
363456 # since no default was included, we need to figure out one in a clever way... but let the user know
364457 # that we are "thinking" for him/her
365 warning("Generating default value for parameter [%s]. Galaxy requires the attribute 'value' to be set for integer/floats."\
366 "You might want to edit the CTD file and provide a suitable default value." % param.name)
458 warning("Generating default value for parameter [%s]. Galaxy requires the attribute 'value' to be set for integer/floats. "\
459 "Edit the CTD file and provide a suitable default value." % param.name)
367460 # check if there's a min/max and try to use them
368461 default_value = None
369 if type(param.restrictions) is _NumericRange:
370 default_value = param.restrictions.n_min
371 if default_value is None:
372 default_value = param.restrictions.n_max
373 if default_value is None:
374 # no min/max provided... just use 0 and see what happens
375 default_value = 0
462 if param.restrictions is not None:
463 if type(param.restrictions) is _NumericRange:
464 default_value = param.restrictions.n_min
465 if default_value is None:
466 default_value = param.restrictions.n_max
467 if default_value is None:
468 # no min/max provided... just use 0 and see what happens
469 default_value = 0
470 else:
471 # should never be here, since we have validated this anyway... this code is here just for documentation purposes
472 # however, better safe than sorry! (it could be that the code changes and then we have an ugly scenario)
473 raise InvalidModelException("Expected either a numeric range for parameter [%(name)s], but instead got [%(type)s]" % {"name":param.name, "type":type(param.restrictions)})
376474 else:
377 # should never be here, since we have validated this anyway... this code is here just for documentation purposes
378 # however, better safe than sorry! (it could be that the code changes and then we have an ugly scenario)
379 raise InvalidModelException("Expected either a numeric range for parameter [%(name)s], but instead got [%(type)s]" % {"name":param.name, "type":type(param.restrictions)})
475 # no restrictions and no default value provided...
476 # make up something
477 default_value = 0
380478 param_node.setAttribute("value", str(default_value))
381479
382480 return param_node