Codebase list ctdconverter / 35734b2
Added first version of the converter Luis de la Garza 10 years ago
2 changed file(s) with 313 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 #!/usr/local/bin/python2.7
1 # encoding: utf-8
2 '''
3 @author: delagarza
4
5 '''
6
7 import sys
8 import os
9 import traceback
10
11 from argparse import ArgumentParser
12 from argparse import RawDescriptionHelpFormatter
13 from CTDopts.CTDopts import CTDModel, _InFile, _OutFile, ParameterGroup, _Choices, _NumericRange, _FileFormat, ModelError
14
15 from xml.dom.minidom import Document
16
17 __all__ = []
18 __version__ = 0.1
19 __date__ = '2014-03-26'
20 __updated__ = '2014-03-26'
21
22 TYPE_TO_GALAXY_TYPE = {int: 'integer', float: 'float', str: 'text', bool: 'boolean', _InFile: 'data',
23 _OutFile: 'data', _Choices: 'select'}
24
25 class CLIError(Exception):
26 '''Generic exception to raise and log different fatal errors.'''
27 def __init__(self, msg):
28 super(CLIError).__init__(type(self))
29 self.msg = "E: %s" % msg
30 def __str__(self):
31 return self.msg
32 def __unicode__(self):
33 return self.msg
34
35 def main(argv=None): # IGNORE:C0111
36 '''Command line options.'''
37
38 if argv is None:
39 argv = sys.argv
40 else:
41 sys.argv.extend(argv)
42
43 program_name = os.path.basename(sys.argv[0])
44 program_version = "v%s" % __version__
45 program_build_date = str(__updated__)
46 program_version_message = '%%(prog)s %s (%s)' % (program_version, program_build_date)
47 program_shortdesc = "GalaxyConfigGenerator - A project from the GenericWorkflowNodes family (https://github.com/orgs/genericworkflownodes)"
48 program_usage = '''
49 USAGE:
50
51 Parse a single CTD file and generate a Galaxy wrapper:
52 $ python generator.py -i input.ctd -o output.xml
53
54 Parse all found CTD files (files with .ctd and .xml extension) in a given folder and output converted Galaxy wrappers in a given folder:
55 $ python generator.py --input-directory /home/johndoe/ctds --output-directory /home/johndoe/galaxywrappers
56 '''
57 program_license = '''%(shortdesc)s
58 Copyright 2014, Luis de la Garza
59
60 Licensed under the Apache License, Version 2.0 (the "License");
61 you may not use this file except in compliance with the License.
62 You may obtain a copy of the License at
63
64 http://www.apache.org/licenses/LICENSE-2.0
65
66 Unless required by applicable law or agreed to in writing, software
67 distributed under the License is distributed on an "AS IS" BASIS,
68 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
69 See the License for the specific language governing permissions and
70 limitations under the License.
71
72 %(usage)s
73 ''' % {'shortdesc':program_shortdesc, 'usage':program_usage}
74
75
76
77 try:
78 # Setup argument parser
79 parser = ArgumentParser(description=program_license, formatter_class=RawDescriptionHelpFormatter, add_help=True)
80 parser.add_argument("-i", "--input-file", dest="input_file", help="provide a single input CTD file to convert", required=True)
81 parser.add_argument("-o", "--output-file", dest="output_file", help="provide a single output Galaxy wrapper file", required=True)
82 # verbosity will be added later on, will not waste time on this now
83 # parser.add_argument("-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %(default)s]")
84 parser.add_argument("-V", "--version", action='version', version=program_version_message)
85
86 # Process arguments
87 args = parser.parse_args()
88
89 # collect arguments
90 input_file = args.input_file
91 output_file = args.output_file
92
93 #if verbose > 0:
94 # print("Verbose mode on")
95 convert(input_file, output_file)
96 return 0
97
98 except KeyboardInterrupt:
99 ### handle keyboard interrupt ###
100 return 0
101 except IOError, e:
102 indent = len(program_name) * " "
103 sys.stderr.write(program_name + ": " + repr(e) + "\n")
104 sys.stderr.write(indent + "Could not access input file [%(input)s] or output file [%(output)s]\n" % {"input":input_file, "output":output_file})
105 sys.stderr.write(indent + "For help use --help\n")
106 # #define EX_NOINPUT 66 /* cannot open input */
107 return 66
108 except ModelError, e:
109 indent = len(program_name) * " "
110 sys.stderr.write(program_name + ": " + repr(e) + "\n")
111 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)
112 sys.stderr.write(indent + "For help use --help\n")
113 return 1
114 except Exception, e:
115 traceback.print_exc()
116 return 2
117
118 def convert(input_file, output_file):
119 # first, generate a model
120 print("Parsing CTD from [%s]" % input_file)
121 model = CTDModel(from_file=input_file)
122
123 doc = Document()
124 tool = create_tool(doc, model)
125 doc.appendChild(tool)
126 create_description(doc, tool, model)
127 create_requirements(doc, tool, model)
128 create_command(doc, tool, model)
129 create_inputs(doc, tool, model)
130 create_outputs(doc, tool, model)
131 create_help(doc, tool, model)
132
133 # finally, serialize the tool
134 doc.writexml(open(output_file, 'w'), indent=" ", addindent=" ", newl='\n')
135 print("Generated Galaxy wrapper in [%s]\n" % output_file)
136
137 def create_tool(doc, model):
138 tool = doc.createElement("tool")
139 # use the same name of the tool... maybe a future version would contain a way to add a specific ID?
140 tool.setAttribute("id", model.name)
141 tool.setAttribute("version", model.version)
142 tool.setAttribute("name", model.name)
143 return tool
144
145 def create_description(doc, tool, model):
146 if "description" in model.opt_attribs.keys() and model.opt_attribs["description"] is not None:
147 description_node = doc.createElement("description")
148 description = doc.createTextNode(model.opt_attribs["description"])
149 description_node.appendChild(description)
150 tool.appendChild(description_node)
151
152 def create_requirements(doc, tool, model):
153 #TODO: how to pass requirements? command line? included in CTD?
154 pass
155
156 def create_command(doc, tool, model):
157 command = model.name + ' '
158 for param in extract_parameters(model):
159 # for boolean types, we only need the placeholder
160 if param.type is not bool:
161 # add the parameter name
162 command += '-' + param.name + ' '
163 # we need to add the placeholder
164 command += "$" + get_galaxy_parameter_name(param.name) + ' '
165
166 command_node = doc.createElement("command")
167 command_text_node = doc.createTextNode(command.strip())
168 command_node.appendChild(command_text_node)
169 tool.appendChild(command_node)
170
171 def get_galaxy_parameter_name(param_name):
172 return "param_%s" % param_name
173
174 def create_inputs(doc, tool, model):
175 inputs_node = doc.createElement("inputs")
176 # treat all non output-file parameters as inputs
177 for param in extract_parameters(model):
178 if param.type is not _OutFile:
179 inputs_node.appendChild(create_param_node(doc, param))
180 tool.appendChild(inputs_node)
181
182 def create_param_node(doc, param):
183 param_node = doc.createElement("param")
184 param_node.setAttribute("name", get_galaxy_parameter_name(param.name))
185 label = ""
186 if param.description is not None:
187 label = param.description
188 else:
189 label = "%s parameter" % param.name
190 param_node.setAttribute("label", label)
191
192 param_type = TYPE_TO_GALAXY_TYPE[param.type]
193 if param_type is None:
194 raise ModelError("Unrecognized parameter type '%(type)' for parameter '%(name)'" % {"type":param.type, "name":param.name})
195 param_node.setAttribute("type", param_type)
196
197 if param.type is _InFile:
198 # assume it's just data unless restrictions are provided
199 param_format = "data"
200 if param.restrictions is not None:
201 # join all supported_formats for the file... this MUST be a _FileFormat
202 if type(param.restrictions) is _FileFormat:
203 param_format = ','.join(param.restrictions.formats)
204 else:
205 raise InvalidModelException("Expected 'file type' restrictions for input file [%(name)s], but instead got [%(type)s]" % {"name":param.name, "type":type(param.restrictions)})
206 param_node.setAttribute("format", param_format)
207 param_type = "data"
208
209 # check for parameters with restricted values (which will correspond to a "select" in galaxy)
210 if param.restrictions is not None:
211 # it could be either _Choices or _NumericRange
212 if type(param.restrictions) is _Choices:
213 # create as many <option> elements as restriction values
214 for choice in param.restrictions.choices:
215 option_node = doc.createElement("option")
216 option_node.setAttribute("value", str(choice))
217 option_label = doc.createTextNode(str(choice))
218 option_node.appendChild(option_label)
219 param_node.appendChild(option_node)
220 elif type(param.restrictions) is _NumericRange:
221 if param.type is not int and param.type is not float:
222 raise InvalidModelException("Expected either 'int' or 'float' in the numeric range restriction for parameter [%(name)s], but instead got [%(type)s]" % {"name":param.name, "type":type(param.restrictions)})
223 # extract the min and max values and add them as attributes
224 param_node.setAttribute("min", str(param.restrictions.n_min))
225 param_node.setAttribute("max", str(param.restrictions.n_max))
226 elif type(param.restrictions) is _FileFormat:
227 param_node.setAttribute("format", ",".join(param.restrictions.formats))
228 else:
229 raise InvalidModelException("Unrecognized restriction type [%(type)s] for parameter [%(name)s]" % {"type":type(param.restrictions), "name":param.name})
230
231 if param.type is str:
232 # add size attribute... this is the length of a textbox field in Galaxy (it could also be 15x2, for instance)
233 param_node.setAttribute("size", "15")
234
235
236 # check for default value
237 if param.default is not None:
238 param_node.setAttribute("value", str(param.default))
239
240 return param_node
241
242 def create_outputs(doc, tool, model):
243 outputs_node = doc.createElement("outputs")
244 for param in extract_parameters(model):
245 if param.type is _OutFile:
246 outputs_node.appendChild(create_data_node(doc, param))
247 tool.appendChild(outputs_node)
248
249 def create_data_node(doc, param):
250 data_node = doc.createElement("data")
251 data_node.setAttribute("name", get_galaxy_parameter_name(param.name))
252 data_format = "data"
253 if param.restrictions is not None:
254 if type(param.restrictions) is _FileFormat:
255 data_format = ','.join(param.restrictions.formats)
256 else:
257 raise InvalidModelException("Unrecognized restriction type [%(type)s] for output [%(name)s]" % {"type":type(param.restrictions), "name":param.name})
258 data_node.setAttribute("format", data_format)
259
260 if param.description is not None:
261 data_node.setAttribute("label", param.description)
262
263 return data_node
264
265 def create_help(doc, tool, model):
266 manual = None
267 doc_url = None
268 if 'manual' in model.opt_attribs.keys():
269 model.opt_attribs["manual"]
270 if 'docurl' in model.opt_attribs.keys():
271 model.opt_attribs["docurl"]
272 help_text = "No help available"
273 if manual is not None:
274 help_text = manual
275 if doc_url is not None:
276 help_text = ("" if manual is None else manual) + " For more information, visit %s" % doc_url
277
278 help_node = doc.createElement("help")
279 help_node.appendChild(doc.createTextNode(help_text))
280 tool.appendChild(help_node)
281
282 # since a model might contain several ParameterGroup elements, we want to simply 'flatten' the parameters to generate the Galaxy wrapper
283 def extract_parameters(model):
284 parameters = []
285 if len(model.parameters.parameters) > 0:
286 # use this to put parameters that are to be processed
287 # we know that CTDModel has one parent ParameterGroup
288 pending = [model.parameters]
289 while len(pending) > 0:
290 # take one element from 'pending'
291 parameter = pending.pop()
292 if type(parameter) is not ParameterGroup:
293 parameters.append(parameter)
294 else:
295 # append the first-level children of this ParameterGroup
296 pending.extend(parameter.parameters.values())
297 # returned the reversed list of parameters (as it is now, we have the last parameter in the CTD as first in the list)
298 return reversed(parameters)
299
300 class InvalidModelException(ModelError):
301 def __init__(self, message):
302 super(InvalidModelException, self).__init__()
303 self.message = message
304
305 def __str__(self):
306 return self.message
307
308 def __repr__(self):
309 return self.message
310
311 if __name__ == "__main__":
312 sys.exit(main())