1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/d5870d822b24/ Changeset: d5870d822b24 User: jmchilton Date: 2014-07-28 19:15:32 Summary: Merged in jmchilton/galaxy-central-fork-1 (pull request #440) Initial BibTeX/DOI citation support in tools and histories. Affected #: 16 files diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae .hgignore --- a/.hgignore +++ b/.hgignore @@ -16,6 +16,7 @@ # Database stuff database/beaker_sessions +database/citations database/community_files database/compiled_templates database/files diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -376,6 +376,10 @@ # Default chunk size for chunkable datatypes -- 64k self.display_chunk_size = int( kwargs.get( 'display_chunk_size', 65536) ) + self.citation_cache_type = kwargs.get( "citation_cache_type", "file" ) + self.citation_cache_data_dir = self.resolve_path( kwargs.get( "citation_cache_data_dir", "database/citations/data" ) ) + self.citation_cache_lock_dir = self.resolve_path( kwargs.get( "citation_cache_lock_dir", "database/citations/locks" ) ) + @property def sentry_dsn_public( self ): """ @@ -570,6 +574,10 @@ tool_configs = self.config.tool_configs if self.config.migrated_tools_config not in tool_configs: tool_configs.append( self.config.migrated_tools_config ) + + from galaxy.managers.citations import CitationsManager + self.citations_manager = CitationsManager( self ) + from galaxy import tools self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self ) # Search support for tools diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/managers/citations.py --- /dev/null +++ b/lib/galaxy/managers/citations.py @@ -0,0 +1,170 @@ +import pkg_resources + +import functools +import os +import urllib2 + +pkg_resources.require('Beaker') +from beaker.cache import CacheManager +from beaker.util import parse_cache_config_options + +import logging +log = logging.getLogger( __name__ ) + + +class CitationsManager( object ): + + def __init__( self, app ): + self.app = app + self.doi_cache = DoiCache( app.config ) + + def citations_for_tool( self, tool ): + return tool.citations + + def citations_for_tool_ids( self, tool_ids ): + citation_collection = CitationCollection() + for tool_id in tool_ids: + tool = self._get_tool( tool_id ) + for citation in self.citations_for_tool( tool ): + citation_collection.add( citation ) + return citation_collection.citations + + def parse_citation( self, citation_elem, tool_directory ): + return parse_citation( citation_elem, tool_directory, self ) + + def _get_tool( self, tool_id ): + tool = self.app.toolbox.get_tool( tool_id ) + return tool + + +class DoiCache( object ): + + def __init__( self, config ): + cache_opts = { + 'cache.type': getattr( config, 'citation_cache_type', 'file'), + 'cache.data_dir': getattr( config, 'citation_cache_data_dir', None), + 'cache.lock_dir': getattr( config, 'citation_cache_lock_dir', None), + } + self._cache = CacheManager(**parse_cache_config_options(cache_opts)).get_cache('doi') + + def _raw_get_bibtex( self, doi ): + dx_url = "http://dx.doi.org/" + doi + headers = {'Accept': "text/bibliography; style=bibtex" } + req = urllib2.Request(dx_url, data="", headers=headers) + response = urllib2.urlopen(req) + bibtex = response.read() + return bibtex + + def get_bibtex( self, doi ): + createfunc = functools.partial(self._raw_get_bibtex, doi) + return self._cache.get(key=doi, createfunc=createfunc) + + +def parse_citation( elem, directory, citation_manager ): + """ Parse an abstract citation entry from the specified XML element. + The directory parameter should be used to find external files for this + citation. + """ + citation_type = elem.attrib.get( 'type', None ) + citation_class = CITATION_CLASSES.get( citation_type, None ) + if not citation_class: + log.warn("Unknown or unspecified citation type: %s" % citation_type) + return None + return citation_class( elem, directory, citation_manager ) + + +class CitationCollection( object ): + + def __init__( self ): + self.citations = [] + + def __iter__( self ): + return self.citations.__iter__() + + def __len__( self ): + return len( self.citations ) + + def add( self, new_citation ): + for citation in self.citations: + if citation.equals( new_citation ): + # TODO: We have two equivalent citations, pick the more + # informative/complete/correct. + return False + + self.citations.append( new_citation ) + return True + + +class BaseCitation( object ): + + def to_dict( self, citation_format ): + if citation_format == "bibtex": + return dict( + format="bibtex", + content=self.to_bibtex(), + ) + else: + raise Exception("Unknown citation format %s" % citation_format) + + def equals( self, other_citation ): + if self.has_doi() and other_citation.has_doi(): + return self.doi() == other_citation.doi() + else: + # TODO: Do a better job figuring out if this is the same citation. + return self.to_bibtex() == other_citation.to_bibtex() + + def has_doi( self ): + return False + + +class BibtexCitation( BaseCitation ): + + def __init__( self, elem, directory, citation_manager ): + bibtex_file = elem.attrib.get("file", None) + if bibtex_file: + raw_bibtex = open(os.path.join(directory, bibtex_file), "r").read() + else: + raw_bibtex = elem.text.strip() + self._set_raw_bibtex( raw_bibtex ) + + def _set_raw_bibtex( self, raw_bibtex ): + self.raw_bibtex = raw_bibtex + + def to_bibtex( self ): + return self.raw_bibtex + + +class DoiCitation( BaseCitation ): + BIBTEX_UNSET = object() + + def __init__( self, elem, directory, citation_manager ): + self.__doi = elem.text.strip() + self.doi_cache = citation_manager.doi_cache + self.raw_bibtex = DoiCitation.BIBTEX_UNSET + + def has_doi( self ): + return True + + def doi( self ): + return self.__doi + + def to_bibtex( self ): + if self.raw_bibtex is DoiCitation.BIBTEX_UNSET: + try: + self.raw_bibtex = self.doi_cache.get_bibtex(self.__doi) + except Exception: + log.exception("Failed to fetch bibtex for DOI %s" % self.__doi) + + if self.raw_bibtex is DoiCitation.BIBTEX_UNSET: + return """@MISC{%s, + DOI = '%s', + note = 'Failed to fetch BibTeX for DOI.' + }""" % (self.__doi, self.__doi) + else: + return self.raw_bibtex + + +CITATION_CLASSES = dict( + bibtex=BibtexCitation, + doi=DoiCitation, +) diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/tools/__init__.py --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1360,6 +1360,9 @@ requirements, containers = parse_requirements_from_xml( root ) self.requirements = requirements self.containers = containers + + self.citations = self._parse_citations( root ) + # Determine if this tool can be used in workflows self.is_workflow_compatible = self.check_workflow_compatible(root) # Trackster configuration. @@ -1686,6 +1689,20 @@ trace_msg = repr( traceback.format_tb( trace ) ) log.error( "Traceback: %s" % trace_msg ) + def _parse_citations( self, root ): + citations = [] + citations_elem = root.find("citations") + if not citations_elem: + return citations + + for citation_elem in citations_elem: + if citation_elem.tag != "citation": + pass + citation = self.app.citations_manager.parse_citation( citation_elem, self.tool_dir ) + if citation: + citations.append( citation ) + return citations + # TODO: This method doesn't have to be part of the Tool class. def parse_error_level( self, err_level ): """ diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/api/histories.py --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -18,7 +18,7 @@ from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin -from galaxy.managers import histories +from galaxy.managers import histories, citations from galaxy import util from galaxy.util import string_as_bool @@ -34,6 +34,7 @@ def __init__( self, app ): super( HistoriesController, self ).__init__( app ) + self.citations_manager = citations.CitationsManager( app ) self.mgrs = util.bunch.Bunch( histories=histories.HistoryManager() ) @@ -117,6 +118,20 @@ history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) return history_data + @expose_api_anonymous + def citations( self, trans, history_id, **kwd ): + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), check_ownership=False, check_accessible=True ) + tool_ids = set([]) + for dataset in history.datasets: + job = dataset.creating_job + if not job: + continue + tool_id = job.tool_id + if not tool_id: + continue + tool_ids.add(tool_id) + return map( lambda citation: citation.to_dict( "bibtex" ), self.citations_manager.citations_for_tool_ids( tool_ids ) ) + @expose_api def set_as_current( self, trans, id, **kwd ): """ diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/api/tools.py --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -1,6 +1,8 @@ import urllib +from galaxy import exceptions from galaxy import web, util +from galaxy.web import _future_expose_api_anonymous from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesVisualizationMixin from galaxy.web.base.controller import UsesHistoryMixin @@ -44,7 +46,7 @@ trans.response.status = 500 return { 'error': str( exc ) } - @web.expose_api + @_future_expose_api_anonymous def show( self, trans, id, **kwd ): """ GET /api/tools/{tool_id} @@ -52,18 +54,16 @@ """ io_details = util.string_as_bool( kwd.get( 'io_details', False ) ) link_details = util.string_as_bool( kwd.get( 'link_details', False ) ) - try: - id = urllib.unquote_plus( id ) - tool = self.app.toolbox.get_tool( id ) - if not tool: - trans.response.status = 404 - return { 'error': 'tool not found', 'id': id } - return tool.to_dict( trans, io_details=io_details, link_details=link_details ) + tool = self._get_tool( id ) + return tool.to_dict( trans, io_details=io_details, link_details=link_details ) - except Exception, exc: - log.error( 'could not convert tool (%s) to dictionary: %s', id, str( exc ), exc_info=True ) - trans.response.status = 500 - return { 'error': str( exc ) } + @_future_expose_api_anonymous + def citations( self, trans, id, **kwds ): + tool = self._get_tool( id ) + rval = [] + for citation in tool.citations: + rval.append( citation.to_dict( 'bibtex' ) ) + return rval @web.expose_api_anonymous def create( self, trans, payload, **kwd ): @@ -170,6 +170,12 @@ # # -- Helper methods -- # + def _get_tool( self, id ): + id = urllib.unquote_plus( id ) + tool = self.app.toolbox.get_tool( id ) + if not tool: + raise exceptions.ObjectNotFound("Could not find tool with id '%s'" % id) + return tool def _rerun_tool( self, trans, payload, **kwargs ): """ diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -174,6 +174,7 @@ webapp.mapper.resource( 'ftp_file', 'ftp_files', path_prefix='/api' ) webapp.mapper.resource( 'group', 'groups', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) + webapp.mapper.connect( '/api/tools/{id:.+?}/citations', action='citations', controller="tools" ) webapp.mapper.connect( '/api/tools/{id:.+?}', action='show', controller="tools" ) webapp.mapper.resource( 'tool', 'tools', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) @@ -181,6 +182,7 @@ webapp.mapper.resource( 'visualization', 'visualizations', path_prefix='/api' ) webapp.mapper.resource( 'workflow', 'workflows', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'history', 'histories', path_prefix='/api' ) + webapp.mapper.connect( '/api/histories/{history_id}/citations', action='citations', controller="histories" ) webapp.mapper.resource( 'configuration', 'configuration', path_prefix='/api' ) webapp.mapper.resource( 'datatype', 'datatypes', diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/controllers/history.py --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -448,6 +448,13 @@ # ......................................................................... html @web.expose + def citations( self, trans ): + # Get history + history = trans.history + history_id = trans.security.encode_id( history.id ) + return trans.fill_template( "history/citations.mako", history=history, history_id=history_id ) + + @web.expose def display_structured( self, trans, id=None ): """ Display a history as a nested structure showing the jobs and workflow This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.