| 43 | 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 | 
|  | 9 # suggestions for improvement and bug fixes welcome at https://github.com/fubar2/toolfactory | 
|  | 10 # | 
|  | 11 # July 2020: BCC was fun and I feel like rip van winkle after 5 years. | 
|  | 12 # Decided to | 
|  | 13 # 1. Fix the toolfactory so it works - done for simplest case | 
|  | 14 # 2. Fix planemo so the toolfactory function works | 
|  | 15 # 3. Rewrite bits using galaxyxml functions where that makes sense - done | 
|  | 16 # | 
|  | 17 # removed all the old complications including making the new tool use this same script | 
|  | 18 # galaxyxml now generates the tool xml https://github.com/hexylena/galaxyxml | 
|  | 19 # No support for automatic HTML file creation from arbitrary outputs | 
|  | 20 # essential problem is to create two command lines - one for the tool xml and a different | 
|  | 21 # one to run the executable with the supplied test data and settings | 
|  | 22 # Be simpler to write the tool, then run it with planemo and soak up the test outputs. | 
|  | 23 | 
|  | 24 | 
|  | 25 | 
|  | 26 import argparse | 
|  | 27 import logging | 
|  | 28 import os | 
|  | 29 import re | 
|  | 30 import shutil | 
|  | 31 import subprocess | 
|  | 32 import sys | 
|  | 33 import tarfile | 
|  | 34 import tempfile | 
|  | 35 import time | 
|  | 36 | 
|  | 37 import galaxyxml.tool as gxt | 
|  | 38 import galaxyxml.tool.parameters as gxtp | 
|  | 39 | 
|  | 40 import lxml | 
|  | 41 | 
|  | 42 myversion = "V2.1 July 2020" | 
|  | 43 verbose = True | 
|  | 44 debug = True | 
|  | 45 toolFactoryURL = "https://github.com/fubar2/toolfactory" | 
|  | 46 ourdelim = "~~~" | 
|  | 47 | 
|  | 48 # --input_files="$input_files~~~$CL~~~$input_formats~~~$input_label | 
|  | 49 # ~~~$input_help" | 
|  | 50 IPATHPOS = 0 | 
|  | 51 ICLPOS = 1 | 
|  | 52 IFMTPOS = 2 | 
|  | 53 ILABPOS = 3 | 
|  | 54 IHELPOS = 4 | 
|  | 55 IOCLPOS = 5 | 
|  | 56 | 
|  | 57 # --output_files "$otab.history_name~~~$otab.history_format~~~$otab.CL | 
|  | 58 ONAMEPOS = 0 | 
|  | 59 OFMTPOS = 1 | 
|  | 60 OCLPOS = 2 | 
|  | 61 OOCLPOS = 3 | 
|  | 62 | 
|  | 63 # --additional_parameters="$i.param_name~~~$i.param_value~~~ | 
|  | 64 # $i.param_label~~~$i.param_help~~~$i.param_type~~~$i.CL~~~i$.param_CLoverride" | 
|  | 65 ANAMEPOS = 0 | 
|  | 66 AVALPOS = 1 | 
|  | 67 ALABPOS = 2 | 
|  | 68 AHELPPOS = 3 | 
|  | 69 ATYPEPOS = 4 | 
|  | 70 ACLPOS = 5 | 
|  | 71 AOVERPOS = 6 | 
|  | 72 AOCLPOS = 7 | 
|  | 73 | 
|  | 74 | 
|  | 75 foo = len(lxml.__version__) | 
|  | 76 # fug you, flake8. Say my name! | 
|  | 77 | 
|  | 78 def timenow(): | 
|  | 79     """return current time as a string | 
|  | 80     """ | 
|  | 81     return time.strftime("%d/%m/%Y %H:%M:%S", time.localtime(time.time())) | 
|  | 82 | 
|  | 83 | 
|  | 84 def quote_non_numeric(s): | 
|  | 85     """return a prequoted string for non-numerics | 
|  | 86     useful for perl and Rscript parameter passing? | 
|  | 87     """ | 
|  | 88     try: | 
|  | 89         _ = float(s) | 
|  | 90         return s | 
|  | 91     except ValueError: | 
|  | 92         return '"%s"' % s | 
|  | 93 | 
|  | 94 | 
|  | 95 html_escape_table = {"&": "&", ">": ">", "<": "<", "$": r"\$"} | 
|  | 96 | 
|  | 97 | 
|  | 98 def html_escape(text): | 
|  | 99     """Produce entities within text.""" | 
|  | 100     return "".join(html_escape_table.get(c, c) for c in text) | 
|  | 101 | 
|  | 102 | 
|  | 103 def html_unescape(text): | 
|  | 104     """Revert entities within text. Multiple character targets so use replace""" | 
|  | 105     t = text.replace("&", "&") | 
|  | 106     t = t.replace(">", ">") | 
|  | 107     t = t.replace("<", "<") | 
|  | 108     t = t.replace("\\$", "$") | 
|  | 109     return t | 
|  | 110 | 
|  | 111 | 
|  | 112 def parse_citations(citations_text): | 
|  | 113     """ | 
|  | 114     """ | 
|  | 115     citations = [c for c in citations_text.split("**ENTRY**") if c.strip()] | 
|  | 116     citation_tuples = [] | 
|  | 117     for citation in citations: | 
|  | 118         if citation.startswith("doi"): | 
|  | 119             citation_tuples.append(("doi", citation[len("doi") :].strip())) | 
|  | 120         else: | 
|  | 121             citation_tuples.append( | 
|  | 122                 ("bibtex", citation[len("bibtex") :].strip()) | 
|  | 123             ) | 
|  | 124     return citation_tuples | 
|  | 125 | 
|  | 126 | 
|  | 127 class ScriptRunner: | 
|  | 128     """Wrapper for an arbitrary script | 
|  | 129     uses galaxyxml | 
|  | 130 | 
|  | 131     """ | 
|  | 132 | 
|  | 133     def __init__(self, args=None): | 
|  | 134         """ | 
|  | 135         prepare command line cl for running the tool here | 
|  | 136         and prepare elements needed for galaxyxml tool generation | 
|  | 137         """ | 
|  | 138 | 
|  | 139         self.infiles = [x.split(ourdelim) for x in args.input_files] | 
|  | 140         self.outfiles = [x.split(ourdelim) for x in args.output_files] | 
|  | 141         self.addpar = [x.split(ourdelim) for x in args.additional_parameters] | 
|  | 142         self.args = args | 
|  | 143         self.cleanuppar() | 
|  | 144         self.lastclredirect = None | 
|  | 145         self.lastxclredirect = None | 
|  | 146         self.cl = [] | 
|  | 147         self.xmlcl = [] | 
|  | 148         self.is_positional = self.args.parampass == "positional" | 
|  | 149         aCL = self.cl.append | 
|  | 150         assert args.parampass in [ | 
|  | 151             "0", | 
|  | 152             "argparse", | 
|  | 153             "positional", | 
|  | 154         ], 'Parameter passing in args.parampass must be "0","positional" or "argparse"' | 
|  | 155         self.tool_name = re.sub("[^a-zA-Z0-9_]+", "", args.tool_name) | 
|  | 156         self.tool_id = self.tool_name | 
|  | 157         if self.args.interpreter_name: | 
|  | 158             exe = "$runMe" | 
|  | 159         else: | 
|  | 160             exe = self.args.exe_package | 
|  | 161         assert ( | 
|  | 162             exe is not None | 
|  | 163         ), "No interpeter or executable passed in - nothing to run so cannot build" | 
|  | 164         self.tool = gxt.Tool( | 
|  | 165             self.args.tool_name, | 
|  | 166             self.tool_id, | 
|  | 167             self.args.tool_version, | 
|  | 168             self.args.tool_desc, | 
|  | 169             exe, | 
|  | 170         ) | 
|  | 171         self.tinputs = gxtp.Inputs() | 
|  | 172         self.toutputs = gxtp.Outputs() | 
|  | 173         self.testparam = [] | 
|  | 174         if ( | 
|  | 175             self.args.runmode == "Executable" or self.args.runmode == "system" | 
| 46 | 176         ): | 
|  | 177             if len(self.args.cl_override) > 0: | 
|  | 178                 for x in self.args.cl_override.split(' '): | 
|  | 179                     aCL(x) | 
|  | 180             else: | 
|  | 181                 aCL(self.args.exe_package)  # this little CL will just run | 
| 43 | 182         else: | 
|  | 183             self.prepScript() | 
| 46 | 184             aCL(self.args.interpreter_name) | 
|  | 185             aCL(self.sfile) | 
|  | 186 | 
| 43 | 187         self.elog = "%s_error_log.txt" % self.tool_name | 
|  | 188         self.tlog = "%s_runner_log.txt" % self.tool_name | 
|  | 189 | 
|  | 190         if self.args.parampass == "0": | 
|  | 191             self.clsimple() | 
|  | 192         else: | 
|  | 193             clsuffix = [] | 
|  | 194             xclsuffix = [] | 
|  | 195             for i, p in enumerate(self.infiles): | 
|  | 196                 if p[IOCLPOS] == "STDIN": | 
|  | 197                     appendme = [ | 
|  | 198                         p[IOCLPOS], | 
|  | 199                         p[ICLPOS], | 
|  | 200                         p[IPATHPOS], | 
|  | 201                         "< %s" % p[IPATHPOS], | 
|  | 202                     ] | 
|  | 203                     xappendme = [ | 
|  | 204                         p[IOCLPOS], | 
|  | 205                         p[ICLPOS], | 
|  | 206                         p[IPATHPOS], | 
|  | 207                         "< $%s" % p[ICLPOS], | 
|  | 208                     ] | 
|  | 209                 else: | 
|  | 210                     appendme = [p[IOCLPOS], p[ICLPOS], p[IPATHPOS], ""] | 
|  | 211                     xappendme = [p[IOCLPOS], p[ICLPOS], "$%s" % p[ICLPOS], ""] | 
|  | 212                 clsuffix.append(appendme) | 
|  | 213                 xclsuffix.append(xappendme) | 
|  | 214                 # print('##infile i=%d, appendme=%s' % (i,appendme)) | 
|  | 215             for i, p in enumerate(self.outfiles): | 
|  | 216                 if p[OOCLPOS] == "STDOUT": | 
|  | 217                     self.lastclredirect = [">", p[ONAMEPOS]] | 
|  | 218                     self.lastxclredirect = [">", "$%s" % p[OCLPOS]] | 
|  | 219                 else: | 
|  | 220                     clsuffix.append([p[OOCLPOS], p[OCLPOS], p[ONAMEPOS], ""]) | 
|  | 221                     xclsuffix.append( | 
|  | 222                         [p[OOCLPOS], p[OCLPOS], "$%s" % p[ONAMEPOS], ""] | 
|  | 223                     ) | 
|  | 224             for p in self.addpar: | 
|  | 225                 clsuffix.append( | 
|  | 226                     [p[AOCLPOS], p[ACLPOS], p[AVALPOS], p[AOVERPOS]] | 
|  | 227                 ) | 
|  | 228                 xclsuffix.append( | 
|  | 229                     [p[AOCLPOS], p[ACLPOS], '"$%s"' % p[ANAMEPOS], p[AOVERPOS]] | 
|  | 230                 ) | 
|  | 231             clsuffix.sort() | 
|  | 232             xclsuffix.sort() | 
|  | 233             self.xclsuffix = xclsuffix | 
|  | 234             self.clsuffix = clsuffix | 
|  | 235             if self.args.parampass == "positional": | 
|  | 236                 self.clpositional() | 
|  | 237             else: | 
|  | 238                 self.clargparse() | 
|  | 239 | 
|  | 240     def prepScript(self): | 
|  | 241         rx = open(self.args.script_path, "r").readlines() | 
|  | 242         rx = [x.rstrip() for x in rx] | 
|  | 243         rxcheck = [x.strip() for x in rx if x.strip() > ""] | 
|  | 244         assert len(rxcheck) > 0, "Supplied script is empty. Cannot run" | 
|  | 245         self.script = "\n".join(rx) | 
|  | 246         fhandle, self.sfile = tempfile.mkstemp( | 
|  | 247             prefix=self.tool_name, suffix="_%s" % (self.args.interpreter_name) | 
|  | 248         ) | 
|  | 249         tscript = open(self.sfile, "w") | 
|  | 250         tscript.write(self.script) | 
|  | 251         tscript.close() | 
|  | 252         self.indentedScript = "  %s" % "\n".join( | 
|  | 253             [" %s" % html_escape(x) for x in rx] | 
|  | 254         ) | 
|  | 255         self.escapedScript = "%s" % "\n".join( | 
|  | 256             [" %s" % html_escape(x) for x in rx] | 
|  | 257         ) | 
|  | 258         art = "%s.%s" % (self.tool_name, self.args.interpreter_name) | 
|  | 259         artifact = open(art, "wb") | 
|  | 260         if self.args.interpreter_name == "python": | 
|  | 261             artifact.write(bytes("#!/usr/bin/env python\n", "utf8")) | 
|  | 262         artifact.write(bytes(self.script, "utf8")) | 
|  | 263         artifact.close() | 
| 46 | 264 | 
|  | 265 | 
|  | 266 | 
| 43 | 267     def cleanuppar(self): | 
|  | 268         """ positional parameters are complicated by their numeric ordinal""" | 
|  | 269         for i, p in enumerate(self.infiles): | 
|  | 270             if self.args.parampass == "positional": | 
|  | 271                 assert p[ICLPOS].isdigit(), ( | 
|  | 272                     "Positional parameters must be ordinal integers - got %s for %s" | 
|  | 273                     % (p[ICLPOS], p[ILABPOS]) | 
|  | 274                 ) | 
|  | 275             p.append(p[ICLPOS]) | 
|  | 276             if p[ICLPOS].isdigit() or self.args.parampass == "0": | 
|  | 277                 scl = "input%d" % (i + 1) | 
|  | 278                 p[ICLPOS] = scl | 
|  | 279             self.infiles[i] = p | 
|  | 280         for i, p in enumerate( | 
|  | 281             self.outfiles | 
|  | 282         ):  # trying to automagically gather using extensions | 
|  | 283             if self.args.parampass == "positional" and p[OCLPOS] != "STDOUT": | 
|  | 284                 assert p[OCLPOS].isdigit(), ( | 
|  | 285                     "Positional parameters must be ordinal integers - got %s for %s" | 
|  | 286                     % (p[OCLPOS], p[ONAMEPOS]) | 
|  | 287                 ) | 
|  | 288             p.append(p[OCLPOS]) | 
|  | 289             if p[OCLPOS].isdigit() or p[OCLPOS] == "STDOUT": | 
|  | 290                 scl = p[ONAMEPOS] | 
|  | 291                 p[OCLPOS] = scl | 
|  | 292             self.outfiles[i] = p | 
|  | 293         for i, p in enumerate(self.addpar): | 
|  | 294             if self.args.parampass == "positional": | 
|  | 295                 assert p[ACLPOS].isdigit(), ( | 
|  | 296                     "Positional parameters must be ordinal integers - got %s for %s" | 
|  | 297                     % (p[ACLPOS], p[ANAMEPOS]) | 
|  | 298                 ) | 
|  | 299             p.append(p[ACLPOS]) | 
|  | 300             if p[ACLPOS].isdigit(): | 
|  | 301                 scl = "input%s" % p[ACLPOS] | 
|  | 302                 p[ACLPOS] = scl | 
|  | 303             self.addpar[i] = p | 
|  | 304 | 
|  | 305     def clsimple(self): | 
|  | 306         """ no parameters - uses < and > for i/o | 
|  | 307         """ | 
|  | 308         aCL = self.cl.append | 
|  | 309         aCL("<") | 
|  | 310         aCL(self.infiles[0][IPATHPOS]) | 
|  | 311         aCL(">") | 
|  | 312         aCL(self.outfiles[0][OCLPOS]) | 
|  | 313         aXCL = self.xmlcl.append | 
|  | 314         aXCL("<") | 
|  | 315         aXCL("$%s" % self.infiles[0][ICLPOS]) | 
|  | 316         aXCL(">") | 
|  | 317         aXCL("$%s" % self.outfiles[0][ONAMEPOS]) | 
|  | 318 | 
|  | 319     def clpositional(self): | 
|  | 320         # inputs in order then params | 
|  | 321         aCL = self.cl.append | 
|  | 322         for (o_v, k, v, koverride) in self.clsuffix: | 
|  | 323             if " " in v: | 
|  | 324                 aCL("%s" % v) | 
|  | 325             else: | 
|  | 326                 aCL(v) | 
|  | 327         aXCL = self.xmlcl.append | 
|  | 328         for (o_v, k, v, koverride) in self.xclsuffix: | 
|  | 329             aXCL(v) | 
|  | 330         if self.lastxclredirect: | 
|  | 331             aXCL(self.lastxclredirect[0]) | 
|  | 332             aXCL(self.lastxclredirect[1]) | 
|  | 333 | 
|  | 334     def clargparse(self): | 
|  | 335         """ argparse style | 
|  | 336         """ | 
|  | 337         aCL = self.cl.append | 
|  | 338         aXCL = self.xmlcl.append | 
|  | 339         # inputs then params in argparse named form | 
|  | 340         for (o_v, k, v, koverride) in self.xclsuffix: | 
|  | 341             if koverride > "": | 
|  | 342                 k = koverride | 
|  | 343             elif len(k.strip()) == 1: | 
|  | 344                 k = "-%s" % k | 
|  | 345             else: | 
|  | 346                 k = "--%s" % k | 
|  | 347             aXCL(k) | 
|  | 348             aXCL(v) | 
|  | 349         for (o_v, k, v, koverride) in self.clsuffix: | 
|  | 350             if koverride > "": | 
|  | 351                 k = koverride | 
|  | 352             elif len(k.strip()) == 1: | 
|  | 353                 k = "-%s" % k | 
|  | 354             else: | 
|  | 355                 k = "--%s" % k | 
|  | 356             aCL(k) | 
|  | 357             aCL(v) | 
|  | 358 | 
|  | 359     def getNdash(self, newname): | 
|  | 360         if self.is_positional: | 
|  | 361             ndash = 0 | 
|  | 362         else: | 
|  | 363             ndash = 2 | 
|  | 364             if len(newname) < 2: | 
|  | 365                 ndash = 1 | 
|  | 366         return ndash | 
|  | 367 | 
|  | 368     def doXMLparam(self): | 
|  | 369         """flake8 made me do this...""" | 
|  | 370         for p in self.outfiles: | 
|  | 371             newname, newfmt, newcl, oldcl = p | 
|  | 372             ndash = self.getNdash(newcl) | 
|  | 373             aparm = gxtp.OutputData(newcl, format=newfmt, num_dashes=ndash) | 
|  | 374             aparm.positional = self.is_positional | 
|  | 375             if self.is_positional: | 
|  | 376                 if oldcl == "STDOUT": | 
|  | 377                     aparm.positional = 9999999 | 
|  | 378                     aparm.command_line_override = "> $%s" % newcl | 
|  | 379                 else: | 
|  | 380                     aparm.positional = int(oldcl) | 
|  | 381                     aparm.command_line_override = "$%s" % newcl | 
|  | 382             self.toutputs.append(aparm) | 
|  | 383             tp = gxtp.TestOutput( | 
|  | 384                 name=newcl, value="%s_sample" % newcl, format=newfmt | 
|  | 385             ) | 
|  | 386             self.testparam.append(tp) | 
|  | 387         for p in self.infiles: | 
|  | 388             newname = p[ICLPOS] | 
|  | 389             newfmt = p[IFMTPOS] | 
|  | 390             ndash = self.getNdash(newname) | 
|  | 391             if not len(p[ILABPOS]) > 0: | 
|  | 392                 alab = p[ICLPOS] | 
|  | 393             else: | 
|  | 394                 alab = p[ILABPOS] | 
|  | 395             aninput = gxtp.DataParam( | 
|  | 396                 newname, | 
|  | 397                 optional=False, | 
|  | 398                 label=alab, | 
|  | 399                 help=p[IHELPOS], | 
|  | 400                 format=newfmt, | 
|  | 401                 multiple=False, | 
|  | 402                 num_dashes=ndash, | 
|  | 403             ) | 
|  | 404             aninput.positional = self.is_positional | 
|  | 405             self.tinputs.append(aninput) | 
|  | 406             tparm = gxtp.TestParam(name=newname, value="%s_sample" % newname) | 
|  | 407             self.testparam.append(tparm) | 
|  | 408         for p in self.addpar: | 
|  | 409             newname, newval, newlabel, newhelp, newtype, newcl, override, oldcl = p | 
|  | 410             if not len(newlabel) > 0: | 
|  | 411                 newlabel = newname | 
|  | 412             ndash = self.getNdash(newname) | 
|  | 413             if newtype == "text": | 
|  | 414                 aparm = gxtp.TextParam( | 
|  | 415                     newname, | 
|  | 416                     label=newlabel, | 
|  | 417                     help=newhelp, | 
|  | 418                     value=newval, | 
|  | 419                     num_dashes=ndash, | 
|  | 420                 ) | 
|  | 421             elif newtype == "integer": | 
|  | 422                 aparm = gxtp.IntegerParam( | 
|  | 423                     newname, | 
|  | 424                     label=newname, | 
|  | 425                     help=newhelp, | 
|  | 426                     value=newval, | 
|  | 427                     num_dashes=ndash, | 
|  | 428                 ) | 
|  | 429             elif newtype == "float": | 
|  | 430                 aparm = gxtp.FloatParam( | 
|  | 431                     newname, | 
|  | 432                     label=newname, | 
|  | 433                     help=newhelp, | 
|  | 434                     value=newval, | 
|  | 435                     num_dashes=ndash, | 
|  | 436                 ) | 
|  | 437             else: | 
|  | 438                 raise ValueError( | 
|  | 439                     'Unrecognised parameter type "%s" for\ | 
|  | 440                  additional parameter %s in makeXML' | 
|  | 441                     % (newtype, newname) | 
|  | 442                 ) | 
|  | 443             aparm.positional = self.is_positional | 
|  | 444             if self.is_positional: | 
|  | 445                 aninput.positional = int(oldcl) | 
|  | 446             self.tinputs.append(aparm) | 
|  | 447             self.tparm = gxtp.TestParam(newname, value=newval) | 
|  | 448             self.testparam.append(tparm) | 
|  | 449 | 
|  | 450     def doNoXMLparam(self): | 
|  | 451         alab = self.infiles[0][ILABPOS] | 
|  | 452         if len(alab) == 0: | 
|  | 453             alab = self.infiles[0][ICLPOS] | 
|  | 454         max1s = ( | 
|  | 455             "Maximum one input if parampass is 0 - more than one input files supplied - %s" | 
|  | 456             % str(self.infiles) | 
|  | 457         ) | 
|  | 458         assert len(self.infiles) == 1, max1s | 
|  | 459         newname = self.infiles[0][ICLPOS] | 
|  | 460         aninput = gxtp.DataParam( | 
|  | 461             newname, | 
|  | 462             optional=False, | 
|  | 463             label=alab, | 
|  | 464             help=self.infiles[0][IHELPOS], | 
|  | 465             format=self.infiles[0][IFMTPOS], | 
|  | 466             multiple=False, | 
|  | 467             num_dashes=0, | 
|  | 468         ) | 
|  | 469         aninput.command_line_override = "< $%s" % newname | 
|  | 470         aninput.positional = self.is_positional | 
|  | 471         self.tinputs.append(aninput) | 
|  | 472         tp = gxtp.TestParam(name=newname, value="%s_sample" % newname) | 
|  | 473         self.testparam.append(tp) | 
|  | 474         newname = self.outfiles[0][OCLPOS] | 
|  | 475         newfmt = self.outfiles[0][OFMTPOS] | 
|  | 476         anout = gxtp.OutputData(newname, format=newfmt, num_dashes=0) | 
|  | 477         anout.command_line_override = "> $%s" % newname | 
|  | 478         anout.positional = self.is_positional | 
|  | 479         self.toutputs.append(anout) | 
|  | 480         tp = gxtp.TestOutput( | 
|  | 481             name=newname, value="%s_sample" % newname, format=newfmt | 
|  | 482         ) | 
|  | 483         self.testparam.append(tp) | 
|  | 484 | 
|  | 485     def makeXML(self): | 
|  | 486         """ | 
|  | 487         Create a Galaxy xml tool wrapper for the new script | 
|  | 488         Uses galaxyhtml | 
|  | 489         Hmmm. How to get the command line into correct order... | 
|  | 490         """ | 
| 46 | 491         if self.args.cl_override: | 
|  | 492             self.tool.command_line_override = self.args.cl_override.split(' ') + self.xmlcl | 
|  | 493         else: | 
|  | 494             self.tool.command_line_override = self.xmlcl | 
| 43 | 495         if self.args.interpreter_name: | 
|  | 496             self.tool.interpreter = self.args.interpreter_name | 
|  | 497         if self.args.help_text: | 
|  | 498             helptext = open(self.args.help_text, "r").readlines() | 
|  | 499             helptext = [html_escape(x) for x in helptext] | 
|  | 500             self.tool.help = "".join([x for x in helptext]) | 
|  | 501         else: | 
|  | 502             self.tool.help = ( | 
|  | 503                 "Please ask the tool author (%s) for help \ | 
|  | 504               as none was supplied at tool generation\n" | 
|  | 505                 % (self.args.user_email) | 
|  | 506             ) | 
|  | 507         self.tool.version_command = None  # do not want | 
|  | 508         requirements = gxtp.Requirements() | 
|  | 509 | 
|  | 510         if self.args.interpreter_name: | 
| 46 | 511             if self.args.dependencies: | 
|  | 512                 for d in self.args.dependencies.split(','): | 
|  | 513                     requirements.append( | 
|  | 514                         gxtp.Requirement( | 
|  | 515                          "package", d, "" | 
|  | 516                         ) | 
|  | 517                     ) | 
| 43 | 518             if self.args.interpreter_name == "python": | 
|  | 519                 requirements.append( | 
|  | 520                     gxtp.Requirement( | 
|  | 521                         "package", "python", self.args.interpreter_version | 
|  | 522                     ) | 
|  | 523                 ) | 
|  | 524             elif self.args.interpreter_name not in ["bash", "sh"]: | 
|  | 525                 requirements.append( | 
|  | 526                     gxtp.Requirement( | 
|  | 527                         "package", | 
|  | 528                         self.args.interpreter_name, | 
|  | 529                         self.args.interpreter_version, | 
|  | 530                     ) | 
|  | 531                 ) | 
|  | 532         else: | 
|  | 533             if self.args.exe_package and self.args.parampass != "system": | 
|  | 534                 requirements.append( | 
|  | 535                     gxtp.Requirement( | 
|  | 536                         "package", | 
|  | 537                         self.args.exe_package, | 
|  | 538                         self.args.exe_package_version, | 
|  | 539                     ) | 
|  | 540                 ) | 
|  | 541         self.tool.requirements = requirements | 
|  | 542         if self.args.parampass == "0": | 
|  | 543             self.doNoXMLparam() | 
|  | 544         else: | 
|  | 545             self.doXMLparam() | 
|  | 546         self.tool.outputs = self.toutputs | 
|  | 547         self.tool.inputs = self.tinputs | 
|  | 548         if self.args.runmode not in ["Executable", "system"]: | 
|  | 549             configfiles = gxtp.Configfiles() | 
|  | 550             configfiles.append(gxtp.Configfile(name="runMe", text=self.script)) | 
|  | 551             self.tool.configfiles = configfiles | 
|  | 552         tests = gxtp.Tests() | 
|  | 553         test_a = gxtp.Test() | 
|  | 554         for tp in self.testparam: | 
|  | 555             test_a.append(tp) | 
|  | 556         tests.append(test_a) | 
|  | 557         self.tool.tests = tests | 
|  | 558         self.tool.add_comment( | 
|  | 559             "Created by %s at %s using the Galaxy Tool Factory." | 
|  | 560             % (self.args.user_email, timenow()) | 
|  | 561         ) | 
|  | 562         self.tool.add_comment("Source in git at: %s" % (toolFactoryURL)) | 
|  | 563         self.tool.add_comment( | 
|  | 564             "Cite: Creating re-usable tools from scripts doi: \ | 
|  | 565             10.1093/bioinformatics/bts573" | 
|  | 566         ) | 
|  | 567         exml = self.tool.export() | 
|  | 568         xf = open('%s.xml' % self.tool_name, "w") | 
|  | 569         xf.write(exml) | 
|  | 570         xf.write("\n") | 
|  | 571         xf.close() | 
|  | 572         # ready for the tarball | 
|  | 573 | 
|  | 574     def makeTooltar(self): | 
|  | 575         """ | 
|  | 576         a tool is a gz tarball with eg | 
|  | 577         /toolname/tool.xml /toolname/tool.py /toolname/test-data/test1_in.foo ... | 
|  | 578         NOTE names for test inputs and outputs are munged here so must | 
|  | 579         correspond to actual input and output names used on the generated cl | 
|  | 580         """ | 
|  | 581         retval = self.run() | 
|  | 582         if retval: | 
|  | 583             sys.stderr.write( | 
|  | 584                 "## Run failed. Cannot build yet. Please fix and retry" | 
|  | 585             ) | 
|  | 586             sys.exit(1) | 
|  | 587         tdir = "tfout" | 
|  | 588         if not os.path.exists(tdir): | 
|  | 589             os.mkdir(tdir) | 
|  | 590         self.makeXML() | 
|  | 591         testdir = os.path.join(tdir, "test-data") | 
|  | 592         if not os.path.exists(testdir): | 
|  | 593             os.mkdir(testdir)  # make tests directory | 
|  | 594         for p in self.infiles: | 
|  | 595             pth = p[IPATHPOS] | 
|  | 596             dest = os.path.join(testdir, "%s_sample" % p[ICLPOS]) | 
|  | 597             shutil.copyfile(pth, dest) | 
|  | 598         for p in self.outfiles: | 
|  | 599             pth = p[OCLPOS] | 
|  | 600             if p[OOCLPOS] == "STDOUT" or self.args.parampass == "0": | 
|  | 601                 pth = p[ONAMEPOS] | 
|  | 602                 dest = os.path.join(testdir, "%s_sample" % p[ONAMEPOS]) | 
|  | 603                 shutil.copyfile(pth, dest) | 
|  | 604                 dest = os.path.join(tdir, p[ONAMEPOS]) | 
|  | 605                 shutil.copyfile(pth, dest) | 
|  | 606             else: | 
|  | 607                 pth = p[OCLPOS] | 
|  | 608                 dest = os.path.join(testdir, "%s_sample" % p[OCLPOS]) | 
|  | 609                 shutil.copyfile(pth, dest) | 
|  | 610                 dest = os.path.join(tdir, p[OCLPOS]) | 
|  | 611                 shutil.copyfile(pth, dest) | 
|  | 612 | 
|  | 613         if os.path.exists(self.tlog) and os.stat(self.tlog).st_size > 0: | 
|  | 614             shutil.copyfile(self.tlog, os.path.join(testdir, "test1_log_outfiletxt")) | 
|  | 615         if self.args.runmode not in ["Executable", "system"]: | 
|  | 616             stname = os.path.join(tdir, "%s" % (self.sfile)) | 
|  | 617             if not os.path.exists(stname): | 
|  | 618                 shutil.copyfile(self.sfile, stname) | 
|  | 619         xreal = '%s.xml' % self.tool_name | 
|  | 620         xout = os.path.join(tdir,xreal) | 
|  | 621         shutil.copyfile(xreal, xout) | 
|  | 622         tarpath = "toolfactory_%s.tgz" % self.tool_name | 
|  | 623         tf = tarfile.open(tarpath, "w:gz") | 
|  | 624         tf.add(name=tdir, arcname=self.tool_name) | 
|  | 625         tf.close() | 
|  | 626         shutil.copyfile(tarpath, self.args.new_tool) | 
|  | 627         shutil.copyfile(xreal,"tool_xml.txt") | 
|  | 628         repdir = "TF_run_report_tempdir" | 
|  | 629         if not os.path.exists(repdir): | 
|  | 630             os.mkdir(repdir) | 
|  | 631         repoutnames = [x[OCLPOS] for x in self.outfiles] | 
|  | 632         with os.scandir('.') as outs: | 
|  | 633             for entry in outs: | 
|  | 634                 if entry.name.endswith('.tgz') or not entry.is_file(): | 
|  | 635                     continue | 
|  | 636                 if entry.name in repoutnames: | 
|  | 637                     shutil.copyfile(entry.name,os.path.join(repdir,entry.name)) | 
|  | 638                 elif entry.name == "%s.xml" % self.tool_name: | 
|  | 639                     shutil.copyfile(entry.name,os.path.join(repdir,"new_tool_xml")) | 
|  | 640         return retval | 
|  | 641 | 
|  | 642     def run(self): | 
|  | 643         """ | 
|  | 644         Some devteam tools have this defensive stderr read so I'm keeping with the faith | 
|  | 645         Feel free to update. | 
|  | 646         """ | 
|  | 647         s = "run cl=%s" % str(self.cl) | 
|  | 648 | 
|  | 649         logging.debug(s) | 
|  | 650         scl = " ".join(self.cl) | 
|  | 651         err = None | 
|  | 652         if self.args.parampass != "0": | 
|  | 653             ste = open(self.elog, "wb") | 
|  | 654             if self.lastclredirect: | 
|  | 655                 sto = open( | 
|  | 656                     self.lastclredirect[1], "wb" | 
|  | 657                 )  # is name of an output file | 
|  | 658             else: | 
|  | 659                 sto = open(self.tlog, "wb") | 
|  | 660                 sto.write( | 
|  | 661                     bytes( | 
|  | 662                         "## Executing Toolfactory generated command line = %s\n" | 
|  | 663                         % scl, | 
|  | 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 | 
|  | 683         else:  # work around special case of simple scripts that take stdin and write to stdout | 
|  | 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 p.returncode != 0 and err:  # problem | 
|  | 696             sys.stderr.write(err) | 
|  | 697         logging.debug("run done") | 
|  | 698         return retval | 
|  | 699 | 
|  | 700 | 
|  | 701 def main(): | 
|  | 702     """ | 
|  | 703     This is a Galaxy wrapper. It expects to be called by a special purpose tool.xml as: | 
|  | 704     <command interpreter="python">rgBaseScriptWrapper.py --script_path "$scriptPath" --tool_name "foo" --interpreter "Rscript" | 
|  | 705     </command> | 
|  | 706     """ | 
|  | 707     parser = argparse.ArgumentParser() | 
|  | 708     a = parser.add_argument | 
|  | 709     a("--script_path", default="") | 
| 46 | 710     a("--dependencies", default="") | 
|  | 711     a("--cl_override", default="") | 
| 43 | 712     a("--tool_name", default=None) | 
|  | 713     a("--interpreter_name", default=None) | 
|  | 714     a("--interpreter_version", default=None) | 
|  | 715     a("--exe_package", default=None) | 
|  | 716     a("--exe_package_version", default=None) | 
|  | 717     a("--input_files", default=[], action="append") | 
|  | 718     a("--output_files", default=[], action="append") | 
|  | 719     a("--user_email", default="Unknown") | 
|  | 720     a("--bad_user", default=None) | 
|  | 721     a("--make_Tool", default=None) | 
|  | 722     a("--help_text", default=None) | 
|  | 723     a("--tool_desc", default=None) | 
|  | 724     a("--tool_version", default=None) | 
|  | 725     a("--citations", default=None) | 
|  | 726     a("--additional_parameters", action="append", default=[]) | 
|  | 727     a("--edit_additional_parameters", action="store_true", default=False) | 
|  | 728     a("--parampass", default="positional") | 
|  | 729     a("--tfout", default="./tfout") | 
|  | 730     a("--new_tool", default="new_tool") | 
|  | 731     a("--runmode", default=None) | 
|  | 732     args = parser.parse_args() | 
|  | 733     assert not args.bad_user, ( | 
|  | 734         'UNAUTHORISED: %s is NOT authorized to use this tool until Galaxy admin adds %s to "admin_users" in the Galaxy configuration file' | 
|  | 735         % (args.bad_user, args.bad_user) | 
|  | 736     ) | 
|  | 737     assert ( | 
|  | 738         args.tool_name | 
|  | 739     ), "## Tool Factory expects a tool name - eg --tool_name=DESeq" | 
|  | 740     assert ( | 
|  | 741         args.interpreter_name or args.exe_package | 
|  | 742     ), "## Tool Factory wrapper expects an interpreter or an executable package" | 
|  | 743     assert args.exe_package or ( | 
|  | 744         len(args.script_path) > 0 and os.path.isfile(args.script_path) | 
|  | 745     ), "## Tool Factory wrapper expects a script path - eg --script_path=foo.R if no executable" | 
|  | 746     args.input_files = [ | 
|  | 747         x.replace('"', "").replace("'", "") for x in args.input_files | 
|  | 748     ] | 
|  | 749     # remove quotes we need to deal with spaces in CL params | 
|  | 750     for i, x in enumerate(args.additional_parameters): | 
|  | 751         args.additional_parameters[i] = args.additional_parameters[i].replace( | 
|  | 752             '"', "" | 
|  | 753         ) | 
|  | 754     r = ScriptRunner(args) | 
|  | 755     if args.make_Tool: | 
|  | 756         retcode = r.makeTooltar() | 
|  | 757     else: | 
|  | 758         retcode = r.run() | 
|  | 759     if retcode: | 
|  | 760         sys.exit(retcode)  # indicate failure to job runner | 
|  | 761 | 
|  | 762 | 
|  | 763 if __name__ == "__main__": | 
|  | 764     main() |