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