PageRenderTime 77ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/galaxy/tools/__init__.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 3260 lines | 3210 code | 12 blank | 38 comment | 68 complexity | 766fca0cbb3c5eeef2edff23518a8b96 MD5 | raw file
  1. """
  2. Classes encapsulating galaxy tools and tool configuration.
  3. """
  4. import binascii
  5. import glob
  6. import json
  7. import logging
  8. import os
  9. import pipes
  10. import re
  11. import shutil
  12. import sys
  13. import tempfile
  14. import threading
  15. import traceback
  16. import types
  17. import urllib
  18. from math import isinf
  19. from galaxy import eggs
  20. eggs.require( "MarkupSafe" ) # MarkupSafe must load before mako
  21. eggs.require( "Mako" )
  22. eggs.require( "elementtree" )
  23. eggs.require( "Paste" )
  24. eggs.require( "SQLAlchemy >= 0.4" )
  25. from cgi import FieldStorage
  26. from elementtree import ElementTree
  27. from mako.template import Template
  28. from paste import httpexceptions
  29. from sqlalchemy import and_
  30. from galaxy import jobs, model
  31. from galaxy.jobs.error_level import StdioErrorLevel
  32. from galaxy.datatypes.metadata import JobExternalOutputMetadataWrapper
  33. from galaxy.jobs import ParallelismInfo
  34. from galaxy.tools.actions import DefaultToolAction
  35. from galaxy.tools.actions.data_source import DataSourceToolAction
  36. from galaxy.tools.actions.data_manager import DataManagerToolAction
  37. from galaxy.tools.deps import build_dependency_manager
  38. from galaxy.tools.deps.requirements import parse_requirements_from_xml
  39. from galaxy.tools.parameters import check_param, params_from_strings, params_to_strings
  40. from galaxy.tools.parameters.basic import (BaseURLToolParameter,
  41. DataToolParameter, HiddenToolParameter, LibraryDatasetToolParameter,
  42. SelectToolParameter, ToolParameter, UnvalidatedValue,
  43. IntegerToolParameter, FloatToolParameter)
  44. from galaxy.tools.parameters.grouping import Conditional, ConditionalWhen, Repeat, UploadDataset
  45. from galaxy.tools.parameters.input_translation import ToolInputTranslator
  46. from galaxy.tools.parameters.output import ToolOutputActionGroup
  47. from galaxy.tools.parameters.validation import LateValidationError
  48. from galaxy.tools.filters import FilterFactory
  49. from galaxy.tools.test import parse_tests_elem
  50. from galaxy.util import listify, parse_xml, rst_to_html, string_as_bool, string_to_object, xml_text, xml_to_string
  51. from galaxy.util.bunch import Bunch
  52. from galaxy.util.expressions import ExpressionContext
  53. from galaxy.util.hash_util import hmac_new
  54. from galaxy.util.none_like import NoneDataset
  55. from galaxy.util.odict import odict
  56. from galaxy.util.template import fill_template
  57. from galaxy.web import url_for
  58. from galaxy.web.form_builder import SelectField
  59. from galaxy.model.item_attrs import Dictifiable
  60. from galaxy.model import Workflow
  61. from tool_shed.util import shed_util_common as suc
  62. from .loader import load_tool, template_macro_params
  63. from .wrappers import (
  64. ToolParameterValueWrapper,
  65. RawObjectWrapper,
  66. LibraryDatasetValueWrapper,
  67. InputValueWrapper,
  68. SelectToolParameterWrapper,
  69. DatasetFilenameWrapper,
  70. DatasetListWrapper,
  71. )
  72. log = logging.getLogger( __name__ )
  73. WORKFLOW_PARAMETER_REGULAR_EXPRESSION = re.compile( '''\$\{.+?\}''' )
  74. class ToolNotFoundException( Exception ):
  75. pass
  76. def to_dict_helper( obj, kwargs ):
  77. """ Helper function that provides the appropriate kwargs to to_dict an object. """
  78. # Label.to_dict cannot have kwargs.
  79. if isinstance( obj, ToolSectionLabel ):
  80. kwargs = {}
  81. return obj.to_dict( **kwargs )
  82. class ToolBox( object, Dictifiable ):
  83. """Container for a collection of tools"""
  84. def __init__( self, config_filenames, tool_root_dir, app ):
  85. """
  86. Create a toolbox from the config files named by `config_filenames`, using
  87. `tool_root_dir` as the base directory for finding individual tool config files.
  88. """
  89. # The shed_tool_confs list contains dictionaries storing information about the tools defined in each
  90. # shed-related shed_tool_conf.xml file.
  91. self.shed_tool_confs = []
  92. self.tools_by_id = {}
  93. self.workflows_by_id = {}
  94. # In-memory dictionary that defines the layout of the tool panel.
  95. self.tool_panel = odict()
  96. self.index = 0
  97. self.data_manager_tools = odict()
  98. # File that contains the XML section and tool tags from all tool panel config files integrated into a
  99. # single file that defines the tool panel layout. This file can be changed by the Galaxy administrator
  100. # (in a way similar to the single tool_conf.xml file in the past) to alter the layout of the tool panel.
  101. self.integrated_tool_panel_config = os.path.join( app.config.root, 'integrated_tool_panel.xml' )
  102. # In-memory dictionary that defines the layout of the tool_panel.xml file on disk.
  103. self.integrated_tool_panel = odict()
  104. self.integrated_tool_panel_config_has_contents = os.path.exists( self.integrated_tool_panel_config ) and os.stat( self.integrated_tool_panel_config ).st_size > 0
  105. if self.integrated_tool_panel_config_has_contents:
  106. self.load_integrated_tool_panel_keys()
  107. # The following refers to the tool_path config setting for backward compatibility. The shed-related
  108. # (e.g., shed_tool_conf.xml) files include the tool_path attribute within the <toolbox> tag.
  109. self.tool_root_dir = tool_root_dir
  110. self.app = app
  111. self.filter_factory = FilterFactory( self )
  112. self.init_dependency_manager()
  113. config_filenames = listify( config_filenames )
  114. for config_filename in config_filenames:
  115. if os.path.isdir( config_filename ):
  116. directory_contents = sorted( os.listdir( config_filename ) )
  117. directory_config_files = [ config_file for config_file in directory_contents if config_file.endswith( ".xml" ) ]
  118. config_filenames.remove( config_filename )
  119. config_filenames.extend( directory_config_files )
  120. for config_filename in config_filenames:
  121. try:
  122. self.init_tools( config_filename )
  123. except:
  124. log.exception( "Error loading tools defined in config %s", config_filename )
  125. if self.integrated_tool_panel_config_has_contents:
  126. # Load self.tool_panel based on the order in self.integrated_tool_panel.
  127. self.load_tool_panel()
  128. if app.config.update_integrated_tool_panel:
  129. # Write the current in-memory integrated_tool_panel to the integrated_tool_panel.xml file.
  130. # This will cover cases where the Galaxy administrator manually edited one or more of the tool panel
  131. # config files, adding or removing locally developed tools or workflows. The value of integrated_tool_panel
  132. # will be False when things like functional tests are the caller.
  133. self.fix_integrated_tool_panel_dict()
  134. self.write_integrated_tool_panel_config_file()
  135. def fix_integrated_tool_panel_dict( self ):
  136. # HACK: instead of fixing after the fact, I suggest some combination of:
  137. # 1) adjusting init_tools() and called methods to get this right
  138. # 2) redesigning the code and/or data structure used to read/write integrated_tool_panel.xml
  139. for key, value in self.integrated_tool_panel.iteritems():
  140. if isinstance( value, ToolSection ):
  141. for section_key, section_value in value.elems.iteritems():
  142. if section_value is None:
  143. if isinstance( section_value, Tool ):
  144. tool_id = section_key[5:]
  145. value.elems[section_key] = self.tools_by_id.get( tool_id )
  146. elif isinstance( section_value, Workflow ):
  147. workflow_id = section_key[9:]
  148. value.elems[section_key] = self.workflows_by_id.get( workflow_id )
  149. def init_tools( self, config_filename ):
  150. """
  151. Read the configuration file and load each tool. The following tags are currently supported:
  152. .. raw:: xml
  153. <toolbox>
  154. <tool file="data_source/upload.xml"/> # tools outside sections
  155. <label text="Basic Tools" id="basic_tools" /> # labels outside sections
  156. <workflow id="529fd61ab1c6cc36" /> # workflows outside sections
  157. <section name="Get Data" id="getext"> # sections
  158. <tool file="data_source/biomart.xml" /> # tools inside sections
  159. <label text="In Section" id="in_section" /> # labels inside sections
  160. <workflow id="adb5f5c93f827949" /> # workflows inside sections
  161. </section>
  162. </toolbox>
  163. """
  164. if self.app.config.get_bool( 'enable_tool_tags', False ):
  165. log.info("removing all tool tag associations (" + str( self.sa_session.query( self.app.model.ToolTagAssociation ).count() ) + ")" )
  166. self.sa_session.query( self.app.model.ToolTagAssociation ).delete()
  167. self.sa_session.flush()
  168. log.info( "Parsing the tool configuration %s" % config_filename )
  169. tree = parse_xml( config_filename )
  170. root = tree.getroot()
  171. tool_path = root.get( 'tool_path' )
  172. if tool_path:
  173. # We're parsing a shed_tool_conf file since we have a tool_path attribute.
  174. parsing_shed_tool_conf = True
  175. # Keep an in-memory list of xml elements to enable persistence of the changing tool config.
  176. config_elems = []
  177. else:
  178. parsing_shed_tool_conf = False
  179. # Default to backward compatible config setting.
  180. tool_path = self.tool_root_dir
  181. # Only load the panel_dict under certain conditions.
  182. load_panel_dict = not self.integrated_tool_panel_config_has_contents
  183. for _, elem in enumerate( root ):
  184. index = self.index
  185. self.index += 1
  186. if parsing_shed_tool_conf:
  187. config_elems.append( elem )
  188. if elem.tag == 'tool':
  189. self.load_tool_tag_set( elem, self.tool_panel, self.integrated_tool_panel, tool_path, load_panel_dict, guid=elem.get( 'guid' ), index=index )
  190. elif elem.tag == 'workflow':
  191. self.load_workflow_tag_set( elem, self.tool_panel, self.integrated_tool_panel, load_panel_dict, index=index )
  192. elif elem.tag == 'section':
  193. self.load_section_tag_set( elem, tool_path, load_panel_dict, index=index )
  194. elif elem.tag == 'label':
  195. self.load_label_tag_set( elem, self.tool_panel, self.integrated_tool_panel, load_panel_dict, index=index )
  196. if parsing_shed_tool_conf:
  197. shed_tool_conf_dict = dict( config_filename=config_filename,
  198. tool_path=tool_path,
  199. config_elems=config_elems )
  200. self.shed_tool_confs.append( shed_tool_conf_dict )
  201. def get_shed_config_dict_by_filename( self, filename, default=None ):
  202. for shed_config_dict in self.shed_tool_confs:
  203. if shed_config_dict[ 'config_filename' ] == filename:
  204. return shed_config_dict
  205. return default
  206. def __add_tool_to_tool_panel( self, tool, panel_component, section=False ):
  207. # See if a version of this tool is already loaded into the tool panel. The value of panel_component
  208. # will be a ToolSection (if the value of section=True) or self.tool_panel (if section=False).
  209. tool_id = str( tool.id )
  210. tool = self.tools_by_id[ tool_id ]
  211. if section:
  212. panel_dict = panel_component.elems
  213. else:
  214. panel_dict = panel_component
  215. already_loaded = False
  216. loaded_version_key = None
  217. lineage_id = None
  218. for lineage_id in tool.lineage_ids:
  219. if lineage_id in self.tools_by_id:
  220. loaded_version_key = 'tool_%s' % lineage_id
  221. if loaded_version_key in panel_dict:
  222. already_loaded = True
  223. break
  224. if already_loaded:
  225. if tool.lineage_ids.index( tool_id ) > tool.lineage_ids.index( lineage_id ):
  226. key = 'tool_%s' % tool.id
  227. index = panel_dict.keys().index( loaded_version_key )
  228. del panel_dict[ loaded_version_key ]
  229. panel_dict.insert( index, key, tool )
  230. log.debug( "Loaded tool id: %s, version: %s into tool panel." % ( tool.id, tool.version ) )
  231. else:
  232. inserted = False
  233. key = 'tool_%s' % tool.id
  234. # The value of panel_component is the in-memory tool panel dictionary.
  235. for index, integrated_panel_key in enumerate( self.integrated_tool_panel.keys() ):
  236. if key == integrated_panel_key:
  237. panel_dict.insert( index, key, tool )
  238. if not inserted:
  239. inserted = True
  240. if not inserted:
  241. # Check the tool's installed versions.
  242. for lineage_id in tool.lineage_ids:
  243. lineage_id_key = 'tool_%s' % lineage_id
  244. for index, integrated_panel_key in enumerate( self.integrated_tool_panel.keys() ):
  245. if lineage_id_key == integrated_panel_key:
  246. panel_dict.insert( index, key, tool )
  247. if not inserted:
  248. inserted = True
  249. if not inserted:
  250. if tool.guid is None or \
  251. tool.tool_shed is None or \
  252. tool.repository_name is None or \
  253. tool.repository_owner is None or \
  254. tool.installed_changeset_revision is None:
  255. # We have a tool that was not installed from the Tool Shed, but is also not yet defined in
  256. # integrated_tool_panel.xml, so append it to the tool panel.
  257. panel_dict[ key ] = tool
  258. log.debug( "Loaded tool id: %s, version: %s into tool panel.." % ( tool.id, tool.version ) )
  259. else:
  260. # We are in the process of installing the tool.
  261. tool_version = self.__get_tool_version( tool_id )
  262. tool_lineage_ids = tool_version.get_version_ids( self.app, reverse=True )
  263. for lineage_id in tool_lineage_ids:
  264. if lineage_id in self.tools_by_id:
  265. loaded_version_key = 'tool_%s' % lineage_id
  266. if loaded_version_key in panel_dict:
  267. if not already_loaded:
  268. already_loaded = True
  269. if not already_loaded:
  270. # If the tool is not defined in integrated_tool_panel.xml, append it to the tool panel.
  271. panel_dict[ key ] = tool
  272. log.debug( "Loaded tool id: %s, version: %s into tool panel...." % ( tool.id, tool.version ) )
  273. def load_tool_panel( self ):
  274. for key, val in self.integrated_tool_panel.items():
  275. if isinstance( val, Tool ):
  276. tool_id = key.replace( 'tool_', '', 1 )
  277. if tool_id in self.tools_by_id:
  278. self.__add_tool_to_tool_panel( val, self.tool_panel, section=False )
  279. elif isinstance( val, Workflow ):
  280. workflow_id = key.replace( 'workflow_', '', 1 )
  281. if workflow_id in self.workflows_by_id:
  282. workflow = self.workflows_by_id[ workflow_id ]
  283. self.tool_panel[ key ] = workflow
  284. log.debug( "Loaded workflow: %s %s" % ( workflow_id, workflow.name ) )
  285. elif isinstance( val, ToolSectionLabel ):
  286. self.tool_panel[ key ] = val
  287. elif isinstance( val, ToolSection ):
  288. elem = ElementTree.Element( 'section' )
  289. elem.attrib[ 'id' ] = val.id or ''
  290. elem.attrib[ 'name' ] = val.name or ''
  291. elem.attrib[ 'version' ] = val.version or ''
  292. section = ToolSection( elem )
  293. log.debug( "Loading section: %s" % elem.get( 'name' ) )
  294. for section_key, section_val in val.elems.items():
  295. if isinstance( section_val, Tool ):
  296. tool_id = section_key.replace( 'tool_', '', 1 )
  297. if tool_id in self.tools_by_id:
  298. self.__add_tool_to_tool_panel( section_val, section, section=True )
  299. elif isinstance( section_val, Workflow ):
  300. workflow_id = section_key.replace( 'workflow_', '', 1 )
  301. if workflow_id in self.workflows_by_id:
  302. workflow = self.workflows_by_id[ workflow_id ]
  303. section.elems[ section_key ] = workflow
  304. log.debug( "Loaded workflow: %s %s" % ( workflow_id, workflow.name ) )
  305. elif isinstance( section_val, ToolSectionLabel ):
  306. if section_val:
  307. section.elems[ section_key ] = section_val
  308. log.debug( "Loaded label: %s" % ( section_val.text ) )
  309. self.tool_panel[ key ] = section
  310. def load_integrated_tool_panel_keys( self ):
  311. """
  312. Load the integrated tool panel keys, setting values for tools and workflows to None. The values will
  313. be reset when the various tool panel config files are parsed, at which time the tools and workflows are
  314. loaded.
  315. """
  316. tree = parse_xml( self.integrated_tool_panel_config )
  317. root = tree.getroot()
  318. for elem in root:
  319. if elem.tag == 'tool':
  320. key = 'tool_%s' % elem.get( 'id' )
  321. self.integrated_tool_panel[ key ] = None
  322. elif elem.tag == 'workflow':
  323. key = 'workflow_%s' % elem.get( 'id' )
  324. self.integrated_tool_panel[ key ] = None
  325. elif elem.tag == 'section':
  326. section = ToolSection( elem )
  327. for section_elem in elem:
  328. if section_elem.tag == 'tool':
  329. key = 'tool_%s' % section_elem.get( 'id' )
  330. section.elems[ key ] = None
  331. elif section_elem.tag == 'workflow':
  332. key = 'workflow_%s' % section_elem.get( 'id' )
  333. section.elems[ key ] = None
  334. elif section_elem.tag == 'label':
  335. key = 'label_%s' % section_elem.get( 'id' )
  336. section.elems[ key ] = None
  337. key = elem.get( 'id' )
  338. self.integrated_tool_panel[ key ] = section
  339. elif elem.tag == 'label':
  340. key = 'label_%s' % elem.get( 'id' )
  341. self.integrated_tool_panel[ key ] = None
  342. def write_integrated_tool_panel_config_file( self ):
  343. """
  344. Write the current in-memory version of the integrated_tool_panel.xml file to disk. Since Galaxy administrators
  345. use this file to manage the tool panel, we'll not use xml_to_string() since it doesn't write XML quite right.
  346. """
  347. fd, filename = tempfile.mkstemp()
  348. os.write( fd, '<?xml version="1.0"?>\n' )
  349. os.write( fd, '<toolbox>\n' )
  350. for key, item in self.integrated_tool_panel.items():
  351. if item:
  352. if isinstance( item, Tool ):
  353. os.write( fd, ' <tool id="%s" />\n' % item.id )
  354. elif isinstance( item, Workflow ):
  355. os.write( fd, ' <workflow id="%s" />\n' % item.id )
  356. elif isinstance( item, ToolSectionLabel ):
  357. label_id = item.id or ''
  358. label_text = item.text or ''
  359. label_version = item.version or ''
  360. os.write( fd, ' <label id="%s" text="%s" version="%s" />\n' % ( label_id, label_text, label_version ) )
  361. elif isinstance( item, ToolSection ):
  362. section_id = item.id or ''
  363. section_name = item.name or ''
  364. section_version = item.version or ''
  365. os.write( fd, ' <section id="%s" name="%s" version="%s">\n' % ( section_id, section_name, section_version ) )
  366. for section_key, section_item in item.elems.items():
  367. if isinstance( section_item, Tool ):
  368. if section_item:
  369. os.write( fd, ' <tool id="%s" />\n' % section_item.id )
  370. elif isinstance( section_item, Workflow ):
  371. if section_item:
  372. os.write( fd, ' <workflow id="%s" />\n' % section_item.id )
  373. elif isinstance( section_item, ToolSectionLabel ):
  374. if section_item:
  375. label_id = section_item.id or ''
  376. label_text = section_item.text or ''
  377. label_version = section_item.version or ''
  378. os.write( fd, ' <label id="%s" text="%s" version="%s" />\n' % ( label_id, label_text, label_version ) )
  379. os.write( fd, ' </section>\n' )
  380. os.write( fd, '</toolbox>\n' )
  381. os.close( fd )
  382. shutil.move( filename, os.path.abspath( self.integrated_tool_panel_config ) )
  383. os.chmod( self.integrated_tool_panel_config, 0644 )
  384. def get_tool( self, tool_id, tool_version=None, get_all_versions=False ):
  385. """Attempt to locate a tool in the tool box."""
  386. if tool_id in self.tools_by_id and not get_all_versions:
  387. #tool_id exactly matches an available tool by id (which is 'old' tool_id or guid)
  388. return self.tools_by_id[ tool_id ]
  389. #exact tool id match not found, or all versions requested, search for other options, e.g. migrated tools or different versions
  390. rval = []
  391. tv = self.__get_tool_version( tool_id )
  392. if tv:
  393. tool_version_ids = tv.get_version_ids( self.app )
  394. for tool_version_id in tool_version_ids:
  395. if tool_version_id in self.tools_by_id:
  396. rval.append( self.tools_by_id[ tool_version_id ] )
  397. if not rval:
  398. #still no tool, do a deeper search and try to match by old ids
  399. for tool in self.tools_by_id.itervalues():
  400. if tool.old_id == tool_id:
  401. rval.append( tool )
  402. if rval:
  403. if get_all_versions:
  404. return rval
  405. else:
  406. if tool_version:
  407. #return first tool with matching version
  408. for tool in rval:
  409. if tool.version == tool_version:
  410. return tool
  411. #No tool matches by version, simply return the first available tool found
  412. return rval[0]
  413. #We now likely have a Toolshed guid passed in, but no supporting database entries
  414. #If the tool exists by exact id and is loaded then provide exact match within a list
  415. if tool_id in self.tools_by_id:
  416. return[ self.tools_by_id[ tool_id ] ]
  417. return None
  418. def get_loaded_tools_by_lineage( self, tool_id ):
  419. """Get all loaded tools associated by lineage to the tool whose id is tool_id."""
  420. tv = self.__get_tool_version( tool_id )
  421. if tv:
  422. tool_version_ids = tv.get_version_ids( self.app )
  423. available_tool_versions = []
  424. for tool_version_id in tool_version_ids:
  425. if tool_version_id in self.tools_by_id:
  426. available_tool_versions.append( self.tools_by_id[ tool_version_id ] )
  427. return available_tool_versions
  428. else:
  429. if tool_id in self.tools_by_id:
  430. tool = self.tools_by_id[ tool_id ]
  431. return [ tool ]
  432. return []
  433. def __get_tool_version( self, tool_id ):
  434. """Return a ToolVersion if one exists for the tool_id"""
  435. return self.app.install_model.context.query( self.app.install_model.ToolVersion ) \
  436. .filter( self.app.install_model.ToolVersion.table.c.tool_id == tool_id ) \
  437. .first()
  438. def __get_tool_shed_repository( self, tool_shed, name, owner, installed_changeset_revision ):
  439. return self.app.install_model.context.query( self.app.install_model.ToolShedRepository ) \
  440. .filter( and_( self.app.install_model.ToolShedRepository.table.c.tool_shed == tool_shed,
  441. self.app.install_model.ToolShedRepository.table.c.name == name,
  442. self.app.install_model.ToolShedRepository.table.c.owner == owner,
  443. self.app.install_model.ToolShedRepository.table.c.installed_changeset_revision == installed_changeset_revision ) ) \
  444. .first()
  445. def get_tool_components( self, tool_id, tool_version=None, get_loaded_tools_by_lineage=False, set_selected=False ):
  446. """
  447. Retrieve all loaded versions of a tool from the toolbox and return a select list enabling
  448. selection of a different version, the list of the tool's loaded versions, and the specified tool.
  449. """
  450. toolbox = self
  451. tool_version_select_field = None
  452. tools = []
  453. tool = None
  454. # Backwards compatibility for datasource tools that have default tool_id configured, but which
  455. # are now using only GALAXY_URL.
  456. tool_ids = listify( tool_id )
  457. for tool_id in tool_ids:
  458. if get_loaded_tools_by_lineage:
  459. tools = toolbox.get_loaded_tools_by_lineage( tool_id )
  460. else:
  461. tools = toolbox.get_tool( tool_id, tool_version=tool_version, get_all_versions=True )
  462. if tools:
  463. tool = toolbox.get_tool( tool_id, tool_version=tool_version, get_all_versions=False )
  464. if len( tools ) > 1:
  465. tool_version_select_field = self.build_tool_version_select_field( tools, tool.id, set_selected )
  466. break
  467. return tool_version_select_field, tools, tool
  468. def build_tool_version_select_field( self, tools, tool_id, set_selected ):
  469. """Build a SelectField whose options are the ids for the received list of tools."""
  470. options = []
  471. refresh_on_change_values = []
  472. for tool in tools:
  473. options.insert( 0, ( tool.version, tool.id ) )
  474. refresh_on_change_values.append( tool.id )
  475. select_field = SelectField( name='tool_id', refresh_on_change=True, refresh_on_change_values=refresh_on_change_values )
  476. for option_tup in options:
  477. selected = set_selected and option_tup[ 1 ] == tool_id
  478. if selected:
  479. select_field.add_option( 'version %s' % option_tup[ 0 ], option_tup[ 1 ], selected=True )
  480. else:
  481. select_field.add_option( 'version %s' % option_tup[ 0 ], option_tup[ 1 ] )
  482. return select_field
  483. def load_tool_tag_set( self, elem, panel_dict, integrated_panel_dict, tool_path, load_panel_dict, guid=None, index=None ):
  484. try:
  485. path = elem.get( "file" )
  486. repository_id = None
  487. if guid is None:
  488. tool_shed_repository = None
  489. can_load_into_panel_dict = True
  490. else:
  491. # The tool is contained in an installed tool shed repository, so load
  492. # the tool only if the repository has not been marked deleted.
  493. tool_shed = elem.find( "tool_shed" ).text
  494. repository_name = elem.find( "repository_name" ).text
  495. repository_owner = elem.find( "repository_owner" ).text
  496. installed_changeset_revision_elem = elem.find( "installed_changeset_revision" )
  497. if installed_changeset_revision_elem is None:
  498. # Backward compatibility issue - the tag used to be named 'changeset_revision'.
  499. installed_changeset_revision_elem = elem.find( "changeset_revision" )
  500. installed_changeset_revision = installed_changeset_revision_elem.text
  501. tool_shed_repository = self.__get_tool_shed_repository( tool_shed, repository_name, repository_owner, installed_changeset_revision )
  502. if tool_shed_repository:
  503. # Only load tools if the repository is not deactivated or uninstalled.
  504. can_load_into_panel_dict = not tool_shed_repository.deleted
  505. repository_id = self.app.security.encode_id( tool_shed_repository.id )
  506. else:
  507. # If there is not yet a tool_shed_repository record, we're in the process of installing
  508. # a new repository, so any included tools can be loaded into the tool panel.
  509. can_load_into_panel_dict = True
  510. tool = self.load_tool( os.path.join( tool_path, path ), guid=guid, repository_id=repository_id )
  511. key = 'tool_%s' % str( tool.id )
  512. if can_load_into_panel_dict:
  513. if guid is not None:
  514. tool.tool_shed = tool_shed
  515. tool.repository_name = repository_name
  516. tool.repository_owner = repository_owner
  517. tool.installed_changeset_revision = installed_changeset_revision
  518. tool.guid = guid
  519. tool.version = elem.find( "version" ).text
  520. # Make sure the tool has a tool_version.
  521. if not self.__get_tool_version( tool.id ):
  522. tool_version = self.app.install_model.ToolVersion( tool_id=tool.id, tool_shed_repository=tool_shed_repository )
  523. self.app.install_model.context.add( tool_version )
  524. self.app.install_model.context.flush()
  525. # Load the tool's lineage ids.
  526. tool.lineage_ids = tool.tool_version.get_version_ids( self.app )
  527. if self.app.config.get_bool( 'enable_tool_tags', False ):
  528. tag_names = elem.get( "tags", "" ).split( "," )
  529. for tag_name in tag_names:
  530. if tag_name == '':
  531. continue
  532. tag = self.sa_session.query( self.app.model.Tag ).filter_by( name=tag_name ).first()
  533. if not tag:
  534. tag = self.app.model.Tag( name=tag_name )
  535. self.sa_session.add( tag )
  536. self.sa_session.flush()
  537. tta = self.app.model.ToolTagAssociation( tool_id=tool.id, tag_id=tag.id )
  538. self.sa_session.add( tta )
  539. self.sa_session.flush()
  540. else:
  541. for tagged_tool in tag.tagged_tools:
  542. if tagged_tool.tool_id == tool.id:
  543. break
  544. else:
  545. tta = self.app.model.ToolTagAssociation( tool_id=tool.id, tag_id=tag.id )
  546. self.sa_session.add( tta )
  547. self.sa_session.flush()
  548. # Allow for the same tool to be loaded into multiple places in the tool panel. We have to handle
  549. # the case where the tool is contained in a repository installed from the tool shed, and the Galaxy
  550. # administrator has retrieved updates to the installed repository. In this case, the tool may have
  551. # been updated, but the version was not changed, so the tool should always be reloaded here. We used
  552. # to only load the tool if it's it was not found in self.tools_by_id, but performing that check did
  553. # not enable this scenario.
  554. self.tools_by_id[ tool.id ] = tool
  555. if load_panel_dict:
  556. self.__add_tool_to_tool_panel( tool, panel_dict, section=isinstance( panel_dict, ToolSection ) )
  557. # Always load the tool into the integrated_panel_dict, or it will not be included in the integrated_tool_panel.xml file.
  558. if key in integrated_panel_dict or index is None:
  559. integrated_panel_dict[ key ] = tool
  560. else:
  561. integrated_panel_dict.insert( index, key, tool )
  562. except:
  563. log.exception( "Error reading tool from path: %s" % path )
  564. def load_workflow_tag_set( self, elem, panel_dict, integrated_panel_dict, load_panel_dict, index=None ):
  565. try:
  566. # TODO: should id be encoded?
  567. workflow_id = elem.get( 'id' )
  568. workflow = self.load_workflow( workflow_id )
  569. self.workflows_by_id[ workflow_id ] = workflow
  570. key = 'workflow_' + workflow_id
  571. if load_panel_dict:
  572. panel_dict[ key ] = workflow
  573. # Always load workflows into the integrated_panel_dict.
  574. if key in integrated_panel_dict or index is None:
  575. integrated_panel_dict[ key ] = workflow
  576. else:
  577. integrated_panel_dict.insert( index, key, workflow )
  578. except:
  579. log.exception( "Error loading workflow: %s" % workflow_id )
  580. def load_label_tag_set( self, elem, panel_dict, integrated_panel_dict, load_panel_dict, index=None ):
  581. label = ToolSectionLabel( elem )
  582. key = 'label_' + label.id
  583. if load_panel_dict:
  584. panel_dict[ key ] = label
  585. if key in integrated_panel_dict or index is None:
  586. integrated_panel_dict[ key ] = label
  587. else:
  588. integrated_panel_dict.insert( index, key, label )
  589. def load_section_tag_set( self, elem, tool_path, load_panel_dict, index=None ):
  590. key = elem.get( "id" )
  591. if key in self.tool_panel:
  592. section = self.tool_panel[ key ]
  593. elems = section.elems
  594. else:
  595. section = ToolSection( elem )
  596. elems = section.elems
  597. if key in self.integrated_tool_panel:
  598. integrated_section = self.integrated_tool_panel[ key ]
  599. integrated_elems = integrated_section.elems
  600. else:
  601. integrated_section = ToolSection( elem )
  602. integrated_elems = integrated_section.elems
  603. for sub_index, sub_elem in enumerate( elem ):
  604. if sub_elem.tag == 'tool':
  605. self.load_tool_tag_set( sub_elem, elems, integrated_elems, tool_path, load_panel_dict, guid=sub_elem.get( 'guid' ), index=sub_index )
  606. elif sub_elem.tag == 'workflow':
  607. self.load_workflow_tag_set( sub_elem, elems, integrated_elems, load_panel_dict, index=sub_index )
  608. elif sub_elem.tag == 'label':
  609. self.load_label_tag_set( sub_elem, elems, integrated_elems, load_panel_dict, index=sub_index )
  610. if load_panel_dict:
  611. self.tool_panel[ key ] = section
  612. # Always load sections into the integrated_tool_panel.
  613. if key in self.integrated_tool_panel or index is None:
  614. self.integrated_tool_panel[ key ] = integrated_section
  615. else:
  616. self.integrated_tool_panel.insert( index, key, integrated_section )
  617. def load_tool( self, config_file, guid=None, repository_id=None, **kwds ):
  618. """Load a single tool from the file named by `config_file` and return an instance of `Tool`."""
  619. # Parse XML configuration file and get the root element
  620. tree = load_tool( config_file )
  621. root = tree.getroot()
  622. # Allow specifying a different tool subclass to instantiate
  623. if root.find( "type" ) is not None:
  624. type_elem = root.find( "type" )
  625. module = type_elem.get( 'module', 'galaxy.tools' )
  626. cls = type_elem.get( 'class' )
  627. mod = __import__( module, globals(), locals(), [cls] )
  628. ToolClass = getattr( mod, cls )
  629. elif root.get( 'tool_type', None ) is not None:
  630. ToolClass = tool_types.get( root.get( 'tool_type' ) )
  631. else:
  632. ToolClass = Tool
  633. return ToolClass( config_file, root, self.app, guid=guid, repository_id=repository_id, **kwds )
  634. def reload_tool_by_id( self, tool_id ):
  635. """
  636. Attempt to reload the tool identified by 'tool_id', if successful
  637. replace the old tool.
  638. """
  639. if tool_id not in self.tools_by_id:
  640. message = "No tool with id %s" % tool_id
  641. status = 'error'
  642. else:
  643. old_tool = self.tools_by_id[ tool_id ]
  644. new_tool = self.load_tool( old_tool.config_file )
  645. # The tool may have been installed from a tool shed, so set the tool shed attributes.
  646. # Since the tool version may have changed, we don't override it here.
  647. new_tool.id = old_tool.id
  648. new_tool.guid = old_tool.guid
  649. new_tool.tool_shed = old_tool.tool_shed
  650. new_tool.repository_name = old_tool.repository_name
  651. new_tool.repository_owner = old_tool.repository_owner
  652. new_tool.installed_changeset_revision = old_tool.installed_changeset_revision
  653. new_tool.old_id = old_tool.old_id
  654. # Replace old_tool with new_tool in self.tool_panel
  655. tool_key = 'tool_' + tool_id
  656. for key, val in self.tool_panel.items():
  657. if key == tool_key:
  658. self.tool_panel[ key ] = new_tool
  659. break
  660. elif key.startswith( 'section' ):
  661. if tool_key in val.elems:
  662. self.tool_panel[ key ].elems[ tool_key ] = new_tool
  663. break
  664. self.tools_by_id[ tool_id ] = new_tool
  665. message = "Reloaded the tool:<br/>"
  666. message += "<b>name:</b> %s<br/>" % old_tool.name
  667. message += "<b>id:</b> %s<br/>" % old_tool.id
  668. message += "<b>version:</b> %s" % old_tool.version
  669. status = 'done'
  670. return message, status
  671. def remove_tool_by_id( self, tool_id ):
  672. """
  673. Attempt to remove the tool identified by 'tool_id'.
  674. """
  675. if tool_id not in self.tools_by_id:
  676. message = "No tool with id %s" % tool_id
  677. status = 'error'
  678. else:
  679. tool = self.tools_by_id[ tool_id ]
  680. del self.tools_by_id[ tool_id ]
  681. tool_key = 'tool_' + tool_id
  682. for key, val in self.tool_panel.items():
  683. if key == tool_key:
  684. del self.tool_panel[ key ]
  685. break
  686. elif key.startswith( 'section' ):
  687. if tool_key in val.elems:
  688. del self.tool_panel[ key ].elems[ tool_key ]
  689. break
  690. if tool_id in self.data_manager_tools:
  691. del self.data_manager_tools[ tool_id ]
  692. #TODO: do we need to manually remove from the integrated panel here?
  693. message = "Removed the tool:<br/>"
  694. message += "<b>name:</b> %s<br/>" % tool.name
  695. message += "<b>id:</b> %s<br/>" % tool.id
  696. message += "<b>version:</b> %s" % tool.version
  697. status = 'done'
  698. return message, status
  699. def load_workflow( self, workflow_id ):
  700. """
  701. Return an instance of 'Workflow' identified by `id`,
  702. which is encoded in the tool panel.
  703. """
  704. id = self.app.security.decode_id( workflow_id )
  705. stored = self.app.model.context.query( self.app.model.StoredWorkflow ).get( id )
  706. return stored.latest_workflow
  707. def init_dependency_manager( self ):
  708. self.dependency_manager = build_dependency_manager( self.app.config )
  709. @property
  710. def sa_session( self ):
  711. """
  712. Returns a SQLAlchemy session
  713. """
  714. return self.app.model.context
  715. def to_dict( self, trans, in_panel=True, **kwds ):
  716. """
  717. to_dict toolbox.
  718. """
  719. context = Bunch( toolbox=self, trans=trans, **kwds )
  720. if in_panel:
  721. panel_elts = [ val for val in self.tool_panel.itervalues() ]
  722. filters = self.filter_factory.build_filters( trans, **kwds )
  723. filtered_panel_elts = []
  724. for index, elt in enumerate( panel_elts ):
  725. elt = _filter_for_panel( elt, filters, context )
  726. if elt:
  727. filtered_panel_elts.append( elt )
  728. panel_elts = filtered_panel_elts
  729. # Produce panel.
  730. rval = []
  731. kwargs = dict(
  732. trans=trans,
  733. link_details=True
  734. )
  735. for elt in panel_elts:
  736. rval.append( to_dict_helper( elt, kwargs ) )
  737. else:
  738. tools = []
  739. for id, tool in self.tools_by_id.items():
  740. tools.append( tool.to_dict( trans, link_details=True ) )
  741. rval = tools
  742. return rval
  743. def _filter_for_panel( item, filters, context ):
  744. """
  745. Filters tool panel elements so that only those that are compatible
  746. with provided filters are kept.
  747. """
  748. def _apply_filter( filter_item, filter_list ):
  749. for filter_method in filter_list:
  750. if not filter_method( context, filter_item ):
  751. return False
  752. return True
  753. if isinstance( item, Tool ):
  754. if _apply_filter( item, filters[ 'tool' ] ):
  755. return item
  756. elif isinstance( item, ToolSectionLabel ):
  757. if _apply_filter( item, filters[ 'label' ] ):
  758. return item
  759. elif isinstance( item, ToolSection ):
  760. # Filter section item-by-item. Only show a label if there are
  761. # non-filtered tools below it.
  762. if _apply_filter( item, filters[ 'section' ] ):
  763. cur_label_key = None
  764. tools_under_label = False
  765. filtered_elems = item.elems.copy()
  766. for key, section_item in item.elems.items():
  767. if isinstance( section_item, Tool ):
  768. # Filter tool.
  769. if _apply_filter( section_item, filters[ 'tool' ] ):
  770. tools_under_label = True
  771. else:
  772. del filtered_elems[ key ]
  773. elif isinstance( section_item, ToolSectionLabel ):
  774. # If there is a label and it does not have tools,
  775. # remove it.
  776. if ( cur_label_key and not tools_under_label ) or not _apply_filter( section_item, filters[ 'label' ] ):
  777. del filtered_elems[ cur_label_key ]
  778. # Reset attributes for new label.
  779. cur_label_key = key
  780. tools_under_label = False
  781. # Handle last label.
  782. if cur_label_key and not tools_under_label:
  783. del filtered_elems[ cur_label_key ]
  784. # Only return section if there are elements.
  785. if len( filtered_elems ) != 0:
  786. copy = item.copy()
  787. copy.elems = filtered_elems
  788. return copy
  789. return None
  790. class ToolSection( object, Dictifiable ):
  791. """
  792. A group of tools with similar type/purpose that will be displayed as a
  793. group in the user interface.
  794. """
  795. dict_collection_visible_keys = ( 'id', 'name', 'version' )
  796. def __init__( self, elem=None ):
  797. f = lambda elem, val: elem is not None and elem.get( val ) or ''
  798. self.name = f( elem, 'name' )
  799. self.id = f( elem, 'id' )
  800. self.version = f( elem, 'version' )
  801. self.elems = odict()
  802. def copy( self ):
  803. copy = ToolSection()
  804. copy.name = self.name
  805. copy.id = self.id
  806. copy.version = self.version
  807. copy.elems = self.elems.copy()
  808. return copy
  809. def to_dict( self, trans, link_details=False ):
  810. """ Return a dict that includes section's attributes. """
  811. section_dict = super( ToolSection, self ).to_dict()
  812. section_elts = []
  813. kwargs = dict(
  814. trans=trans,
  815. link_details=link_details
  816. )
  817. for elt in self.elems.values():
  818. section_elts.append( to_dict_helper( elt, kwargs ) )
  819. section_dict[ 'elems' ] = section_elts
  820. return section_dict
  821. class ToolSectionLabel( object, Dictifiable ):
  822. """
  823. A label for a set of tools that can be displayed above groups of tools
  824. and sections in the user interface
  825. """
  826. dict_collection_visible_keys = ( 'id', 'text', 'version' )
  827. def __init__( self, elem ):
  828. self.text = elem.get( "text" )
  829. self.id = elem.get( "id" )
  830. self.version = elem.get( "version" ) or ''
  831. class DefaultToolState( object ):
  832. """
  833. Keeps track of the state of a users interaction with a tool between
  834. requests. The default tool state keeps track of the current page (for
  835. multipage "wizard" tools) and the values of all
  836. """
  837. def __init__( self ):
  838. self.page = 0
  839. self.rerun_remap_job_id = None
  840. self.inputs = None
  841. def encode( self, tool, app, secure=True ):
  842. """
  843. Convert the data to a string
  844. """
  845. # Convert parameters to a dictionary of strings, and save curent
  846. # page in that dict
  847. value = params_to_strings( tool.inputs, self.inputs, app )
  848. value["__page__"] = self.page
  849. value["__rerun_remap_job_id__"] = self.rerun_remap_job_id
  850. value = json.dumps( value )
  851. # Make it secure
  852. if secure:
  853. a = hmac_new( app.config.tool_secret, value )
  854. b = binascii.hexlify( value )
  855. return "%s:%s" % ( a, b )
  856. else:
  857. return value
  858. def decode( self, value, tool, app, secure=True ):
  859. """
  860. Restore the state from a string
  861. """
  862. if secure:
  863. # Extract and verify hash
  864. a, b = value.split( ":" )
  865. value = binascii.unhexlify( b )
  866. test = hmac_new( app.config.tool_secret, value )
  867. assert a == test
  868. # Restore from string
  869. values = json_fix( json.loads( value ) )
  870. self.page = values.pop( "__page__" )
  871. if '__rerun_remap_job_id__' in values:
  872. self.rerun_remap_job_id = values.pop( "__rerun_remap_job_id__" )
  873. else:
  874. self.rerun_remap_job_id = None
  875. self.inputs = params_from_strings( tool.inputs, values, app, ignore_errors=True )
  876. class ToolOutput( object, Dictifiable ):
  877. """
  878. Represents an output datasets produced by a tool. For backward
  879. compatibility this behaves as if it were the tuple::
  880. (format, metadata_source, parent)
  881. """
  882. dict_collection_visible_keys = ( 'name', 'format', 'label', 'hidden' )
  883. def __init__( self, name, format=None, format_source=None, metadata_source=None,
  884. parent=None, label=None, filters=None, actions=None, hidden=False ):
  885. self.name = name
  886. self.format = format
  887. self.format_source = format_source
  888. self.metadata_source = metadata_source
  889. self.parent = parent
  890. self.label = label
  891. self.filters = filters or []
  892. self.actions = actions
  893. self.hidden = hidden
  894. # Tuple emulation
  895. def __len__( self ):
  896. return 3
  897. def __getitem__( self, index ):
  898. if index == 0:
  899. return self.format
  900. elif index == 1:
  901. return self.metadata_source
  902. elif index == 2:
  903. return self.parent
  904. else:
  905. raise IndexError( index )
  906. def __iter__( self ):
  907. return iter( ( self.format, self.metadata_source, self.parent ) )
  908. class Tool( object, Dictifiable ):
  909. """
  910. Represents a computational tool that can be executed through Galaxy.
  911. """
  912. tool_type = 'default'
  913. requires_setting_metadata = True
  914. default_tool_action = DefaultToolAction
  915. dict_collection_visible_keys = ( 'id', 'name', 'version', 'description' )
  916. def __init__( self, config_file, root, app, guid=None, repository_id=None ):
  917. """Load a tool from the config named by `config_file`"""
  918. # Determine the full path of the directory where the tool config is
  919. self.config_file = config_file
  920. self.tool_dir = os.path.dirname( config_file )
  921. self.app = app
  922. self.repository_id = repository_id
  923. #setup initial attribute values
  924. self.inputs = odict()
  925. self.stdio_exit_codes = list()
  926. self.stdio_regexes = list()
  927. self.inputs_by_page = list()
  928. self.display_by_page = list()
  929. self.action = '/tool_runner/index'
  930. self.target = 'galaxy_main'
  931. self.method = 'post'
  932. self.check_values = True
  933. self.nginx_upload = False
  934. self.input_required = False
  935. self.display_interface = True
  936. self.require_login = False
  937. self.rerun = False
  938. # Define a place to keep track of all input These
  939. # differ from the inputs dictionary in that inputs can be page
  940. # elements like conditionals, but input_params are basic form
  941. # parameters like SelectField objects. This enables us to more
  942. # easily ensure that parameter dependencies like index files or
  943. # tool_data_table_conf.xml entries exist.
  944. self.input_params = []
  945. # Attributes of tools installed from Galaxy tool sheds.
  946. self.tool_shed = None
  947. self.repository_name = None
  948. self.repository_owner = None
  949. self.installed_changeset_revision = None
  950. # The tool.id value will be the value of guid, but we'll keep the
  951. # guid attribute since it is useful to have.
  952. self.guid = guid
  953. self.old_id = None
  954. self.version = None
  955. # Enable easy access to this tool's version lineage.
  956. self.lineage_ids = []
  957. #populate toolshed repository info, if available
  958. self.populate_tool_shed_info()
  959. # Parse XML element containing configuration
  960. self.parse( root, guid=guid )
  961. self.external_runJob_script = app.config.drmaa_external_runjob_script
  962. @property
  963. def sa_session( self ):
  964. """Returns a SQLAlchemy session"""
  965. return self.app.model.context
  966. @property
  967. def tool_version( self ):
  968. """Return a ToolVersion if one exists for our id"""
  969. return self.app.install_model.context.query( self.app.install_model.ToolVersion ) \
  970. .filter( self.app.install_model.ToolVersion.table.c.tool_id == self.id ) \
  971. .first()
  972. @property
  973. def tool_versions( self ):
  974. # If we have versions, return them.
  975. tool_version = self.tool_version
  976. if tool_version:
  977. return tool_version.get_versions( self.app )
  978. return []
  979. @property
  980. def tool_version_ids( self ):
  981. # If we have versions, return a list of their tool_ids.
  982. tool_version = self.tool_version
  983. if tool_version:
  984. return tool_version.get_version_ids( self.app )
  985. return []
  986. @property
  987. def tool_shed_repository( self ):
  988. # If this tool is included in an installed tool shed repository, return it.
  989. if self.tool_shed:
  990. return suc.get_tool_shed_repository_by_shed_name_owner_installed_changeset_revision( self.app,
  991. self.tool_shed,
  992. self.repository_name,
  993. self.repository_owner,
  994. self.installed_changeset_revision )
  995. return None
  996. def __get_job_tool_configuration(self, job_params=None):
  997. """Generalized method for getting this tool's job configuration.
  998. :type job_params: dict or None
  999. :returns: `galaxy.jobs.JobToolConfiguration` -- JobToolConfiguration that matches this `Tool` and the given `job_params`
  1000. """
  1001. rval = None
  1002. if len(self.job_tool_configurations) == 1:
  1003. # If there's only one config, use it rather than wasting time on comparisons
  1004. rval = self.job_tool_configurations[0]
  1005. elif job_params is None:
  1006. for job_tool_config in self.job_tool_configurations:
  1007. if not job_tool_config.params:
  1008. rval = job_tool_config
  1009. break
  1010. else:
  1011. for job_tool_config in self.job_tool_configurations:
  1012. if job_tool_config.params:
  1013. # There are job params and this config has params defined
  1014. for param, value in job_params.items():
  1015. if param not in job_tool_config.params or job_tool_config.params[param] != job_params[param]:
  1016. break
  1017. else:
  1018. # All params match, use this config
  1019. rval = job_tool_config
  1020. break
  1021. else:
  1022. rval = job_tool_config
  1023. assert rval is not None, 'Could not get a job tool configuration for Tool %s with job_params %s, this is a bug' % (self.id, job_params)
  1024. return rval
  1025. def get_job_handler(self, job_params=None):
  1026. """Get a suitable job handler for this `Tool` given the provided `job_params`. If multiple handlers are valid for combination of `Tool` and `job_params` (e.g. the defined handler is a handler tag), one will be selected at random.
  1027. :param job_params: Any params specific to this job (e.g. the job source)
  1028. :type job_params: dict or None
  1029. :returns: str -- The id of a job handler for a job run of this `Tool`
  1030. """
  1031. # convert tag to ID if necessary
  1032. return self.app.job_config.get_handler(self.__get_job_tool_configuration(job_params=job_params).handler)
  1033. def get_job_destination(self, job_params=None):
  1034. """
  1035. :returns: galaxy.jobs.JobDestination -- The destination definition and runner parameters.
  1036. """
  1037. return self.app.job_config.get_destination(self.__get_job_tool_configuration(job_params=job_params).destination)
  1038. def get_panel_section( self ):
  1039. for key, item in self.app.toolbox.integrated_tool_panel.items():
  1040. if item:
  1041. if isinstance( item, Tool ):
  1042. if item.id == self.id:
  1043. return '', ''
  1044. if isinstance( item, ToolSection ):
  1045. section_id = item.id or ''
  1046. section_name = item.name or ''
  1047. for section_key, section_item in item.elems.items():
  1048. if isinstance( section_item, Tool ):
  1049. if section_item:
  1050. if section_item.id == self.id:
  1051. return section_id, section_name
  1052. return None, None
  1053. def parse( self, root, guid=None ):
  1054. """
  1055. Read tool configuration from the element `root` and fill in `self`.
  1056. """
  1057. # Get the (user visible) name of the tool
  1058. self.name = root.get( "name" )
  1059. if not self.name:
  1060. raise Exception( "Missing tool 'name'" )
  1061. # Get the UNIQUE id for the tool
  1062. self.old_id = root.get( "id" )
  1063. if guid is None:
  1064. self.id = self.old_id
  1065. else:
  1066. self.id = guid
  1067. if not self.id:
  1068. raise Exception( "Missing tool 'id'" )
  1069. self.version = root.get( "version" )
  1070. if not self.version:
  1071. # For backward compatibility, some tools may not have versions yet.
  1072. self.version = "1.0.0"
  1073. # Support multi-byte tools
  1074. self.is_multi_byte = string_as_bool( root.get( "is_multi_byte", False ) )
  1075. # Force history to fully refresh after job execution for this tool.
  1076. # Useful i.e. when an indeterminate number of outputs are created by
  1077. # a tool.
  1078. self.force_history_refresh = string_as_bool( root.get( 'force_history_refresh', 'False' ) )
  1079. self.display_interface = string_as_bool( root.get( 'display_interface', str( self.display_interface ) ) )
  1080. self.require_login = string_as_bool( root.get( 'require_login', str( self.require_login ) ) )
  1081. # Load input translator, used by datasource tools to change names/values of incoming parameters
  1082. self.input_translator = root.find( "request_param_translation" )
  1083. if self.input_translator:
  1084. self.input_translator = ToolInputTranslator.from_element( self.input_translator )
  1085. # Command line (template). Optional for tools that do not invoke a local program
  1086. command = root.find("command")
  1087. if command is not None and command.text is not None:
  1088. self.command = command.text.lstrip() # get rid of leading whitespace
  1089. # Must pre-pend this AFTER processing the cheetah command template
  1090. self.interpreter = command.get( "interpreter", None )
  1091. else:
  1092. self.command = ''
  1093. self.interpreter = None
  1094. # Parameters used to build URL for redirection to external app
  1095. redirect_url_params = root.find( "redirect_url_params" )
  1096. if redirect_url_params is not None and redirect_url_params.text is not None:
  1097. # get rid of leading / trailing white space
  1098. redirect_url_params = redirect_url_params.text.strip()
  1099. # Replace remaining white space with something we can safely split on later
  1100. # when we are building the params
  1101. self.redirect_url_params = redirect_url_params.replace( ' ', '**^**' )
  1102. else:
  1103. self.redirect_url_params = ''
  1104. # Short description of the tool
  1105. self.description = xml_text(root, "description")
  1106. # Versioning for tools
  1107. self.version_string_cmd = None
  1108. version_cmd = root.find("version_command")
  1109. if version_cmd is not None:
  1110. self.version_string_cmd = version_cmd.text.strip()
  1111. version_cmd_interpreter = version_cmd.get( "interpreter", None )
  1112. if version_cmd_interpreter:
  1113. executable = self.version_string_cmd.split()[0]
  1114. abs_executable = os.path.abspath(os.path.join(self.tool_dir, executable))
  1115. command_line = self.version_string_cmd.replace(executable, abs_executable, 1)
  1116. self.version_string_cmd = self.interpreter + " " + command_line
  1117. # Parallelism for tasks, read from tool config.
  1118. parallelism = root.find("parallelism")
  1119. if parallelism is not None and parallelism.get("method"):
  1120. self.parallelism = ParallelismInfo(parallelism)
  1121. else:
  1122. self.parallelism = None
  1123. # Get JobToolConfiguration(s) valid for this particular Tool. At least
  1124. # a 'default' will be provided that uses the 'default' handler and
  1125. # 'default' destination. I thought about moving this to the
  1126. # job_config, but it makes more sense to store here. -nate
  1127. self_ids = [ self.id.lower() ]
  1128. if self.old_id != self.id:
  1129. # Handle toolshed guids
  1130. self_ids = [ self.id.lower(), self.id.lower().rsplit('/', 1)[0], self.old_id.lower() ]
  1131. self.all_ids = self_ids
  1132. # In the toolshed context, there is no job config.
  1133. if 'job_config' in dir(self.app):
  1134. self.job_tool_configurations = self.app.job_config.get_job_tool_configurations(self_ids)
  1135. # Is this a 'hidden' tool (hidden in tool menu)
  1136. self.hidden = xml_text(root, "hidden")
  1137. if self.hidden:
  1138. self.hidden = string_as_bool(self.hidden)
  1139. # Load any tool specific code (optional) Edit: INS 5/29/2007,
  1140. # allow code files to have access to the individual tool's
  1141. # "module" if it has one. Allows us to reuse code files, etc.
  1142. self.code_namespace = dict()
  1143. self.hook_map = {}
  1144. for code_elem in root.findall("code"):
  1145. for hook_elem in code_elem.findall("hook"):
  1146. for key, value in hook_elem.items():
  1147. # map hook to function
  1148. self.hook_map[key] = value
  1149. file_name = code_elem.get("file")
  1150. code_path = os.path.join( self.tool_dir, file_name )
  1151. execfile( code_path, self.code_namespace )
  1152. # Load any tool specific options (optional)
  1153. self.options = dict( sanitize=True, refresh=False )
  1154. for option_elem in root.findall("options"):
  1155. for option, value in self.options.copy().items():
  1156. if isinstance(value, type(False)):
  1157. self.options[option] = string_as_bool(option_elem.get(option, str(value)))
  1158. else:
  1159. self.options[option] = option_elem.get(option, str(value))
  1160. self.options = Bunch(** self.options)
  1161. # Parse tool inputs (if there are any required)
  1162. self.parse_inputs( root )
  1163. # Parse tool help
  1164. self.parse_help( root )
  1165. # Description of outputs produced by an invocation of the tool
  1166. self.parse_outputs( root )
  1167. # Parse result handling for tool exit codes and stdout/stderr messages:
  1168. self.parse_stdio( root )
  1169. # Any extra generated config files for the tool
  1170. self.config_files = []
  1171. conf_parent_elem = root.find("configfiles")
  1172. if conf_parent_elem:
  1173. for conf_elem in conf_parent_elem.findall( "configfile" ):
  1174. name = conf_elem.get( "name" )
  1175. filename = conf_elem.get( "filename", None )
  1176. text = conf_elem.text
  1177. self.config_files.append( ( name, filename, text ) )
  1178. # Action
  1179. action_elem = root.find( "action" )
  1180. if action_elem is None:
  1181. self.tool_action = self.default_tool_action()
  1182. else:
  1183. module = action_elem.get( 'module' )
  1184. cls = action_elem.get( 'class' )
  1185. mod = __import__( module, globals(), locals(), [cls])
  1186. self.tool_action = getattr( mod, cls )()
  1187. # User interface hints
  1188. self.uihints = {}
  1189. uihints_elem = root.find( "uihints" )
  1190. if uihints_elem is not None:
  1191. for key, value in uihints_elem.attrib.iteritems():
  1192. self.uihints[ key ] = value
  1193. # Tests
  1194. self.__tests_elem = root.find( "tests" )
  1195. self.__tests_populated = False
  1196. # Requirements (dependencies)
  1197. self.requirements = parse_requirements_from_xml( root )
  1198. # Determine if this tool can be used in workflows
  1199. self.is_workflow_compatible = self.check_workflow_compatible(root)
  1200. # Trackster configuration.
  1201. trackster_conf = root.find( "trackster_conf" )
  1202. if trackster_conf is not None:
  1203. self.trackster_conf = TracksterConfig.parse( trackster_conf )
  1204. else:
  1205. self.trackster_conf = None
  1206. @property
  1207. def tests( self ):
  1208. if not self.__tests_populated:
  1209. tests_elem = self.__tests_elem
  1210. if tests_elem:
  1211. try:
  1212. self.__tests = parse_tests_elem( self, tests_elem )
  1213. except:
  1214. log.exception( "Failed to parse tool tests" )
  1215. else:
  1216. self.__tests = None
  1217. self.__tests_populated = True
  1218. return self.__tests
  1219. def parse_inputs( self, root ):
  1220. """
  1221. Parse the "<inputs>" element and create appropriate `ToolParameter`s.
  1222. This implementation supports multiple pages and grouping constructs.
  1223. """
  1224. # Load parameters (optional)
  1225. input_elem = root.find("inputs")
  1226. enctypes = set()
  1227. if input_elem:
  1228. # Handle properties of the input form
  1229. self.check_values = string_as_bool( input_elem.get("check_values", self.check_values ) )
  1230. self.nginx_upload = string_as_bool( input_elem.get( "nginx_upload", self.nginx_upload ) )
  1231. self.action = input_elem.get( 'action', self.action )
  1232. # If we have an nginx upload, save the action as a tuple instead of
  1233. # a string. The actual action needs to get url_for run to add any
  1234. # prefixes, and we want to avoid adding the prefix to the
  1235. # nginx_upload_path. This logic is handled in the tool_form.mako
  1236. # template.
  1237. if self.nginx_upload and self.app.config.nginx_upload_path:
  1238. if '?' in urllib.unquote_plus( self.action ):
  1239. raise Exception( 'URL parameters in a non-default tool action can not be used ' \
  1240. 'in conjunction with nginx upload. Please convert them to ' \
  1241. 'hidden POST parameters' )
  1242. self.action = (self.app.config.nginx_upload_path + '?nginx_redir=',
  1243. urllib.unquote_plus(self.action))
  1244. self.target = input_elem.get( "target", self.target )
  1245. self.method = input_elem.get( "method", self.method )
  1246. # Parse the actual parameters
  1247. # Handle multiple page case
  1248. pages = input_elem.findall( "page" )
  1249. for page in ( pages or [ input_elem ] ):
  1250. display, inputs = self.parse_input_page( page, enctypes )
  1251. self.inputs_by_page.append( inputs )
  1252. self.inputs.update( inputs )
  1253. self.display_by_page.append( display )
  1254. else:
  1255. self.inputs_by_page.append( self.inputs )
  1256. self.display_by_page.append( None )
  1257. self.display = self.display_by_page[0]
  1258. self.npages = len( self.inputs_by_page )
  1259. self.last_page = len( self.inputs_by_page ) - 1
  1260. self.has_multiple_pages = bool( self.last_page )
  1261. # Determine the needed enctype for the form
  1262. if len( enctypes ) == 0:
  1263. self.enctype = "application/x-www-form-urlencoded"
  1264. elif len( enctypes ) == 1:
  1265. self.enctype = enctypes.pop()
  1266. else:
  1267. raise Exception( "Conflicting required enctypes: %s" % str( enctypes ) )
  1268. # Check if the tool either has no parameters or only hidden (and
  1269. # thus hardcoded) FIXME: hidden parameters aren't
  1270. # parameters at all really, and should be passed in a different
  1271. # way, making this check easier.
  1272. self.template_macro_params = template_macro_params(root)
  1273. for param in self.inputs.values():
  1274. if not isinstance( param, ( HiddenToolParameter, BaseURLToolParameter ) ):
  1275. self.input_required = True
  1276. break
  1277. def parse_help( self, root ):
  1278. """
  1279. Parse the help text for the tool. Formatted in reStructuredText, but
  1280. stored as Mako to allow for dynamic image paths.
  1281. This implementation supports multiple pages.
  1282. """
  1283. # TODO: Allow raw HTML or an external link.
  1284. self.help = root.find("help")
  1285. self.help_by_page = list()
  1286. help_header = ""
  1287. help_footer = ""
  1288. if self.help is not None:
  1289. if self.repository_id and self.help.text.find( '.. image:: ' ) >= 0:
  1290. # Handle tool help image display for tools that are contained in repositories in the tool shed or installed into Galaxy.
  1291. lock = threading.Lock()
  1292. lock.acquire( True )
  1293. try:
  1294. self.help.text = suc.set_image_paths( self.app, self.repository_id, self.help.text )
  1295. except Exception, e:
  1296. log.exception( "Exception in parse_help, so images may not be properly displayed:\n%s" % str( e ) )
  1297. finally:
  1298. lock.release()
  1299. help_pages = self.help.findall( "page" )
  1300. help_header = self.help.text
  1301. try:
  1302. self.help = Template( rst_to_html(self.help.text), input_encoding='utf-8',
  1303. output_encoding='utf-8', default_filters=[ 'decode.utf8' ],
  1304. encoding_errors='replace' )
  1305. except:
  1306. log.exception( "error in help for tool %s" % self.name )
  1307. # Multiple help page case
  1308. if help_pages:
  1309. for help_page in help_pages:
  1310. self.help_by_page.append( help_page.text )
  1311. help_footer = help_footer + help_page.tail
  1312. # Each page has to rendered all-together because of backreferences allowed by rst
  1313. try:
  1314. self.help_by_page = [ Template( rst_to_html( help_header + x + help_footer,
  1315. input_encoding='utf-8', output_encoding='utf-8',
  1316. default_filters=[ 'decode.utf8' ],
  1317. encoding_errors='replace' ) )
  1318. for x in self.help_by_page ]
  1319. except:
  1320. log.exception( "error in multi-page help for tool %s" % self.name )
  1321. # Pad out help pages to match npages ... could this be done better?
  1322. while len( self.help_by_page ) < self.npages:
  1323. self.help_by_page.append( self.help )
  1324. def parse_outputs( self, root ):
  1325. """
  1326. Parse <outputs> elements and fill in self.outputs (keyed by name)
  1327. """
  1328. self.outputs = odict()
  1329. out_elem = root.find("outputs")
  1330. if not out_elem:
  1331. return
  1332. for data_elem in out_elem.findall("data"):
  1333. output = ToolOutput( data_elem.get("name") )
  1334. output.format = data_elem.get("format", "data")
  1335. output.change_format = data_elem.findall("change_format")
  1336. output.format_source = data_elem.get("format_source", None)
  1337. output.metadata_source = data_elem.get("metadata_source", "")
  1338. output.parent = data_elem.get("parent", None)
  1339. output.label = xml_text( data_elem, "label" )
  1340. output.count = int( data_elem.get("count", 1) )
  1341. output.filters = data_elem.findall( 'filter' )
  1342. output.from_work_dir = data_elem.get("from_work_dir", None)
  1343. output.hidden = string_as_bool( data_elem.get("hidden", "") )
  1344. output.tool = self
  1345. output.actions = ToolOutputActionGroup( output, data_elem.find( 'actions' ) )
  1346. self.outputs[ output.name ] = output
  1347. # TODO: Include the tool's name in any parsing warnings.
  1348. def parse_stdio( self, root ):
  1349. """
  1350. Parse <stdio> element(s) and fill in self.return_codes,
  1351. self.stderr_rules, and self.stdout_rules. Return codes have a range
  1352. and an error type (fault or warning). Stderr and stdout rules have
  1353. a regular expression and an error level (fault or warning).
  1354. """
  1355. try:
  1356. self.stdio_exit_codes = list()
  1357. self.stdio_regexes = list()
  1358. # We should have a single <stdio> element, but handle the case for
  1359. # multiples.
  1360. # For every stdio element, add all of the exit_code and regex
  1361. # subelements that we find:
  1362. for stdio_elem in ( root.findall( 'stdio' ) ):
  1363. self.parse_stdio_exit_codes( stdio_elem )
  1364. self.parse_stdio_regexes( stdio_elem )
  1365. except Exception:
  1366. log.error( "Exception in parse_stdio! " + str(sys.exc_info()) )
  1367. def parse_stdio_exit_codes( self, stdio_elem ):
  1368. """
  1369. Parse the tool's <stdio> element's <exit_code> subelements.
  1370. This will add all of those elements, if any, to self.stdio_exit_codes.
  1371. """
  1372. try:
  1373. # Look for all <exit_code> elements. Each exit_code element must
  1374. # have a range/value.
  1375. # Exit-code ranges have precedence over a single exit code.
  1376. # So if there are value and range attributes, we use the range
  1377. # attribute. If there is neither a range nor a value, then print
  1378. # a warning and skip to the next.
  1379. for exit_code_elem in ( stdio_elem.findall( "exit_code" ) ):
  1380. exit_code = ToolStdioExitCode()
  1381. # Each exit code has an optional description that can be
  1382. # part of the "desc" or "description" attributes:
  1383. exit_code.desc = exit_code_elem.get( "desc" )
  1384. if None == exit_code.desc:
  1385. exit_code.desc = exit_code_elem.get( "description" )
  1386. # Parse the error level:
  1387. exit_code.error_level = (
  1388. self.parse_error_level( exit_code_elem.get( "level" )))
  1389. code_range = exit_code_elem.get( "range", "" )
  1390. if None == code_range:
  1391. code_range = exit_code_elem.get( "value", "" )
  1392. if None == code_range:
  1393. log.warning( "Tool stdio exit codes must have "
  1394. + "a range or value" )
  1395. continue
  1396. # Parse the range. We look for:
  1397. # :Y
  1398. # X:
  1399. # X:Y - Split on the colon. We do not allow a colon
  1400. # without a beginning or end, though we could.
  1401. # Also note that whitespace is eliminated.
  1402. # TODO: Turn this into a single match - it should be
  1403. # more efficient.
  1404. code_range = re.sub( "\s", "", code_range )
  1405. code_ranges = re.split( ":", code_range )
  1406. if ( len( code_ranges ) == 2 ):
  1407. if ( None == code_ranges[0] or '' == code_ranges[0] ):
  1408. exit_code.range_start = float( "-inf" )
  1409. else:
  1410. exit_code.range_start = int( code_ranges[0] )
  1411. if ( None == code_ranges[1] or '' == code_ranges[1] ):
  1412. exit_code.range_end = float( "inf" )
  1413. else:
  1414. exit_code.range_end = int( code_ranges[1] )
  1415. # If we got more than one colon, then ignore the exit code.
  1416. elif ( len( code_ranges ) > 2 ):
  1417. log.warning( "Invalid tool exit_code range %s - ignored"
  1418. % code_range )
  1419. continue
  1420. # Else we have a singular value. If it's not an integer, then
  1421. # we'll just write a log message and skip this exit_code.
  1422. else:
  1423. try:
  1424. exit_code.range_start = int( code_range )
  1425. except:
  1426. log.error( code_range )
  1427. log.warning( "Invalid range start for tool's exit_code %s: exit_code ignored" % code_range )
  1428. continue
  1429. exit_code.range_end = exit_code.range_start
  1430. # TODO: Check if we got ">", ">=", "<", or "<=":
  1431. # Check that the range, regardless of how we got it,
  1432. # isn't bogus. If we have two infinite values, then
  1433. # the start must be -inf and the end must be +inf.
  1434. # So at least warn about this situation:
  1435. if ( isinf( exit_code.range_start ) and
  1436. isinf( exit_code.range_end ) ):
  1437. log.warning( "Tool exit_code range %s will match on "
  1438. + "all exit codes" % code_range )
  1439. self.stdio_exit_codes.append( exit_code )
  1440. except Exception:
  1441. log.error( "Exception in parse_stdio_exit_codes! "
  1442. + str(sys.exc_info()) )
  1443. trace = sys.exc_info()[2]
  1444. if ( None != trace ):
  1445. trace_msg = repr( traceback.format_tb( trace ) )
  1446. log.error( "Traceback: %s" % trace_msg )
  1447. def parse_stdio_regexes( self, stdio_elem ):
  1448. """
  1449. Look in the tool's <stdio> elem for all <regex> subelements
  1450. that define how to look for warnings and fatal errors in
  1451. stdout and stderr. This will add all such regex elements
  1452. to the Tols's stdio_regexes list.
  1453. """
  1454. try:
  1455. # Look for every <regex> subelement. The regular expression
  1456. # will have "match" and "source" (or "src") attributes.
  1457. for regex_elem in ( stdio_elem.findall( "regex" ) ):
  1458. # TODO: Fill in ToolStdioRegex
  1459. regex = ToolStdioRegex()
  1460. # Each regex has an optional description that can be
  1461. # part of the "desc" or "description" attributes:
  1462. regex.desc = regex_elem.get( "desc" )
  1463. if None == regex.desc:
  1464. regex.desc = regex_elem.get( "description" )
  1465. # Parse the error level
  1466. regex.error_level = (
  1467. self.parse_error_level( regex_elem.get( "level" ) ) )
  1468. regex.match = regex_elem.get( "match", "" )
  1469. if None == regex.match:
  1470. # TODO: Convert the offending XML element to a string
  1471. log.warning( "Ignoring tool's stdio regex element %s - "
  1472. "the 'match' attribute must exist" )
  1473. continue
  1474. # Parse the output sources. We look for the "src", "source",
  1475. # and "sources" attributes, in that order. If there is no
  1476. # such source, then the source defaults to stderr & stdout.
  1477. # Look for a comma and then look for "err", "error", "out",
  1478. # and "output":
  1479. output_srcs = regex_elem.get( "src" )
  1480. if None == output_srcs:
  1481. output_srcs = regex_elem.get( "source" )
  1482. if None == output_srcs:
  1483. output_srcs = regex_elem.get( "sources" )
  1484. if None == output_srcs:
  1485. output_srcs = "output,error"
  1486. output_srcs = re.sub( "\s", "", output_srcs )
  1487. src_list = re.split( ",", output_srcs )
  1488. # Just put together anything to do with "out", including
  1489. # "stdout", "output", etc. Repeat for "stderr", "error",
  1490. # and anything to do with "err". If neither stdout nor
  1491. # stderr were specified, then raise a warning and scan both.
  1492. for src in src_list:
  1493. if re.search( "both", src, re.IGNORECASE ):
  1494. regex.stdout_match = True
  1495. regex.stderr_match = True
  1496. if re.search( "out", src, re.IGNORECASE ):
  1497. regex.stdout_match = True
  1498. if re.search( "err", src, re.IGNORECASE ):
  1499. regex.stderr_match = True
  1500. if (not regex.stdout_match and not regex.stderr_match):
  1501. log.warning( "Tool id %s: unable to determine if tool "
  1502. "stream source scanning is output, error, "
  1503. "or both. Defaulting to use both." % self.id )
  1504. regex.stdout_match = True
  1505. regex.stderr_match = True
  1506. self.stdio_regexes.append( regex )
  1507. except Exception:
  1508. log.error( "Exception in parse_stdio_exit_codes! "
  1509. + str(sys.exc_info()) )
  1510. trace = sys.exc_info()[2]
  1511. if ( None != trace ):
  1512. trace_msg = repr( traceback.format_tb( trace ) )
  1513. log.error( "Traceback: %s" % trace_msg )
  1514. # TODO: This method doesn't have to be part of the Tool class.
  1515. def parse_error_level( self, err_level ):
  1516. """
  1517. Parses error level and returns error level enumeration. If
  1518. unparsable, returns 'fatal'
  1519. """
  1520. return_level = StdioErrorLevel.FATAL
  1521. try:
  1522. if err_level:
  1523. if ( re.search( "log", err_level, re.IGNORECASE ) ):
  1524. return_level = StdioErrorLevel.LOG
  1525. elif ( re.search( "warning", err_level, re.IGNORECASE ) ):
  1526. return_level = StdioErrorLevel.WARNING
  1527. elif ( re.search( "fatal", err_level, re.IGNORECASE ) ):
  1528. return_level = StdioErrorLevel.FATAL
  1529. else:
  1530. log.debug( "Tool %s: error level %s did not match log/warning/fatal" %
  1531. ( self.id, err_level ) )
  1532. except Exception:
  1533. log.error( "Exception in parse_error_level "
  1534. + str(sys.exc_info() ) )
  1535. trace = sys.exc_info()[2]
  1536. if ( None != trace ):
  1537. trace_msg = repr( traceback.format_tb( trace ) )
  1538. log.error( "Traceback: %s" % trace_msg )
  1539. return return_level
  1540. def parse_input_page( self, input_elem, enctypes ):
  1541. """
  1542. Parse a page of inputs. This basically just calls 'parse_input_elem',
  1543. but it also deals with possible 'display' elements which are supported
  1544. only at the top/page level (not in groups).
  1545. """
  1546. inputs = self.parse_input_elem( input_elem, enctypes )
  1547. # Display
  1548. display_elem = input_elem.find("display")
  1549. if display_elem is not None:
  1550. display = xml_to_string(display_elem)
  1551. else:
  1552. display = None
  1553. return display, inputs
  1554. def parse_input_elem( self, parent_elem, enctypes, context=None ):
  1555. """
  1556. Parse a parent element whose children are inputs -- these could be
  1557. groups (repeat, conditional) or param elements. Groups will be parsed
  1558. recursively.
  1559. """
  1560. rval = odict()
  1561. context = ExpressionContext( rval, context )
  1562. for elem in parent_elem:
  1563. # Repeat group
  1564. if elem.tag == "repeat":
  1565. group = Repeat()
  1566. group.name = elem.get( "name" )
  1567. group.title = elem.get( "title" )
  1568. group.help = elem.get( "help", None )
  1569. group.inputs = self.parse_input_elem( elem, enctypes, context )
  1570. group.default = int( elem.get( "default", 0 ) )
  1571. group.min = int( elem.get( "min", 0 ) )
  1572. # Use float instead of int so that 'inf' can be used for no max
  1573. group.max = float( elem.get( "max", "inf" ) )
  1574. assert group.min <= group.max, \
  1575. ValueError( "Min repeat count must be less-than-or-equal to the max." )
  1576. # Force default to be within min-max range
  1577. group.default = min( max( group.default, group.min ), group.max )
  1578. rval[group.name] = group
  1579. elif elem.tag == "conditional":
  1580. group = Conditional()
  1581. group.name = elem.get( "name" )
  1582. group.value_ref = elem.get( 'value_ref', None )
  1583. group.value_ref_in_group = string_as_bool( elem.get( 'value_ref_in_group', 'True' ) )
  1584. value_from = elem.get( "value_from" )
  1585. if value_from:
  1586. value_from = value_from.split( ':' )
  1587. group.value_from = locals().get( value_from[0] )
  1588. group.test_param = rval[ group.value_ref ]
  1589. group.test_param.refresh_on_change = True
  1590. for attr in value_from[1].split( '.' ):
  1591. group.value_from = getattr( group.value_from, attr )
  1592. for case_value, case_inputs in group.value_from( context, group, self ).iteritems():
  1593. case = ConditionalWhen()
  1594. case.value = case_value
  1595. if case_inputs:
  1596. case.inputs = self.parse_input_elem(
  1597. ElementTree.XML( "<when>%s</when>" % case_inputs ), enctypes, context )
  1598. else:
  1599. case.inputs = odict()
  1600. group.cases.append( case )
  1601. else:
  1602. # Should have one child "input" which determines the case
  1603. input_elem = elem.find( "param" )
  1604. assert input_elem is not None, "<conditional> must have a child <param>"
  1605. group.test_param = self.parse_param_elem( input_elem, enctypes, context )
  1606. possible_cases = list( group.test_param.legal_values ) # store possible cases, undefined whens will have no inputs
  1607. # Must refresh when test_param changes
  1608. group.test_param.refresh_on_change = True
  1609. # And a set of possible cases
  1610. for case_elem in elem.findall( "when" ):
  1611. case = ConditionalWhen()
  1612. case.value = case_elem.get( "value" )
  1613. case.inputs = self.parse_input_elem( case_elem, enctypes, context )
  1614. group.cases.append( case )
  1615. try:
  1616. possible_cases.remove( case.value )
  1617. except:
  1618. log.warning( "Tool %s: a when tag has been defined for '%s (%s) --> %s', but does not appear to be selectable." %
  1619. ( self.id, group.name, group.test_param.name, case.value ) )
  1620. for unspecified_case in possible_cases:
  1621. log.warning( "Tool %s: a when tag has not been defined for '%s (%s) --> %s', assuming empty inputs." %
  1622. ( self.id, group.name, group.test_param.name, unspecified_case ) )
  1623. case = ConditionalWhen()
  1624. case.value = unspecified_case
  1625. case.inputs = odict()
  1626. group.cases.append( case )
  1627. rval[group.name] = group
  1628. elif elem.tag == "upload_dataset":
  1629. group = UploadDataset()
  1630. group.name = elem.get( "name" )
  1631. group.title = elem.get( "title" )
  1632. group.file_type_name = elem.get( 'file_type_name', group.file_type_name )
  1633. group.default_file_type = elem.get( 'default_file_type', group.default_file_type )
  1634. group.metadata_ref = elem.get( 'metadata_ref', group.metadata_ref )
  1635. rval[ group.file_type_name ].refresh_on_change = True
  1636. rval[ group.file_type_name ].refresh_on_change_values = \
  1637. self.app.datatypes_registry.get_composite_extensions()
  1638. group.inputs = self.parse_input_elem( elem, enctypes, context )
  1639. rval[ group.name ] = group
  1640. elif elem.tag == "param":
  1641. param = self.parse_param_elem( elem, enctypes, context )
  1642. rval[param.name] = param
  1643. if hasattr( param, 'data_ref' ):
  1644. param.ref_input = context[ param.data_ref ]
  1645. self.input_params.append( param )
  1646. return rval
  1647. def parse_param_elem( self, input_elem, enctypes, context ):
  1648. """
  1649. Parse a single "<param>" element and return a ToolParameter instance.
  1650. Also, if the parameter has a 'required_enctype' add it to the set
  1651. enctypes.
  1652. """
  1653. param = ToolParameter.build( self, input_elem )
  1654. param_enctype = param.get_required_enctype()
  1655. if param_enctype:
  1656. enctypes.add( param_enctype )
  1657. # If parameter depends on any other paramters, we must refresh the
  1658. # form when it changes
  1659. for name in param.get_dependencies():
  1660. context[ name ].refresh_on_change = True
  1661. return param
  1662. def populate_tool_shed_info( self ):
  1663. if self.repository_id is not None and self.app.name == 'galaxy':
  1664. repository_id = self.app.security.decode_id( self.repository_id )
  1665. tool_shed_repository = self.app.install_model.context.query( self.app.install_model.ToolShedRepository ).get( repository_id )
  1666. if tool_shed_repository:
  1667. self.tool_shed = tool_shed_repository.tool_shed
  1668. self.repository_name = tool_shed_repository.name
  1669. self.repository_owner = tool_shed_repository.owner
  1670. self.installed_changeset_revision = tool_shed_repository.installed_changeset_revision
  1671. def check_workflow_compatible( self, root ):
  1672. """
  1673. Determine if a tool can be used in workflows. External tools and the
  1674. upload tool are currently not supported by workflows.
  1675. """
  1676. # Multiple page tools are not supported -- we're eliminating most
  1677. # of these anyway
  1678. if self.has_multiple_pages:
  1679. return False
  1680. # This is probably the best bet for detecting external web tools
  1681. # right now
  1682. if self.tool_type.startswith( 'data_source' ):
  1683. return False
  1684. if not string_as_bool( root.get( "workflow_compatible", "True" ) ):
  1685. return False
  1686. # TODO: Anyway to capture tools that dynamically change their own
  1687. # outputs?
  1688. return True
  1689. def new_state( self, trans, all_pages=False, history=None ):
  1690. """
  1691. Create a new `DefaultToolState` for this tool. It will be initialized
  1692. with default values for inputs.
  1693. Only inputs on the first page will be initialized unless `all_pages` is
  1694. True, in which case all inputs regardless of page are initialized.
  1695. """
  1696. state = DefaultToolState()
  1697. state.inputs = {}
  1698. if all_pages:
  1699. inputs = self.inputs
  1700. else:
  1701. inputs = self.inputs_by_page[ 0 ]
  1702. self.fill_in_new_state( trans, inputs, state.inputs, history=history )
  1703. return state
  1704. def fill_in_new_state( self, trans, inputs, state, context=None, history=None ):
  1705. """
  1706. Fill in a tool state dictionary with default values for all parameters
  1707. in the dictionary `inputs`. Grouping elements are filled in recursively.
  1708. """
  1709. context = ExpressionContext( state, context )
  1710. for input in inputs.itervalues():
  1711. state[ input.name ] = input.get_initial_value( trans, context, history=history )
  1712. def get_param_html_map( self, trans, page=0, other_values={} ):
  1713. """
  1714. Return a dictionary containing the HTML representation of each
  1715. parameter. This is used for rendering display elements. It is
  1716. currently not compatible with grouping constructs.
  1717. NOTE: This should be considered deprecated, it is only used for tools
  1718. with `display` elements. These should be eliminated.
  1719. """
  1720. rval = dict()
  1721. for key, param in self.inputs_by_page[page].iteritems():
  1722. if not isinstance( param, ToolParameter ):
  1723. raise Exception( "'get_param_html_map' only supported for simple paramters" )
  1724. rval[key] = param.get_html( trans, other_values=other_values )
  1725. return rval
  1726. def get_param( self, key ):
  1727. """
  1728. Returns the parameter named `key` or None if there is no such
  1729. parameter.
  1730. """
  1731. return self.inputs.get( key, None )
  1732. def get_hook(self, name):
  1733. """
  1734. Returns an object from the code file referenced by `code_namespace`
  1735. (this will normally be a callable object)
  1736. """
  1737. if self.code_namespace:
  1738. # Try to look up hook in self.hook_map, otherwise resort to default
  1739. if name in self.hook_map and self.hook_map[name] in self.code_namespace:
  1740. return self.code_namespace[self.hook_map[name]]
  1741. elif name in self.code_namespace:
  1742. return self.code_namespace[name]
  1743. return None
  1744. def visit_inputs( self, value, callback ):
  1745. """
  1746. Call the function `callback` on each parameter of this tool. Visits
  1747. grouping parameters recursively and constructs unique prefixes for
  1748. each nested set of The callback method is then called as:
  1749. `callback( level_prefix, parameter, parameter_value )`
  1750. """
  1751. # HACK: Yet another hack around check_values -- WHY HERE?
  1752. if not self.check_values:
  1753. return
  1754. for input in self.inputs.itervalues():
  1755. if isinstance( input, ToolParameter ):
  1756. callback( "", input, value[input.name] )
  1757. else:
  1758. input.visit_inputs( "", value[input.name], callback )
  1759. def handle_input( self, trans, incoming, history=None, old_errors=None, process_state='update', source='html' ):
  1760. """
  1761. Process incoming parameters for this tool from the dict `incoming`,
  1762. update the tool state (or create if none existed), and either return
  1763. to the form or execute the tool (only if 'execute' was clicked and
  1764. there were no errors).
  1765. process_state can be either 'update' (to incrementally build up the state
  1766. over several calls - one repeat per handle for instance) or 'populate'
  1767. force a complete build of the state and submission all at once (like
  1768. from API). May want an incremental version of the API also at some point,
  1769. that is why this is not just called for_api.
  1770. """
  1771. all_pages = ( process_state == "populate" ) # If process_state = update, handle all pages at once.
  1772. rerun_remap_job_id = None
  1773. if 'rerun_remap_job_id' in incoming:
  1774. try:
  1775. rerun_remap_job_id = trans.app.security.decode_id( incoming[ 'rerun_remap_job_id' ] )
  1776. except Exception:
  1777. message = 'Failure executing tool (attempting to rerun invalid job).'
  1778. return 'message.mako', dict( status='error', message=message, refresh_frames=[] )
  1779. state, state_new = self.__fetch_state( trans, incoming, history, all_pages=all_pages )
  1780. if state_new:
  1781. # This feels a bit like a hack. It allows forcing full processing
  1782. # of inputs even when there is no state in the incoming dictionary
  1783. # by providing either 'runtool_btn' (the name of the submit button
  1784. # on the standard run form) or "URL" (a parameter provided by
  1785. # external data source tools).
  1786. if "runtool_btn" not in incoming and "URL" not in incoming:
  1787. if not self.display_interface:
  1788. return self.__no_display_interface_response()
  1789. if len(incoming):
  1790. self.update_state( trans, self.inputs_by_page[state.page], state.inputs, incoming, old_errors=old_errors or {}, source=source )
  1791. return "tool_form.mako", dict( errors={}, tool_state=state, param_values={}, incoming={} )
  1792. errors, params = self.__check_param_values( trans, incoming, state, old_errors, process_state, history=history, source=source )
  1793. if self.__should_refresh_state( incoming ):
  1794. template, template_vars = self.__handle_state_refresh( trans, state, errors )
  1795. else:
  1796. # User actually clicked next or execute.
  1797. # If there were errors, we stay on the same page and display
  1798. # error messages
  1799. if errors:
  1800. error_message = "One or more errors were found in the input you provided. The specific errors are marked below."
  1801. template = "tool_form.mako"
  1802. template_vars = dict( errors=errors, tool_state=state, incoming=incoming, error_message=error_message )
  1803. # If we've completed the last page we can execute the tool
  1804. elif all_pages or state.page == self.last_page:
  1805. tool_executed, result = self.__handle_tool_execute( trans, rerun_remap_job_id, params, history )
  1806. if tool_executed:
  1807. template = 'tool_executed.mako'
  1808. template_vars = dict( out_data=result )
  1809. else:
  1810. template = 'message.mako'
  1811. template_vars = dict( status='error', message=result, refresh_frames=[] )
  1812. # Otherwise move on to the next page
  1813. else:
  1814. template, template_vars = self.__handle_page_advance( trans, state, errors )
  1815. return template, template_vars
  1816. def __should_refresh_state( self, incoming ):
  1817. return not( 'runtool_btn' in incoming or 'URL' in incoming or 'ajax_upload' in incoming )
  1818. def __handle_tool_execute( self, trans, rerun_remap_job_id, params, history ):
  1819. """
  1820. Return a pair with whether execution is successful as well as either
  1821. resulting output data or an error message indicating the problem.
  1822. """
  1823. try:
  1824. _, out_data = self.execute( trans, incoming=params, history=history, rerun_remap_job_id=rerun_remap_job_id )
  1825. except httpexceptions.HTTPFound, e:
  1826. #if it's a paste redirect exception, pass it up the stack
  1827. raise e
  1828. except Exception, e:
  1829. log.exception('Exception caught while attempting tool execution:')
  1830. message = 'Error executing tool: %s' % str(e)
  1831. return False, message
  1832. if isinstance( out_data, odict ):
  1833. return True, out_data
  1834. else:
  1835. if isinstance( out_data, str ):
  1836. message = out_data
  1837. else:
  1838. message = 'Failure executing tool (invalid data returned from tool execution)'
  1839. return False, message
  1840. def __handle_state_refresh( self, trans, state, errors ):
  1841. try:
  1842. self.find_fieldstorage( state.inputs )
  1843. except InterruptedUpload:
  1844. # If inputs contain a file it won't persist. Most likely this
  1845. # is an interrupted upload. We should probably find a more
  1846. # standard method of determining an incomplete POST.
  1847. return self.handle_interrupted( trans, state.inputs )
  1848. except:
  1849. pass
  1850. # Just a refresh, render the form with updated state and errors.
  1851. if not self.display_interface:
  1852. return self.__no_display_interface_response()
  1853. return 'tool_form.mako', dict( errors=errors, tool_state=state )
  1854. def __handle_page_advance( self, trans, state, errors ):
  1855. state.page += 1
  1856. # Fill in the default values for the next page
  1857. self.fill_in_new_state( trans, self.inputs_by_page[ state.page ], state.inputs )
  1858. if not self.display_interface:
  1859. return self.__no_display_interface_response()
  1860. return 'tool_form.mako', dict( errors=errors, tool_state=state )
  1861. def __no_display_interface_response( self ):
  1862. return 'message.mako', dict( status='info', message="The interface for this tool cannot be displayed", refresh_frames=['everything'] )
  1863. def __fetch_state( self, trans, incoming, history, all_pages ):
  1864. # Get the state or create if not found
  1865. if "tool_state" in incoming:
  1866. encoded_state = string_to_object( incoming["tool_state"] )
  1867. state = DefaultToolState()
  1868. state.decode( encoded_state, self, trans.app )
  1869. new = False
  1870. else:
  1871. state = self.new_state( trans, history=history, all_pages=all_pages )
  1872. new = True
  1873. return state, new
  1874. def __check_param_values( self, trans, incoming, state, old_errors, process_state, history, source ):
  1875. # Process incoming data
  1876. if not( self.check_values ):
  1877. # If `self.check_values` is false we don't do any checking or
  1878. # processing on input This is used to pass raw values
  1879. # through to/from external sites. FIXME: This should be handled
  1880. # more cleanly, there is no reason why external sites need to
  1881. # post back to the same URL that the tool interface uses.
  1882. errors = {}
  1883. params = incoming
  1884. else:
  1885. # Update state for all inputs on the current page taking new
  1886. # values from `incoming`.
  1887. if process_state == "update":
  1888. inputs = self.inputs_by_page[state.page]
  1889. errors = self.update_state( trans, inputs, state.inputs, incoming, old_errors=old_errors or {}, source=source )
  1890. elif process_state == "populate":
  1891. inputs = self.inputs
  1892. errors = self.populate_state( trans, inputs, state.inputs, incoming, history, source=source )
  1893. else:
  1894. raise Exception("Unknown process_state type %s" % process_state)
  1895. # If the tool provides a `validate_input` hook, call it.
  1896. validate_input = self.get_hook( 'validate_input' )
  1897. if validate_input:
  1898. validate_input( trans, errors, state.inputs, inputs )
  1899. params = state.inputs
  1900. return errors, params
  1901. def find_fieldstorage( self, x ):
  1902. if isinstance( x, FieldStorage ):
  1903. raise InterruptedUpload( None )
  1904. elif type( x ) is types.DictType:
  1905. [ self.find_fieldstorage( y ) for y in x.values() ]
  1906. elif type( x ) is types.ListType:
  1907. [ self.find_fieldstorage( y ) for y in x ]
  1908. def handle_interrupted( self, trans, inputs ):
  1909. """
  1910. Upon handling inputs, if it appears that we have received an incomplete
  1911. form, do some cleanup or anything else deemed necessary. Currently
  1912. this is only likely during file uploads, but this method could be
  1913. generalized and a method standardized for handling other tools.
  1914. """
  1915. # If the async upload tool has uploading datasets, we need to error them.
  1916. if 'async_datasets' in inputs and inputs['async_datasets'] not in [ 'None', '', None ]:
  1917. for id in inputs['async_datasets'].split(','):
  1918. try:
  1919. data = self.sa_session.query( trans.model.HistoryDatasetAssociation ).get( int( id ) )
  1920. except:
  1921. log.exception( 'Unable to load precreated dataset (%s) sent in upload form' % id )
  1922. continue
  1923. if trans.user is None and trans.galaxy_session.current_history != data.history:
  1924. log.error( 'Got a precreated dataset (%s) but it does not belong to anonymous user\'s current session (%s)'
  1925. % ( data.id, trans.galaxy_session.id ) )
  1926. elif data.history.user != trans.user:
  1927. log.error( 'Got a precreated dataset (%s) but it does not belong to current user (%s)'
  1928. % ( data.id, trans.user.id ) )
  1929. else:
  1930. data.state = data.states.ERROR
  1931. data.info = 'Upload of this dataset was interrupted. Please try uploading again or'
  1932. self.sa_session.add( data )
  1933. self.sa_session.flush()
  1934. # It's unlikely the user will ever see this.
  1935. return 'message.mako', dict( status='error',
  1936. message='Your upload was interrupted. If this was uninentional, please retry it.',
  1937. refresh_frames=[], cont=None )
  1938. def populate_state( self, trans, inputs, state, incoming, history, source, prefix="", context=None ):
  1939. errors = dict()
  1940. # Push this level onto the context stack
  1941. context = ExpressionContext( state, context )
  1942. for input in inputs.itervalues():
  1943. key = prefix + input.name
  1944. if isinstance( input, Repeat ):
  1945. group_state = state[input.name]
  1946. # Create list of empty errors for each previously existing state
  1947. group_errors = [ ]
  1948. any_group_errors = False
  1949. rep_index = 0
  1950. del group_state[:] # Clear prepopulated defaults if repeat.min set.
  1951. while True:
  1952. rep_name = "%s_%d" % ( key, rep_index )
  1953. if not any( [ incoming_key.startswith(rep_name) for incoming_key in incoming.keys() ] ):
  1954. break
  1955. if rep_index < input.max:
  1956. new_state = {}
  1957. new_state['__index__'] = rep_index
  1958. self.fill_in_new_state( trans, input.inputs, new_state, context, history=history )
  1959. group_state.append( new_state )
  1960. group_errors.append( {} )
  1961. rep_errors = self.populate_state( trans,
  1962. input.inputs,
  1963. new_state,
  1964. incoming,
  1965. history,
  1966. source,
  1967. prefix=rep_name + "|",
  1968. context=context )
  1969. if rep_errors:
  1970. any_group_errors = True
  1971. group_errors[rep_index].update( rep_errors )
  1972. else:
  1973. group_errors[-1] = { '__index__': 'Cannot add repeat (max size=%i).' % input.max }
  1974. any_group_errors = True
  1975. rep_index += 1
  1976. elif isinstance( input, Conditional ):
  1977. group_state = state[input.name]
  1978. group_prefix = "%s|" % ( key )
  1979. # Deal with the 'test' element and see if it's value changed
  1980. if input.value_ref and not input.value_ref_in_group:
  1981. # We are referencing an existent parameter, which is not
  1982. # part of this group
  1983. test_param_key = prefix + input.test_param.name
  1984. else:
  1985. test_param_key = group_prefix + input.test_param.name
  1986. # Get value of test param and determine current case
  1987. value, test_param_error = check_param_from_incoming( trans,
  1988. group_state,
  1989. input.test_param,
  1990. incoming,
  1991. test_param_key,
  1992. context,
  1993. source )
  1994. current_case = input.get_current_case( value, trans )
  1995. # Current case has changed, throw away old state
  1996. group_state = state[input.name] = {}
  1997. # TODO: we should try to preserve values if we can
  1998. self.fill_in_new_state( trans, input.cases[current_case].inputs, group_state, context, history=history )
  1999. group_errors = self.populate_state( trans,
  2000. input.cases[current_case].inputs,
  2001. group_state,
  2002. incoming,
  2003. history,
  2004. source,
  2005. prefix=group_prefix,
  2006. context=context,
  2007. )
  2008. if test_param_error:
  2009. group_errors[ input.test_param.name ] = test_param_error
  2010. if group_errors:
  2011. errors[ input.name ] = group_errors
  2012. # Store the current case in a special value
  2013. group_state['__current_case__'] = current_case
  2014. # Store the value of the test element
  2015. group_state[ input.test_param.name ] = value
  2016. elif isinstance( input, UploadDataset ):
  2017. group_state = state[input.name]
  2018. group_errors = []
  2019. any_group_errors = False
  2020. d_type = input.get_datatype( trans, context )
  2021. writable_files = d_type.writable_files
  2022. #remove extra files
  2023. while len( group_state ) > len( writable_files ):
  2024. del group_state[-1]
  2025. # Add new fileupload as needed
  2026. while len( writable_files ) > len( group_state ):
  2027. new_state = {}
  2028. new_state['__index__'] = len( group_state )
  2029. self.fill_in_new_state( trans, input.inputs, new_state, context )
  2030. group_state.append( new_state )
  2031. if any_group_errors:
  2032. group_errors.append( {} )
  2033. # Update state
  2034. for i, rep_state in enumerate( group_state ):
  2035. rep_index = rep_state['__index__']
  2036. rep_prefix = "%s_%d|" % ( key, rep_index )
  2037. rep_errors = self.populate_state( trans,
  2038. input.inputs,
  2039. rep_state,
  2040. incoming,
  2041. history,
  2042. source,
  2043. prefix=rep_prefix,
  2044. context=context)
  2045. if rep_errors:
  2046. any_group_errors = True
  2047. group_errors.append( rep_errors )
  2048. else:
  2049. group_errors.append( {} )
  2050. # Were there *any* errors for any repetition?
  2051. if any_group_errors:
  2052. errors[input.name] = group_errors
  2053. else:
  2054. value, error = check_param_from_incoming( trans, state, input, incoming, key, context, source )
  2055. if error:
  2056. errors[ input.name ] = error
  2057. state[ input.name ] = value
  2058. return errors
  2059. def update_state( self, trans, inputs, state, incoming, source='html', prefix="", context=None,
  2060. update_only=False, old_errors={}, item_callback=None ):
  2061. """
  2062. Update the tool state in `state` using the user input in `incoming`.
  2063. This is designed to be called recursively: `inputs` contains the
  2064. set of inputs being processed, and `prefix` specifies a prefix to
  2065. add to the name of each input to extract it's value from `incoming`.
  2066. If `update_only` is True, values that are not in `incoming` will
  2067. not be modified. In this case `old_errors` can be provided, and any
  2068. errors for parameters which were *not* updated will be preserved.
  2069. """
  2070. errors = dict()
  2071. # Push this level onto the context stack
  2072. context = ExpressionContext( state, context )
  2073. # Iterate inputs and update (recursively)
  2074. for input in inputs.itervalues():
  2075. key = prefix + input.name
  2076. if isinstance( input, Repeat ):
  2077. group_state = state[input.name]
  2078. # Create list of empty errors for each previously existing state
  2079. group_errors = [ {} for i in range( len( group_state ) ) ]
  2080. group_old_errors = old_errors.get( input.name, None )
  2081. any_group_errors = False
  2082. # Check any removals before updating state -- only one
  2083. # removal can be performed, others will be ignored
  2084. for i, rep_state in enumerate( group_state ):
  2085. rep_index = rep_state['__index__']
  2086. if key + "_" + str(rep_index) + "_remove" in incoming:
  2087. if len( group_state ) > input.min:
  2088. del group_state[i]
  2089. del group_errors[i]
  2090. if group_old_errors:
  2091. del group_old_errors[i]
  2092. break
  2093. else:
  2094. group_errors[i] = { '__index__': 'Cannot remove repeat (min size=%i).' % input.min }
  2095. any_group_errors = True
  2096. # Only need to find one that can't be removed due to size, since only
  2097. # one removal is processed at # a time anyway
  2098. break
  2099. elif group_old_errors and group_old_errors[i]:
  2100. group_errors[i] = group_old_errors[i]
  2101. any_group_errors = True
  2102. # Update state
  2103. max_index = -1
  2104. for i, rep_state in enumerate( group_state ):
  2105. rep_index = rep_state['__index__']
  2106. max_index = max( max_index, rep_index )
  2107. rep_prefix = "%s_%d|" % ( key, rep_index )
  2108. if group_old_errors:
  2109. rep_old_errors = group_old_errors[i]
  2110. else:
  2111. rep_old_errors = {}
  2112. rep_errors = self.update_state( trans,
  2113. input.inputs,
  2114. rep_state,
  2115. incoming,
  2116. source=source,
  2117. prefix=rep_prefix,
  2118. context=context,
  2119. update_only=update_only,
  2120. old_errors=rep_old_errors,
  2121. item_callback=item_callback )
  2122. if rep_errors:
  2123. any_group_errors = True
  2124. group_errors[i].update( rep_errors )
  2125. # Check for addition
  2126. if key + "_add" in incoming:
  2127. if len( group_state ) < input.max:
  2128. new_state = {}
  2129. new_state['__index__'] = max_index + 1
  2130. self.fill_in_new_state( trans, input.inputs, new_state, context )
  2131. group_state.append( new_state )
  2132. group_errors.append( {} )
  2133. else:
  2134. group_errors[-1] = { '__index__': 'Cannot add repeat (max size=%i).' % input.max }
  2135. any_group_errors = True
  2136. # Were there *any* errors for any repetition?
  2137. if any_group_errors:
  2138. errors[input.name] = group_errors
  2139. elif isinstance( input, Conditional ):
  2140. group_state = state[input.name]
  2141. group_old_errors = old_errors.get( input.name, {} )
  2142. old_current_case = group_state['__current_case__']
  2143. group_prefix = "%s|" % ( key )
  2144. # Deal with the 'test' element and see if it's value changed
  2145. if input.value_ref and not input.value_ref_in_group:
  2146. # We are referencing an existent parameter, which is not
  2147. # part of this group
  2148. test_param_key = prefix + input.test_param.name
  2149. else:
  2150. test_param_key = group_prefix + input.test_param.name
  2151. test_param_error = None
  2152. test_incoming = get_incoming_value( incoming, test_param_key, None )
  2153. if test_param_key not in incoming \
  2154. and "__force_update__" + test_param_key not in incoming \
  2155. and update_only:
  2156. # Update only, keep previous value and state, but still
  2157. # recurse in case there are nested changes
  2158. value = group_state[ input.test_param.name ]
  2159. current_case = old_current_case
  2160. if input.test_param.name in old_errors:
  2161. errors[ input.test_param.name ] = old_errors[ input.test_param.name ]
  2162. else:
  2163. # Get value of test param and determine current case
  2164. value, test_param_error = \
  2165. check_param( trans, input.test_param, test_incoming, context, source=source )
  2166. current_case = input.get_current_case( value, trans )
  2167. if current_case != old_current_case:
  2168. # Current case has changed, throw away old state
  2169. group_state = state[input.name] = {}
  2170. # TODO: we should try to preserve values if we can
  2171. self.fill_in_new_state( trans, input.cases[current_case].inputs, group_state, context )
  2172. group_errors = dict()
  2173. group_old_errors = dict()
  2174. else:
  2175. # Current case has not changed, update children
  2176. group_errors = self.update_state( trans,
  2177. input.cases[current_case].inputs,
  2178. group_state,
  2179. incoming,
  2180. prefix=group_prefix,
  2181. context=context,
  2182. source=source,
  2183. update_only=update_only,
  2184. old_errors=group_old_errors,
  2185. item_callback=item_callback )
  2186. if input.test_param.name in group_old_errors and not test_param_error:
  2187. test_param_error = group_old_errors[ input.test_param.name ]
  2188. if test_param_error:
  2189. group_errors[ input.test_param.name ] = test_param_error
  2190. if group_errors:
  2191. errors[ input.name ] = group_errors
  2192. # Store the current case in a special value
  2193. group_state['__current_case__'] = current_case
  2194. # Store the value of the test element
  2195. group_state[ input.test_param.name ] = value
  2196. elif isinstance( input, UploadDataset ):
  2197. group_state = state[input.name]
  2198. group_errors = []
  2199. group_old_errors = old_errors.get( input.name, None )
  2200. any_group_errors = False
  2201. d_type = input.get_datatype( trans, context )
  2202. writable_files = d_type.writable_files
  2203. #remove extra files
  2204. while len( group_state ) > len( writable_files ):
  2205. del group_state[-1]
  2206. if group_old_errors:
  2207. del group_old_errors[-1]
  2208. # Update state
  2209. max_index = -1
  2210. for i, rep_state in enumerate( group_state ):
  2211. rep_index = rep_state['__index__']
  2212. max_index = max( max_index, rep_index )
  2213. rep_prefix = "%s_%d|" % ( key, rep_index )
  2214. if group_old_errors:
  2215. rep_old_errors = group_old_errors[i]
  2216. else:
  2217. rep_old_errors = {}
  2218. rep_errors = self.update_state( trans,
  2219. input.inputs,
  2220. rep_state,
  2221. incoming,
  2222. prefix=rep_prefix,
  2223. context=context,
  2224. source=source,
  2225. update_only=update_only,
  2226. old_errors=rep_old_errors,
  2227. item_callback=item_callback )
  2228. if rep_errors:
  2229. any_group_errors = True
  2230. group_errors.append( rep_errors )
  2231. else:
  2232. group_errors.append( {} )
  2233. # Add new fileupload as needed
  2234. offset = 1
  2235. while len( writable_files ) > len( group_state ):
  2236. new_state = {}
  2237. new_state['__index__'] = max_index + offset
  2238. offset += 1
  2239. self.fill_in_new_state( trans, input.inputs, new_state, context )
  2240. group_state.append( new_state )
  2241. if any_group_errors:
  2242. group_errors.append( {} )
  2243. # Were there *any* errors for any repetition?
  2244. if any_group_errors:
  2245. errors[input.name] = group_errors
  2246. else:
  2247. if key not in incoming \
  2248. and "__force_update__" + key not in incoming \
  2249. and update_only:
  2250. # No new value provided, and we are only updating, so keep
  2251. # the old value (which should already be in the state) and
  2252. # preserve the old error message.
  2253. if input.name in old_errors:
  2254. errors[ input.name ] = old_errors[ input.name ]
  2255. else:
  2256. incoming_value = get_incoming_value( incoming, key, None )
  2257. value, error = check_param( trans, input, incoming_value, context, source=source )
  2258. # If a callback was provided, allow it to process the value
  2259. if item_callback:
  2260. old_value = state.get( input.name, None )
  2261. value, error = item_callback( trans, key, input, value, error, old_value, context )
  2262. if error:
  2263. errors[ input.name ] = error
  2264. state[ input.name ] = value
  2265. return errors
  2266. @property
  2267. def params_with_missing_data_table_entry( self ):
  2268. """
  2269. Return all parameters that are dynamically generated select lists whose
  2270. options require an entry not currently in the tool_data_table_conf.xml file.
  2271. """
  2272. params = []
  2273. for input_param in self.input_params:
  2274. if isinstance( input_param, SelectToolParameter ) and input_param.is_dynamic:
  2275. options = input_param.options
  2276. if options and options.missing_tool_data_table_name and input_param not in params:
  2277. params.append( input_param )
  2278. return params
  2279. @property
  2280. def params_with_missing_index_file( self ):
  2281. """
  2282. Return all parameters that are dynamically generated
  2283. select lists whose options refer to a missing .loc file.
  2284. """
  2285. params = []
  2286. for input_param in self.input_params:
  2287. if isinstance( input_param, SelectToolParameter ) and input_param.is_dynamic:
  2288. options = input_param.options
  2289. if options and options.missing_index_file and input_param not in params:
  2290. params.append( input_param )
  2291. return params
  2292. def get_static_param_values( self, trans ):
  2293. """
  2294. Returns a map of parameter names and values if the tool does not
  2295. require any user input. Will raise an exception if any parameter
  2296. does require input.
  2297. """
  2298. args = dict()
  2299. for key, param in self.inputs.iteritems():
  2300. if isinstance( param, HiddenToolParameter ):
  2301. args[key] = model.User.expand_user_properties( trans.user, param.value )
  2302. elif isinstance( param, BaseURLToolParameter ):
  2303. args[key] = param.get_value( trans )
  2304. else:
  2305. raise Exception( "Unexpected parameter type" )
  2306. return args
  2307. def execute( self, trans, incoming={}, set_output_hid=True, history=None, **kwargs ):
  2308. """
  2309. Execute the tool using parameter values in `incoming`. This just
  2310. dispatches to the `ToolAction` instance specified by
  2311. `self.tool_action`. In general this will create a `Job` that
  2312. when run will build the tool's outputs, e.g. `DefaultToolAction`.
  2313. """
  2314. return self.tool_action.execute( self, trans, incoming=incoming, set_output_hid=set_output_hid, history=history, **kwargs )
  2315. def params_to_strings( self, params, app ):
  2316. return params_to_strings( self.inputs, params, app )
  2317. def params_from_strings( self, params, app, ignore_errors=False ):
  2318. return params_from_strings( self.inputs, params, app, ignore_errors )
  2319. def check_and_update_param_values( self, values, trans, update_values=True, allow_workflow_parameters=False ):
  2320. """
  2321. Check that all parameters have values, and fill in with default
  2322. values where necessary. This could be called after loading values
  2323. from a database in case new parameters have been added.
  2324. """
  2325. messages = {}
  2326. self.check_and_update_param_values_helper( self.inputs, values, trans, messages, update_values=update_values, allow_workflow_parameters=allow_workflow_parameters )
  2327. return messages
  2328. def check_and_update_param_values_helper( self, inputs, values, trans, messages, context=None, prefix="", update_values=True, allow_workflow_parameters=False ):
  2329. """
  2330. Recursive helper for `check_and_update_param_values_helper`
  2331. """
  2332. context = ExpressionContext( values, context )
  2333. for input in inputs.itervalues():
  2334. # No value, insert the default
  2335. if input.name not in values:
  2336. if isinstance( input, Conditional ):
  2337. messages[ input.name ] = { input.test_param.name: "No value found for '%s%s', used default" % ( prefix, input.label ) }
  2338. test_value = input.test_param.get_initial_value( trans, context )
  2339. current_case = input.get_current_case( test_value, trans )
  2340. self.check_and_update_param_values_helper( input.cases[ current_case ].inputs, {}, trans, messages[ input.name ], context, prefix, allow_workflow_parameters=allow_workflow_parameters )
  2341. elif isinstance( input, Repeat ):
  2342. if input.min:
  2343. messages[ input.name ] = []
  2344. for i in range( input.min ):
  2345. rep_prefix = prefix + "%s %d > " % ( input.title, i + 1 )
  2346. rep_dict = dict()
  2347. messages[ input.name ].append( rep_dict )
  2348. self.check_and_update_param_values_helper( input.inputs, {}, trans, rep_dict, context, rep_prefix, allow_workflow_parameters=allow_workflow_parameters )
  2349. else:
  2350. messages[ input.name ] = "No value found for '%s%s', used default" % ( prefix, input.label )
  2351. values[ input.name ] = input.get_initial_value( trans, context )
  2352. # Value, visit recursively as usual
  2353. else:
  2354. if isinstance( input, Repeat ):
  2355. for i, d in enumerate( values[ input.name ] ):
  2356. rep_prefix = prefix + "%s %d > " % ( input.title, i + 1 )
  2357. self.check_and_update_param_values_helper( input.inputs, d, trans, messages, context, rep_prefix, allow_workflow_parameters=allow_workflow_parameters )
  2358. elif isinstance( input, Conditional ):
  2359. group_values = values[ input.name ]
  2360. if input.test_param.name not in group_values:
  2361. # No test param invalidates the whole conditional
  2362. values[ input.name ] = group_values = input.get_initial_value( trans, context )
  2363. messages[ input.test_param.name ] = "No value found for '%s%s', used default" % ( prefix, input.test_param.label )
  2364. current_case = group_values['__current_case__']
  2365. for child_input in input.cases[current_case].inputs.itervalues():
  2366. messages[ child_input.name ] = "Value no longer valid for '%s%s', replaced with default" % ( prefix, child_input.label )
  2367. else:
  2368. current = group_values["__current_case__"]
  2369. self.check_and_update_param_values_helper( input.cases[current].inputs, group_values, trans, messages, context, prefix, allow_workflow_parameters=allow_workflow_parameters )
  2370. else:
  2371. # Regular tool parameter, no recursion needed
  2372. try:
  2373. ck_param = True
  2374. if allow_workflow_parameters and isinstance( values[ input.name ], basestring ):
  2375. if WORKFLOW_PARAMETER_REGULAR_EXPRESSION.search( values[ input.name ] ):
  2376. ck_param = False
  2377. #this will fail when a parameter's type has changed to a non-compatible one: e.g. conditional group changed to dataset input
  2378. if ck_param:
  2379. input.value_from_basic( input.value_to_basic( values[ input.name ], trans.app ), trans.app, ignore_errors=False )
  2380. except:
  2381. messages[ input.name ] = "Value no longer valid for '%s%s', replaced with default" % ( prefix, input.label )
  2382. if update_values:
  2383. values[ input.name ] = input.get_initial_value( trans, context )
  2384. def handle_unvalidated_param_values( self, input_values, app ):
  2385. """
  2386. Find any instances of `UnvalidatedValue` within input_values and
  2387. validate them (by calling `ToolParameter.from_html` and
  2388. `ToolParameter.validate`).
  2389. """
  2390. # No validation is done when check_values is False
  2391. if not self.check_values:
  2392. return
  2393. self.handle_unvalidated_param_values_helper( self.inputs, input_values, app )
  2394. def handle_unvalidated_param_values_helper( self, inputs, input_values, app, context=None, prefix="" ):
  2395. """
  2396. Recursive helper for `handle_unvalidated_param_values`
  2397. """
  2398. context = ExpressionContext( input_values, context )
  2399. for input in inputs.itervalues():
  2400. if isinstance( input, Repeat ):
  2401. for i, d in enumerate( input_values[ input.name ] ):
  2402. rep_prefix = prefix + "%s %d > " % ( input.title, i + 1 )
  2403. self.handle_unvalidated_param_values_helper( input.inputs, d, app, context, rep_prefix )
  2404. elif isinstance( input, Conditional ):
  2405. values = input_values[ input.name ]
  2406. current = values["__current_case__"]
  2407. # NOTE: The test param doesn't need to be checked since
  2408. # there would be no way to tell what case to use at
  2409. # workflow build time. However I'm not sure if we are
  2410. # actually preventing such a case explicately.
  2411. self.handle_unvalidated_param_values_helper( input.cases[current].inputs, values, app, context, prefix )
  2412. else:
  2413. # Regular tool parameter
  2414. value = input_values[ input.name ]
  2415. if isinstance( value, UnvalidatedValue ):
  2416. try:
  2417. # Convert from html representation
  2418. if value.value is None:
  2419. # If value.value is None, it could not have been
  2420. # submited via html form and therefore .from_html
  2421. # can't be guaranteed to work
  2422. value = None
  2423. else:
  2424. value = input.from_html( value.value, None, context )
  2425. # Do any further validation on the value
  2426. input.validate( value, None )
  2427. except Exception, e:
  2428. # Wrap an re-raise any generated error so we can
  2429. # generate a more informative message
  2430. message = "Failed runtime validation of %s%s (%s)" \
  2431. % ( prefix, input.label, e )
  2432. raise LateValidationError( message )
  2433. input_values[ input.name ] = value
  2434. def handle_job_failure_exception( self, e ):
  2435. """
  2436. Called by job.fail when an exception is generated to allow generation
  2437. of a better error message (returning None yields the default behavior)
  2438. """
  2439. message = None
  2440. # If the exception was generated by late validation, use its error
  2441. # message (contains the parameter name and value)
  2442. if isinstance( e, LateValidationError ):
  2443. message = e.message
  2444. return message
  2445. def build_dependency_shell_commands( self ):
  2446. """Return a list of commands to be run to populate the current environment to include this tools requirements."""
  2447. if self.tool_shed_repository:
  2448. installed_tool_dependencies = self.tool_shed_repository.tool_dependencies_installed_or_in_error
  2449. else:
  2450. installed_tool_dependencies = None
  2451. return self.app.toolbox.dependency_manager.dependency_shell_commands( self.requirements,
  2452. installed_tool_dependencies=installed_tool_dependencies )
  2453. def build_redirect_url_params( self, param_dict ):
  2454. """
  2455. Substitute parameter values into self.redirect_url_params
  2456. """
  2457. if not self.redirect_url_params:
  2458. return
  2459. redirect_url_params = None
  2460. # Substituting parameter values into the url params
  2461. redirect_url_params = fill_template( self.redirect_url_params, context=param_dict )
  2462. # Remove newlines
  2463. redirect_url_params = redirect_url_params.replace( "\n", " " ).replace( "\r", " " )
  2464. return redirect_url_params
  2465. def parse_redirect_url( self, data, param_dict ):
  2466. """
  2467. Parse the REDIRECT_URL tool param. Tools that send data to an external
  2468. application via a redirect must include the following 3 tool params:
  2469. 1) REDIRECT_URL - the url to which the data is being sent
  2470. 2) DATA_URL - the url to which the receiving application will send an
  2471. http post to retrieve the Galaxy data
  2472. 3) GALAXY_URL - the url to which the external application may post
  2473. data as a response
  2474. """
  2475. redirect_url = param_dict.get( 'REDIRECT_URL' )
  2476. redirect_url_params = self.build_redirect_url_params( param_dict )
  2477. # Add the parameters to the redirect url. We're splitting the param
  2478. # string on '**^**' because the self.parse() method replaced white
  2479. # space with that separator.
  2480. params = redirect_url_params.split( '**^**' )
  2481. rup_dict = {}
  2482. for param in params:
  2483. p_list = param.split( '=' )
  2484. p_name = p_list[0]
  2485. p_val = p_list[1]
  2486. rup_dict[ p_name ] = p_val
  2487. DATA_URL = param_dict.get( 'DATA_URL', None )
  2488. assert DATA_URL is not None, "DATA_URL parameter missing in tool config."
  2489. DATA_URL += "/%s/display" % str( data.id )
  2490. redirect_url += "?DATA_URL=%s" % DATA_URL
  2491. # Add the redirect_url_params to redirect_url
  2492. for p_name in rup_dict:
  2493. redirect_url += "&%s=%s" % ( p_name, rup_dict[ p_name ] )
  2494. # Add the current user email to redirect_url
  2495. if data.history.user:
  2496. USERNAME = str( data.history.user.email )
  2497. else:
  2498. USERNAME = 'Anonymous'
  2499. redirect_url += "&USERNAME=%s" % USERNAME
  2500. return redirect_url
  2501. def call_hook( self, hook_name, *args, **kwargs ):
  2502. """
  2503. Call the custom code hook function identified by 'hook_name' if any,
  2504. and return the results
  2505. """
  2506. try:
  2507. code = self.get_hook( hook_name )
  2508. if code:
  2509. return code( *args, **kwargs )
  2510. except Exception, e:
  2511. original_message = ''
  2512. if len( e.args ):
  2513. original_message = e.args[0]
  2514. e.args = ( "Error in '%s' hook '%s', original message: %s" % ( self.name, hook_name, original_message ), )
  2515. raise
  2516. def exec_before_job( self, app, inp_data, out_data, param_dict={} ):
  2517. pass
  2518. def exec_after_process( self, app, inp_data, out_data, param_dict, job=None ):
  2519. pass
  2520. def job_failed( self, job_wrapper, message, exception=False ):
  2521. """
  2522. Called when a job has failed
  2523. """
  2524. pass
  2525. def collect_associated_files( self, output, job_working_directory ):
  2526. """
  2527. Find extra files in the job working directory and move them into
  2528. the appropriate dataset's files directory
  2529. """
  2530. for name, hda in output.items():
  2531. temp_file_path = os.path.join( job_working_directory, "dataset_%s_files" % ( hda.dataset.id ) )
  2532. extra_dir = None
  2533. try:
  2534. # This skips creation of directories - object store
  2535. # automatically creates them. However, empty directories will
  2536. # not be created in the object store at all, which might be a
  2537. # problem.
  2538. for root, dirs, files in os.walk( temp_file_path ):
  2539. extra_dir = root.replace(job_working_directory, '', 1).lstrip(os.path.sep)
  2540. for f in files:
  2541. self.app.object_store.update_from_file(hda.dataset,
  2542. extra_dir=extra_dir,
  2543. alt_name=f,
  2544. file_name=os.path.join(root, f),
  2545. create=True,
  2546. preserve_symlinks=True
  2547. )
  2548. # Clean up after being handled by object store.
  2549. # FIXME: If the object (e.g., S3) becomes async, this will
  2550. # cause issues so add it to the object store functionality?
  2551. if extra_dir is not None:
  2552. # there was an extra_files_path dir, attempt to remove it
  2553. shutil.rmtree(temp_file_path)
  2554. except Exception, e:
  2555. log.debug( "Error in collect_associated_files: %s" % ( e ) )
  2556. continue
  2557. def collect_child_datasets( self, output, job_working_directory ):
  2558. """
  2559. Look for child dataset files, create HDA and attach to parent.
  2560. """
  2561. children = {}
  2562. # Loop through output file names, looking for generated children in
  2563. # form of 'child_parentId_designation_visibility_extension'
  2564. for name, outdata in output.items():
  2565. filenames = []
  2566. if 'new_file_path' in self.app.config.collect_outputs_from:
  2567. filenames.extend( glob.glob(os.path.join(self.app.config.new_file_path, "child_%i_*" % outdata.id) ) )
  2568. if 'job_working_directory' in self.app.config.collect_outputs_from:
  2569. filenames.extend( glob.glob(os.path.join(job_working_directory, "child_%i_*" % outdata.id) ) )
  2570. for filename in filenames:
  2571. if not name in children:
  2572. children[name] = {}
  2573. fields = os.path.basename(filename).split("_")
  2574. fields.pop(0)
  2575. parent_id = int(fields.pop(0))
  2576. designation = fields.pop(0)
  2577. visible = fields.pop(0).lower()
  2578. if visible == "visible":
  2579. visible = True
  2580. else:
  2581. visible = False
  2582. ext = fields.pop(0).lower()
  2583. child_dataset = self.app.model.HistoryDatasetAssociation( extension=ext,
  2584. parent_id=outdata.id,
  2585. designation=designation,
  2586. visible=visible,
  2587. dbkey=outdata.dbkey,
  2588. create_dataset=True,
  2589. sa_session=self.sa_session )
  2590. self.app.security_agent.copy_dataset_permissions( outdata.dataset, child_dataset.dataset )
  2591. # Move data from temp location to dataset location
  2592. self.app.object_store.update_from_file(child_dataset.dataset, file_name=filename, create=True)
  2593. self.sa_session.add( child_dataset )
  2594. self.sa_session.flush()
  2595. child_dataset.set_size()
  2596. child_dataset.name = "Secondary Dataset (%s)" % ( designation )
  2597. child_dataset.init_meta()
  2598. child_dataset.set_meta()
  2599. child_dataset.set_peek()
  2600. # Associate new dataset with job
  2601. job = None
  2602. for assoc in outdata.creating_job_associations:
  2603. job = assoc.job
  2604. break
  2605. if job:
  2606. assoc = self.app.model.JobToOutputDatasetAssociation( '__new_child_file_%s|%s__' % ( name, designation ), child_dataset )
  2607. assoc.job = job
  2608. self.sa_session.add( assoc )
  2609. self.sa_session.flush()
  2610. child_dataset.state = outdata.state
  2611. self.sa_session.add( child_dataset )
  2612. self.sa_session.flush()
  2613. # Add child to return dict
  2614. children[name][designation] = child_dataset
  2615. # Need to update all associated output hdas, i.e. history was
  2616. # shared with job running
  2617. for dataset in outdata.dataset.history_associations:
  2618. if outdata == dataset:
  2619. continue
  2620. # Create new child dataset
  2621. child_data = child_dataset.copy( parent_id=dataset.id )
  2622. self.sa_session.add( child_data )
  2623. self.sa_session.flush()
  2624. return children
  2625. def collect_primary_datasets( self, output, job_working_directory ):
  2626. """
  2627. Find any additional datasets generated by a tool and attach (for
  2628. cases where number of outputs is not known in advance).
  2629. """
  2630. new_primary_datasets = {}
  2631. try:
  2632. json_file = open( os.path.join( job_working_directory, jobs.TOOL_PROVIDED_JOB_METADATA_FILE ), 'r' )
  2633. for line in json_file:
  2634. line = json.loads( line )
  2635. if line.get( 'type' ) == 'new_primary_dataset':
  2636. new_primary_datasets[ os.path.split( line.get( 'filename' ) )[-1] ] = line
  2637. except Exception:
  2638. # This should not be considered an error or warning condition, this file is optional
  2639. pass
  2640. # Loop through output file names, looking for generated primary
  2641. # datasets in form of:
  2642. # 'primary_associatedWithDatasetID_designation_visibility_extension(_DBKEY)'
  2643. primary_datasets = {}
  2644. for name, outdata in output.items():
  2645. filenames = []
  2646. if 'new_file_path' in self.app.config.collect_outputs_from:
  2647. filenames.extend( glob.glob(os.path.join(self.app.config.new_file_path, "primary_%i_*" % outdata.id) ) )
  2648. if 'job_working_directory' in self.app.config.collect_outputs_from:
  2649. filenames.extend( glob.glob(os.path.join(job_working_directory, "primary_%i_*" % outdata.id) ) )
  2650. for filename in filenames:
  2651. if not name in primary_datasets:
  2652. primary_datasets[name] = {}
  2653. fields = os.path.basename(filename).split("_")
  2654. fields.pop(0)
  2655. parent_id = int(fields.pop(0))
  2656. designation = fields.pop(0)
  2657. visible = fields.pop(0).lower()
  2658. if visible == "visible":
  2659. visible = True
  2660. else:
  2661. visible = False
  2662. ext = fields.pop(0).lower()
  2663. dbkey = outdata.dbkey
  2664. if fields:
  2665. dbkey = fields[ 0 ]
  2666. # Create new primary dataset
  2667. primary_data = self.app.model.HistoryDatasetAssociation( extension=ext,
  2668. designation=designation,
  2669. visible=visible,
  2670. dbkey=dbkey,
  2671. create_dataset=True,
  2672. sa_session=self.sa_session )
  2673. self.app.security_agent.copy_dataset_permissions( outdata.dataset, primary_data.dataset )
  2674. self.sa_session.add( primary_data )
  2675. self.sa_session.flush()
  2676. # Move data from temp location to dataset location
  2677. self.app.object_store.update_from_file(primary_data.dataset, file_name=filename, create=True)
  2678. primary_data.set_size()
  2679. primary_data.name = "%s (%s)" % ( outdata.name, designation )
  2680. primary_data.info = outdata.info
  2681. primary_data.init_meta( copy_from=outdata )
  2682. primary_data.dbkey = dbkey
  2683. # Associate new dataset with job
  2684. job = None
  2685. for assoc in outdata.creating_job_associations:
  2686. job = assoc.job
  2687. break
  2688. if job:
  2689. assoc = self.app.model.JobToOutputDatasetAssociation( '__new_primary_file_%s|%s__' % ( name, designation ), primary_data )
  2690. assoc.job = job
  2691. self.sa_session.add( assoc )
  2692. self.sa_session.flush()
  2693. primary_data.state = outdata.state
  2694. #add tool/metadata provided information
  2695. new_primary_datasets_attributes = new_primary_datasets.get( os.path.split( filename )[-1] )
  2696. if new_primary_datasets_attributes:
  2697. dataset_att_by_name = dict( ext='extension' )
  2698. for att_set in [ 'name', 'info', 'ext', 'dbkey' ]:
  2699. dataset_att_name = dataset_att_by_name.get( att_set, att_set )
  2700. setattr( primary_data, dataset_att_name, new_primary_datasets_attributes.get( att_set, getattr( primary_data, dataset_att_name ) ) )
  2701. primary_data.set_meta()
  2702. primary_data.set_peek()
  2703. self.sa_session.add( primary_data )
  2704. self.sa_session.flush()
  2705. outdata.history.add_dataset( primary_data )
  2706. # Add dataset to return dict
  2707. primary_datasets[name][designation] = primary_data
  2708. # Need to update all associated output hdas, i.e. history was
  2709. # shared with job running
  2710. for dataset in outdata.dataset.history_associations:
  2711. if outdata == dataset:
  2712. continue
  2713. new_data = primary_data.copy()
  2714. dataset.history.add_dataset( new_data )
  2715. self.sa_session.add( new_data )
  2716. self.sa_session.flush()
  2717. return primary_datasets
  2718. def to_dict( self, trans, link_details=False, io_details=False ):
  2719. """ Returns dict of tool. """
  2720. # Basic information
  2721. tool_dict = super( Tool, self ).to_dict()
  2722. # Add link details.
  2723. if link_details:
  2724. # Add details for creating a hyperlink to the tool.
  2725. if not isinstance( self, DataSourceTool ):
  2726. link = url_for( controller='tool_runner', tool_id=self.id )
  2727. else:
  2728. link = url_for( controller='tool_runner', action='data_source_redirect', tool_id=self.id )
  2729. # Basic information
  2730. tool_dict.update( { 'link': link,
  2731. 'min_width': self.uihints.get( 'minwidth', -1 ),
  2732. 'target': self.target } )
  2733. # Add input and output details.
  2734. if io_details:
  2735. tool_dict[ 'inputs' ] = [ input.to_dict( trans ) for input in self.inputs.values() ]
  2736. tool_dict[ 'outputs' ] = [ output.to_dict() for output in self.outputs.values() ]
  2737. tool_dict[ 'panel_section_id' ], tool_dict[ 'panel_section_name' ] = self.get_panel_section()
  2738. return tool_dict
  2739. def get_default_history_by_trans( self, trans, create=False ):
  2740. return trans.get_history( create=create )
  2741. class OutputParameterJSONTool( Tool ):
  2742. """
  2743. Alternate implementation of Tool that provides parameters and other values
  2744. JSONified within the contents of an output dataset
  2745. """
  2746. tool_type = 'output_parameter_json'
  2747. def _prepare_json_list( self, param_list ):
  2748. rval = []
  2749. for value in param_list:
  2750. if isinstance( value, dict ):
  2751. rval.append( self._prepare_json_param_dict( value ) )
  2752. elif isinstance( value, list ):
  2753. rval.append( self._prepare_json_list( value ) )
  2754. else:
  2755. rval.append( str( value ) )
  2756. return rval
  2757. def _prepare_json_param_dict( self, param_dict ):
  2758. rval = {}
  2759. for key, value in param_dict.iteritems():
  2760. if isinstance( value, dict ):
  2761. rval[ key ] = self._prepare_json_param_dict( value )
  2762. elif isinstance( value, list ):
  2763. rval[ key ] = self._prepare_json_list( value )
  2764. else:
  2765. rval[ key ] = str( value )
  2766. return rval
  2767. def exec_before_job( self, app, inp_data, out_data, param_dict=None ):
  2768. if param_dict is None:
  2769. param_dict = {}
  2770. json_params = {}
  2771. json_params[ 'param_dict' ] = self._prepare_json_param_dict( param_dict ) # it would probably be better to store the original incoming parameters here, instead of the Galaxy modified ones?
  2772. json_params[ 'output_data' ] = []
  2773. json_params[ 'job_config' ] = dict( GALAXY_DATATYPES_CONF_FILE=param_dict.get( 'GALAXY_DATATYPES_CONF_FILE' ), GALAXY_ROOT_DIR=param_dict.get( 'GALAXY_ROOT_DIR' ), TOOL_PROVIDED_JOB_METADATA_FILE=jobs.TOOL_PROVIDED_JOB_METADATA_FILE )
  2774. json_filename = None
  2775. for i, ( out_name, data ) in enumerate( out_data.iteritems() ):
  2776. #use wrapped dataset to access certain values
  2777. wrapped_data = param_dict.get( out_name )
  2778. #allow multiple files to be created
  2779. file_name = str( wrapped_data )
  2780. extra_files_path = str( wrapped_data.files_path )
  2781. data_dict = dict( out_data_name=out_name,
  2782. ext=data.ext,
  2783. dataset_id=data.dataset.id,
  2784. hda_id=data.id,
  2785. file_name=file_name,
  2786. extra_files_path=extra_files_path )
  2787. json_params[ 'output_data' ].append( data_dict )
  2788. if json_filename is None:
  2789. json_filename = file_name
  2790. out = open( json_filename, 'w' )
  2791. out.write( json.dumps( json_params ) )
  2792. out.close()
  2793. class DataSourceTool( OutputParameterJSONTool ):
  2794. """
  2795. Alternate implementation of Tool for data_source tools -- those that
  2796. allow the user to query and extract data from another web site.
  2797. """
  2798. tool_type = 'data_source'
  2799. default_tool_action = DataSourceToolAction
  2800. def _build_GALAXY_URL_parameter( self ):
  2801. return ToolParameter.build( self, ElementTree.XML( '<param name="GALAXY_URL" type="baseurl" value="/tool_runner?tool_id=%s" />' % self.id ) )
  2802. def parse_inputs( self, root ):
  2803. super( DataSourceTool, self ).parse_inputs( root )
  2804. if 'GALAXY_URL' not in self.inputs:
  2805. self.inputs[ 'GALAXY_URL' ] = self._build_GALAXY_URL_parameter()
  2806. self.inputs_by_page[0][ 'GALAXY_URL' ] = self.inputs[ 'GALAXY_URL' ]
  2807. def exec_before_job( self, app, inp_data, out_data, param_dict=None ):
  2808. if param_dict is None:
  2809. param_dict = {}
  2810. dbkey = param_dict.get( 'dbkey' )
  2811. info = param_dict.get( 'info' )
  2812. data_type = param_dict.get( 'data_type' )
  2813. name = param_dict.get( 'name' )
  2814. json_params = {}
  2815. json_params[ 'param_dict' ] = self._prepare_json_param_dict( param_dict ) # it would probably be better to store the original incoming parameters here, instead of the Galaxy modified ones?
  2816. json_params[ 'output_data' ] = []
  2817. json_params[ 'job_config' ] = dict( GALAXY_DATATYPES_CONF_FILE=param_dict.get( 'GALAXY_DATATYPES_CONF_FILE' ), GALAXY_ROOT_DIR=param_dict.get( 'GALAXY_ROOT_DIR' ), TOOL_PROVIDED_JOB_METADATA_FILE=jobs.TOOL_PROVIDED_JOB_METADATA_FILE )
  2818. json_filename = None
  2819. for i, ( out_name, data ) in enumerate( out_data.iteritems() ):
  2820. #use wrapped dataset to access certain values
  2821. wrapped_data = param_dict.get( out_name )
  2822. #allow multiple files to be created
  2823. cur_base_param_name = 'GALAXY|%s|' % out_name
  2824. cur_name = param_dict.get( cur_base_param_name + 'name', name )
  2825. cur_dbkey = param_dict.get( cur_base_param_name + 'dkey', dbkey )
  2826. cur_info = param_dict.get( cur_base_param_name + 'info', info )
  2827. cur_data_type = param_dict.get( cur_base_param_name + 'data_type', data_type )
  2828. if cur_name:
  2829. data.name = cur_name
  2830. if not data.info and cur_info:
  2831. data.info = cur_info
  2832. if cur_dbkey:
  2833. data.dbkey = cur_dbkey
  2834. if cur_data_type:
  2835. data.extension = cur_data_type
  2836. file_name = str( wrapped_data )
  2837. extra_files_path = str( wrapped_data.files_path )
  2838. data_dict = dict( out_data_name=out_name,
  2839. ext=data.ext,
  2840. dataset_id=data.dataset.id,
  2841. hda_id=data.id,
  2842. file_name=file_name,
  2843. extra_files_path=extra_files_path )
  2844. json_params[ 'output_data' ].append( data_dict )
  2845. if json_filename is None:
  2846. json_filename = file_name
  2847. out = open( json_filename, 'w' )
  2848. out.write( json.dumps( json_params ) )
  2849. out.close()
  2850. class AsyncDataSourceTool( DataSourceTool ):
  2851. tool_type = 'data_source_async'
  2852. def _build_GALAXY_URL_parameter( self ):
  2853. return ToolParameter.build( self, ElementTree.XML( '<param name="GALAXY_URL" type="baseurl" value="/async/%s" />' % self.id ) )
  2854. class DataDestinationTool( Tool ):
  2855. tool_type = 'data_destination'
  2856. class SetMetadataTool( Tool ):
  2857. """
  2858. Tool implementation for special tool that sets metadata on an existing
  2859. dataset.
  2860. """
  2861. tool_type = 'set_metadata'
  2862. requires_setting_metadata = False
  2863. def exec_after_process( self, app, inp_data, out_data, param_dict, job=None ):
  2864. for name, dataset in inp_data.iteritems():
  2865. external_metadata = JobExternalOutputMetadataWrapper( job )
  2866. if external_metadata.external_metadata_set_successfully( dataset, app.model.context ):
  2867. dataset.metadata.from_JSON_dict( external_metadata.get_output_filenames_by_dataset( dataset, app.model.context ).filename_out )
  2868. else:
  2869. dataset._state = model.Dataset.states.FAILED_METADATA
  2870. self.sa_session.add( dataset )
  2871. self.sa_session.flush()
  2872. return
  2873. # If setting external metadata has failed, how can we inform the
  2874. # user? For now, we'll leave the default metadata and set the state
  2875. # back to its original.
  2876. dataset.datatype.after_setting_metadata( dataset )
  2877. if job and job.tool_id == '1.0.0':
  2878. dataset.state = param_dict.get( '__ORIGINAL_DATASET_STATE__' )
  2879. else:
  2880. # Revert dataset.state to fall back to dataset.dataset.state
  2881. dataset._state = None
  2882. # Need to reset the peek, which may rely on metadata
  2883. dataset.set_peek()
  2884. self.sa_session.add( dataset )
  2885. self.sa_session.flush()
  2886. def job_failed( self, job_wrapper, message, exception=False ):
  2887. job = job_wrapper.sa_session.query( model.Job ).get( job_wrapper.job_id )
  2888. if job:
  2889. inp_data = {}
  2890. for dataset_assoc in job.input_datasets:
  2891. inp_data[dataset_assoc.name] = dataset_assoc.dataset
  2892. return self.exec_after_process( job_wrapper.app, inp_data, {}, job_wrapper.get_param_dict(), job=job )
  2893. class ExportHistoryTool( Tool ):
  2894. tool_type = 'export_history'
  2895. class ImportHistoryTool( Tool ):
  2896. tool_type = 'import_history'
  2897. class GenomeIndexTool( Tool ):
  2898. tool_type = 'index_genome'
  2899. class DataManagerTool( OutputParameterJSONTool ):
  2900. tool_type = 'manage_data'
  2901. default_tool_action = DataManagerToolAction
  2902. def __init__( self, config_file, root, app, guid=None, data_manager_id=None, **kwds ):
  2903. self.data_manager_id = data_manager_id
  2904. super( DataManagerTool, self ).__init__( config_file, root, app, guid=guid, **kwds )
  2905. if self.data_manager_id is None:
  2906. self.data_manager_id = self.id
  2907. def exec_after_process( self, app, inp_data, out_data, param_dict, job=None, **kwds ):
  2908. #run original exec_after_process
  2909. super( DataManagerTool, self ).exec_after_process( app, inp_data, out_data, param_dict, job=job, **kwds )
  2910. #process results of tool
  2911. if job and job.state == job.states.ERROR:
  2912. return
  2913. #Job state may now be 'running' instead of previous 'error', but datasets are still set to e.g. error
  2914. for dataset in out_data.itervalues():
  2915. if dataset.state != dataset.states.OK:
  2916. return
  2917. data_manager_id = job.data_manager_association.data_manager_id
  2918. data_manager = self.app.data_managers.get_manager( data_manager_id, None )
  2919. assert data_manager is not None, "Invalid data manager (%s) requested. It may have been removed before the job completed." % ( data_manager_id )
  2920. data_manager.process_result( out_data )
  2921. def get_default_history_by_trans( self, trans, create=False ):
  2922. def _create_data_manager_history( user ):
  2923. history = trans.app.model.History( name='Data Manager History (automatically created)', user=user )
  2924. data_manager_association = trans.app.model.DataManagerHistoryAssociation( user=user, history=history )
  2925. trans.sa_session.add_all( ( history, data_manager_association ) )
  2926. trans.sa_session.flush()
  2927. return history
  2928. user = trans.user
  2929. assert user, 'You must be logged in to use this tool.'
  2930. history = user.data_manager_histories
  2931. if not history:
  2932. #create
  2933. if create:
  2934. history = _create_data_manager_history( user )
  2935. else:
  2936. history = None
  2937. else:
  2938. for history in reversed( history ):
  2939. history = history.history
  2940. if not history.deleted:
  2941. break
  2942. if history.deleted:
  2943. if create:
  2944. history = _create_data_manager_history( user )
  2945. else:
  2946. history = None
  2947. return history
  2948. # Populate tool_type to ToolClass mappings
  2949. tool_types = {}
  2950. for tool_class in [ Tool, DataDestinationTool, SetMetadataTool, DataSourceTool, AsyncDataSourceTool, DataManagerTool ]:
  2951. tool_types[ tool_class.tool_type ] = tool_class
  2952. # ---- Utility classes to be factored out -----------------------------------
  2953. class TracksterConfig:
  2954. """ Trackster configuration encapsulation. """
  2955. def __init__( self, actions ):
  2956. self.actions = actions
  2957. @staticmethod
  2958. def parse( root ):
  2959. actions = []
  2960. for action_elt in root.findall( "action" ):
  2961. actions.append( SetParamAction.parse( action_elt ) )
  2962. return TracksterConfig( actions )
  2963. class SetParamAction:
  2964. """ Set parameter action. """
  2965. def __init__( self, name, output_name ):
  2966. self.name = name
  2967. self.output_name = output_name
  2968. @staticmethod
  2969. def parse( elt ):
  2970. """ Parse action from element. """
  2971. return SetParamAction( elt.get( "name" ), elt.get( "output_name" ) )
  2972. class BadValue( object ):
  2973. def __init__( self, value ):
  2974. self.value = value
  2975. class ToolStdioRegex( object ):
  2976. """
  2977. This is a container for the <stdio> element's regex subelement.
  2978. The regex subelement has a "match" attribute, a "sources"
  2979. attribute that contains "output" and/or "error", and a "level"
  2980. attribute that contains "warning" or "fatal".
  2981. """
  2982. def __init__( self ):
  2983. self.match = ""
  2984. self.stdout_match = False
  2985. self.stderr_match = False
  2986. # TODO: Define a common class or constant for error level:
  2987. self.error_level = "fatal"
  2988. self.desc = ""
  2989. class ToolStdioExitCode( object ):
  2990. """
  2991. This is a container for the <stdio> element's <exit_code> subelement.
  2992. The exit_code element has a range of exit codes and the error level.
  2993. """
  2994. def __init__( self ):
  2995. self.range_start = float( "-inf" )
  2996. self.range_end = float( "inf" )
  2997. # TODO: Define a common class or constant for error level:
  2998. self.error_level = "fatal"
  2999. self.desc = ""
  3000. def json_fix( val ):
  3001. if isinstance( val, list ):
  3002. return [ json_fix( v ) for v in val ]
  3003. elif isinstance( val, dict ):
  3004. return dict( [ ( json_fix( k ), json_fix( v ) ) for ( k, v ) in val.iteritems() ] )
  3005. elif isinstance( val, unicode ):
  3006. return val.encode( "utf8" )
  3007. else:
  3008. return val
  3009. def check_param_from_incoming( trans, state, input, incoming, key, context, source ):
  3010. """
  3011. Unlike "update" state, this preserves default if no incoming value found.
  3012. This lets API user specify just a subset of params and allow defaults to be
  3013. used when available.
  3014. """
  3015. default_input_value = state.get( input.name, None )
  3016. incoming_value = get_incoming_value( incoming, key, default_input_value )
  3017. value, error = check_param( trans, input, incoming_value, context, source=source )
  3018. return value, error
  3019. def get_incoming_value( incoming, key, default ):
  3020. """
  3021. Fetch value from incoming dict directly or check special nginx upload
  3022. created variants of this key.
  3023. """
  3024. if "__" + key + "__is_composite" in incoming:
  3025. composite_keys = incoming["__" + key + "__keys"].split()
  3026. value = dict()
  3027. for composite_key in composite_keys:
  3028. value[composite_key] = incoming[key + "_" + composite_key]
  3029. return value
  3030. else:
  3031. return incoming.get( key, default )
  3032. class InterruptedUpload( Exception ):
  3033. pass