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