diff --git a/eggs.ini b/eggs.ini index 38692a98526..a198487ecad 100644 --- a/eggs.ini +++ b/eggs.ini @@ -41,7 +41,7 @@ Paste = 1.6 PasteDeploy = 1.3.3 PasteScript = 1.7.3 pexpect = 2.4 -Routes = 1.11 +Routes = 1.12.3 simplejson = 1.5 SQLAlchemy = 0.5.6 sqlalchemy_migrate = 0.5.4 diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index a1b6ed21155..8e6401be6c7 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -39,6 +39,8 @@ class Configuration( object ): self.file_path = resolve_path( kwargs.get( "file_path", "database/files" ), self.root ) self.new_file_path = resolve_path( kwargs.get( "new_file_path", "database/tmp" ), self.root ) self.cookie_path = kwargs.get( "cookie_path", "/" ) + # web API + self.enable_api = string_as_bool( kwargs.get( 'enable_api', False ) ) # dataset Track files self.track_store_path = kwargs.get( "track_store_path", "${extra_files_path}/tracks") self.tool_path = resolve_path( kwargs.get( "tool_path", "tools" ), self.root ) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 49aceacfa90..4cf834fee48 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -829,6 +829,8 @@ class HistoryDatasetAssociationDisplayAtAuthorization( object ): class Library( object ): permitted_actions = get_permitted_actions( filter='LIBRARY' ) + api_collection_visible_keys = ( 'id', 'name' ) + api_element_visible_keys = ( 'name', 'description', 'synopsys' ) def __init__( self, name=None, description=None, synopsis=None, root_folder=None ): self.name = name or "Unnamed library" self.description = description @@ -870,8 +872,21 @@ class Library( object ): if isinstance( name, str ): name = unicode( name, 'utf-8' ) return name + def get_api_value( self, view='collection' ): + rval = {} + try: + visible_keys = self.__getattribute__( 'api_' + view + '_visible_keys' ) + except AttributeError: + raise Exception( 'Unknown API view: %s' % view ) + for key in visible_keys: + try: + rval[key] = self.__getattribute__( key ) + except AttributeError: + rval[key] = None + return rval class LibraryFolder( object ): + api_element_visible_keys = ( 'name', 'description', 'item_count', 'genome_build' ) def __init__( self, name=None, description=None, item_count=0, order_id=None ): self.name = name or "Unnamed folder" self.description = description @@ -961,6 +976,18 @@ class LibraryFolder( object ): if isinstance( name, str ): name = unicode( name, 'utf-8' ) return name + def get_api_value( self, view='collection' ): + rval = {} + try: + visible_keys = self.__getattribute__( 'api_' + view + '_visible_keys' ) + except AttributeError: + raise Exception( 'Unknown API view: %s' % view ) + for key in visible_keys: + try: + rval[key] = self.__getattribute__( key ) + except AttributeError: + rval[key] = None + return rval @property def parent_library( self ): f = self @@ -1011,6 +1038,28 @@ class LibraryDataset( object ): if not purged and self.purged: raise Exception( "Cannot unpurge once purged" ) purged = property( get_purged, set_purged ) + def get_api_value( self, view='collection' ): + # Since this class is a proxy to rather complex attributes we want to + # display in other objects, we can't use the simpler method used by + # other model classes. + ldda = self.library_dataset_dataset_association + rval = dict( name = ldda.name, + uploaded_by = ldda.user.email, + message = ldda.message, + date_uploaded = ldda.create_time.isoformat(), + file_size = int( ldda.get_size() ), + data_type = ldda.ext, + genome_build = ldda.dbkey, + misc_info = ldda.info, + misc_blurb = ldda.blurb ) + for name, spec in ldda.metadata.spec.items(): + val = ldda.metadata.get( name ) + if isinstance( val, MetadataFile ): + val = val.file_name + elif isinstance( val, list ): + val = ', '.join( val ) + rval['metadata_' + name] = val + return rval class LibraryDatasetDatasetAssociation( DatasetInstance ): def __init__( self, @@ -1734,6 +1783,9 @@ class UserAction( object ): self.params = params self.context = context +class APIKeys( object ): + pass + ## ---- Utility methods ------------------------------------------------------- def directory_hash_id( id ): @@ -1748,5 +1800,3 @@ def directory_hash_id( id ): padded = padded[:-3] # Break into chunks of three return [ padded[i*3:(i+1)*3] for i in range( len( padded ) // 3 ) ] - - diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 9efb8291c91..b0eda4469e9 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -889,6 +889,12 @@ UserAction.table = Table( "user_action", metadata, Column( "context", Unicode( 512 ) ), Column( "params", Unicode( 1024 ) ) ) +APIKeys.table = Table( "api_keys", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "key", TrimmedString( 32 ), index=True, unique=True ) ) + # With the tables defined we can define the mappers and setup the # relationships between the model objects. @@ -1067,8 +1073,9 @@ assign_mapper( context, User, User.table, _preferences=relation( UserPreference, backref="user", collection_class=attribute_mapped_collection('name')), # addresses=relation( UserAddress, # primaryjoin=( User.table.c.id == UserAddress.table.c.user_id ) ) - values=relation( FormValues, - primaryjoin=( User.table.c.form_values_id == FormValues.table.c.id ) ), + values=relation( FormValues, + primaryjoin=( User.table.c.form_values_id == FormValues.table.c.id ) ), + api_keys=relation( APIKeys, backref="user", order_by=desc( APIKeys.table.c.create_time ) ), ) ) # Set up proxy so that this syntax is possible: @@ -1485,6 +1492,9 @@ assign_mapper( context, UserAction, UserAction.table, properties = dict( user=relation( User.mapper ) ) ) +assign_mapper( context, APIKeys, APIKeys.table, + properties = {} ) + def db_next_hid( self ): """ Override __next_hid to generate from the database in a concurrency diff --git a/lib/galaxy/model/migrate/versions/0049_api_keys_table.py b/lib/galaxy/model/migrate/versions/0049_api_keys_table.py new file mode 100644 index 00000000000..106920b2e5c --- /dev/null +++ b/lib/galaxy/model/migrate/versions/0049_api_keys_table.py @@ -0,0 +1,38 @@ +""" +Migration script to add the api_keys table. +""" + +from sqlalchemy import * +from migrate import * +from migrate.changeset import * +from galaxy.model.custom_types import * + +import datetime +now = datetime.datetime.utcnow + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) + +APIKeys_table = Table( "api_keys", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "key", TrimmedString( 32 ), index=True, unique=True ) ) + +def upgrade(): + print __doc__ + metadata.reflect() + try: + APIKeys_table.create() + except Exception, e: + log.debug( "Creating api_keys table failed: %s" % str( e ) ) + +def downgrade(): + # Load existing tables + metadata.reflect() + try: + APIKeys_table.drop() + except Exception, e: + log.debug( "Dropping api_keys table failed: %s" % str( e ) ) diff --git a/lib/galaxy/tools/actions/upload_common.py b/lib/galaxy/tools/actions/upload_common.py index 6e56fba429a..5d0db288884 100644 --- a/lib/galaxy/tools/actions/upload_common.py +++ b/lib/galaxy/tools/actions/upload_common.py @@ -124,7 +124,7 @@ def new_history_upload( trans, uploaded_dataset, state=None ): return hda def new_library_upload( trans, cntrller, uploaded_dataset, library_bunch, state=None ): current_user_roles = trans.get_current_user_roles() - if not ( cntrller in [ 'library_admin' ] or trans.app.security_agent.can_add_library_item( current_user_roles, library_bunch.folder ) ): + if not ( ( trans.user_is_admin() and cntrller in [ 'library_admin', 'api' ] ) or trans.app.security_agent.can_add_library_item( current_user_roles, library_bunch.folder ) ): # This doesn't have to be pretty - the only time this should happen is if someone's being malicious. raise Exception( "User is not authorized to add datasets to this library." ) folder = library_bunch.folder diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index 0789dbb6cc7..8f1ab4b7134 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -417,6 +417,14 @@ def stringify_dictionary_keys( in_dict ): out_dict[ str( key ) ] = value return out_dict +def recursively_stringify_dictionary_keys( d ): + if isinstance(d, dict): + return dict([(k.encode('utf-8'), recursively_stringify_dictionary_keys(v)) for k,v in d.iteritems()]) + elif isinstance(d, list): + return [recursively_stringify_dictionary_keys(x) for x in d] + else: + return d + def mkstemp_ln( src, prefix='mkstemp_ln_' ): """ From tempfile._mkstemp_inner, generate a hard link in the same dir with a diff --git a/lib/galaxy/web/__init__.py b/lib/galaxy/web/__init__.py index e9c2c41a52c..1c9295630e0 100644 --- a/lib/galaxy/web/__init__.py +++ b/lib/galaxy/web/__init__.py @@ -2,6 +2,6 @@ The Galaxy web application. """ -from framework import expose, json, json_pretty, require_login, require_admin, url_for, error, form, FormBuilder +from framework import expose, json, json_pretty, require_login, require_admin, url_for, error, form, FormBuilder, expose_api from framework.base import httpexceptions diff --git a/lib/galaxy/web/api/__init__.py b/lib/galaxy/web/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/lib/galaxy/web/api/contents.py b/lib/galaxy/web/api/contents.py new file mode 100644 index 00000000000..e31fc267355 --- /dev/null +++ b/lib/galaxy/web/api/contents.py @@ -0,0 +1,154 @@ +""" +API operations on the contents of a library. +""" +import logging, os, string, shutil, urllib, re, socket +from cgi import escape, FieldStorage +from galaxy import util, datatypes, jobs, web, util +from galaxy.web.base.controller import * +from galaxy.util.sanitize_html import sanitize_html +from galaxy.model.orm import * + +log = logging.getLogger( __name__ ) + +class ContentsController( BaseController ): + + @web.expose_api + def index( self, trans, library_id, **kwd ): + """ + GET /api/libraries/{encoded_library_id}/contents + Displays a collection (list) of library contents (files and folders). + """ + rval = [] + current_user_roles = trans.get_current_user_roles() + def traverse( folder ): + admin = trans.user_is_admin() + rval = [] + for subfolder in folder.active_folders: + if not admin: + can_access, folder_ids = trans.app.security_agent.check_folder_contents( trans.user, current_user_roles, subfolder ) + if admin or can_access: + subfolder.api_path = folder.api_path + '/' + subfolder.name + subfolder.api_type = 'folder' + rval.append( subfolder ) + rval.extend( traverse( subfolder ) ) + for ld in folder.datasets: + if not admin: + can_access = trans.app.security_agent.can_access_dataset( current_user_roles, ld.library_dataset_dataset_association.dataset ) + if admin or can_access: + ld.api_path = folder.api_path + '/' + ld.name + ld.api_type = 'file' + rval.append( ld ) + return rval + try: + decoded_library_id = trans.security.decode_id( library_id ) + except TypeError: + trans.response.status = 400 + return "Malformed library id ( %s ) specified, unable to decode." % str( library_id ) + try: + library = trans.sa_session.query( trans.app.model.Library ).get( decoded_library_id ) + except: + library = None + if not library or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library( current_user_roles, library ) ): + trans.response.status = 400 + return "Invalid library id ( %s ) specified." % str( library_id ) + encoded_id = trans.security.encode_id( 'folder.%s' % library.root_folder.id ) + rval.append( dict( id = encoded_id, + type = 'folder', + name = '/', + url = url_for( 'content', library_id=library_id, id=encoded_id ) ) ) + library.root_folder.api_path = '' + for content in traverse( library.root_folder ): + encoded_id = trans.security.encode_id( '%s.%s' % ( content.api_type, content.id ) ) + rval.append( dict( id = encoded_id, + type = content.api_type, + name = content.api_path, + url = url_for( 'content', library_id=library_id, id=encoded_id, ) ) ) + return rval + + @web.expose_api + def show( self, trans, id, library_id, **kwd ): + """ + GET /api/libraries/{encoded_library_id}/contents/{encoded_content_type_and_id} + Displays information about a library content (file or folder). + """ + content_id = id + try: + decoded_type_and_id = trans.security.decode_string_id( content_id ) + content_type, decoded_content_id = decoded_type_and_id.split( '.' ) + except: + trans.response.status = 400 + return "Malformed content id ( %s ) specified, unable to decode." % str( content_id ) + if content_type == 'folder': + model_class = trans.app.model.LibraryFolder + elif content_type == 'file': + model_class = trans.app.model.LibraryDataset + else: + trans.response.status = 400 + return "Invalid type ( %s ) specified." % str( content_type ) + try: + content = trans.sa_session.query( model_class ).get( decoded_content_id ) + except: + content = None + if not content or ( not trans.user_is_admin() and not trans.app.security_agent.can_access_library_item( trans.get_current_user_roles(), content, trans.user ) ): + trans.response.status = 400 + return "Invalid %s id ( %s ) specified." % ( content_type, str( content_id ) ) + return content.get_api_value( view='element' ) + + @web.expose_api + def create( self, trans, library_id, payload, **kwd ): + """ + POST /api/libraries/{encoded_library_id}/contents + Creates a new library content item (file or folder). + """ + create_type = None + if 'create_type' not in payload: + trans.response.status = 400 + return "Missing required 'create_type' parameter. Please consult the API documentation for help." + else: + create_type = payload.pop( 'create_type' ) + if create_type not in ( 'file', 'folder' ): + trans.response.status = 400 + return "Invalid value for 'create_type' parameter ( %s ) specified. Please consult the API documentation for help." % create_type + try: + content_id = str( payload.pop( 'folder_id' ) ) + decoded_type_and_id = trans.security.decode_string_id( content_id ) + parent_type, decoded_parent_id = decoded_type_and_id.split( '.' ) + assert parent_type in ( 'folder', 'file' ) + except: + trans.response.status = 400 + return "Malformed parent id ( %s ) specified, unable to decode." % content_id + # "content" can be either a folder or a file, but the parent of new contents can only be folders. + if parent_type == 'file': + trans.response.status = 400 + try: + # With admins or people who can access the dataset provided as the parent, be descriptive. + dataset = trans.sa_session.query( trans.app.model.LibraryDataset ).get( decoded_parent_id ).library_dataset_dataset_association.dataset + assert trans.user_is_admin() or trans.app.security_agent.can_access_dataset( trans.get_current_user_roles(), dataset ) + return "The parent id ( %s ) points to a file, not a folder." % content_id + except: + # If you can't access the parent we don't want to reveal its existence. + return "Invalid parent folder id ( %s ) specified." % content_id + # The rest of the security happens in the library_common controller. + folder_id = trans.security.encode_id( decoded_parent_id ) + # Now create the desired content object, either file or folder. + if create_type == 'file': + status, output = trans.webapp.controllers['library_common'].upload_library_dataset( trans, 'api', library_id, folder_id, **payload ) + elif create_type == 'folder': + status, output = trans.webapp.controllers['library_common'].create_folder( trans, 'api', folder_id, library_id, **payload ) + if status != 200: + trans.response.status = status + # We don't want to reveal the encoded folder_id since it's invalid + # in the API context. Instead, return the content_id originally + # supplied by the client. + output = output.replace( folder_id, content_id ) + return output + else: + rval = [] + for k, v in output.items(): + if type( v ) == trans.app.model.LibraryDatasetDatasetAssociation: + v = v.library_dataset + encoded_id = trans.security.encode_id( create_type + '.' + str( v.id ) ) + rval.append( dict( id = encoded_id, + name = v.name, + url = url_for( 'content', library_id=library_id, id=encoded_id ) ) ) + return rval diff --git a/lib/galaxy/web/api/libraries.py b/lib/galaxy/web/api/libraries.py new file mode 100644 index 00000000000..a5b06989b21 --- /dev/null +++ b/lib/galaxy/web/api/libraries.py @@ -0,0 +1,62 @@ +""" +API operations on a library. +""" +import logging, os, string, shutil, urllib, re, socket +from cgi import escape, FieldStorage +from galaxy import util, datatypes, jobs, web, util +from galaxy.web.base.controller import * +from galaxy.util.sanitize_html import sanitize_html +from galaxy.model.orm import * + +log = logging.getLogger( __name__ ) + +class LibrariesController( BaseController ): + + @web.expose_api + def index( self, trans, **kwd ): + """ + GET /api/libraries + Displays a collection (list) of libraries. + """ + query = trans.sa_session.query( trans.app.model.Library ).filter( trans.app.model.Library.table.c.deleted == False ) + current_user_role_ids = [ role.id for role in trans.get_current_user_roles() ] + library_access_action = trans.app.security_agent.permitted_actions.LIBRARY_ACCESS.action + restricted_library_ids = [ lp.library_id for lp in trans.sa_session.query( trans.model.LibraryPermissions ) \ + .filter( trans.model.LibraryPermissions.table.c.action == library_access_action ) \ + .distinct() ] + accessible_restricted_library_ids = [ lp.library_id for lp in trans.sa_session.query( trans.model.LibraryPermissions ) \ + .filter( and_( trans.model.LibraryPermissions.table.c.action == library_access_action, + trans.model.LibraryPermissions.table.c.role_id.in_( current_user_role_ids ) ) ) ] + query = query.filter( or_( not_( trans.model.Library.table.c.id.in_( restricted_library_ids ) ), + trans.model.Library.table.c.id.in_( accessible_restricted_library_ids ) ) ) + rval = [] + for library in query: + item = library.get_api_value() + item['url'] = url_for( 'library', id=trans.security.encode_id( library.id ) ) + item['id'] = trans.security.encode_id( item['id'] ) + rval.append( item ) + return rval + + @web.expose_api + def show( self, trans, id, **kwd ): + """ + GET /api/libraries/{encoded_library_id} + Displays information about a library. + """ + library_id = id + params = util.Params( kwd ) + try: + decoded_library_id = trans.security.decode_id( library_id ) + except TypeError: + trans.response.status = 400 + return "Malformed library id ( %s ) specified, unable to decode." % str( library_id ) + try: + library = trans.sa_session.query( trans.app.model.Library ).get( decoded_library_id ) + except: + library = None + if not library or not ( trans.user_is_admin() or trans.app.security_agent.can_access_library( trans.get_current_user_roles(), library ) ): + trans.response.status = 400 + return "Invalid library id ( %s ) specified." % str( library_id ) + item = library.get_api_value( view='element' ) + item['contents_url'] = url_for( 'contents', library_id=library_id ) + return item diff --git a/lib/galaxy/web/buildapp.py b/lib/galaxy/web/buildapp.py index 389db165e0c..4cb33c6f83b 100644 --- a/lib/galaxy/web/buildapp.py +++ b/lib/galaxy/web/buildapp.py @@ -48,6 +48,27 @@ def add_controllers( webapp, app ): if isclass( T ) and T is not BaseController and issubclass( T, BaseController ): webapp.add_controller( name, T( app ) ) +def add_api_controllers( webapp, app ): + from galaxy.web.base.controller import BaseController + from galaxy.web.base.controller import ControllerUnavailable + import galaxy.web.api + controller_dir = galaxy.web.api.__path__[0] + for fname in os.listdir( controller_dir ): + if not( fname.startswith( "_" ) ) and fname.endswith( ".py" ): + name = fname[:-3] + module_name = "galaxy.web.api." + name + try: + module = __import__( module_name ) + except ControllerUnavailable, exc: + log.debug("%s could not be loaded: %s" % (module_name, str(exc))) + continue + for comp in module_name.split( "." )[1:]: + module = getattr( module, comp ) + for key in dir( module ): + T = getattr( module, key ) + if isclass( T ) and T is not BaseController and issubclass( T, BaseController ): + webapp.add_api_controller( name, T( app ) ) + def app_factory( global_conf, **kwargs ): """ Return a wsgi application serving the root object @@ -80,6 +101,11 @@ def app_factory( global_conf, **kwargs ): webapp.add_route( '/u/:username/h/:slug', controller='history', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/w/:slug', controller='workflow', action='display_by_username_and_slug' ) webapp.add_route( '/u/:username/v/:slug', controller='visualization', action='display_by_username_and_slug' ) + # If enabled, add the web API + if asbool( kwargs.get( 'enable_api', False ) ): + add_api_controllers( webapp, app ) + webapp.api_mapper.resource( 'content', 'contents', path_prefix='/api/libraries/:library_id', parent_resources=dict( member_name='library', collection_name='libraries' ) ) + webapp.api_mapper.resource( 'library', 'libraries', path_prefix='/api' ) webapp.finalize_config() # Wrap the webapp in some useful middleware if kwargs.get( 'middleware', True ): diff --git a/lib/galaxy/web/controllers/library_common.py b/lib/galaxy/web/controllers/library_common.py index 0fce00d5380..3fea1124629 100644 --- a/lib/galaxy/web/controllers/library_common.py +++ b/lib/galaxy/web/controllers/library_common.py @@ -280,7 +280,7 @@ class LibraryCommon( BaseController ): status = params.get( 'status', 'done' ) show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) ) use_panels = util.string_as_bool( params.get( 'use_panels', False ) ) - is_admin = trans.user_is_admin() and cntrller == 'library_admin' + is_admin = trans.user_is_admin() and cntrller in ( 'library_admin', 'api' ) current_user_roles = trans.get_current_user_roles() try: parent_folder = trans.sa_session.query( trans.app.model.LibraryFolder ).get( trans.security.decode_id( parent_id ) ) @@ -294,6 +294,8 @@ class LibraryCommon( BaseController ): # its parent library, or if they are not able to see the folder's contents. if not parent_folder or ( not is_admin and not trans.app.security_agent.can_access_library_item( current_user_roles, parent_folder, trans.user ) ): message = "Invalid parent folder id ( %s ) specified." % str( parent_id ) + if cntrller == 'api': + return 400, message # This doesn't give away the library's existence since # browse_library will simply punt to browse_libraries if the # user-supplied id is invalid or inaccessible. @@ -309,6 +311,8 @@ class LibraryCommon( BaseController ): if not ( is_admin or trans.app.security_agent.can_add_library_item( current_user_roles, parent_folder ) ): message = "You are not authorized to create a folder in parent folder '%s'." % parent_folder.name # Redirect to the real parent library since we know we have access to it. + if cntrller == 'api': + return 403, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -317,7 +321,7 @@ class LibraryCommon( BaseController ): show_deleted=show_deleted, message=util.sanitize_text( message ), status='error' ) ) - if params.get( 'new_folder_button', False ): + if params.get( 'new_folder_button', False ) or cntrller == 'api': new_folder = trans.app.model.LibraryFolder( name=util.restore_text( params.name ), description=util.restore_text( params.description ) ) # We are associating the last used genome build with folders, so we will always @@ -329,6 +333,9 @@ class LibraryCommon( BaseController ): trans.sa_session.flush() # New folders default to having the same permissions as their parent folder trans.app.security_agent.copy_library_permissions( parent_folder, new_folder ) + # If we're creating in the API, we're done + if cntrller == 'api': + return 200, dict( created=new_folder ) # If we have an inheritable template, redirect to the folder_info page so information # can be filled in immediately. widgets = [] @@ -654,7 +661,7 @@ class LibraryCommon( BaseController ): show_deleted=show_deleted, message=util.sanitize_text( message ), status='error' ) ) - if ( trans.user_is_admin() and cntrller == 'library_admin' ): + if is_admin: # Get all associated hdas and lddas that use the same disk file. associated_hdas = trans.sa_session.query( trans.model.HistoryDatasetAssociation ) \ .filter( and_( trans.model.HistoryDatasetAssociation.deleted == False, @@ -848,7 +855,7 @@ class LibraryCommon( BaseController ): last_used_build = dbkey[0] else: last_used_build = dbkey - is_admin = trans.user_is_admin() and cntrller == 'library_admin' + is_admin = trans.user_is_admin() and cntrller in ( 'library_admin', 'api' ) current_user_roles = trans.get_current_user_roles() if replace_id not in [ None, 'None' ]: try: @@ -859,6 +866,8 @@ class LibraryCommon( BaseController ): # its parent library, or if they are not able to view the dataset itself. if not replace_dataset or ( not is_admin and not trans.app.security_agent.can_access_library_item( current_user_roles, replace_dataset, trans.user ) ): message = "Invalid library dataset id ( %s ) to replace specified." % replace_id + if cntrller == 'api': + return 400, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -870,6 +879,8 @@ class LibraryCommon( BaseController ): # Deny access if the user is not an admin and does not have the LIBRARY_MODIFY permission. if not ( is_admin or trans.app.security_agent.can_modify_library_item( current_user_roles, replace_dataset ) ): message = "You are not authorized to replace library dataset '%s'." % replace_dataset.name + if cntrller == 'api': + return 403, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -896,6 +907,8 @@ class LibraryCommon( BaseController ): # its parent library, or if they are not able to see the folder's contents. if not folder or ( not is_admin and not trans.app.security_agent.can_access_library_item( current_user_roles, folder, trans.user ) ): message = "Invalid parent folder id ( %s ) specified." % str( folder_id ) + if cntrller == 'api': + return 400, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -907,6 +920,8 @@ class LibraryCommon( BaseController ): # Deny access if the user is not an admin and does not have the LIBRARY_ADD permission. if not ( is_admin or trans.app.security_agent.can_add_library_item( current_user_roles, folder ) ): message = "You are not authorized to create a library dataset in parent folder '%s'." % folder.name + if cntrller == 'api': + return 403, message return trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -918,7 +933,7 @@ class LibraryCommon( BaseController ): library = folder.parent_library if folder and last_used_build in [ 'None', None, '?' ]: last_used_build = folder.genome_build - if params.get( 'runtool_btn', False ) or params.get( 'ajax_upload', False ): + if params.get( 'runtool_btn', False ) or params.get( 'ajax_upload', False ) or cntrller == 'api': # Check to see if the user selected roles to associate with the DATASET_ACCESS permission # on the dataset that would cause accessibility issues. roles = params.get( 'roles', False ) @@ -931,7 +946,8 @@ class LibraryCommon( BaseController ): permissions, in_roles, error, message = \ trans.app.security_agent.derive_roles_from_access( trans, library.id, cntrller, library=True, **vars ) if error: - status = 'error' + if cntrller == 'api': + return 400, message trans.response.send_redirect( web.url_for( controller='library_common', action='upload_library_dataset', cntrller=cntrller, @@ -961,6 +977,11 @@ class LibraryCommon( BaseController ): replace_dataset=replace_dataset, **kwd ) if created_outputs_dict: + if cntrller == 'api': + # created_outputs_dict can only ever be a string if cntrller == 'api' + if type( created_outputs_dict ) == str: + return 400, created_outputs_dict + return 200, created_outputs_dict total_added = len( created_outputs_dict.keys() ) ldda_id_list = [ str( v.id ) for k, v in created_outputs_dict.items() ] created_ldda_ids=",".join( ldda_id_list ) @@ -1001,6 +1022,9 @@ class LibraryCommon( BaseController ): created_ldda_ids = '' message = "Upload failed" status='error' + if cntrller == 'api': + return 400, message + response_code = 400 trans.response.send_redirect( web.url_for( controller='library_common', action='browse_library', cntrller=cntrller, @@ -1068,7 +1092,6 @@ class LibraryCommon( BaseController ): # Library-specific params params = util.Params( kwd ) # is this filetoolparam safe? show_deleted = util.string_as_bool( params.get( 'show_deleted', False ) ) - library_bunch = upload_common.handle_library_params( trans, params, folder_id, replace_dataset ) message = util.restore_text( params.get( 'message', '' ) ) status = params.get( 'status', 'done' ) server_dir = util.restore_text( params.get( 'server_dir', '' ) ) @@ -1077,11 +1100,11 @@ class LibraryCommon( BaseController ): else: replace_id = None upload_option = params.get( 'upload_option', 'upload_file' ) - err_redirect = False + response_code = 200 if upload_option == 'upload_directory': if server_dir in [ None, 'None', '' ]: - err_redirect = True - if cntrller == 'library_admin': + response_code = 400 + if cntrller == 'library_admin' or ( cntrller == 'api' and trans.user_is_admin ): import_dir = trans.app.config.library_import_dir import_dir_desc = 'library_import_dir' full_dir = os.path.join( import_dir, server_dir ) @@ -1095,21 +1118,35 @@ class LibraryCommon( BaseController ): if import_dir: message = 'Select a directory' else: + response_code = 403 message = '"%s" is not defined in the Galaxy configuration file' % import_dir_desc - # Proceed with (mostly) regular upload processing - precreated_datasets = upload_common.get_precreated_datasets( trans, tool_params, trans.app.model.LibraryDatasetDatasetAssociation, controller=cntrller ) - if upload_option == 'upload_file': - tool_params = upload_common.persist_uploads( tool_params ) - uploaded_datasets = upload_common.get_uploaded_datasets( trans, cntrller, tool_params, precreated_datasets, dataset_upload_inputs, library_bunch=library_bunch ) - elif upload_option == 'upload_directory': - uploaded_datasets, err_redirect, message = self.get_server_dir_uploaded_datasets( trans, cntrller, params, full_dir, import_dir_desc, library_bunch, err_redirect, message ) elif upload_option == 'upload_paths': - uploaded_datasets, err_redirect, message = self.get_path_paste_uploaded_datasets( trans, cntrller, params, library_bunch, err_redirect, message ) - upload_common.cleanup_unused_precreated_datasets( precreated_datasets ) - if upload_option == 'upload_file' and not uploaded_datasets: - message = 'Select a file, enter a URL or enter text' - err_redirect = True - if err_redirect: + if not trans.app.config.allow_library_path_paste: + response_code = 403 + message = '"allow_library_path_paste" is not defined in the Galaxy configuration file' + # Some error handling should be added to this method. + try: + library_bunch = upload_common.handle_library_params( trans, params, folder_id, replace_dataset ) + except: + response_code = 500 + message = "Unable to parse upload parameters, please report this error." + # Proceed with (mostly) regular upload processing if we're still errorless + if response_code == 200: + precreated_datasets = upload_common.get_precreated_datasets( trans, tool_params, trans.app.model.LibraryDatasetDatasetAssociation, controller=cntrller ) + if upload_option == 'upload_file': + tool_params = upload_common.persist_uploads( tool_params ) + uploaded_datasets = upload_common.get_uploaded_datasets( trans, cntrller, tool_params, precreated_datasets, dataset_upload_inputs, library_bunch=library_bunch ) + elif upload_option == 'upload_directory': + uploaded_datasets, response_code, message = self.get_server_dir_uploaded_datasets( trans, cntrller, params, full_dir, import_dir_desc, library_bunch, response_code, message ) + elif upload_option == 'upload_paths': + uploaded_datasets, response_code, message = self.get_path_paste_uploaded_datasets( trans, cntrller, params, library_bunch, response_code, message ) + upload_common.cleanup_unused_precreated_datasets( precreated_datasets ) + if upload_option == 'upload_file' and not uploaded_datasets: + response_code = 400 + message = 'Select a file, enter a URL or enter text' + if response_code != 200: + if cntrller == 'api': + return ( response_code, message ) trans.response.send_redirect( web.url_for( controller='library_common', action='upload_library_dataset', cntrller=cntrller, @@ -1145,7 +1182,7 @@ class LibraryCommon( BaseController ): trans.sa_session.add_all( ( uploaded_dataset.data, uploaded_dataset.data.dataset ) ) trans.sa_session.flush() return uploaded_dataset - def get_server_dir_uploaded_datasets( self, trans, cntrller, params, full_dir, import_dir_desc, library_bunch, err_redirect, message ): + def get_server_dir_uploaded_datasets( self, trans, cntrller, params, full_dir, import_dir_desc, library_bunch, response_code, message ): files = [] try: for entry in os.listdir( full_dir ): @@ -1173,22 +1210,22 @@ class LibraryCommon( BaseController ): files.append( path ) except Exception, e: message = "Unable to get file list for configured %s, error: %s" % ( import_dir_desc, str( e ) ) - err_redirect = True - return None, err_redirect, message + response_code = 500 + return None, response_code, message if not files: message = "The directory '%s' contains no valid files" % full_dir - err_redirect = True - return None, err_redirect, message + response_code = 400 + return None, response_code, message uploaded_datasets = [] for file in files: name = os.path.basename( file ) uploaded_datasets.append( self.make_library_uploaded_dataset( trans, cntrller, params, name, file, 'server_dir', library_bunch ) ) - return uploaded_datasets, None, None - def get_path_paste_uploaded_datasets( self, trans, cntrller, params, library_bunch, err_redirect, message ): + return uploaded_datasets, 200, None + def get_path_paste_uploaded_datasets( self, trans, cntrller, params, library_bunch, response_code, message ): if params.get( 'filesystem_paths', '' ) == '': message = "No paths entered in the upload form" - err_redirect = True - return None, err_redirect, message + response_code = 400 + return None, response_code, message preserve_dirs = True if params.get( 'dont_preserve_dirs', False ): preserve_dirs = False @@ -1222,8 +1259,8 @@ class LibraryCommon( BaseController ): in_folder ) ) if bad_paths: message = "Invalid paths:
" % "
  • ".join( bad_paths ) - err_redirect = True - return None, err_redirect, message + response_code = 400 + return None, response_code, message return uploaded_datasets, None, None @web.expose def add_history_datasets_to_library( self, trans, cntrller, library_id, folder_id, hda_ids='', **kwd ): diff --git a/lib/galaxy/web/controllers/user.py b/lib/galaxy/web/controllers/user.py index 81fdd0edadf..ccfa7a111f4 100644 --- a/lib/galaxy/web/controllers/user.py +++ b/lib/galaxy/web/controllers/user.py @@ -978,4 +978,26 @@ class User( BaseController ): message=message, lines_skipped=lines_skipped ) - \ No newline at end of file + + @web.expose + def new_api_key( self, trans, **kwd ): + params = util.Params( kwd ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + admin_view = util.string_as_bool( params.get( 'admin_view', False ) ) + error = '' + user = trans.sa_session.query( trans.app.model.User ).get( int( params.get( 'user_id', None ) ) ) + if params.get( 'new_api_key_button', None ) == 'Generate a new key now': + new_key = trans.app.model.APIKeys() + new_key.user_id = user.id + new_key.key = trans.app.security.get_new_guid() + trans.sa_session.add( new_key ) + trans.sa_session.flush() + message = "Generated a new web API key" + status = "done" + return trans.response.send_redirect( web.url_for( controller='user', + action='show_info', + admin_view=admin_view, + user_id=user.id, + message=message, + status=status ) ) diff --git a/lib/galaxy/web/framework/__init__.py b/lib/galaxy/web/framework/__init__.py index 22148771b85..440bce1e002 100644 --- a/lib/galaxy/web/framework/__init__.py +++ b/lib/galaxy/web/framework/__init__.py @@ -30,6 +30,7 @@ from babel.support import Translations pkg_resources.require( "SQLAlchemy >= 0.4" ) from sqlalchemy import and_ +from sqlalchemy.orm.exc import NoResultFound pkg_resources.require( "pexpect" ) pkg_resources.require( "amqplib" ) @@ -87,6 +88,43 @@ def require_login( verb="perform this action", use_panels=False, webapp='galaxy' return decorator return argcatcher +def expose_api( func ): + def decorator( self, trans, *args, **kwargs ): + def error( environ, start_response ): + start_response( error_status, [('Content-type', 'text/plain')] ) + return error_message + error_status = '403 Forbidden' + if 'key' not in kwargs: + error_message = 'No API key provided with request, please consult the API documentation.' + return error + try: + provided_key = trans.sa_session.query( trans.app.model.APIKeys ).filter( trans.app.model.APIKeys.table.c.key == kwargs['key'] ).one() + except NoResultFound: + error_message = 'Provided API key is not valid.' + return error + newest_key = provided_key.user.api_keys[0] + if newest_key.key != provided_key.key: + error_message = 'Provided API key has expired.' + return error + if trans.request.body: + try: + payload = util.recursively_stringify_dictionary_keys( simplejson.loads( trans.request.body ) ) + kwargs['payload'] = payload + except ValueError: + error_status = '400 Bad Request' + error_message = 'Your request did not appear to be valid JSON, please consult the API documentation' + return error + trans.response.set_content_type( "application/json" ) + trans.set_user( provided_key.user ) + if trans.debug: + return simplejson.dumps( func( self, trans, *args, **kwargs ), indent=4, sort_keys=True ) + else: + return simplejson.dumps( func( self, trans, *args, **kwargs ) ) + if not hasattr(func, '_orig'): + decorator._orig = func + decorator.exposed = True + return decorator + def require_admin( func ): def decorator( self, trans, *args, **kwargs ): admin_users = trans.app.config.get( "admin_users", "" ).split( "," ) @@ -119,7 +157,7 @@ def form( *args, **kwargs ): class WebApplication( base.WebApplication ): def __init__( self, galaxy_app, session_cookie='galaxysession' ): base.WebApplication.__init__( self ) - self.set_transaction_factory( lambda e: UniverseWebTransaction( e, galaxy_app, self, session_cookie ) ) + self.set_transaction_factory( lambda e: self.transaction_chooser( e, galaxy_app, session_cookie ) ) # Mako support self.mako_template_lookup = mako.lookup.TemplateLookup( directories = [ galaxy_app.config.template_path ] , @@ -135,21 +173,21 @@ class WebApplication( base.WebApplication ): if isinstance( body, FormBuilder ): body = trans.show_form( body ) return base.WebApplication.make_body_iterable( self, trans, body ) + def transaction_chooser( self, environ, galaxy_app, session_cookie ): + if 'is_api_request' in environ: + return GalaxyWebAPITransaction( environ, galaxy_app, self ) + else: + return GalaxyWebUITransaction( environ, galaxy_app, self, session_cookie ) -class UniverseWebTransaction( base.DefaultWebTransaction ): +class GalaxyWebTransaction( base.DefaultWebTransaction ): """ - Encapsulates web transaction specific state for the Universe application + Encapsulates web transaction specific state for the Galaxy application (specifically the user's "cookie" session and history) """ - def __init__( self, environ, app, webapp, session_cookie ): + def __init__( self, environ, app, webapp ): self.app = app self.webapp = webapp self.security = webapp.security - # FIXME: the following 3 attributes are not currently used - # Remove them if they are not going to be... - self.__user = NOT_SET - self.__history = NOT_SET - self.__galaxy_session = NOT_SET base.DefaultWebTransaction.__init__( self, environ ) self.setup_i18n() self.sa_session.expunge_all() @@ -158,13 +196,6 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): # that the current history should not be used for parameter values # and such). self.workflow_building_mode = False - # Always have a valid galaxy session - self.__ensure_valid_session( session_cookie ) - # Prevent deleted users from accessing Galaxy - if self.app.config.use_remote_user and self.galaxy_session.user.deleted: - self.response.send_redirect( url_for( '/static/user_disabled.html' ) ) - if self.app.config.require_login: - self.__ensure_logged_in_user( environ ) def setup_i18n( self ): if 'HTTP_ACCEPT_LANGUAGE' in self.environ: # locales looks something like: ['en', 'en-us;q=0.7', 'ja;q=0.3'] @@ -252,12 +283,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): tstamp = time.localtime ( time.time() + 3600 * 24 * age ) self.response.cookies[name]['expires'] = time.strftime( '%a, %d-%b-%Y %H:%M:%S GMT', tstamp ) self.response.cookies[name]['version'] = version - #@property - #def galaxy_session( self ): - # if not self.__galaxy_session: - # self.__ensure_valid_session() - # return self.__galaxy_session - def __ensure_valid_session( self, session_cookie ): + def _ensure_valid_session( self, session_cookie ): """ Ensure that a valid Galaxy session exists and is available as trans.session (part of initialization) @@ -344,7 +370,7 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): # If the old session was invalid, get a new history with our new session if invalidate_existing_session: self.new_history() - def __ensure_logged_in_user( self, environ ): + def _ensure_logged_in_user( self, environ ): allowed_paths = ( url_for( controller='root', action='index' ), url_for( controller='root', action='tool_menu' ), @@ -537,15 +563,6 @@ class UniverseWebTransaction( base.DefaultWebTransaction ): self.sa_session.add_all( ( self.galaxy_session, history ) ) self.sa_session.flush() return history - def get_user( self ): - """Return the current user if logged in or None.""" - return self.galaxy_session.user - def set_user( self, user ): - """Set the current user.""" - self.galaxy_session.user = user - self.sa_session.add( self.galaxy_session ) - self.sa_session.flush() - user = property( get_user, set_user ) def get_current_user_roles( self ): user = self.get_user() if user: @@ -735,6 +752,53 @@ class FormInput( object ): self.help = help self.use_label = use_label +class GalaxyWebAPITransaction( GalaxyWebTransaction ): + def __init__( self, environ, app, webapp ): + GalaxyWebTransaction.__init__( self, environ, app, webapp ) + self.__user = None + self._ensure_valid_session( None ) + def _ensure_valid_session( self, session_cookie ): + self.galaxy_session = Bunch() + self.galaxy_session.history = self.galaxy_session.current_history = Bunch() + self.galaxy_session.history.genome_build = None + self.galaxy_session.is_api = True + def get_user( self ): + """Return the current user (the expose_api decorator ensures that it is set).""" + return self.__user + def set_user( self, user ): + """Compatibility method""" + self.__user = user + user = property( get_user, set_user ) + @property + def db_builds( self ): + dbnames = [] + if 'dbkeys' in self.user.preferences: + user_keys = from_json_string( self.user.preferences['dbkeys'] ) + for key, chrom_dict in user_keys.iteritems(): + dbnames.append((key, "%s (%s) [Custom]" % (chrom_dict['name'], key) )) + dbnames.extend( util.dbnames ) + return dbnames + +class GalaxyWebUITransaction( GalaxyWebTransaction ): + def __init__( self, environ, app, webapp, session_cookie ): + GalaxyWebTransaction.__init__( self, environ, app, webapp ) + # Always have a valid galaxy session + self._ensure_valid_session( session_cookie ) + # Prevent deleted users from accessing Galaxy + if self.app.config.use_remote_user and self.galaxy_session.user.deleted: + self.response.send_redirect( url_for( '/static/user_disabled.html' ) ) + if self.app.config.require_login: + self._ensure_logged_in_user( environ ) + def get_user( self ): + """Return the current user if logged in or None.""" + return self.galaxy_session.user + def set_user( self, user ): + """Set the current user.""" + self.galaxy_session.user = user + self.sa_session.add( self.galaxy_session ) + self.sa_session.flush() + user = property( get_user, set_user ) + class SelectInput( FormInput ): """ A select form input. """ def __init__( self, name, label, value=None, options=[], error=None, help=None, use_label=True ): diff --git a/lib/galaxy/web/framework/base.py b/lib/galaxy/web/framework/base.py index 15ae4bd22d1..a39babdb981 100644 --- a/lib/galaxy/web/framework/base.py +++ b/lib/galaxy/web/framework/base.py @@ -45,7 +45,13 @@ class WebApplication( object ): and `__call__` to handle a request (WSGI style). """ self.controllers = dict() + self.api_controllers = dict() self.mapper = routes.Mapper() + # FIXME: The following two options are deprecated and should be + # removed. Consult the Routes documentation. + self.mapper.minimization = True + self.mapper.explicit = False + self.api_mapper = routes.Mapper() self.transaction_factory = DefaultWebTransaction def add_controller( self, controller_name, controller ): """ @@ -56,6 +62,10 @@ class WebApplication( object ): log.debug( "Enabling '%s' controller, class: %s", controller_name, controller.__class__.__name__ ) self.controllers[ controller_name ] = controller + def add_api_controller( self, controller_name, controller ): + log.debug( "Enabling '%s' API controller, class: %s", + controller_name, controller.__class__.__name__ ) + self.api_controllers[ controller_name ] = controller def add_route( self, route, **kwargs ): """ Add a route to match a URL with a method. Accepts all keyword @@ -80,6 +90,7 @@ class WebApplication( object ): """ # Create/compile the regular expressions for route mapping self.mapper.create_regs( self.controllers.keys() ) + self.api_mapper.create_regs( self.api_controllers.keys() ) def __call__( self, environ, start_response ): """ Call interface as specified by WSGI. Wraps the environment in user @@ -88,12 +99,20 @@ class WebApplication( object ): """ # Map url using routes path_info = environ.get( 'PATH_INFO', '' ) - map = self.mapper.match( path_info ) + map = self.mapper.match( path_info, environ ) + if map is None: + environ[ 'is_api_request' ] = True + map = self.api_mapper.match( path_info, environ ) + mapper = self.api_mapper + controllers = self.api_controllers + else: + mapper = self.mapper + controllers = self.controllers if map == None: raise httpexceptions.HTTPNotFound( "No route for " + path_info ) # Setup routes rc = routes.request_config() - rc.mapper = self.mapper + rc.mapper = mapper rc.mapper_dict = map rc.environ = environ # Setup the transaction @@ -101,7 +120,7 @@ class WebApplication( object ): rc.redirect = trans.response.send_redirect # Get the controller class controller_name = map.pop( 'controller', None ) - controller = self.controllers.get( controller_name, None ) + controller = controllers.get( controller_name, None ) if controller_name is None: raise httpexceptions.HTTPNotFound( "No controller for " + path_info ) # Resolve action method on controller @@ -112,7 +131,7 @@ class WebApplication( object ): if method is None: raise httpexceptions.HTTPNotFound( "No action for " + path_info ) # Is the method exposed - if not getattr( method, 'exposed', False ): + if not getattr( method, 'exposed', False ): raise httpexceptions.HTTPNotFound( "Action not exposed for " + path_info ) # Is the method callable if not callable( method ): diff --git a/lib/galaxy/web/security/__init__.py b/lib/galaxy/web/security/__init__.py index 04adc34fd13..bfcc03d0436 100644 --- a/lib/galaxy/web/security/__init__.py +++ b/lib/galaxy/web/security/__init__.py @@ -43,6 +43,8 @@ class SecurityHelper( object ): return self.id_cipher.encrypt( s ).encode( 'hex' ) def decode_id( self, obj_id ): return int( self.id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) ) + def decode_string_id( self, obj_id ): + return self.id_cipher.decrypt( obj_id.decode( 'hex' ) ).lstrip( "!" ) def encode_guid( self, session_key ): # Session keys are strings # Pad to a multiple of 8 with leading "!" @@ -55,4 +57,4 @@ class SecurityHelper( object ): def get_new_guid( self ): # Generate a unique, high entropy 128 bit random number return get_random_bytes( 16 ) - \ No newline at end of file + diff --git a/scripts/api/README b/scripts/api/README new file mode 100644 index 00000000000..9503d008ea0 --- /dev/null +++ b/scripts/api/README @@ -0,0 +1,106 @@ +This is not documentation. These are hints and examples to get you started +until the documentation is written. + +Set these options in universe_wsgi.ini and start the server: + +enable_api = True +admin_users = you@example.org +library_import_dir = /path/to/some/directory + +In the directory you specified for 'library_import_dir', create some +subdirectories, and put (or symlink) files to import into Galaxy into those +subdirectories. + +In Galaxy, create an account that matches the address you put in 'admin_users', +then browse to that user's preferences and generate a new API Key. Copy the +key to your clipboard. Create a new library (doing this via the API is not yet +implemented). Then take your API Key and use the scripts in scripts/api/ to do +things: + +% ./display.py my_key http://localhost:4096/api/libraries +Collection Members +------------------ +/api/libraries/f3f73e481f432006 + name: api_test + id: f3f73e481f432006 + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006 +Member Information +------------------ +synopsys: None +contents_url: /api/libraries/f3f73e481f432006/contents +description: API Test Library +name: api_test + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents +Collection Members +------------------ +/api/libraries/f3f73e481f432006/contents/28202595c0d2591f61ddda595d2c3670 + name: / + type: folder + id: 28202595c0d2591f61ddda595d2c3670 + +% ./library_create_folder.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents 28202595c0d2591f61ddda595d2c3670 api_test_folder1 'API Test Folder 1' +Response +-------- +/api/libraries/f3f73e481f432006/contents/28202595c0d2591fa4f9089d2303fd89 + name: api_test_folder1 + id: 28202595c0d2591fa4f9089d2303fd89 + +% ./library_upload_from_import_dir.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents 28202595c0d2591fa4f9089d2303fd89 bed bed hg19 +Response +-------- +/api/libraries/f3f73e481f432006/contents/e9ef7fdb2db87d7b + name: 2.bed + id: e9ef7fdb2db87d7b +/api/libraries/f3f73e481f432006/contents/3b7f6a31f80a5018 + name: 3.bed + id: 3b7f6a31f80a5018 + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents +Collection Members +------------------ +/api/libraries/f3f73e481f432006/contents/28202595c0d2591f61ddda595d2c3670 + name: / + type: folder + id: 28202595c0d2591f61ddda595d2c3670 +/api/libraries/f3f73e481f432006/contents/28202595c0d2591fa4f9089d2303fd89 + name: /api_test_folder1 + type: folder + id: 28202595c0d2591fa4f9089d2303fd89 +/api/libraries/f3f73e481f432006/contents/e9ef7fdb2db87d7b + name: /api_test_folder1/2.bed + type: file + id: e9ef7fdb2db87d7b +/api/libraries/f3f73e481f432006/contents/3b7f6a31f80a5018 + name: /api_test_folder1/3.bed + type: file + id: 3b7f6a31f80a5018 + +% ./display.py my_key http://localhost:4096/api/libraries/f3f73e481f432006/contents/e9ef7fdb2db87d7b +Member Information +------------------ +misc_blurb: 68 regions +metadata_endCol: 3 +data_type: bed +metadata_columns: 6 +metadata_nameCol: 4 +uploaded_by: nate@... +metadata_strandCol: 6 +name: 2.bed +genome_build: hg19 +metadata_comment_lines: None +metadata_startCol: 2 +metadata_chromCol: 1 +file_size: 4272 +metadata_data_lines: 68 +message: +metadata_dbkey: hg19 +misc_info: uploaded bed file +date_uploaded: 2010-06-22T17:01:51.266119 +metadata_column_types: str, int, int, str, int, str + +Other parameters are valid when uploading, they are the same parameters as are +used in the web form, like 'link_data_only' and etc. + +The request and response format should be considered alpha and are subject to change. diff --git a/scripts/api/common.py b/scripts/api/common.py new file mode 100644 index 00000000000..5a83bf5d643 --- /dev/null +++ b/scripts/api/common.py @@ -0,0 +1,94 @@ +import os, sys, urllib, urllib2 + +new_path = [ os.path.join( os.path.dirname( __file__ ), '..', '..', 'lib' ) ] +new_path.extend( sys.path[1:] ) +sys.path = new_path + +from galaxy import eggs +import pkg_resources + +pkg_resources.require( "simplejson" ) +import simplejson + +def make_url( api_key, url, args=None ): + # Adds the API Key to the URL if it's not already there. + if args is None: + args = [] + argsep = '&' + if '?' not in url: + argsep = '?' + if '?key=' not in url and '&key=' not in url: + args.insert( 0, ( 'key', api_key ) ) + return url + argsep + '&'.join( [ '='.join( t ) for t in args ] ) + +def get( api_key, url ): + # Do the actual GET. + url = make_url( api_key, url ) + return simplejson.loads( urllib2.urlopen( url ).read() ) + +def post( api_key, url, data ): + # Do the actual POST. + url = make_url( api_key, url ) + req = urllib2.Request( url, headers = { 'Content-Type': 'application/json' }, data = simplejson.dumps( data ) ) + return simplejson.loads( urllib2.urlopen( req ).read() ) + +def display( api_key, url ): + # Sends an API GET request and acts as a generic formatter for the JSON response. + try: + r = get( api_key, url ) + except urllib2.HTTPError, e: + print e + print e.read( 1024 ) # Only return the first 1K of errors. + sys.exit( 1 ) + if type( r ) == unicode: + print 'error: %s' % r + return None + elif type( r ) == list: + # Response is a collection as defined in the REST style. + print 'Collection Members' + print '------------------' + for i in r: + # All collection members should have a name and url in the response. + print i.pop( 'url' ) + print ' name: %s' % i.pop( 'name' ) + for k, v in i.items(): + print ' %s: %s' % ( k, v ) + print '' + print '%d elements in collection' % len( r ) + elif type( r ) == dict: + # Response is an element as defined in the REST style. + print 'Member Information' + print '------------------' + for k, v in r.items(): + print '%s: %s' % ( k, v ) + else: + print 'response is unknown type: %s' % type( r ) + +def submit( api_key, url, data ): + # Sends an API POST request and acts as a generic formatter for the JSON response. + # 'data' will become the JSON payload read by Galaxy. + try: + r = post( api_key, url, data ) + except urllib2.HTTPError, e: + print e + print e.read( 1024 ) + sys.exit( 1 ) + print 'Response' + print '--------' + if type( r ) == list: + # Currently the only implemented responses are lists of dicts, because + # submission creates some number of collection elements. + for i in r: + if type( i ) == dict: + if 'url' in i: + print i.pop( 'url' ) + else: + print '----' + if 'name' in i: + print ' name: %s' % i.pop( 'name' ) + for k, v in i.items(): + print ' %s: %s' % ( k, v ) + else: + print i + else: + print r diff --git a/scripts/api/display.py b/scripts/api/display.py new file mode 100755 index 00000000000..d0ac26c3147 --- /dev/null +++ b/scripts/api/display.py @@ -0,0 +1,11 @@ +#!/usr/bin/python + +import os, sys +sys.path.insert( 0, os.path.dirname( __file__ ) ) +from common import display + +try: + display( *sys.argv[1:3] ) +except TypeError: + print 'usage: %s key url' % os.path.basename( sys.argv[0] ) + sys.exit( 1 ) diff --git a/scripts/api/library_create_folder.py b/scripts/api/library_create_folder.py new file mode 100755 index 00000000000..f392fa9f25d --- /dev/null +++ b/scripts/api/library_create_folder.py @@ -0,0 +1,20 @@ +#!/usr/bin/python + +import os, sys +sys.path.insert( 0, os.path.dirname( __file__ ) ) +from common import submit + +try: + data = {} + data[ 'folder_id' ] = sys.argv[3] + data[ 'name' ] = sys.argv[4] + data[ 'create_type' ] = 'folder' +except IndexError: + print 'usage: %s key url folder_id name [description]' % os.path.basename( sys.argv[0] ) + sys.exit( 1 ) +try: + data[ 'description' ] = sys.argv[5] +except IndexError: + data[ 'description' ] = '' + +submit( sys.argv[1], sys.argv[2], data ) diff --git a/scripts/api/library_upload_from_import_dir.py b/scripts/api/library_upload_from_import_dir.py new file mode 100755 index 00000000000..3ea7887e583 --- /dev/null +++ b/scripts/api/library_upload_from_import_dir.py @@ -0,0 +1,19 @@ +#!/usr/bin/python + +import os, sys +sys.path.insert( 0, os.path.dirname( __file__ ) ) +from common import submit + +try: + data = {} + data[ 'folder_id' ] = sys.argv[3] + data[ 'file_type' ] = sys.argv[4] + data[ 'server_dir' ] = sys.argv[5] + data[ 'dbkey' ] = sys.argv[6] + data[ 'upload_option' ] = 'upload_directory' + data[ 'create_type' ] = 'file' +except IndexError: + print 'usage: %s key url folder_id file_type server_dir dbkey' % os.path.basename( sys.argv[0] ) + sys.exit( 1 ) + +submit( sys.argv[1], sys.argv[2], data ) diff --git a/templates/webapps/galaxy/user/info.mako b/templates/webapps/galaxy/user/info.mako index 222eadbccbe..471b2a599a0 100644 --- a/templates/webapps/galaxy/user/info.mako +++ b/templates/webapps/galaxy/user/info.mako @@ -66,8 +66,9 @@ ${render_user_info()}

    %endif +

    +

    -

    User Addresses
    @@ -118,5 +119,37 @@ ${render_user_info()}
    -

    + +

    + +%if trans.app.config.enable_api: +

    +
    +
    Web API Key
    +
    +
    + + %if user.api_keys: + ${user.api_keys[0].key} + %else: + none set + %endif +
    +
    + + %if user.api_keys: + (invalidates old key) + %endif +
    + An API key will allow you to access Galaxy via its web + API (documentation forthcoming). Please note that + this key acts as an alternate means to access + your account, and should be treated with the same care + as your login password. +
    +
    +
    +
    +
    +%endif diff --git a/universe_wsgi.ini.sample b/universe_wsgi.ini.sample index d342bfc18f9..5eb2a9b186d 100644 --- a/universe_wsgi.ini.sample +++ b/universe_wsgi.ini.sample @@ -112,6 +112,9 @@ gbrowse_display_sites = wormbase,tair,modencode_worm,modencode_fly # Define your GeneTrack servers in tool-data/shared/genetrack/genetrack_sites.txt #genetrack_display_sites = +# Enable the (experimental! beta!) Web API. Documentation forthcoming. +#enable_api = False + # Serving static files (needed if running standalone) static_enabled = True static_cache_time = 360