| 48 | 1 #!/usr/bin/env python | 
|  | 2 # rgToolFactory.py | 
|  | 3 # see https://github.com/fubar2/toolfactory | 
|  | 4 # | 
|  | 5 # copyright ross lazarus (ross stop lazarus at gmail stop com) May 2012 | 
|  | 6 # | 
|  | 7 # all rights reserved | 
|  | 8 # Licensed under the LGPL | 
| 49 | 9 # suggestions for improvement and bug fixes welcome at | 
|  | 10 # https://github.com/fubar2/toolfactory | 
| 48 | 11 # | 
|  | 12 # July 2020: BCC was fun and I feel like rip van winkle after 5 years. | 
|  | 13 # Decided to | 
|  | 14 # 1. Fix the toolfactory so it works - done for simplest case | 
|  | 15 # 2. Fix planemo so the toolfactory function works | 
|  | 16 # 3. Rewrite bits using galaxyxml functions where that makes sense - done | 
|  | 17 # | 
|  | 18 # removed all the old complications including making the new tool use this same script | 
|  | 19 # galaxyxml now generates the tool xml https://github.com/hexylena/galaxyxml | 
|  | 20 # No support for automatic HTML file creation from arbitrary outputs | 
|  | 21 # essential problem is to create two command lines - one for the tool xml and a different | 
|  | 22 # one to run the executable with the supplied test data and settings | 
|  | 23 # Be simpler to write the tool, then run it with planemo and soak up the test outputs. | 
|  | 24 | 
|  | 25 | 
|  | 26 | 
|  | 27 import argparse | 
|  | 28 import logging | 
|  | 29 import os | 
|  | 30 import re | 
|  | 31 import shutil | 
|  | 32 import subprocess | 
|  | 33 import sys | 
|  | 34 import tarfile | 
|  | 35 import tempfile | 
|  | 36 import time | 
|  | 37 | 
|  | 38 import galaxyxml.tool as gxt | 
|  | 39 import galaxyxml.tool.parameters as gxtp | 
|  | 40 | 
|  | 41 import lxml | 
|  | 42 | 
|  | 43 import yaml | 
|  | 44 | 
|  | 45 myversion = "V2.1 July 2020" | 
|  | 46 verbose = True | 
|  | 47 debug = True | 
|  | 48 toolFactoryURL = "https://github.com/fubar2/toolfactory" | 
|  | 49 ourdelim = "~~~" | 
| 49 | 50 ALOT = 10000000 # srsly. command or test overrides use read() so just in case | 
|  | 51 STDIOXML = """<stdio> | 
|  | 52 <exit_code range="100:" level="debug" description="shite happens" /> | 
|  | 53 </stdio>""" | 
| 48 | 54 | 
|  | 55 # --input_files="$input_files~~~$CL~~~$input_formats~~~$input_label | 
|  | 56 # ~~~$input_help" | 
|  | 57 IPATHPOS = 0 | 
|  | 58 ICLPOS = 1 | 
|  | 59 IFMTPOS = 2 | 
|  | 60 ILABPOS = 3 | 
|  | 61 IHELPOS = 4 | 
|  | 62 IOCLPOS = 5 | 
|  | 63 | 
| 49 | 64 # --output_files "$otab.history_name~~~$otab.history_format~~~$otab.CL~~~otab.history_test | 
| 48 | 65 ONAMEPOS = 0 | 
|  | 66 OFMTPOS = 1 | 
|  | 67 OCLPOS = 2 | 
| 49 | 68 OTESTPOS = 3 | 
|  | 69 OOCLPOS = 4 | 
|  | 70 | 
| 48 | 71 | 
|  | 72 # --additional_parameters="$i.param_name~~~$i.param_value~~~ | 
|  | 73 # $i.param_label~~~$i.param_help~~~$i.param_type~~~$i.CL~~~i$.param_CLoverride" | 
|  | 74 ANAMEPOS = 0 | 
|  | 75 AVALPOS = 1 | 
|  | 76 ALABPOS = 2 | 
|  | 77 AHELPPOS = 3 | 
|  | 78 ATYPEPOS = 4 | 
|  | 79 ACLPOS = 5 | 
|  | 80 AOVERPOS = 6 | 
|  | 81 AOCLPOS = 7 | 
|  | 82 | 
|  | 83 | 
|  | 84 foo = len(lxml.__version__) | 
|  | 85 # fug you, flake8. Say my name! | 
| 49 | 86 FAKEEXE = "~~~REMOVE~~~ME~~~" | 
|  | 87 # need this until a PR/version bump to fix galaxyxml prepending the exe even | 
|  | 88 # with override. | 
|  | 89 | 
| 48 | 90 | 
|  | 91 def timenow(): | 
|  | 92     """return current time as a string | 
|  | 93     """ | 
|  | 94     return time.strftime("%d/%m/%Y %H:%M:%S", time.localtime(time.time())) | 
|  | 95 | 
|  | 96 | 
|  | 97 def quote_non_numeric(s): | 
|  | 98     """return a prequoted string for non-numerics | 
|  | 99     useful for perl and Rscript parameter passing? | 
|  | 100     """ | 
|  | 101     try: | 
|  | 102         _ = float(s) | 
|  | 103         return s | 
|  | 104     except ValueError: | 
|  | 105         return '"%s"' % s | 
|  | 106 | 
|  | 107 | 
|  | 108 html_escape_table = {"&": "&", ">": ">", "<": "<", "$": r"\$"} | 
|  | 109 | 
|  | 110 | 
|  | 111 def html_escape(text): | 
|  | 112     """Produce entities within text.""" | 
|  | 113     return "".join(html_escape_table.get(c, c) for c in text) | 
|  | 114 | 
|  | 115 | 
|  | 116 def html_unescape(text): | 
|  | 117     """Revert entities within text. Multiple character targets so use replace""" | 
|  | 118     t = text.replace("&", "&") | 
|  | 119     t = t.replace(">", ">") | 
|  | 120     t = t.replace("<", "<") | 
|  | 121     t = t.replace("\\$", "$") | 
|  | 122     return t | 
|  | 123 | 
|  | 124 | 
|  | 125 def parse_citations(citations_text): | 
|  | 126     """ | 
|  | 127     """ | 
|  | 128     citations = [c for c in citations_text.split("**ENTRY**") if c.strip()] | 
|  | 129     citation_tuples = [] | 
|  | 130     for citation in citations: | 
|  | 131         if citation.startswith("doi"): | 
|  | 132             citation_tuples.append(("doi", citation[len("doi") :].strip())) | 
|  | 133         else: | 
| 49 | 134             citation_tuples.append(("bibtex", citation[len("bibtex") :].strip())) | 
| 48 | 135     return citation_tuples | 
|  | 136 | 
|  | 137 | 
|  | 138 class ScriptRunner: | 
|  | 139     """Wrapper for an arbitrary script | 
|  | 140     uses galaxyxml | 
|  | 141 | 
|  | 142     """ | 
|  | 143 | 
|  | 144     def __init__(self, args=None): | 
|  | 145         """ | 
|  | 146         prepare command line cl for running the tool here | 
|  | 147         and prepare elements needed for galaxyxml tool generation | 
|  | 148         """ | 
|  | 149         self.infiles = [x.split(ourdelim) for x in args.input_files] | 
|  | 150         self.outfiles = [x.split(ourdelim) for x in args.output_files] | 
|  | 151         self.addpar = [x.split(ourdelim) for x in args.additional_parameters] | 
|  | 152         self.args = args | 
|  | 153         self.cleanuppar() | 
|  | 154         self.lastclredirect = None | 
|  | 155         self.lastxclredirect = None | 
|  | 156         self.cl = [] | 
|  | 157         self.xmlcl = [] | 
|  | 158         self.is_positional = self.args.parampass == "positional" | 
| 49 | 159         if self.args.packages: | 
|  | 160             self.executeme = self.args.packages.split(",")[0].split(":")[0] | 
|  | 161         else: | 
|  | 162             self.executeme = self.args.sysexe | 
|  | 163         assert ( | 
|  | 164             self.executeme is not None | 
|  | 165         ), "No system or managed executable passed in. Cannot build" | 
| 48 | 166         aCL = self.cl.append | 
| 49 | 167         aXCL = self.xmlcl.append | 
| 48 | 168         assert args.parampass in [ | 
|  | 169             "0", | 
|  | 170             "argparse", | 
|  | 171             "positional", | 
| 49 | 172         ], 'args.parampass must be "0","positional" or "argparse"' | 
| 48 | 173         self.tool_name = re.sub("[^a-zA-Z0-9_]+", "", args.tool_name) | 
|  | 174         self.tool_id = self.tool_name | 
|  | 175         self.tool = gxt.Tool( | 
|  | 176             self.args.tool_name, | 
|  | 177             self.tool_id, | 
|  | 178             self.args.tool_version, | 
|  | 179             self.args.tool_desc, | 
| 49 | 180             FAKEEXE, | 
| 48 | 181         ) | 
| 49 | 182         if self.args.script_path: | 
|  | 183             self.tool.interpreter = self.executeme | 
| 48 | 184         self.tooloutdir = "tfout" | 
|  | 185         self.repdir = "TF_run_report_tempdir" | 
|  | 186         self.testdir = os.path.join(self.tooloutdir, "test-data") | 
|  | 187         if not os.path.exists(self.tooloutdir): | 
|  | 188             os.mkdir(self.tooloutdir) | 
|  | 189         if not os.path.exists(self.testdir): | 
|  | 190             os.mkdir(self.testdir)  # make tests directory | 
|  | 191         if not os.path.exists(self.repdir): | 
|  | 192             os.mkdir(self.repdir) | 
|  | 193         self.tinputs = gxtp.Inputs() | 
|  | 194         self.toutputs = gxtp.Outputs() | 
|  | 195         self.testparam = [] | 
| 49 | 196         if self.args.script_path: | 
|  | 197             self.prepScript() | 
|  | 198         if self.args.command_override: | 
|  | 199             scos = open(self.args.command_override, "r").readlines() | 
|  | 200             self.command_override = [x.rstrip() for x in scos] | 
|  | 201         else: | 
|  | 202             self.command_override = None | 
|  | 203         if self.args.test_override: | 
|  | 204             stos = open(self.args.test_override, "r").readlines() | 
|  | 205             self.test_override = [x.rstrip() for x in stos] | 
|  | 206         else: | 
|  | 207             self.test_override = None | 
|  | 208         if self.args.cl_prefix: # DIY CL start | 
|  | 209             clp = self.args.cl_prefix.split(" ") | 
|  | 210             for c in clp: | 
|  | 211                 aCL(c) | 
|  | 212                 aXCL(c) | 
|  | 213         else: | 
|  | 214             if self.args.runmode == "Executable": | 
|  | 215                 if self.args.script_path: | 
|  | 216                     aCL(self.executeme) | 
|  | 217                     aCL(self.sfile) | 
|  | 218                     aXCL("$runme") | 
|  | 219                 else: | 
|  | 220                     aCL(self.executeme)  # this little CL will just run | 
|  | 221                     aXCL(self.executeme) | 
| 48 | 222             else: | 
| 49 | 223                 if self.args.script_path: | 
|  | 224                     aCL(self.executeme) | 
|  | 225                     aCL(self.sfile) | 
|  | 226                     aXCL("$runme") | 
|  | 227                 else: | 
|  | 228                     aCL(self.executeme)  # this little CL will just run | 
|  | 229                     aXCL(self.executeme) | 
|  | 230         self.elog = os.path.join(self.repdir,"%s_error_log.txt" % self.tool_name) | 
|  | 231         self.tlog = os.path.join(self.repdir,"%s_runner_log.txt" % self.tool_name) | 
| 48 | 232 | 
|  | 233         if self.args.parampass == "0": | 
|  | 234             self.clsimple() | 
|  | 235         else: | 
|  | 236             clsuffix = [] | 
|  | 237             xclsuffix = [] | 
|  | 238             for i, p in enumerate(self.infiles): | 
|  | 239                 if p[IOCLPOS] == "STDIN": | 
|  | 240                     appendme = [ | 
|  | 241                         p[IOCLPOS], | 
|  | 242                         p[ICLPOS], | 
|  | 243                         p[IPATHPOS], | 
|  | 244                         "< %s" % p[IPATHPOS], | 
|  | 245                     ] | 
|  | 246                     xappendme = [ | 
|  | 247                         p[IOCLPOS], | 
|  | 248                         p[ICLPOS], | 
|  | 249                         p[IPATHPOS], | 
|  | 250                         "< $%s" % p[ICLPOS], | 
|  | 251                     ] | 
|  | 252                 else: | 
|  | 253                     appendme = [p[IOCLPOS], p[ICLPOS], p[IPATHPOS], ""] | 
|  | 254                     xappendme = [p[IOCLPOS], p[ICLPOS], "$%s" % p[ICLPOS], ""] | 
|  | 255                 clsuffix.append(appendme) | 
|  | 256                 xclsuffix.append(xappendme) | 
|  | 257             for i, p in enumerate(self.outfiles): | 
|  | 258                 if p[OOCLPOS] == "STDOUT": | 
|  | 259                     self.lastclredirect = [">", p[ONAMEPOS]] | 
|  | 260                     self.lastxclredirect = [">", "$%s" % p[OCLPOS]] | 
|  | 261                 else: | 
|  | 262                     clsuffix.append([p[OOCLPOS], p[OCLPOS], p[ONAMEPOS], ""]) | 
| 49 | 263                     xclsuffix.append([p[OOCLPOS], p[OCLPOS], "$%s" % p[ONAMEPOS], ""]) | 
| 48 | 264             for p in self.addpar: | 
| 49 | 265                 clsuffix.append([p[AOCLPOS], p[ACLPOS], p[AVALPOS], p[AOVERPOS]]) | 
| 48 | 266                 xclsuffix.append( | 
|  | 267                     [p[AOCLPOS], p[ACLPOS], '"$%s"' % p[ANAMEPOS], p[AOVERPOS]] | 
|  | 268                 ) | 
|  | 269             clsuffix.sort() | 
|  | 270             xclsuffix.sort() | 
|  | 271             self.xclsuffix = xclsuffix | 
|  | 272             self.clsuffix = clsuffix | 
|  | 273             if self.args.parampass == "positional": | 
|  | 274                 self.clpositional() | 
|  | 275             else: | 
|  | 276                 self.clargparse() | 
|  | 277 | 
|  | 278     def prepScript(self): | 
|  | 279         rx = open(self.args.script_path, "r").readlines() | 
|  | 280         rx = [x.rstrip() for x in rx] | 
|  | 281         rxcheck = [x.strip() for x in rx if x.strip() > ""] | 
|  | 282         assert len(rxcheck) > 0, "Supplied script is empty. Cannot run" | 
|  | 283         self.script = "\n".join(rx) | 
|  | 284         fhandle, self.sfile = tempfile.mkstemp( | 
| 49 | 285             prefix=self.tool_name, suffix="_%s" % (self.executeme) | 
| 48 | 286         ) | 
|  | 287         tscript = open(self.sfile, "w") | 
|  | 288         tscript.write(self.script) | 
|  | 289         tscript.close() | 
| 49 | 290         self.indentedScript = "  %s" % "\n".join([" %s" % html_escape(x) for x in rx]) | 
|  | 291         self.escapedScript = "%s" % "\n".join([" %s" % html_escape(x) for x in rx]) | 
|  | 292         art = "%s.%s" % (self.tool_name, self.executeme) | 
| 48 | 293         artifact = open(art, "wb") | 
|  | 294         artifact.write(bytes(self.script, "utf8")) | 
|  | 295         artifact.close() | 
|  | 296 | 
|  | 297     def cleanuppar(self): | 
|  | 298         """ positional parameters are complicated by their numeric ordinal""" | 
|  | 299         for i, p in enumerate(self.infiles): | 
|  | 300             if self.args.parampass == "positional": | 
|  | 301                 assert p[ICLPOS].isdigit(), ( | 
|  | 302                     "Positional parameters must be ordinal integers - got %s for %s" | 
|  | 303                     % (p[ICLPOS], p[ILABPOS]) | 
|  | 304                 ) | 
|  | 305             p.append(p[ICLPOS]) | 
|  | 306             if p[ICLPOS].isdigit() or self.args.parampass == "0": | 
|  | 307                 scl = "input%d" % (i + 1) | 
|  | 308                 p[ICLPOS] = scl | 
|  | 309             self.infiles[i] = p | 
|  | 310         for i, p in enumerate( | 
|  | 311             self.outfiles | 
|  | 312         ):  # trying to automagically gather using extensions | 
|  | 313             if self.args.parampass == "positional" and p[OCLPOS] != "STDOUT": | 
|  | 314                 assert p[OCLPOS].isdigit(), ( | 
|  | 315                     "Positional parameters must be ordinal integers - got %s for %s" | 
|  | 316                     % (p[OCLPOS], p[ONAMEPOS]) | 
|  | 317                 ) | 
|  | 318             p.append(p[OCLPOS]) | 
|  | 319             if p[OCLPOS].isdigit() or p[OCLPOS] == "STDOUT": | 
|  | 320                 scl = p[ONAMEPOS] | 
|  | 321                 p[OCLPOS] = scl | 
|  | 322             self.outfiles[i] = p | 
|  | 323         for i, p in enumerate(self.addpar): | 
|  | 324             if self.args.parampass == "positional": | 
|  | 325                 assert p[ACLPOS].isdigit(), ( | 
|  | 326                     "Positional parameters must be ordinal integers - got %s for %s" | 
|  | 327                     % (p[ACLPOS], p[ANAMEPOS]) | 
|  | 328                 ) | 
|  | 329             p.append(p[ACLPOS]) | 
|  | 330             if p[ACLPOS].isdigit(): | 
|  | 331                 scl = "input%s" % p[ACLPOS] | 
|  | 332                 p[ACLPOS] = scl | 
|  | 333             self.addpar[i] = p | 
|  | 334 | 
|  | 335     def clsimple(self): | 
|  | 336         """ no parameters - uses < and > for i/o | 
|  | 337         """ | 
|  | 338         aCL = self.cl.append | 
|  | 339         aCL("<") | 
|  | 340         aCL(self.infiles[0][IPATHPOS]) | 
|  | 341         aCL(">") | 
|  | 342         aCL(self.outfiles[0][OCLPOS]) | 
|  | 343         aXCL = self.xmlcl.append | 
|  | 344         aXCL("<") | 
|  | 345         aXCL("$%s" % self.infiles[0][ICLPOS]) | 
|  | 346         aXCL(">") | 
|  | 347         aXCL("$%s" % self.outfiles[0][ONAMEPOS]) | 
|  | 348 | 
|  | 349     def clpositional(self): | 
|  | 350         # inputs in order then params | 
|  | 351         aCL = self.cl.append | 
|  | 352         for (o_v, k, v, koverride) in self.clsuffix: | 
|  | 353             if " " in v: | 
|  | 354                 aCL("%s" % v) | 
|  | 355             else: | 
|  | 356                 aCL(v) | 
|  | 357         aXCL = self.xmlcl.append | 
|  | 358         for (o_v, k, v, koverride) in self.xclsuffix: | 
|  | 359             aXCL(v) | 
|  | 360         if self.lastxclredirect: | 
|  | 361             aXCL(self.lastxclredirect[0]) | 
|  | 362             aXCL(self.lastxclredirect[1]) | 
|  | 363 | 
|  | 364     def clargparse(self): | 
|  | 365         """ argparse style | 
|  | 366         """ | 
|  | 367         aCL = self.cl.append | 
|  | 368         aXCL = self.xmlcl.append | 
|  | 369         # inputs then params in argparse named form | 
|  | 370         for (o_v, k, v, koverride) in self.xclsuffix: | 
|  | 371             if koverride > "": | 
|  | 372                 k = koverride | 
|  | 373             elif len(k.strip()) == 1: | 
|  | 374                 k = "-%s" % k | 
|  | 375             else: | 
|  | 376                 k = "--%s" % k | 
|  | 377             aXCL(k) | 
|  | 378             aXCL(v) | 
|  | 379         for (o_v, k, v, koverride) in self.clsuffix: | 
|  | 380             if koverride > "": | 
|  | 381                 k = koverride | 
|  | 382             elif len(k.strip()) == 1: | 
|  | 383                 k = "-%s" % k | 
|  | 384             else: | 
|  | 385                 k = "--%s" % k | 
|  | 386             aCL(k) | 
|  | 387             aCL(v) | 
|  | 388 | 
|  | 389     def getNdash(self, newname): | 
|  | 390         if self.is_positional: | 
|  | 391             ndash = 0 | 
|  | 392         else: | 
|  | 393             ndash = 2 | 
|  | 394             if len(newname) < 2: | 
|  | 395                 ndash = 1 | 
|  | 396         return ndash | 
|  | 397 | 
|  | 398     def doXMLparam(self): | 
|  | 399         """flake8 made me do this...""" | 
|  | 400         for p in self.outfiles: | 
| 49 | 401             newname, newfmt, newcl, test, oldcl = p | 
| 48 | 402             ndash = self.getNdash(newcl) | 
|  | 403             aparm = gxtp.OutputData(newcl, format=newfmt, num_dashes=ndash) | 
|  | 404             aparm.positional = self.is_positional | 
|  | 405             if self.is_positional: | 
|  | 406                 if oldcl == "STDOUT": | 
|  | 407                     aparm.positional = 9999999 | 
|  | 408                     aparm.command_line_override = "> $%s" % newcl | 
|  | 409                 else: | 
|  | 410                     aparm.positional = int(oldcl) | 
|  | 411                     aparm.command_line_override = "$%s" % newcl | 
|  | 412             self.toutputs.append(aparm) | 
| 49 | 413             usetest = None | 
|  | 414             ld = None | 
|  | 415             if test > '': | 
|  | 416                 if test.startswith('diff'): | 
|  | 417                     usetest = 'diff' | 
|  | 418                     if test.split(':')[1].isdigit: | 
|  | 419                         ld = int(test.split(':')[1]) | 
|  | 420                 else: | 
|  | 421                     usetest = test | 
|  | 422             tp = gxtp.TestOutput(name=newcl, value="%s_sample" % newcl, format=newfmt, | 
|  | 423                 compare=usetest, lines_diff=ld, delta=None,) | 
| 48 | 424             self.testparam.append(tp) | 
|  | 425         for p in self.infiles: | 
|  | 426             newname = p[ICLPOS] | 
|  | 427             newfmt = p[IFMTPOS] | 
|  | 428             ndash = self.getNdash(newname) | 
|  | 429             if not len(p[ILABPOS]) > 0: | 
|  | 430                 alab = p[ICLPOS] | 
|  | 431             else: | 
|  | 432                 alab = p[ILABPOS] | 
|  | 433             aninput = gxtp.DataParam( | 
|  | 434                 newname, | 
|  | 435                 optional=False, | 
|  | 436                 label=alab, | 
|  | 437                 help=p[IHELPOS], | 
|  | 438                 format=newfmt, | 
|  | 439                 multiple=False, | 
|  | 440                 num_dashes=ndash, | 
|  | 441             ) | 
|  | 442             aninput.positional = self.is_positional | 
|  | 443             self.tinputs.append(aninput) | 
|  | 444             tparm = gxtp.TestParam(name=newname, value="%s_sample" % newname) | 
|  | 445             self.testparam.append(tparm) | 
|  | 446         for p in self.addpar: | 
|  | 447             newname, newval, newlabel, newhelp, newtype, newcl, override, oldcl = p | 
|  | 448             if not len(newlabel) > 0: | 
|  | 449                 newlabel = newname | 
|  | 450             ndash = self.getNdash(newname) | 
|  | 451             if newtype == "text": | 
|  | 452                 aparm = gxtp.TextParam( | 
|  | 453                     newname, | 
|  | 454                     label=newlabel, | 
|  | 455                     help=newhelp, | 
|  | 456                     value=newval, | 
|  | 457                     num_dashes=ndash, | 
|  | 458                 ) | 
|  | 459             elif newtype == "integer": | 
|  | 460                 aparm = gxtp.IntegerParam( | 
|  | 461                     newname, | 
|  | 462                     label=newname, | 
|  | 463                     help=newhelp, | 
|  | 464                     value=newval, | 
|  | 465                     num_dashes=ndash, | 
|  | 466                 ) | 
|  | 467             elif newtype == "float": | 
|  | 468                 aparm = gxtp.FloatParam( | 
|  | 469                     newname, | 
|  | 470                     label=newname, | 
|  | 471                     help=newhelp, | 
|  | 472                     value=newval, | 
|  | 473                     num_dashes=ndash, | 
|  | 474                 ) | 
|  | 475             else: | 
|  | 476                 raise ValueError( | 
|  | 477                     'Unrecognised parameter type "%s" for\ | 
|  | 478                  additional parameter %s in makeXML' | 
|  | 479                     % (newtype, newname) | 
|  | 480                 ) | 
|  | 481             aparm.positional = self.is_positional | 
|  | 482             if self.is_positional: | 
|  | 483                 aninput.positional = int(oldcl) | 
|  | 484             self.tinputs.append(aparm) | 
|  | 485             self.tparm = gxtp.TestParam(newname, value=newval) | 
|  | 486             self.testparam.append(tparm) | 
|  | 487 | 
|  | 488     def doNoXMLparam(self): | 
| 49 | 489         """filter style package - stdin to stdout""" | 
| 48 | 490         alab = self.infiles[0][ILABPOS] | 
|  | 491         if len(alab) == 0: | 
|  | 492             alab = self.infiles[0][ICLPOS] | 
|  | 493         max1s = ( | 
| 49 | 494             "Maximum one input if parampass is 0 but multiple input files supplied - %s" | 
| 48 | 495             % str(self.infiles) | 
|  | 496         ) | 
|  | 497         assert len(self.infiles) == 1, max1s | 
|  | 498         newname = self.infiles[0][ICLPOS] | 
|  | 499         aninput = gxtp.DataParam( | 
|  | 500             newname, | 
|  | 501             optional=False, | 
|  | 502             label=alab, | 
|  | 503             help=self.infiles[0][IHELPOS], | 
|  | 504             format=self.infiles[0][IFMTPOS], | 
|  | 505             multiple=False, | 
|  | 506             num_dashes=0, | 
|  | 507         ) | 
|  | 508         aninput.command_line_override = "< $%s" % newname | 
|  | 509         aninput.positional = self.is_positional | 
|  | 510         self.tinputs.append(aninput) | 
|  | 511         tp = gxtp.TestParam(name=newname, value="%s_sample" % newname) | 
|  | 512         self.testparam.append(tp) | 
|  | 513         newname = self.outfiles[0][OCLPOS] | 
|  | 514         newfmt = self.outfiles[0][OFMTPOS] | 
|  | 515         anout = gxtp.OutputData(newname, format=newfmt, num_dashes=0) | 
|  | 516         anout.command_line_override = "> $%s" % newname | 
|  | 517         anout.positional = self.is_positional | 
|  | 518         self.toutputs.append(anout) | 
| 49 | 519         tp = gxtp.TestOutput(name=newname, value="%s_sample" % newname, format=newfmt) | 
| 48 | 520         self.testparam.append(tp) | 
|  | 521 | 
|  | 522     def makeXML(self): | 
|  | 523         """ | 
|  | 524         Create a Galaxy xml tool wrapper for the new script | 
|  | 525         Uses galaxyhtml | 
|  | 526         Hmmm. How to get the command line into correct order... | 
|  | 527         """ | 
| 49 | 528         if self.command_override: | 
|  | 529             self.tool.command_line_override = self.command_override # config file | 
| 48 | 530         else: | 
|  | 531             self.tool.command_line_override = self.xmlcl | 
| 49 | 532         # if self.args.interpreter_name: | 
|  | 533         #    self.tool.interpreter = self.args.interpreter_name | 
| 48 | 534         if self.args.help_text: | 
|  | 535             helptext = open(self.args.help_text, "r").readlines() | 
|  | 536             helptext = [html_escape(x) for x in helptext] | 
|  | 537             self.tool.help = "".join([x for x in helptext]) | 
|  | 538         else: | 
|  | 539             self.tool.help = ( | 
|  | 540                 "Please ask the tool author (%s) for help \ | 
|  | 541               as none was supplied at tool generation\n" | 
|  | 542                 % (self.args.user_email) | 
|  | 543             ) | 
|  | 544         self.tool.version_command = None  # do not want | 
|  | 545         requirements = gxtp.Requirements() | 
| 49 | 546         if self.args.packages: | 
|  | 547             for d in self.args.packages.split(","): | 
|  | 548                 if ":" in d: | 
|  | 549                     packg, ver = d.split(":") | 
|  | 550                 else: | 
|  | 551                     packg = d | 
|  | 552                     ver = "" | 
|  | 553                 requirements.append(gxtp.Requirement("package", packg.strip(), ver.strip())) | 
| 48 | 554         self.tool.requirements = requirements | 
|  | 555         if self.args.parampass == "0": | 
|  | 556             self.doNoXMLparam() | 
|  | 557         else: | 
|  | 558             self.doXMLparam() | 
|  | 559         self.tool.outputs = self.toutputs | 
|  | 560         self.tool.inputs = self.tinputs | 
| 49 | 561         if ( | 
|  | 562             self.args.script_path | 
|  | 563         ): | 
| 48 | 564             configfiles = gxtp.Configfiles() | 
| 49 | 565             configfiles.append(gxtp.Configfile(name="runme", text=self.script)) | 
| 48 | 566             self.tool.configfiles = configfiles | 
|  | 567         tests = gxtp.Tests() | 
|  | 568         test_a = gxtp.Test() | 
|  | 569         for tp in self.testparam: | 
|  | 570             test_a.append(tp) | 
|  | 571         tests.append(test_a) | 
|  | 572         self.tool.tests = tests | 
|  | 573         self.tool.add_comment( | 
|  | 574             "Created by %s at %s using the Galaxy Tool Factory." | 
|  | 575             % (self.args.user_email, timenow()) | 
|  | 576         ) | 
|  | 577         self.tool.add_comment("Source in git at: %s" % (toolFactoryURL)) | 
|  | 578         self.tool.add_comment( | 
|  | 579             "Cite: Creating re-usable tools from scripts doi: \ | 
|  | 580             10.1093/bioinformatics/bts573" | 
|  | 581         ) | 
| 49 | 582         exml0 = self.tool.export() | 
|  | 583         exml = exml0.replace(FAKEEXE, "")  # temporary work around until PR accepted | 
|  | 584         if self.test_override: # cannot do this inside galaxyxml as it expects lxml objects for tests | 
|  | 585             part1 = exml.split('<tests>')[0] | 
|  | 586             part2 = exml.split('</tests>')[1] | 
|  | 587             fixed = '%s\n%s\n%s' % (part1,self.test_override,part2) | 
|  | 588             exml = fixed | 
|  | 589         xf = open("%s.xml" % self.tool_name, "w") | 
| 48 | 590         xf.write(exml) | 
|  | 591         xf.write("\n") | 
|  | 592         xf.close() | 
|  | 593         # ready for the tarball | 
|  | 594 | 
| 49 | 595     def makeTool(self): | 
|  | 596         """write xmls and samples into place | 
| 48 | 597         """ | 
|  | 598         self.makeXML() | 
| 49 | 599         if self.args.script_path: | 
|  | 600             stname = os.path.join(self.tooloutdir, "%s" % (self.sfile)) | 
|  | 601             if not os.path.exists(stname): | 
|  | 602                 shutil.copyfile(self.sfile, stname) | 
|  | 603         xreal = "%s.xml" % self.tool_name | 
|  | 604         xout = os.path.join(self.tooloutdir, xreal) | 
|  | 605         shutil.copyfile(xreal, xout) | 
| 48 | 606         for p in self.infiles: | 
|  | 607             pth = p[IPATHPOS] | 
|  | 608             dest = os.path.join(self.testdir, "%s_sample" % p[ICLPOS]) | 
|  | 609             shutil.copyfile(pth, dest) | 
| 49 | 610 | 
|  | 611     def makeToolTar(self): | 
| 48 | 612         self.newtarpath = "toolfactory_%s.tgz" % self.tool_name | 
|  | 613         tf = tarfile.open(self.newtarpath, "w:gz") | 
|  | 614         tf.add(name=self.tooloutdir, arcname=self.tool_name) | 
|  | 615         tf.close() | 
|  | 616         shutil.copyfile(self.newtarpath, self.args.new_tool) | 
| 49 | 617         if os.path.exists(self.tlog) and os.stat(self.tlog).st_size > 0: | 
|  | 618             shutil.copyfile( | 
|  | 619                 self.tlog, os.path.join(self.tooloutdir, "test1_log_outfiletxt") | 
|  | 620             ) | 
|  | 621 | 
|  | 622     def moveRunOutputs(self): | 
|  | 623         """need to move files into toolfactory collection after any run - planemo or not | 
|  | 624         """ | 
|  | 625         for p in self.outfiles: | 
|  | 626             naym = p[ONAMEPOS] | 
|  | 627             src = os.path.join(self.tooloutdir,naym) | 
|  | 628             if os.path.isfile(src): | 
|  | 629                 dest = os.path.join(self.testdir, "%s_sample" % naym) | 
|  | 630                 shutil.copyfile(naym, dest) | 
|  | 631             else: | 
|  | 632                 print('### problem - output file %s not found in tooloutdir %s' % (src,self.tooloutdir)) | 
|  | 633         with os.scandir(self.tooloutdir) as outs: | 
| 48 | 634             for entry in outs: | 
| 49 | 635                 if not entry.is_file() or entry.name.startswith('.'): | 
| 48 | 636                     continue | 
| 49 | 637                 if "." in entry.name: | 
|  | 638                     nayme,ext = os.path.splitext(entry.name) | 
|  | 639                 else: | 
|  | 640                     ext = ".txt" | 
|  | 641                 ofn = "%s%s" % (entry.name.replace(".", "_"), ext) | 
|  | 642                 dest = os.path.join(self.repdir, ofn) | 
|  | 643                 src = os.path.join(self.tooloutdir,entry.name) | 
|  | 644                 shutil.copyfile(src, dest) | 
| 48 | 645 | 
|  | 646     def run(self): | 
|  | 647         """ | 
|  | 648 | 
|  | 649         """ | 
|  | 650         s = "run cl=%s" % str(self.cl) | 
|  | 651 | 
|  | 652         logging.debug(s) | 
|  | 653         scl = " ".join(self.cl) | 
|  | 654         err = None | 
|  | 655         if self.args.parampass != "0": | 
|  | 656             ste = open(self.elog, "wb") | 
|  | 657             if self.lastclredirect: | 
| 49 | 658                 sto = open(self.lastclredirect[1], "wb")  # is name of an output file | 
| 48 | 659             else: | 
|  | 660                 sto = open(self.tlog, "wb") | 
|  | 661                 sto.write( | 
|  | 662                     bytes( | 
| 49 | 663                         "## Executing Toolfactory generated command line = %s\n" % scl, | 
| 48 | 664                         "utf8", | 
|  | 665                     ) | 
|  | 666                 ) | 
|  | 667             sto.flush() | 
|  | 668             p = subprocess.run(self.cl, shell=False, stdout=sto, stderr=ste) | 
|  | 669             sto.close() | 
|  | 670             ste.close() | 
|  | 671             tmp_stderr = open(self.elog, "rb") | 
|  | 672             err = "" | 
|  | 673             buffsize = 1048576 | 
|  | 674             try: | 
|  | 675                 while True: | 
|  | 676                     err += str(tmp_stderr.read(buffsize)) | 
|  | 677                     if not err or len(err) % buffsize != 0: | 
|  | 678                         break | 
|  | 679             except OverflowError: | 
|  | 680                 pass | 
|  | 681             tmp_stderr.close() | 
|  | 682             retval = p.returncode | 
| 49 | 683         else:  # work around special case - stdin and write to stdout | 
| 48 | 684             sti = open(self.infiles[0][IPATHPOS], "rb") | 
|  | 685             sto = open(self.outfiles[0][ONAMEPOS], "wb") | 
|  | 686             # must use shell to redirect | 
|  | 687             p = subprocess.run(self.cl, shell=False, stdout=sto, stdin=sti) | 
|  | 688             retval = p.returncode | 
|  | 689             sto.close() | 
|  | 690             sti.close() | 
|  | 691         if os.path.isfile(self.tlog) and os.stat(self.tlog).st_size == 0: | 
|  | 692             os.unlink(self.tlog) | 
|  | 693         if os.path.isfile(self.elog) and os.stat(self.elog).st_size == 0: | 
|  | 694             os.unlink(self.elog) | 
|  | 695         if retval != 0 and err:  # problem | 
|  | 696             sys.stderr.write(err) | 
|  | 697         logging.debug("run done") | 
|  | 698         return retval | 
|  | 699 | 
|  | 700     def planemo_shedload(self): | 
|  | 701         """ | 
|  | 702         planemo shed_create --shed_target testtoolshed | 
|  | 703         planemo shed_update --check_diff --shed_target testtoolshed | 
|  | 704         """ | 
| 49 | 705         if os.path.exists(self.tlog): | 
|  | 706             tout = open(self.tlog,'a') | 
|  | 707         else: | 
|  | 708             tout = open(self.tlog,'w') | 
|  | 709         cll = ["planemo", "shed_create", "--shed_target", "local"] | 
|  | 710         try: | 
|  | 711             p = subprocess.run(cll, shell=False, cwd=self.tooloutdir, stdout=tout, stderr = tout) | 
|  | 712         except: | 
|  | 713             pass | 
| 48 | 714         if p.returncode != 0: | 
| 49 | 715             print("Repository %s exists" % self.args.tool_name) | 
| 48 | 716         else: | 
| 49 | 717             print("initiated %s" % self.args.tool_name) | 
|  | 718         cll = [ | 
|  | 719             "planemo", | 
|  | 720             "shed_upload", | 
|  | 721             "--shed_target", | 
|  | 722             "local", | 
|  | 723             "--owner", | 
|  | 724             "fubar", | 
|  | 725             "--name", | 
|  | 726             self.args.tool_name, | 
|  | 727             "--shed_key", | 
|  | 728             self.args.toolshed_api_key, | 
|  | 729             "--tar", | 
|  | 730             self.newtarpath, | 
|  | 731         ] | 
|  | 732         print("Run", " ".join(cll)) | 
|  | 733         p = subprocess.run(cll, shell=False) | 
|  | 734         print("Ran", " ".join(cll), "got", p.returncode) | 
|  | 735         tout.close() | 
| 48 | 736         return p.returncode | 
|  | 737 | 
| 49 | 738     def planemo_test(self, genoutputs=True): | 
| 48 | 739         """planemo is a requirement so is available | 
|  | 740         """ | 
| 49 | 741         xreal = "%s.xml" % self.tool_name | 
|  | 742         if os.path.exists(self.tlog): | 
|  | 743             tout = open(self.tlog,'a') | 
|  | 744         else: | 
|  | 745             tout = open(self.tlog,'w') | 
|  | 746         if genoutputs: | 
|  | 747             cll = [ | 
|  | 748                 "planemo", | 
|  | 749                 "test", | 
|  | 750                 "--galaxy_root", | 
|  | 751                 self.args.galaxy_root, | 
|  | 752                 "--update_test_data", | 
|  | 753                 xreal, | 
|  | 754             ] | 
|  | 755         else: | 
|  | 756             cll = ["planemo", "test", "--galaxy_root", self.args.galaxy_root, xreal] | 
|  | 757         try: | 
|  | 758             p = subprocess.run(cll, shell=False, cwd=self.tooloutdir, stderr=tout, stdout=tout) | 
|  | 759         except: | 
|  | 760             pass | 
|  | 761         tout.close() | 
| 48 | 762         return p.returncode | 
|  | 763 | 
|  | 764     def eph_galaxy_load(self): | 
|  | 765         """ | 
|  | 766         """ | 
| 49 | 767         if os.path.exists(self.tlog): | 
|  | 768             tout = open(self.tlog,'a') | 
|  | 769         else: | 
|  | 770             tout = open(self.tlog,'w') | 
|  | 771         cll = [ | 
|  | 772             "shed-tools", | 
|  | 773             "install", | 
|  | 774             "-g", | 
|  | 775             self.args.galaxy_url, | 
|  | 776             "--latest", | 
|  | 777             "-a", | 
|  | 778             self.args.galaxy_api_key, | 
|  | 779             "--name", | 
|  | 780             self.args.tool_name, | 
|  | 781             "--owner", | 
|  | 782             "fubar", | 
|  | 783             "--toolshed", | 
|  | 784             self.args.toolshed_url, | 
|  | 785             "--section_label", | 
|  | 786             "Generated Tools", | 
|  | 787             "--install_tool_dependencies", | 
|  | 788         ] | 
|  | 789         print("running\n", " ".join(cll)) | 
|  | 790         p = subprocess.run(cll, shell=False, stderr=tout, stdout=tout) | 
| 48 | 791         if p.returncode != 0: | 
| 49 | 792             print( | 
|  | 793                 "Repository %s installation returned %d" | 
|  | 794                 % (self.args.tool_name, p.returncode) | 
|  | 795             ) | 
| 48 | 796         else: | 
| 49 | 797             print("installed %s" % self.args.tool_name) | 
|  | 798         tout.close() | 
| 48 | 799         return p.returncode | 
|  | 800 | 
|  | 801     def writeShedyml(self): | 
| 49 | 802         yuser = self.args.user_email.split("@")[0] | 
|  | 803         yfname = os.path.join(self.tooloutdir, ".shed.yml") | 
|  | 804         yamlf = open(yfname, "w") | 
|  | 805         odict = { | 
|  | 806             "name": self.tool_name, | 
|  | 807             "owner": yuser, | 
|  | 808             "type": "unrestricted", | 
|  | 809             "description": self.args.tool_desc, | 
|  | 810         } | 
| 48 | 811         yaml.dump(odict, yamlf, allow_unicode=True) | 
|  | 812         yamlf.close() | 
|  | 813 | 
| 49 | 814 | 
|  | 815     def install_load(self): | 
|  | 816         _ = self.planemo_test(genoutputs=True) | 
|  | 817         testres = self.planemo_test(genoutputs=False) | 
|  | 818         if testres == 0: | 
|  | 819             if self.args.make_Tool == "install": | 
|  | 820                 self.planemo_shedload() | 
|  | 821                 self.eph_galaxy_load() | 
|  | 822         else: | 
|  | 823             os.stderr.write( | 
|  | 824                 "Planemo test failed - tool %s was not installed" % self.args.tool_name | 
|  | 825             ) | 
|  | 826 | 
| 48 | 827 def main(): | 
|  | 828     """ | 
|  | 829     This is a Galaxy wrapper. It expects to be called by a special purpose tool.xml as: | 
| 49 | 830     <command interpreter="python">rgBaseScriptWrapper.py --script_path "$scriptPath" | 
|  | 831     --tool_name "foo" --interpreter "Rscript" | 
| 48 | 832     </command> | 
|  | 833     """ | 
|  | 834     parser = argparse.ArgumentParser() | 
|  | 835     a = parser.add_argument | 
| 49 | 836     a("--script_path", default=None) | 
|  | 837     a("--history_test", default=None) | 
|  | 838     a("--cl_prefix", default=None) | 
|  | 839     a("--sysexe", default=None) | 
|  | 840     a("--packages", default=None) | 
| 48 | 841     a("--tool_name", default=None) | 
|  | 842     a("--input_files", default=[], action="append") | 
|  | 843     a("--output_files", default=[], action="append") | 
|  | 844     a("--user_email", default="Unknown") | 
|  | 845     a("--bad_user", default=None) | 
| 49 | 846     a("--make_Tool", default="runonly") | 
| 48 | 847     a("--help_text", default=None) | 
|  | 848     a("--tool_desc", default=None) | 
|  | 849     a("--tool_version", default=None) | 
|  | 850     a("--citations", default=None) | 
| 49 | 851     a("--command_override", default=None) | 
|  | 852     a("--test_override", default=None) | 
| 48 | 853     a("--additional_parameters", action="append", default=[]) | 
|  | 854     a("--edit_additional_parameters", action="store_true", default=False) | 
|  | 855     a("--parampass", default="positional") | 
|  | 856     a("--tfout", default="./tfout") | 
|  | 857     a("--new_tool", default="new_tool") | 
|  | 858     a("--runmode", default=None) | 
| 49 | 859     a("--galaxy_url", default="http://localhost:8080") | 
|  | 860     a("--galaxy_api_key", default="fbdd3c2eecd191e88939fffc02eeeaf8") | 
| 48 | 861     a("--toolshed_url", default="http://localhost:9009") | 
| 49 | 862     a("--toolshed_api_key", default="d46e5ed0e242ed52c6e1f506b5d7f9f7") | 
|  | 863     a("--galaxy_root", default="/home/ross/galaxy") | 
| 48 | 864 | 
|  | 865     args = parser.parse_args() | 
|  | 866     assert not args.bad_user, ( | 
|  | 867         'UNAUTHORISED: %s is NOT authorized to use this tool until Galaxy admin adds %s to "admin_users" in the Galaxy configuration file' | 
|  | 868         % (args.bad_user, args.bad_user) | 
|  | 869     ) | 
| 49 | 870     assert args.tool_name, "## Tool Factory expects a tool name - eg --tool_name=DESeq" | 
| 48 | 871     assert ( | 
| 49 | 872         args.sysexe or args.packages | 
| 48 | 873     ), "## Tool Factory wrapper expects an interpreter or an executable package" | 
| 49 | 874     args.input_files = [x.replace('"', "").replace("'", "") for x in args.input_files] | 
| 48 | 875     # remove quotes we need to deal with spaces in CL params | 
|  | 876     for i, x in enumerate(args.additional_parameters): | 
| 49 | 877         args.additional_parameters[i] = args.additional_parameters[i].replace('"', "") | 
| 48 | 878     r = ScriptRunner(args) | 
| 49 | 879     r.writeShedyml() | 
|  | 880     r.makeTool() | 
|  | 881     if args.make_Tool == "runonly": | 
|  | 882         retcode = r.run() | 
|  | 883         if retcode: | 
|  | 884             sys.stderr.write( | 
|  | 885                 "## Run failed with return code %d. Cannot build yet. Please fix and retry" | 
|  | 886                 % retcode | 
|  | 887             ) | 
|  | 888             sys.exit(1) | 
|  | 889         else: | 
|  | 890             r.moveRunOutputs() | 
|  | 891     elif args.make_Tool in ["gentestinstall", "generate", "gentest"]: | 
|  | 892         retcode = r.run() | 
|  | 893         if retcode: | 
|  | 894             sys.stderr.write( | 
|  | 895                 "## Run failed with return code %d. Cannot build yet. Please fix and retry" | 
|  | 896                 % retcode | 
|  | 897             ) | 
|  | 898             sys.exit(1) | 
|  | 899         r.moveRunOutputs() | 
|  | 900         r.makeToolTar() | 
|  | 901         if args.make_Tool in ["gentestinstall","gentest"]: | 
|  | 902             r.planemo_test(genoutputs=False) | 
|  | 903             r.moveRunOutputs() | 
|  | 904             r.planemo_shedload() | 
|  | 905             r.eph_galaxy_load() | 
| 48 | 906     else: | 
| 49 | 907         retcode = r.planemo_test(genoutputs=True)  # this fails :( | 
|  | 908         r.moveRunOutputs() | 
|  | 909         r.makeToolTar() | 
|  | 910         retcode = r.planemo_test(genoutputs=False) | 
|  | 911         r.moveRunOutputs() | 
|  | 912         if args.make_Tool == "planemotestinstall": | 
|  | 913             r.planemo_shedload() | 
|  | 914             r.eph_galaxy_load() | 
|  | 915         # if retcode: | 
| 48 | 916         # sys.exit(retcode)  # indicate failure to job runner | 
|  | 917 | 
|  | 918 | 
|  | 919 if __name__ == "__main__": | 
|  | 920     main() |