Mercurial > repos > fubar > tool_factory_2
comparison toolfactory/ToolFactory.py @ 3:1c652687a08f draft default tip
Uploaded
author | fubar |
---|---|
date | Fri, 30 Apr 2021 07:06:57 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
2:5fc0c9a93072 | 3:1c652687a08f |
---|---|
1 | |
2 # see https://github.com/fubar2/toolfactory | |
3 # | |
4 # copyright ross lazarus (ross stop lazarus at gmail stop com) May 2012 | |
5 # | |
6 # all rights reserved | |
7 # Licensed under the LGPL | |
8 # suggestions for improvement and bug fixes welcome at | |
9 # https://github.com/fubar2/toolfactory | |
10 # | |
11 # April 2021: Refactored into two tools - generate and test/install | |
12 # as part of GTN tutorial development and biocontainer adoption | |
13 # The tester runs planemo on a non-tested archive, creates the test outputs | |
14 # and returns a new proper tool with test. | |
15 # The tester was generated from the ToolFactory_tester.py script | |
16 | |
17 | |
18 import argparse | |
19 import copy | |
20 import json | |
21 import logging | |
22 import os | |
23 import re | |
24 import shlex | |
25 import shutil | |
26 import subprocess | |
27 import sys | |
28 import tarfile | |
29 import tempfile | |
30 import time | |
31 import urllib | |
32 | |
33 from bioblend import ConnectionError | |
34 from bioblend import galaxy | |
35 from bioblend import toolshed | |
36 | |
37 import galaxyxml.tool as gxt | |
38 import galaxyxml.tool.parameters as gxtp | |
39 | |
40 import lxml.etree as ET | |
41 | |
42 import yaml | |
43 | |
44 myversion = "V2.3 April 2021" | |
45 verbose = True | |
46 debug = True | |
47 toolFactoryURL = "https://github.com/fubar2/toolfactory" | |
48 FAKEEXE = "~~~REMOVE~~~ME~~~" | |
49 # need this until a PR/version bump to fix galaxyxml prepending the exe even | |
50 # with override. | |
51 | |
52 | |
53 def timenow(): | |
54 """return current time as a string""" | |
55 return time.strftime("%d/%m/%Y %H:%M:%S", time.localtime(time.time())) | |
56 | |
57 cheetah_escape_table = {"$": "\\$", "#": "\\#"} | |
58 | |
59 def cheetah_escape(text): | |
60 """Produce entities within text.""" | |
61 return "".join([cheetah_escape_table.get(c, c) for c in text]) | |
62 | |
63 def parse_citations(citations_text): | |
64 """""" | |
65 citations = [c for c in citations_text.split("**ENTRY**") if c.strip()] | |
66 citation_tuples = [] | |
67 for citation in citations: | |
68 if citation.startswith("doi"): | |
69 citation_tuples.append(("doi", citation[len("doi") :].strip())) | |
70 else: | |
71 citation_tuples.append(("bibtex", citation[len("bibtex") :].strip())) | |
72 return citation_tuples | |
73 | |
74 | |
75 class Tool_Conf_Updater(): | |
76 # update config/tool_conf.xml with a new tool unpacked in /tools | |
77 # requires highly insecure docker settings - like write to tool_conf.xml and to tools ! | |
78 # if in a container possibly not so courageous. | |
79 # Fine on your own laptop but security red flag for most production instances | |
80 | |
81 def __init__(self, args, tool_conf_path, new_tool_archive_path, new_tool_name, tool_dir): | |
82 self.args = args | |
83 self.tool_conf_path = os.path.join(args.galaxy_root,tool_conf_path) | |
84 self.tool_dir = os.path.join(args.galaxy_root, tool_dir) | |
85 self.our_name = 'ToolFactory' | |
86 tff = tarfile.open(new_tool_archive_path, "r:*") | |
87 flist = tff.getnames() | |
88 ourdir = os.path.commonpath(flist) # eg pyrevpos | |
89 self.tool_id = ourdir # they are the same for TF tools | |
90 ourxml = [x for x in flist if x.lower().endswith('.xml')] | |
91 res = tff.extractall() | |
92 tff.close() | |
93 self.run_rsync(ourdir, self.tool_dir) | |
94 self.update_toolconf(ourdir,ourxml) | |
95 | |
96 def run_rsync(self, srcf, dstf): | |
97 src = os.path.abspath(srcf) | |
98 dst = os.path.abspath(dstf) | |
99 if os.path.isdir(src): | |
100 cll = ['rsync', '-vr', src, dst] | |
101 else: | |
102 cll = ['rsync', '-v', src, dst] | |
103 p = subprocess.run( | |
104 cll, | |
105 capture_output=False, | |
106 encoding='utf8', | |
107 shell=False, | |
108 ) | |
109 | |
110 def install_deps(self): | |
111 gi = galaxy.GalaxyInstance(url=self.args.galaxy_url, key=self.args.galaxy_api_key) | |
112 x = gi.tools.install_dependencies(self.tool_id) | |
113 print(f"Called install_dependencies on {self.tool_id} - got {x}") | |
114 | |
115 def update_toolconf(self,ourdir,ourxml): # path is relative to tools | |
116 updated = False | |
117 localconf = './local_tool_conf.xml' | |
118 self.run_rsync(self.tool_conf_path,localconf) | |
119 tree = ET.parse(localconf) | |
120 root = tree.getroot() | |
121 hasTF = False | |
122 TFsection = None | |
123 for e in root.findall('section'): | |
124 if e.attrib['name'] == self.our_name: | |
125 hasTF = True | |
126 TFsection = e | |
127 if not hasTF: | |
128 TFsection = ET.Element('section') | |
129 root.insert(0,TFsection) # at the top! | |
130 our_tools = TFsection.findall('tool') | |
131 conf_tools = [x.attrib['file'] for x in our_tools] | |
132 for xml in ourxml: # may be > 1 | |
133 if not xml in conf_tools: # new | |
134 updated = True | |
135 ET.SubElement(TFsection, 'tool', {'file':xml}) | |
136 ET.indent(tree) | |
137 newconf = f"{self.tool_id}_conf" | |
138 tree.write(newconf, pretty_print=True) | |
139 self.run_rsync(newconf,self.tool_conf_path) | |
140 if False and self.args.packages and self.args.packages > '': | |
141 self.install_deps() | |
142 | |
143 class Tool_Factory: | |
144 """Wrapper for an arbitrary script | |
145 uses galaxyxml | |
146 | |
147 """ | |
148 | |
149 def __init__(self, args=None): # noqa | |
150 """ | |
151 prepare command line cl for running the tool here | |
152 and prepare elements needed for galaxyxml tool generation | |
153 """ | |
154 self.ourcwd = os.getcwd() | |
155 self.collections = [] | |
156 if len(args.collection) > 0: | |
157 try: | |
158 self.collections = [ | |
159 json.loads(x) for x in args.collection if len(x.strip()) > 1 | |
160 ] | |
161 except Exception: | |
162 print( | |
163 f"--collections parameter {str(args.collection)} is malformed - should be a dictionary" | |
164 ) | |
165 try: | |
166 self.infiles = [ | |
167 json.loads(x) for x in args.input_files if len(x.strip()) > 1 | |
168 ] | |
169 except Exception: | |
170 print( | |
171 f"--input_files parameter {str(args.input_files)} is malformed - should be a dictionary" | |
172 ) | |
173 try: | |
174 self.outfiles = [ | |
175 json.loads(x) for x in args.output_files if len(x.strip()) > 1 | |
176 ] | |
177 except Exception: | |
178 print( | |
179 f"--output_files parameter {args.output_files} is malformed - should be a dictionary" | |
180 ) | |
181 try: | |
182 self.addpar = [ | |
183 json.loads(x) for x in args.additional_parameters if len(x.strip()) > 1 | |
184 ] | |
185 except Exception: | |
186 print( | |
187 f"--additional_parameters {args.additional_parameters} is malformed - should be a dictionary" | |
188 ) | |
189 try: | |
190 self.selpar = [ | |
191 json.loads(x) for x in args.selecttext_parameters if len(x.strip()) > 1 | |
192 ] | |
193 except Exception: | |
194 print( | |
195 f"--selecttext_parameters {args.selecttext_parameters} is malformed - should be a dictionary" | |
196 ) | |
197 self.args = args | |
198 self.cleanuppar() | |
199 self.lastxclredirect = None | |
200 self.xmlcl = [] | |
201 self.is_positional = self.args.parampass == "positional" | |
202 if self.args.sysexe: | |
203 if ' ' in self.args.sysexe: | |
204 self.executeme = self.args.sysexe.split(' ') | |
205 else: | |
206 self.executeme = [self.args.sysexe, ] | |
207 else: | |
208 if self.args.packages: | |
209 self.executeme = [self.args.packages.split(",")[0].split(":")[0].strip(), ] | |
210 else: | |
211 self.executeme = None | |
212 aXCL = self.xmlcl.append | |
213 assert args.parampass in [ | |
214 "0", | |
215 "argparse", | |
216 "positional", | |
217 ], 'args.parampass must be "0","positional" or "argparse"' | |
218 self.tool_name = re.sub("[^a-zA-Z0-9_]+", "", args.tool_name) | |
219 self.tool_id = self.tool_name | |
220 self.newtool = gxt.Tool( | |
221 self.tool_name, | |
222 self.tool_id, | |
223 self.args.tool_version, | |
224 self.args.tool_desc, | |
225 FAKEEXE, | |
226 ) | |
227 self.newtarpath = "%s_toolshed.gz" % self.tool_name | |
228 self.tooloutdir = "./tfout" | |
229 self.repdir = "./TF_run_report" | |
230 self.testdir = os.path.join(self.tooloutdir, "test-data") | |
231 if not os.path.exists(self.tooloutdir): | |
232 os.mkdir(self.tooloutdir) | |
233 if not os.path.exists(self.testdir): | |
234 os.mkdir(self.testdir) | |
235 if not os.path.exists(self.repdir): | |
236 os.mkdir(self.repdir) | |
237 self.tinputs = gxtp.Inputs() | |
238 self.toutputs = gxtp.Outputs() | |
239 self.testparam = [] | |
240 if self.args.script_path: | |
241 self.prepScript() | |
242 if self.args.command_override: | |
243 scos = open(self.args.command_override, "r").readlines() | |
244 self.command_override = [x.rstrip() for x in scos] | |
245 else: | |
246 self.command_override = None | |
247 if self.args.test_override: | |
248 stos = open(self.args.test_override, "r").readlines() | |
249 self.test_override = [x.rstrip() for x in stos] | |
250 else: | |
251 self.test_override = None | |
252 if self.args.script_path: | |
253 for ex in self.executeme: | |
254 aXCL(ex) | |
255 aXCL("$runme") | |
256 else: | |
257 for ex in self.executeme: | |
258 aXCL(ex) | |
259 | |
260 if self.args.parampass == "0": | |
261 self.clsimple() | |
262 else: | |
263 if self.args.parampass == "positional": | |
264 self.prepclpos() | |
265 self.clpositional() | |
266 else: | |
267 self.prepargp() | |
268 self.clargparse() | |
269 | |
270 def clsimple(self): | |
271 """no parameters or repeats - uses < and > for i/o""" | |
272 aXCL = self.xmlcl.append | |
273 if len(self.infiles) > 0: | |
274 aXCL("<") | |
275 aXCL("$%s" % self.infiles[0]["infilename"]) | |
276 if len(self.outfiles) > 0: | |
277 aXCL(">") | |
278 aXCL("$%s" % self.outfiles[0]["name"]) | |
279 if self.args.cl_user_suffix: # DIY CL end | |
280 clp = shlex.split(self.args.cl_user_suffix) | |
281 for c in clp: | |
282 aXCL(c) | |
283 | |
284 def prepargp(self): | |
285 xclsuffix = [] | |
286 for i, p in enumerate(self.infiles): | |
287 nam = p["infilename"] | |
288 if p["origCL"].strip().upper() == "STDIN": | |
289 xappendme = [ | |
290 nam, | |
291 nam, | |
292 "< $%s" % nam, | |
293 ] | |
294 else: | |
295 rep = p["repeat"] == "1" | |
296 over = "" | |
297 if rep: | |
298 over = f'#for $rep in $R_{nam}:\n--{nam} "$rep.{nam}"\n#end for' | |
299 xappendme = [p["CL"], "$%s" % p["CL"], over] | |
300 xclsuffix.append(xappendme) | |
301 for i, p in enumerate(self.outfiles): | |
302 if p["origCL"].strip().upper() == "STDOUT": | |
303 self.lastxclredirect = [">", "$%s" % p["name"]] | |
304 else: | |
305 xclsuffix.append([p["name"], "$%s" % p["name"], ""]) | |
306 for p in self.addpar: | |
307 nam = p["name"] | |
308 rep = p["repeat"] == "1" | |
309 if rep: | |
310 over = f'#for $rep in $R_{nam}:\n--{nam} "$rep.{nam}"\n#end for' | |
311 else: | |
312 over = p["override"] | |
313 xclsuffix.append([p["CL"], '"$%s"' % nam, over]) | |
314 for p in self.selpar: | |
315 xclsuffix.append([p["CL"], '"$%s"' % p["name"], p["override"]]) | |
316 self.xclsuffix = xclsuffix | |
317 | |
318 def prepclpos(self): | |
319 xclsuffix = [] | |
320 for i, p in enumerate(self.infiles): | |
321 if p["origCL"].strip().upper() == "STDIN": | |
322 xappendme = [ | |
323 "999", | |
324 p["infilename"], | |
325 "< $%s" % p["infilename"], | |
326 ] | |
327 else: | |
328 xappendme = [p["CL"], "$%s" % p["infilename"], ""] | |
329 xclsuffix.append(xappendme) | |
330 for i, p in enumerate(self.outfiles): | |
331 if p["origCL"].strip().upper() == "STDOUT": | |
332 self.lastxclredirect = [">", "$%s" % p["name"]] | |
333 else: | |
334 xclsuffix.append([p["CL"], "$%s" % p["name"], ""]) | |
335 for p in self.addpar: | |
336 nam = p["name"] | |
337 rep = p["repeat"] == "1" # repeats make NO sense | |
338 if rep: | |
339 print(f'### warning. Repeats for {nam} ignored - not permitted in positional parameter command lines!') | |
340 over = p["override"] | |
341 xclsuffix.append([p["CL"], '"$%s"' % nam, over]) | |
342 for p in self.selpar: | |
343 xclsuffix.append([p["CL"], '"$%s"' % p["name"], p["override"]]) | |
344 xclsuffix.sort() | |
345 self.xclsuffix = xclsuffix | |
346 | |
347 def prepScript(self): | |
348 rx = open(self.args.script_path, "r").readlines() | |
349 rx = [x.rstrip() for x in rx] | |
350 rxcheck = [x.strip() for x in rx if x.strip() > ""] | |
351 assert len(rxcheck) > 0, "Supplied script is empty. Cannot run" | |
352 self.script = "\n".join(rx) | |
353 fhandle, self.sfile = tempfile.mkstemp( | |
354 prefix=self.tool_name, suffix="_%s" % (self.executeme[0]) | |
355 ) | |
356 tscript = open(self.sfile, "w") | |
357 tscript.write(self.script) | |
358 tscript.close() | |
359 self.spacedScript = [f" {x}" for x in rx if x.strip() > ""] | |
360 rx.insert(0,'#raw') | |
361 rx.append('#end raw') | |
362 self.escapedScript = rx | |
363 art = "%s.%s" % (self.tool_name, self.executeme[0]) | |
364 artifact = open(art, "wb") | |
365 artifact.write(bytes(self.script, "utf8")) | |
366 artifact.close() | |
367 | |
368 def cleanuppar(self): | |
369 """ positional parameters are complicated by their numeric ordinal""" | |
370 if self.args.parampass == "positional": | |
371 for i, p in enumerate(self.infiles): | |
372 assert ( | |
373 p["CL"].isdigit() or p["CL"].strip().upper() == "STDIN" | |
374 ), "Positional parameters must be ordinal integers - got %s for %s" % ( | |
375 p["CL"], | |
376 p["label"], | |
377 ) | |
378 for i, p in enumerate(self.outfiles): | |
379 assert ( | |
380 p["CL"].isdigit() or p["CL"].strip().upper() == "STDOUT" | |
381 ), "Positional parameters must be ordinal integers - got %s for %s" % ( | |
382 p["CL"], | |
383 p["name"], | |
384 ) | |
385 for i, p in enumerate(self.addpar): | |
386 assert p[ | |
387 "CL" | |
388 ].isdigit(), "Positional parameters must be ordinal integers - got %s for %s" % ( | |
389 p["CL"], | |
390 p["name"], | |
391 ) | |
392 for i, p in enumerate(self.infiles): | |
393 infp = copy.copy(p) | |
394 infp["origCL"] = infp["CL"] | |
395 if self.args.parampass in ["positional", "0"]: | |
396 infp["infilename"] = infp["label"].replace(" ", "_") | |
397 else: | |
398 infp["infilename"] = infp["CL"] | |
399 self.infiles[i] = infp | |
400 for i, p in enumerate(self.outfiles): | |
401 p["origCL"] = p["CL"] # keep copy | |
402 self.outfiles[i] = p | |
403 for i, p in enumerate(self.addpar): | |
404 p["origCL"] = p["CL"] | |
405 self.addpar[i] = p | |
406 | |
407 def clpositional(self): | |
408 # inputs in order then params | |
409 aXCL = self.xmlcl.append | |
410 for (k, v, koverride) in self.xclsuffix: | |
411 aXCL(v) | |
412 if self.lastxclredirect: | |
413 aXCL(self.lastxclredirect[0]) | |
414 aXCL(self.lastxclredirect[1]) | |
415 if self.args.cl_user_suffix: # DIY CL end | |
416 clp = shlex.split(self.args.cl_user_suffix) | |
417 for c in clp: | |
418 aXCL(c) | |
419 | |
420 | |
421 def clargparse(self): | |
422 """argparse style""" | |
423 aXCL = self.xmlcl.append | |
424 # inputs then params in argparse named form | |
425 | |
426 for (k, v, koverride) in self.xclsuffix: | |
427 if koverride > "": | |
428 k = koverride | |
429 aXCL(k) | |
430 else: | |
431 if len(k.strip()) == 1: | |
432 k = "-%s" % k | |
433 else: | |
434 k = "--%s" % k | |
435 aXCL(k) | |
436 aXCL(v) | |
437 if self.lastxclredirect: | |
438 aXCL(self.lastxclredirect[0]) | |
439 aXCL(self.lastxclredirect[1]) | |
440 if self.args.cl_user_suffix: # DIY CL end | |
441 clp = shlex.split(self.args.cl_user_suffix) | |
442 for c in clp: | |
443 aXCL(c) | |
444 | |
445 def getNdash(self, newname): | |
446 if self.is_positional: | |
447 ndash = 0 | |
448 else: | |
449 ndash = 2 | |
450 if len(newname) < 2: | |
451 ndash = 1 | |
452 return ndash | |
453 | |
454 def doXMLparam(self): # noqa | |
455 """Add all needed elements to tool""" | |
456 for p in self.outfiles: | |
457 newname = p["name"] | |
458 newfmt = p["format"] | |
459 newcl = p["CL"] | |
460 test = p["test"] | |
461 oldcl = p["origCL"] | |
462 test = test.strip() | |
463 ndash = self.getNdash(newcl) | |
464 aparm = gxtp.OutputData( | |
465 name=newname, format=newfmt, num_dashes=ndash, label=newname | |
466 ) | |
467 aparm.positional = self.is_positional | |
468 if self.is_positional: | |
469 if oldcl.upper() == "STDOUT": | |
470 aparm.positional = 9999999 | |
471 aparm.command_line_override = "> $%s" % newname | |
472 else: | |
473 aparm.positional = int(oldcl) | |
474 aparm.command_line_override = "$%s" % newname | |
475 self.toutputs.append(aparm) | |
476 ld = None | |
477 if test.strip() > "": | |
478 if test.startswith("diff"): | |
479 c = "diff" | |
480 ld = 0 | |
481 if test.split(":")[1].isdigit: | |
482 ld = int(test.split(":")[1]) | |
483 tp = gxtp.TestOutput( | |
484 name=newname, | |
485 value="%s_sample" % newname, | |
486 compare=c, | |
487 lines_diff=ld, | |
488 ) | |
489 elif test.startswith("sim_size"): | |
490 c = "sim_size" | |
491 tn = test.split(":")[1].strip() | |
492 if tn > "": | |
493 if "." in tn: | |
494 delta = None | |
495 delta_frac = min(1.0, float(tn)) | |
496 else: | |
497 delta = int(tn) | |
498 delta_frac = None | |
499 tp = gxtp.TestOutput( | |
500 name=newname, | |
501 value="%s_sample" % newname, | |
502 compare=c, | |
503 delta=delta, | |
504 delta_frac=delta_frac, | |
505 ) | |
506 else: | |
507 c = test | |
508 tp = gxtp.TestOutput( | |
509 name=newname, | |
510 value="%s_sample" % newname, | |
511 compare=c, | |
512 ) | |
513 self.testparam.append(tp) | |
514 for p in self.infiles: | |
515 newname = p["infilename"] | |
516 newfmt = p["format"] | |
517 ndash = self.getNdash(newname) | |
518 reps = p.get("repeat", "0") == "1" | |
519 if not len(p["label"]) > 0: | |
520 alab = p["CL"] | |
521 else: | |
522 alab = p["label"] | |
523 aninput = gxtp.DataParam( | |
524 newname, | |
525 optional=False, | |
526 label=alab, | |
527 help=p["help"], | |
528 format=newfmt, | |
529 multiple=False, | |
530 num_dashes=ndash, | |
531 ) | |
532 aninput.positional = self.is_positional | |
533 if self.is_positional: | |
534 if p["origCL"].upper() == "STDIN": | |
535 aninput.positional = 9999998 | |
536 aninput.command_line_override = "> $%s" % newname | |
537 else: | |
538 aninput.positional = int(p["origCL"]) | |
539 aninput.command_line_override = "$%s" % newname | |
540 if reps: | |
541 repe = gxtp.Repeat(name=f"R_{newname}", title=f"Add as many {alab} as needed") | |
542 repe.append(aninput) | |
543 self.tinputs.append(repe) | |
544 tparm = gxtp.TestRepeat(name=f"R_{newname}") | |
545 tparm2 = gxtp.TestParam(newname, value="%s_sample" % newname) | |
546 tparm.append(tparm2) | |
547 self.testparam.append(tparm) | |
548 else: | |
549 self.tinputs.append(aninput) | |
550 tparm = gxtp.TestParam(newname, value="%s_sample" % newname) | |
551 self.testparam.append(tparm) | |
552 for p in self.addpar: | |
553 newname = p["name"] | |
554 newval = p["value"] | |
555 newlabel = p["label"] | |
556 newhelp = p["help"] | |
557 newtype = p["type"] | |
558 newcl = p["CL"] | |
559 oldcl = p["origCL"] | |
560 reps = p["repeat"] == "1" | |
561 if not len(newlabel) > 0: | |
562 newlabel = newname | |
563 ndash = self.getNdash(newname) | |
564 if newtype == "text": | |
565 aparm = gxtp.TextParam( | |
566 newname, | |
567 label=newlabel, | |
568 help=newhelp, | |
569 value=newval, | |
570 num_dashes=ndash, | |
571 ) | |
572 elif newtype == "integer": | |
573 aparm = gxtp.IntegerParam( | |
574 newname, | |
575 label=newlabel, | |
576 help=newhelp, | |
577 value=newval, | |
578 num_dashes=ndash, | |
579 ) | |
580 elif newtype == "float": | |
581 aparm = gxtp.FloatParam( | |
582 newname, | |
583 label=newlabel, | |
584 help=newhelp, | |
585 value=newval, | |
586 num_dashes=ndash, | |
587 ) | |
588 elif newtype == "boolean": | |
589 aparm = gxtp.BooleanParam( | |
590 newname, | |
591 label=newlabel, | |
592 help=newhelp, | |
593 value=newval, | |
594 num_dashes=ndash, | |
595 ) | |
596 else: | |
597 raise ValueError( | |
598 'Unrecognised parameter type "%s" for\ | |
599 additional parameter %s in makeXML' | |
600 % (newtype, newname) | |
601 ) | |
602 aparm.positional = self.is_positional | |
603 if self.is_positional: | |
604 aparm.positional = int(oldcl) | |
605 if reps: | |
606 repe = gxtp.Repeat(name=f"R_{newname}", title=f"Add as many {newlabel} as needed") | |
607 repe.append(aparm) | |
608 self.tinputs.append(repe) | |
609 tparm = gxtp.TestRepeat(name=f"R_{newname}") | |
610 tparm2 = gxtp.TestParam(newname, value=newval) | |
611 tparm.append(tparm2) | |
612 self.testparam.append(tparm) | |
613 else: | |
614 self.tinputs.append(aparm) | |
615 tparm = gxtp.TestParam(newname, value=newval) | |
616 self.testparam.append(tparm) | |
617 for p in self.selpar: | |
618 newname = p["name"] | |
619 newval = p["value"] | |
620 newlabel = p["label"] | |
621 newhelp = p["help"] | |
622 newtype = p["type"] | |
623 newcl = p["CL"] | |
624 if not len(newlabel) > 0: | |
625 newlabel = newname | |
626 ndash = self.getNdash(newname) | |
627 if newtype == "selecttext": | |
628 newtext = p["texts"] | |
629 aparm = gxtp.SelectParam( | |
630 newname, | |
631 label=newlabel, | |
632 help=newhelp, | |
633 num_dashes=ndash, | |
634 ) | |
635 for i in range(len(newval)): | |
636 anopt = gxtp.SelectOption( | |
637 value=newval[i], | |
638 text=newtext[i], | |
639 ) | |
640 aparm.append(anopt) | |
641 aparm.positional = self.is_positional | |
642 if self.is_positional: | |
643 aparm.positional = int(newcl) | |
644 self.tinputs.append(aparm) | |
645 tparm = gxtp.TestParam(newname, value=newval) | |
646 self.testparam.append(tparm) | |
647 else: | |
648 raise ValueError( | |
649 'Unrecognised parameter type "%s" for\ | |
650 selecttext parameter %s in makeXML' | |
651 % (newtype, newname) | |
652 ) | |
653 for p in self.collections: | |
654 newkind = p["kind"] | |
655 newname = p["name"] | |
656 newlabel = p["label"] | |
657 newdisc = p["discover"] | |
658 collect = gxtp.OutputCollection(newname, label=newlabel, type=newkind) | |
659 disc = gxtp.DiscoverDatasets( | |
660 pattern=newdisc, directory=f"{newname}", visible="false" | |
661 ) | |
662 collect.append(disc) | |
663 self.toutputs.append(collect) | |
664 try: | |
665 tparm = gxtp.TestOutputCollection(newname) # broken until PR merged. | |
666 self.testparam.append(tparm) | |
667 except Exception: | |
668 print("#### WARNING: Galaxyxml version does not have the PR merged yet - tests for collections must be over-ridden until then!") | |
669 | |
670 def doNoXMLparam(self): | |
671 """filter style package - stdin to stdout""" | |
672 if len(self.infiles) > 0: | |
673 alab = self.infiles[0]["label"] | |
674 if len(alab) == 0: | |
675 alab = self.infiles[0]["infilename"] | |
676 max1s = ( | |
677 "Maximum one input if parampass is 0 but multiple input files supplied - %s" | |
678 % str(self.infiles) | |
679 ) | |
680 assert len(self.infiles) == 1, max1s | |
681 newname = self.infiles[0]["infilename"] | |
682 aninput = gxtp.DataParam( | |
683 newname, | |
684 optional=False, | |
685 label=alab, | |
686 help=self.infiles[0]["help"], | |
687 format=self.infiles[0]["format"], | |
688 multiple=False, | |
689 num_dashes=0, | |
690 ) | |
691 aninput.command_line_override = "< $%s" % newname | |
692 aninput.positional = True | |
693 self.tinputs.append(aninput) | |
694 tp = gxtp.TestParam(name=newname, value="%s_sample" % newname) | |
695 self.testparam.append(tp) | |
696 if len(self.outfiles) > 0: | |
697 newname = self.outfiles[0]["name"] | |
698 newfmt = self.outfiles[0]["format"] | |
699 anout = gxtp.OutputData(newname, format=newfmt, num_dashes=0) | |
700 anout.command_line_override = "> $%s" % newname | |
701 anout.positional = self.is_positional | |
702 self.toutputs.append(anout) | |
703 tp = gxtp.TestOutput(name=newname, value="%s_sample" % newname) | |
704 self.testparam.append(tp) | |
705 | |
706 def makeXML(self): # noqa | |
707 """ | |
708 Create a Galaxy xml tool wrapper for the new script | |
709 Uses galaxyhtml | |
710 Hmmm. How to get the command line into correct order... | |
711 """ | |
712 if self.command_override: | |
713 self.newtool.command_override = self.command_override # config file | |
714 else: | |
715 self.newtool.command_override = self.xmlcl | |
716 cite = gxtp.Citations() | |
717 acite = gxtp.Citation(type="doi", value="10.1093/bioinformatics/bts573") | |
718 cite.append(acite) | |
719 self.newtool.citations = cite | |
720 safertext = "" | |
721 if self.args.help_text: | |
722 helptext = open(self.args.help_text, "r").readlines() | |
723 safertext = "\n".join([cheetah_escape(x) for x in helptext]) | |
724 if len(safertext.strip()) == 0: | |
725 safertext = ( | |
726 "Ask the tool author (%s) to rebuild with help text please\n" | |
727 % (self.args.user_email) | |
728 ) | |
729 if self.args.script_path: | |
730 if len(safertext) > 0: | |
731 safertext = safertext + "\n\n------\n" # transition allowed! | |
732 scr = [x for x in self.spacedScript if x.strip() > ""] | |
733 scr.insert(0, "\n\nScript::\n") | |
734 if len(scr) > 300: | |
735 scr = ( | |
736 scr[:100] | |
737 + [" >300 lines - stuff deleted", " ......"] | |
738 + scr[-100:] | |
739 ) | |
740 scr.append("\n") | |
741 safertext = safertext + "\n".join(scr) | |
742 self.newtool.help = safertext | |
743 self.newtool.version_command = f'echo "{self.args.tool_version}"' | |
744 std = gxtp.Stdios() | |
745 std1 = gxtp.Stdio() | |
746 std.append(std1) | |
747 self.newtool.stdios = std | |
748 requirements = gxtp.Requirements() | |
749 if self.args.packages: | |
750 try: | |
751 for d in self.args.packages.split(","): | |
752 ver = "" | |
753 d = d.replace("==", ":") | |
754 d = d.replace("=", ":") | |
755 if ":" in d: | |
756 packg, ver = d.split(":") | |
757 else: | |
758 packg = d | |
759 requirements.append( | |
760 gxtp.Requirement("package", packg.strip(), ver.strip()) | |
761 ) | |
762 except Exception: | |
763 print('### malformed packages string supplied - cannot parse =',self.args.packages) | |
764 sys.exit(2) | |
765 self.newtool.requirements = requirements | |
766 if self.args.parampass == "0": | |
767 self.doNoXMLparam() | |
768 else: | |
769 self.doXMLparam() | |
770 self.newtool.outputs = self.toutputs | |
771 self.newtool.inputs = self.tinputs | |
772 if self.args.script_path: | |
773 configfiles = gxtp.Configfiles() | |
774 configfiles.append( | |
775 gxtp.Configfile(name="runme", text="\n".join(self.escapedScript)) | |
776 ) | |
777 self.newtool.configfiles = configfiles | |
778 tests = gxtp.Tests() | |
779 test_a = gxtp.Test() | |
780 for tp in self.testparam: | |
781 test_a.append(tp) | |
782 tests.append(test_a) | |
783 self.newtool.tests = tests | |
784 self.newtool.add_comment( | |
785 "Created by %s at %s using the Galaxy Tool Factory." | |
786 % (self.args.user_email, timenow()) | |
787 ) | |
788 self.newtool.add_comment("Source in git at: %s" % (toolFactoryURL)) | |
789 exml0 = self.newtool.export() | |
790 exml = exml0.replace(FAKEEXE, "") # temporary work around until PR accepted | |
791 if ( | |
792 self.test_override | |
793 ): # cannot do this inside galaxyxml as it expects lxml objects for tests | |
794 part1 = exml.split("<tests>")[0] | |
795 part2 = exml.split("</tests>")[1] | |
796 fixed = "%s\n%s\n%s" % (part1, "\n".join(self.test_override), part2) | |
797 exml = fixed | |
798 # exml = exml.replace('range="1:"', 'range="1000:"') | |
799 xf = open("%s.xml" % self.tool_name, "w") | |
800 xf.write(exml) | |
801 xf.write("\n") | |
802 xf.close() | |
803 # ready for the tarball | |
804 | |
805 def writeShedyml(self): | |
806 """for planemo""" | |
807 yuser = self.args.user_email.split("@")[0] | |
808 yfname = os.path.join(self.tooloutdir, ".shed.yml") | |
809 yamlf = open(yfname, "w") | |
810 odict = { | |
811 "name": self.tool_name, | |
812 "owner": yuser, | |
813 "type": "unrestricted", | |
814 "description": self.args.tool_desc, | |
815 "synopsis": self.args.tool_desc, | |
816 "category": "TF Generated Tools", | |
817 } | |
818 yaml.dump(odict, yamlf, allow_unicode=True) | |
819 yamlf.close() | |
820 | |
821 def makeTool(self): | |
822 """write xmls and input samples into place""" | |
823 if self.args.parampass == 0: | |
824 self.doNoXMLparam() | |
825 else: | |
826 self.makeXML() | |
827 if self.args.script_path: | |
828 stname = os.path.join(self.tooloutdir, self.sfile) | |
829 if not os.path.exists(stname): | |
830 shutil.copyfile(self.sfile, stname) | |
831 xreal = "%s.xml" % self.tool_name | |
832 xout = os.path.join(self.tooloutdir, xreal) | |
833 shutil.copyfile(xreal, xout) | |
834 for p in self.infiles: | |
835 pth = p["name"] | |
836 dest = os.path.join(self.testdir, "%s_sample" % p["infilename"]) | |
837 shutil.copyfile(pth, dest) | |
838 dest = os.path.join(self.repdir, "%s_sample.%s" % (p["infilename"],p["format"])) | |
839 shutil.copyfile(pth, dest) | |
840 | |
841 def makeToolTar(self, report_fail=False): | |
842 """move outputs into test-data and prepare the tarball""" | |
843 excludeme = "_planemo_test_report.html" | |
844 | |
845 def exclude_function(tarinfo): | |
846 filename = tarinfo.name | |
847 return None if filename.endswith(excludeme) else tarinfo | |
848 | |
849 for p in self.outfiles: | |
850 oname = p["name"] | |
851 tdest = os.path.join(self.testdir, "%s_sample" % oname) | |
852 src = os.path.join(self.testdir, oname) | |
853 if not os.path.isfile(tdest): | |
854 if os.path.isfile(src): | |
855 shutil.copyfile(src, tdest) | |
856 dest = os.path.join(self.repdir, "%s.sample" % (oname)) | |
857 shutil.copyfile(src, dest) | |
858 else: | |
859 if report_fail: | |
860 print( | |
861 "###Tool may have failed - output file %s not found in testdir after planemo run %s." | |
862 % (tdest, self.testdir) | |
863 ) | |
864 tf = tarfile.open(self.newtarpath, "w:gz") | |
865 tf.add( | |
866 name=self.tooloutdir, | |
867 arcname=self.tool_name, | |
868 filter=exclude_function, | |
869 ) | |
870 tf.close() | |
871 shutil.copyfile(self.newtarpath, self.args.new_tool) | |
872 | |
873 def moveRunOutputs(self): | |
874 """need to move planemo or run outputs into toolfactory collection""" | |
875 with os.scandir(self.tooloutdir) as outs: | |
876 for entry in outs: | |
877 if not entry.is_file(): | |
878 continue | |
879 if not entry.name.endswith('.html'): | |
880 _, ext = os.path.splitext(entry.name) | |
881 newname = f"{entry.name.replace('.','_')}.txt" | |
882 dest = os.path.join(self.repdir, newname) | |
883 src = os.path.join(self.tooloutdir, entry.name) | |
884 shutil.copyfile(src, dest) | |
885 if self.args.include_tests: | |
886 with os.scandir(self.testdir) as outs: | |
887 for entry in outs: | |
888 if (not entry.is_file()) or entry.name.endswith( | |
889 "_planemo_test_report.html" | |
890 ): | |
891 continue | |
892 if "." in entry.name: | |
893 _, ext = os.path.splitext(entry.name) | |
894 if ext in [".tgz", ".json"]: | |
895 continue | |
896 if ext in [".yml", ".xml", ".yaml"]: | |
897 newname = f"{entry.name.replace('.','_')}.txt" | |
898 else: | |
899 newname = entry.name | |
900 else: | |
901 newname = f"{entry.name}.txt" | |
902 dest = os.path.join(self.repdir, newname) | |
903 src = os.path.join(self.testdir, entry.name) | |
904 shutil.copyfile(src, dest) | |
905 | |
906 | |
907 def main(): | |
908 """ | |
909 This is a Galaxy wrapper. | |
910 It expects to be called by a special purpose tool.xml | |
911 | |
912 """ | |
913 parser = argparse.ArgumentParser() | |
914 a = parser.add_argument | |
915 a("--script_path", default=None) | |
916 a("--history_test", default=None) | |
917 a("--cl_user_suffix", default=None) | |
918 a("--sysexe", default=None) | |
919 a("--packages", default=None) | |
920 a("--tool_name", default="newtool") | |
921 a("--tool_dir", default=None) | |
922 a("--input_files", default=[], action="append") | |
923 a("--output_files", default=[], action="append") | |
924 a("--user_email", default="Unknown") | |
925 a("--bad_user", default=None) | |
926 a("--help_text", default=None) | |
927 a("--tool_desc", default=None) | |
928 a("--tool_version", default=None) | |
929 a("--citations", default=None) | |
930 a("--command_override", default=None) | |
931 a("--test_override", default=None) | |
932 a("--additional_parameters", action="append", default=[]) | |
933 a("--selecttext_parameters", action="append", default=[]) | |
934 a("--edit_additional_parameters", action="store_true", default=False) | |
935 a("--parampass", default="positional") | |
936 a("--tfout", default="./tfout") | |
937 a("--new_tool", default="new_tool") | |
938 a("--galaxy_root", default="/galaxy-central") | |
939 a("--galaxy_venv", default="/galaxy_venv") | |
940 a("--collection", action="append", default=[]) | |
941 a("--include_tests", default=False, action="store_true") | |
942 a("--admin_only", default=False, action="store_true") | |
943 a("--install", default=False, action="store_true") | |
944 a("--run_test", default=False, action="store_true") | |
945 a("--local_tools", default='tools') # relative to $__root_dir__ | |
946 a("--tool_conf_path", default='config/tool_conf.xml') # relative to $__root_dir__ | |
947 a("--galaxy_url", default="http://localhost:8080") | |
948 a("--toolshed_url", default="http://localhost:9009") | |
949 # make sure this is identical to tool_sheds_conf.xml | |
950 # localhost != 127.0.0.1 so validation fails | |
951 a("--toolshed_api_key", default="fakekey") | |
952 a("--galaxy_api_key", default="8993d65865e6d6d1773c2c34a1cc207d") | |
953 args = parser.parse_args() | |
954 if args.admin_only: | |
955 assert not args.bad_user, ( | |
956 'UNAUTHORISED: %s is NOT authorized to use this tool until Galaxy \ | |
957 admin adds %s to "admin_users" in the galaxy.yml Galaxy configuration file' | |
958 % (args.bad_user, args.bad_user) | |
959 ) | |
960 assert args.tool_name, "## Tool Factory expects a tool name - eg --tool_name=DESeq" | |
961 r = Tool_Factory(args) | |
962 r.writeShedyml() | |
963 r.makeTool() | |
964 r.makeToolTar() | |
965 if args.install: | |
966 #try: | |
967 tcu = Tool_Conf_Updater(args=args, tool_dir=args.local_tools, | |
968 new_tool_archive_path=r.newtarpath, tool_conf_path=args.tool_conf_path, | |
969 new_tool_name=r.tool_name) | |
970 #except Exception: | |
971 # print("### Unable to install the new tool. Are you sure you have all the required special settings?") | |
972 | |
973 if __name__ == "__main__": | |
974 main() | |
975 |