[2] | 1 | """ |
---|
| 2 | Galaxy Tool Shed data model classes |
---|
| 3 | |
---|
| 4 | Naming: try to use class names that have a distinct plural form so that |
---|
| 5 | the relationship cardinalities are obvious (e.g. prefer Dataset to Data) |
---|
| 6 | """ |
---|
| 7 | import os.path, os, errno, sys, codecs, operator, tempfile, logging, tarfile, mimetypes |
---|
| 8 | from galaxy.util.bunch import Bunch |
---|
| 9 | from galaxy.util.hash_util import * |
---|
| 10 | from galaxy.web.form_builder import * |
---|
| 11 | log = logging.getLogger( __name__ ) |
---|
| 12 | |
---|
| 13 | class User( object ): |
---|
| 14 | def __init__( self, email=None, password=None ): |
---|
| 15 | self.email = email |
---|
| 16 | self.password = password |
---|
| 17 | self.external = False |
---|
| 18 | self.deleted = False |
---|
| 19 | self.purged = False |
---|
| 20 | self.username = None |
---|
| 21 | # Relationships |
---|
| 22 | self.tools = [] |
---|
| 23 | def set_password_cleartext( self, cleartext ): |
---|
| 24 | """Set 'self.password' to the digest of 'cleartext'.""" |
---|
| 25 | self.password = new_secure_hash( text_type=cleartext ) |
---|
| 26 | def check_password( self, cleartext ): |
---|
| 27 | """Check if 'cleartext' matches 'self.password' when hashed.""" |
---|
| 28 | return self.password == new_secure_hash( text_type=cleartext ) |
---|
| 29 | |
---|
| 30 | class Group( object ): |
---|
| 31 | def __init__( self, name = None ): |
---|
| 32 | self.name = name |
---|
| 33 | self.deleted = False |
---|
| 34 | |
---|
| 35 | class Role( object ): |
---|
| 36 | private_id = None |
---|
| 37 | types = Bunch( |
---|
| 38 | PRIVATE = 'private', |
---|
| 39 | SYSTEM = 'system', |
---|
| 40 | USER = 'user', |
---|
| 41 | ADMIN = 'admin', |
---|
| 42 | SHARING = 'sharing' |
---|
| 43 | ) |
---|
| 44 | def __init__( self, name="", description="", type="system", deleted=False ): |
---|
| 45 | self.name = name |
---|
| 46 | self.description = description |
---|
| 47 | self.type = type |
---|
| 48 | self.deleted = deleted |
---|
| 49 | |
---|
| 50 | class UserGroupAssociation( object ): |
---|
| 51 | def __init__( self, user, group ): |
---|
| 52 | self.user = user |
---|
| 53 | self.group = group |
---|
| 54 | |
---|
| 55 | class UserRoleAssociation( object ): |
---|
| 56 | def __init__( self, user, role ): |
---|
| 57 | self.user = user |
---|
| 58 | self.role = role |
---|
| 59 | |
---|
| 60 | class GroupRoleAssociation( object ): |
---|
| 61 | def __init__( self, group, role ): |
---|
| 62 | self.group = group |
---|
| 63 | self.role = role |
---|
| 64 | |
---|
| 65 | class GalaxySession( object ): |
---|
| 66 | def __init__( self, |
---|
| 67 | id=None, |
---|
| 68 | user=None, |
---|
| 69 | remote_host=None, |
---|
| 70 | remote_addr=None, |
---|
| 71 | referer=None, |
---|
| 72 | current_history=None, |
---|
| 73 | session_key=None, |
---|
| 74 | is_valid=False, |
---|
| 75 | prev_session_id=None ): |
---|
| 76 | self.id = id |
---|
| 77 | self.user = user |
---|
| 78 | self.remote_host = remote_host |
---|
| 79 | self.remote_addr = remote_addr |
---|
| 80 | self.referer = referer |
---|
| 81 | self.current_history = current_history |
---|
| 82 | self.session_key = session_key |
---|
| 83 | self.is_valid = is_valid |
---|
| 84 | self.prev_session_id = prev_session_id |
---|
| 85 | |
---|
| 86 | class Tool( object ): |
---|
| 87 | file_path = '/tmp' |
---|
| 88 | states = Bunch( NEW = 'new', |
---|
| 89 | ERROR = 'error', |
---|
| 90 | DELETED = 'deleted', |
---|
| 91 | WAITING = 'waiting', |
---|
| 92 | APPROVED = 'approved', |
---|
| 93 | REJECTED = 'rejected', |
---|
| 94 | ARCHIVED = 'archived' ) |
---|
| 95 | def __init__( self, guid=None, tool_id=None, name=None, description=None, user_description=None, |
---|
| 96 | category=None, version=None, user_id=None, external_filename=None, suite=False ): |
---|
| 97 | self.guid = guid |
---|
| 98 | self.tool_id = tool_id |
---|
| 99 | self.name = name or "Unnamed tool" |
---|
| 100 | self.description = description |
---|
| 101 | self.user_description = user_description |
---|
| 102 | self.version = version or "1.0.0" |
---|
| 103 | self.user_id = user_id |
---|
| 104 | self.external_filename = external_filename |
---|
| 105 | self.deleted = False |
---|
| 106 | self.__extension = None |
---|
| 107 | self.suite = suite |
---|
| 108 | def get_file_name( self ): |
---|
| 109 | if not self.external_filename: |
---|
| 110 | assert self.id is not None, "ID must be set before filename used (commit the object)" |
---|
| 111 | dir = os.path.join( self.file_path, 'tools', *directory_hash_id( self.id ) ) |
---|
| 112 | # Create directory if it does not exist |
---|
| 113 | if not os.path.exists( dir ): |
---|
| 114 | os.makedirs( dir ) |
---|
| 115 | # Return filename inside hashed directory |
---|
| 116 | filename = os.path.join( dir, "tool_%d.dat" % self.id ) |
---|
| 117 | else: |
---|
| 118 | filename = self.external_filename |
---|
| 119 | # Make filename absolute |
---|
| 120 | return os.path.abspath( filename ) |
---|
| 121 | def set_file_name( self, filename ): |
---|
| 122 | if not filename: |
---|
| 123 | self.external_filename = None |
---|
| 124 | else: |
---|
| 125 | self.external_filename = filename |
---|
| 126 | file_name = property( get_file_name, set_file_name ) |
---|
| 127 | def create_from_datatype( self, datatype_bunch ): |
---|
| 128 | # TODO: ensure guid is unique and generate a new one if not. |
---|
| 129 | self.guid = datatype_bunch.guid |
---|
| 130 | self.tool_id = datatype_bunch.id |
---|
| 131 | self.name = datatype_bunch.name |
---|
| 132 | self.description = datatype_bunch.description |
---|
| 133 | self.version = datatype_bunch.version |
---|
| 134 | self.user_id = datatype_bunch.user.id |
---|
| 135 | self.suite = datatype_bunch.suite |
---|
| 136 | @property |
---|
| 137 | def state( self ): |
---|
| 138 | latest_event = self.latest_event |
---|
| 139 | if latest_event: |
---|
| 140 | return latest_event.state |
---|
| 141 | return None |
---|
| 142 | @property |
---|
| 143 | def latest_event( self ): |
---|
| 144 | if self.events: |
---|
| 145 | events = [ tea.event for tea in self.events ] |
---|
| 146 | # Get the last event that occurred ( events mapper is sorted descending ) |
---|
| 147 | return events[0] |
---|
| 148 | return None |
---|
| 149 | # Tool states |
---|
| 150 | @property |
---|
| 151 | def is_new( self ): |
---|
| 152 | return self.state == self.states.NEW |
---|
| 153 | @property |
---|
| 154 | def is_error( self ): |
---|
| 155 | return self.state == self.states.ERROR |
---|
| 156 | @property |
---|
| 157 | def is_deleted( self ): |
---|
| 158 | return self.state == self.states.DELETED |
---|
| 159 | @property |
---|
| 160 | def is_waiting( self ): |
---|
| 161 | return self.state == self.states.WAITING |
---|
| 162 | @property |
---|
| 163 | def is_approved( self ): |
---|
| 164 | return self.state == self.states.APPROVED |
---|
| 165 | @property |
---|
| 166 | def is_rejected( self ): |
---|
| 167 | return self.state == self.states.REJECTED |
---|
| 168 | @property |
---|
| 169 | def is_archived( self ): |
---|
| 170 | return self.state == self.states.ARCHIVED |
---|
| 171 | def get_state_message( self ): |
---|
| 172 | if self.is_suite: |
---|
| 173 | label = 'tool suite' |
---|
| 174 | else: |
---|
| 175 | label = 'tool' |
---|
| 176 | if self.is_new: |
---|
| 177 | return '<font color="red"><b><i>This is an unsubmitted version of this %s</i></b></font>' % label |
---|
| 178 | if self.is_error: |
---|
| 179 | return '<font color="red"><b><i>This %s is in an error state</i></b></font>' % label |
---|
| 180 | if self.is_deleted: |
---|
| 181 | return '<font color="red"><b><i>This is a deleted version of this %s</i></b></font>' % label |
---|
| 182 | if self.is_waiting: |
---|
| 183 | return '<font color="red"><b><i>This version of this %s is awaiting administrative approval</i></b></font>' % label |
---|
| 184 | if self.is_approved: |
---|
| 185 | return '<b><i>This is the latest approved version of this %s</i></b>' % label |
---|
| 186 | if self.is_rejected: |
---|
| 187 | return '<font color="red"><b><i>This version of this %s has been rejected by an administrator</i></b></font>' % label |
---|
| 188 | if self.is_archived: |
---|
| 189 | return '<font color="red"><b><i>This is an archived version of this %s</i></b></font>' % label |
---|
| 190 | @property |
---|
| 191 | def extension( self ): |
---|
| 192 | # if instantiated via a query, this unmapped property won't exist |
---|
| 193 | if '_Tool__extension' not in dir( self ): |
---|
| 194 | self.__extension = None |
---|
| 195 | if self.__extension is None: |
---|
| 196 | head = open( self.file_name, 'rb' ).read( 4 ) |
---|
| 197 | try: |
---|
| 198 | assert head[:3] == 'BZh' |
---|
| 199 | assert int( head[-1] ) in range( 0, 10 ) |
---|
| 200 | self.__extension = 'tar.bz2' |
---|
| 201 | except AssertionError: |
---|
| 202 | pass |
---|
| 203 | if self.__extension is None: |
---|
| 204 | try: |
---|
| 205 | assert head[:2] == '\037\213' |
---|
| 206 | self.__extension = 'tar.gz' |
---|
| 207 | except: |
---|
| 208 | pass |
---|
| 209 | if self.__extension is None: |
---|
| 210 | self.__extension = 'tar' |
---|
| 211 | return self.__extension |
---|
| 212 | @property |
---|
| 213 | def is_suite( self ): |
---|
| 214 | return self.suite |
---|
| 215 | @property |
---|
| 216 | def label( self ): |
---|
| 217 | if self.is_suite: |
---|
| 218 | return 'tool suite' |
---|
| 219 | else: |
---|
| 220 | return 'tool' |
---|
| 221 | @property |
---|
| 222 | def download_file_name( self ): |
---|
| 223 | return '%s_%s.%s' % ( self.tool_id, self.version, self.extension ) |
---|
| 224 | @property |
---|
| 225 | def mimetype( self ): |
---|
| 226 | return mimetypes.guess_type( self.download_file_name )[0] |
---|
| 227 | |
---|
| 228 | class Event( object ): |
---|
| 229 | def __init__( self, state=None, comment='' ): |
---|
| 230 | self.state = state |
---|
| 231 | self.comment = comment |
---|
| 232 | |
---|
| 233 | class ToolEventAssociation( object ): |
---|
| 234 | def __init__( self, tool=None, event=None ): |
---|
| 235 | self.tool = tool |
---|
| 236 | self.event = event |
---|
| 237 | |
---|
| 238 | class ItemRatingAssociation( object ): |
---|
| 239 | def __init__( self, id=None, user=None, item=None, rating=0, comment='' ): |
---|
| 240 | self.id = id |
---|
| 241 | self.user = user |
---|
| 242 | self.item = item |
---|
| 243 | self.rating = rating |
---|
| 244 | self.comment = comment |
---|
| 245 | def set_item( self, item ): |
---|
| 246 | """ Set association's item. """ |
---|
| 247 | pass |
---|
| 248 | |
---|
| 249 | class ToolRatingAssociation( ItemRatingAssociation ): |
---|
| 250 | def set_item( self, tool ): |
---|
| 251 | self.tool = tool |
---|
| 252 | |
---|
| 253 | class Category( object ): |
---|
| 254 | def __init__( self, name=None, description=None, deleted=False ): |
---|
| 255 | self.name = name |
---|
| 256 | self.description = description |
---|
| 257 | self.deleted = deleted |
---|
| 258 | |
---|
| 259 | class ToolCategoryAssociation( object ): |
---|
| 260 | def __init__( self, tool=None, category=None ): |
---|
| 261 | self.tool = tool |
---|
| 262 | self.category = category |
---|
| 263 | |
---|
| 264 | class Tag ( object ): |
---|
| 265 | def __init__( self, id=None, type=None, parent_id=None, name=None ): |
---|
| 266 | self.id = id |
---|
| 267 | self.type = type |
---|
| 268 | self.parent_id = parent_id |
---|
| 269 | self.name = name |
---|
| 270 | def __str__ ( self ): |
---|
| 271 | return "Tag(id=%s, type=%i, parent_id=%s, name=%s)" % ( self.id, self.type, self.parent_id, self.name ) |
---|
| 272 | |
---|
| 273 | class ItemTagAssociation ( object ): |
---|
| 274 | def __init__( self, id=None, user=None, item_id=None, tag_id=None, user_tname=None, value=None ): |
---|
| 275 | self.id = id |
---|
| 276 | self.user = user |
---|
| 277 | self.item_id = item_id |
---|
| 278 | self.tag_id = tag_id |
---|
| 279 | self.user_tname = user_tname |
---|
| 280 | self.value = None |
---|
| 281 | self.user_value = None |
---|
| 282 | |
---|
| 283 | class ToolTagAssociation ( ItemTagAssociation ): |
---|
| 284 | pass |
---|
| 285 | |
---|
| 286 | class ToolAnnotationAssociation( object ): |
---|
| 287 | pass |
---|
| 288 | |
---|
| 289 | ## ---- Utility methods ------------------------------------------------------- |
---|
| 290 | def sort_by_attr( seq, attr ): |
---|
| 291 | """ |
---|
| 292 | Sort the sequence of objects by object's attribute |
---|
| 293 | Arguments: |
---|
| 294 | seq - the list or any sequence (including immutable one) of objects to sort. |
---|
| 295 | attr - the name of attribute to sort by |
---|
| 296 | """ |
---|
| 297 | # Use the "Schwartzian transform" |
---|
| 298 | # Create the auxiliary list of tuples where every i-th tuple has form |
---|
| 299 | # (seq[i].attr, i, seq[i]) and sort it. The second item of tuple is needed not |
---|
| 300 | # only to provide stable sorting, but mainly to eliminate comparison of objects |
---|
| 301 | # (which can be expensive or prohibited) in case of equal attribute values. |
---|
| 302 | intermed = map( None, map( getattr, seq, ( attr, ) * len( seq ) ), xrange( len( seq ) ), seq ) |
---|
| 303 | intermed.sort() |
---|
| 304 | return map( operator.getitem, intermed, ( -1, ) * len( intermed ) ) |
---|
| 305 | def directory_hash_id( id ): |
---|
| 306 | s = str( id ) |
---|
| 307 | l = len( s ) |
---|
| 308 | # Shortcut -- ids 0-999 go under ../000/ |
---|
| 309 | if l < 4: |
---|
| 310 | return [ "000" ] |
---|
| 311 | # Pad with zeros until a multiple of three |
---|
| 312 | padded = ( ( ( 3 - len( s ) ) % 3 ) * "0" ) + s |
---|
| 313 | # Drop the last three digits -- 1000 files per directory |
---|
| 314 | padded = padded[:-3] |
---|
| 315 | # Break into chunks of three |
---|
| 316 | return [ padded[i*3:(i+1)*3] for i in range( len( padded ) // 3 ) ] |
---|