Added first version of the converter
Luis de la Garza
10 years ago
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())⏎ |