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 ) ] |
---|