intermine 0.98.01

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in intermine.gemspec
4
+ gemspec
data/LICENCE ADDED
@@ -0,0 +1,165 @@
1
+ GNU LESSER GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+
9
+ This version of the GNU Lesser General Public License incorporates
10
+ the terms and conditions of version 3 of the GNU General Public
11
+ License, supplemented by the additional permissions listed below.
12
+
13
+ 0. Additional Definitions.
14
+
15
+ As used herein, "this License" refers to version 3 of the GNU Lesser
16
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
17
+ General Public License.
18
+
19
+ "The Library" refers to a covered work governed by this License,
20
+ other than an Application or a Combined Work as defined below.
21
+
22
+ An "Application" is any work that makes use of an interface provided
23
+ by the Library, but which is not otherwise based on the Library.
24
+ Defining a subclass of a class defined by the Library is deemed a mode
25
+ of using an interface provided by the Library.
26
+
27
+ A "Combined Work" is a work produced by combining or linking an
28
+ Application with the Library. The particular version of the Library
29
+ with which the Combined Work was made is also called the "Linked
30
+ Version".
31
+
32
+ The "Minimal Corresponding Source" for a Combined Work means the
33
+ Corresponding Source for the Combined Work, excluding any source code
34
+ for portions of the Combined Work that, considered in isolation, are
35
+ based on the Application, and not on the Linked Version.
36
+
37
+ The "Corresponding Application Code" for a Combined Work means the
38
+ object code and/or source code for the Application, including any data
39
+ and utility programs needed for reproducing the Combined Work from the
40
+ Application, but excluding the System Libraries of the Combined Work.
41
+
42
+ 1. Exception to Section 3 of the GNU GPL.
43
+
44
+ You may convey a covered work under sections 3 and 4 of this License
45
+ without being bound by section 3 of the GNU GPL.
46
+
47
+ 2. Conveying Modified Versions.
48
+
49
+ If you modify a copy of the Library, and, in your modifications, a
50
+ facility refers to a function or data to be supplied by an Application
51
+ that uses the facility (other than as an argument passed when the
52
+ facility is invoked), then you may convey a copy of the modified
53
+ version:
54
+
55
+ a) under this License, provided that you make a good faith effort to
56
+ ensure that, in the event an Application does not supply the
57
+ function or data, the facility still operates, and performs
58
+ whatever part of its purpose remains meaningful, or
59
+
60
+ b) under the GNU GPL, with none of the additional permissions of
61
+ this License applicable to that copy.
62
+
63
+ 3. Object Code Incorporating Material from Library Header Files.
64
+
65
+ The object code form of an Application may incorporate material from
66
+ a header file that is part of the Library. You may convey such object
67
+ code under terms of your choice, provided that, if the incorporated
68
+ material is not limited to numerical parameters, data structure
69
+ layouts and accessors, or small macros, inline functions and templates
70
+ (ten or fewer lines in length), you do both of the following:
71
+
72
+ a) Give prominent notice with each copy of the object code that the
73
+ Library is used in it and that the Library and its use are
74
+ covered by this License.
75
+
76
+ b) Accompany the object code with a copy of the GNU GPL and this license
77
+ document.
78
+
79
+ 4. Combined Works.
80
+
81
+ You may convey a Combined Work under terms of your choice that,
82
+ taken together, effectively do not restrict modification of the
83
+ portions of the Library contained in the Combined Work and reverse
84
+ engineering for debugging such modifications, if you also do each of
85
+ the following:
86
+
87
+ a) Give prominent notice with each copy of the Combined Work that
88
+ the Library is used in it and that the Library and its use are
89
+ covered by this License.
90
+
91
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
92
+ document.
93
+
94
+ c) For a Combined Work that displays copyright notices during
95
+ execution, include the copyright notice for the Library among
96
+ these notices, as well as a reference directing the user to the
97
+ copies of the GNU GPL and this license document.
98
+
99
+ d) Do one of the following:
100
+
101
+ 0) Convey the Minimal Corresponding Source under the terms of this
102
+ License, and the Corresponding Application Code in a form
103
+ suitable for, and under terms that permit, the user to
104
+ recombine or relink the Application with a modified version of
105
+ the Linked Version to produce a modified Combined Work, in the
106
+ manner specified by section 6 of the GNU GPL for conveying
107
+ Corresponding Source.
108
+
109
+ 1) Use a suitable shared library mechanism for linking with the
110
+ Library. A suitable mechanism is one that (a) uses at run time
111
+ a copy of the Library already present on the user's computer
112
+ system, and (b) will operate properly with a modified version
113
+ of the Library that is interface-compatible with the Linked
114
+ Version.
115
+
116
+ e) Provide Installation Information, but only if you would otherwise
117
+ be required to provide such information under section 6 of the
118
+ GNU GPL, and only to the extent that such information is
119
+ necessary to install and execute a modified version of the
120
+ Combined Work produced by recombining or relinking the
121
+ Application with a modified version of the Linked Version. (If
122
+ you use option 4d0, the Installation Information must accompany
123
+ the Minimal Corresponding Source and Corresponding Application
124
+ Code. If you use option 4d1, you must provide the Installation
125
+ Information in the manner specified by section 6 of the GNU GPL
126
+ for conveying Corresponding Source.)
127
+
128
+ 5. Combined Libraries.
129
+
130
+ You may place library facilities that are a work based on the
131
+ Library side by side in a single library together with other library
132
+ facilities that are not Applications and are not covered by this
133
+ License, and convey such a combined library under terms of your
134
+ choice, if you do both of the following:
135
+
136
+ a) Accompany the combined library with a copy of the same work based
137
+ on the Library, uncombined with any other library facilities,
138
+ conveyed under the terms of this License.
139
+
140
+ b) Give prominent notice with the combined library that part of it
141
+ is a work based on the Library, and explaining where to find the
142
+ accompanying uncombined form of the same work.
143
+
144
+ 6. Revised Versions of the GNU Lesser General Public License.
145
+
146
+ The Free Software Foundation may publish revised and/or new versions
147
+ of the GNU Lesser General Public License from time to time. Such new
148
+ versions will be similar in spirit to the present version, but may
149
+ differ in detail to address new problems or concerns.
150
+
151
+ Each version is given a distinguishing version number. If the
152
+ Library as you received it specifies that a certain numbered version
153
+ of the GNU Lesser General Public License "or any later version"
154
+ applies to it, you have the option of following the terms and
155
+ conditions either of that published version or of any later version
156
+ published by the Free Software Foundation. If the Library as you
157
+ received it does not specify a version number of the GNU Lesser
158
+ General Public License, you may choose any version of the GNU Lesser
159
+ General Public License ever published by the Free Software Foundation.
160
+
161
+ If the Library as you received it specifies that a proxy can decide
162
+ whether future versions of the GNU Lesser General Public License shall
163
+ apply, that proxy's public statement of acceptance of any version is
164
+ permanent authorization for you to choose that version for the
165
+ Library.
File without changes
@@ -0,0 +1,79 @@
1
+ = Webservice Client Library for InterMine Data-Warehouses
2
+
3
+ This library provides an interface to the InterMine webservices
4
+ API. It makes construction and execution of queries more
5
+ straightforward, safe and convenient, and allows for results
6
+ to be used directly in Ruby code. As well as traditional row based
7
+ access, the library provides an object-orientated record result
8
+ format (similar to ActiveRecords), and allows for fast, memory
9
+ efficient iteration of result sets.
10
+
11
+ == Example
12
+
13
+ Get all protein domains associated with a set of genes and print their names:
14
+
15
+ require "intermine/service"
16
+
17
+ Service.new("www.flymine.org/query").
18
+ new_query("Pathway")
19
+ select(:name).
20
+ where("genes.symbol" => ["zen", "hox", "h", "bib"]).
21
+ each_row { |row| puts row[:name]}
22
+
23
+ == Who is this for?
24
+
25
+ InterMine data warehouses are typically constructed to hold
26
+ Biological data, and as this library facilitates programmatic
27
+ access to these data, this install is primarily aimed at
28
+ bioinformaticians. In particular, users of the following services
29
+ may find it especially useful:
30
+ * FlyMine (http://www.flymine.org/query)
31
+ * YeastMine (http://yeastmine.yeastgenome.org/yeastmine)
32
+ * RatMine (http://ratmine.mcw.edu/ratmine)
33
+ * modMine (http://intermine.modencode.org/release-23)
34
+ * metabolicMine (http://www.metabolicmine.org/beta)
35
+
36
+ == How to use this library:
37
+
38
+ We have tried to construct an interface to this library that
39
+ does not require you to learn an entirely new set of concepts.
40
+ As such, as well as the underlying methods that are common
41
+ to all libraries, there is an additional set of aliases and sugar
42
+ methods that emulate the DSL style of SQL:
43
+
44
+ === SQL style
45
+
46
+ service = Service.new("www.flymine.org/query")
47
+ service.model.
48
+ table("Gene").
49
+ select("*", "pathways.*").
50
+ where(:symbol => "zen").
51
+ order_by(:symbol).
52
+ outerjoin(:pathways).
53
+ each_row do |r|
54
+ puts r
55
+ end
56
+
57
+ === Common InterMine interface
58
+
59
+ service = Service.new("www.flymine.org/query")
60
+ query = service.new_query("Gene")
61
+ query.add_views("*", "pathways.*")
62
+ query.add_constraint("symbol", "=", "zen")
63
+ query.add_sort_order(:symbol)
64
+ query.add_join(:pathways)
65
+ query.each_row do |r|
66
+ puts r
67
+ end
68
+
69
+ For more details, see the accompanying documentation and the unit tests
70
+ for interface examples. Further documentation is available at www.intermine.org.
71
+
72
+ == Support
73
+
74
+ Support is available on our development mailing list: dev@intermine.org
75
+
76
+ == License
77
+
78
+ This code is Open Source under the LGPL. Source code for all InterMine code
79
+ can be checked out from svn://subversion.flymine.org/flymine
@@ -0,0 +1,67 @@
1
+ require 'rubygems'
2
+ require 'rubygems/specification' unless defined?(Gem::Specification)
3
+ require 'rake/testtask'
4
+ require 'rake/gempackagetask'
5
+
6
+ gem 'rdoc', '=2.1.0'
7
+ require 'rdoc/rdoc'
8
+ require 'rake/rdoctask'
9
+
10
+ gem 'darkfish-rdoc'
11
+ require 'darkfish-rdoc'
12
+
13
+ def gemspec
14
+ @gemspec ||= begin
15
+ Gem::Specification.load(File.expand_path('intermine.gemspec'))
16
+ end
17
+ end
18
+
19
+ task :default => :test
20
+
21
+ desc 'Start a console session'
22
+ task :console do
23
+ system 'irb -I lib -r intermine/service'
24
+ end
25
+
26
+ desc 'Displays the current version'
27
+ task :version do
28
+ puts "Current version: #{gemspec.version}"
29
+ end
30
+
31
+ desc 'Installs the gem locally'
32
+ task :install => :package do
33
+ sh "gem install pkg/#{gemspec.name}-#{gemspec.version}"
34
+ end
35
+
36
+ desc 'Release the gem'
37
+ task :release => :package do
38
+ sh "gem push pkg/#{gemspec.name}-#{gemspec.version}.gem"
39
+ end
40
+
41
+ Rake::GemPackageTask.new(gemspec) do |pkg|
42
+ pkg.need_zip = true
43
+ pkg.need_tar = true
44
+
45
+ end
46
+
47
+ Rake::TestTask.new do |t|
48
+ t.libs << "test"
49
+ t.test_files = FileList['test/unit_tests.rb']
50
+ t.verbose = true
51
+ end
52
+
53
+ Rake::TestTask.new(:live_tests) do |t|
54
+ t.libs << "test"
55
+ t.test_files = FileList['test/live_test.rb']
56
+ t.verbose = true
57
+ end
58
+
59
+ Rake::RDocTask.new do |t|
60
+ t.title = 'InterMine Webservice Client Documentation'
61
+ t.rdoc_files.include 'README.rdoc'
62
+ t.rdoc_files.include 'lib/**/*rb'
63
+ t.main = 'README.rdoc'
64
+ t.options += ['-SHN', '-f', 'darkfish']
65
+ end
66
+
67
+
@@ -0,0 +1,716 @@
1
+ require 'rubygems'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require "stringio"
5
+ require "cgi"
6
+ require 'json'
7
+ require "intermine/service"
8
+
9
+ include InterMine
10
+
11
+ # == List Management Tools
12
+ #
13
+ # Classes that deal with the creation and management
14
+ # of a user's saved lists in an individual webservice.
15
+ #
16
+ module InterMine::Lists
17
+
18
+ #
19
+ # == Synopsis
20
+ #
21
+ # list = service.create_list(%{h eve H bib zen}, "Gene")
22
+ # list.name = "My new list of genes" # Updates name on server
23
+ # puts list.size # 5
24
+ # list.each do |gene| # Inspect the contents
25
+ # puts gene.name
26
+ # end
27
+ #
28
+ # list << "Hox" # Append an element
29
+ # puts list.size
30
+ #
31
+ # == Description
32
+ #
33
+ # A representation of a saved list in the account of an
34
+ # individual user of an InterMine service. Lists represent
35
+ # homogenous collections of objects, which are themselves
36
+ # linked to records in the data-warehouse. A list behaves
37
+ # much as a normal Array would: it has a size, and can be
38
+ # processed with each and map, and allows for positional
39
+ # access with list[idx]. In addition, as this list is
40
+ # backed by its representation in the webapp, it has a name,
41
+ # and description, as well as a type. Any changes to the list,
42
+ # either in its contents or by renaming, are reflected in the
43
+ # stored object.
44
+ #
45
+ #:include:contact_header.rdoc
46
+ #
47
+ class List
48
+
49
+ # The name of the list. This can be changed at any time.
50
+ attr_reader :name
51
+
52
+ # The title of the list. This is fixed.
53
+ attr_reader :title
54
+
55
+ # An informative description.
56
+ attr_reader :description
57
+
58
+ # The kind of object this list holds
59
+ attr_reader :type
60
+
61
+ # The number of elements in the list
62
+ attr_reader :size
63
+
64
+ # The date that this list was originally created
65
+ attr_reader :dateCreated
66
+
67
+ # The categories associated with this list
68
+ attr_reader :tags
69
+
70
+ # Any ids used to construct this list that did not match any objects in the database
71
+ attr_reader :unmatched_identifiers
72
+
73
+ # Construct a new list with details from the webservice.
74
+ #
75
+ # This method is called internally. You will not need to construct
76
+ # new list objects directly.
77
+ #
78
+ # Arguments:
79
+ # [+details+] The information about this list received from the webservice.
80
+ # [+manager+] The object responsible for keeping track of all the known lists
81
+ #
82
+ # list = List.new({"name" => "Foo"}, manager)
83
+ #
84
+ def initialize(details, manager=nil)
85
+ @manager = manager
86
+ details.each {|k,v| instance_variable_set('@' + k, v)}
87
+ @unmatched_identifiers = []
88
+ @tags ||= []
89
+ end
90
+
91
+ # True if the list has no elements.
92
+ def empty?
93
+ @size == 0
94
+ end
95
+
96
+ # True if the list can be changed by the current user.
97
+ #
98
+ # if list.is_authorized?
99
+ # list.remove("h")
100
+ # end
101
+ #
102
+ def is_authorized?
103
+ return @authorized.nil? ? true : @authorized
104
+ end
105
+
106
+ # Returns the first element in the list. The order elements
107
+ # are returned in depends on the fields that its class has.
108
+ # It is not related to the order of the identifiers given at creation.
109
+ #
110
+ # puts list.first.symbol
111
+ #
112
+ def first
113
+ if @size > 0
114
+ return self[0]
115
+ else
116
+ return nil
117
+ end
118
+ end
119
+
120
+ # Retrieve an element at a given position. Negative indices
121
+ # count from the end of the list.
122
+ #
123
+ # puts list[2].length
124
+ # puts list[-1].length
125
+ #
126
+ def [](index)
127
+ if index < 0
128
+ index = @size + index
129
+ end
130
+ unless index < @size && index >= 0
131
+ return nil
132
+ end
133
+ return query.first(index)
134
+ end
135
+
136
+ # Retrieve the object at the given index, or raise an IndexError, unless
137
+ # a default is supplied, in which case that is returned instead.
138
+ #
139
+ # gene = list.fetch(6) # Blows up if the list has only 6 elements or less
140
+ #
141
+ def fetch(index, default=nil)
142
+ if index < 0
143
+ index = @size + index
144
+ end
145
+ unless index < @size && index >= 0
146
+ if default
147
+ return default
148
+ else
149
+ raise IndexError, "#{index} is not a suitable index for this list"
150
+ end
151
+ end
152
+ return query.first(index)
153
+ end
154
+
155
+ # Apply the given block to each element in the list. Return the list.
156
+ #
157
+ # list.each do |gene|
158
+ # puts gene.symbol
159
+ # end
160
+ #
161
+ def each
162
+ query.each_result {|r| yield r}
163
+ return self
164
+ end
165
+
166
+ # Return a list composed of the results of the elements of
167
+ # this list process by the given block
168
+ #
169
+ # symbols = list.map {|gene| gene.symbol}
170
+ #
171
+ def map
172
+ ret = []
173
+ query.each_result {|r|
174
+ ret.push(yield r)
175
+ }
176
+ return ret
177
+ end
178
+
179
+ # Used to create a new list from the contents of this one. This can be used
180
+ # to define a sub-list
181
+ #
182
+ # sub_list = service.create_list(list.list_query.where(:length => {"<" => 500}))
183
+ #
184
+ def list_query
185
+ return @manager.service.query(@type).select(@type + '.id').where(@type => self)
186
+ end
187
+
188
+ # A PathQuery::Query with all attributes selected for output, and restricted
189
+ # to the content of this list. This object is used to fetch elements for other
190
+ # methods. This can be used for composing further filters on a list, or for
191
+ # adding other attributes for output.
192
+ #
193
+ # list.query.select("pathways.*").each_result do |gene|
194
+ # puts "#{gene.symbol}: #{gene.pathways.map {|p| p.identifier}.inspect}"
195
+ # end
196
+ #
197
+ def query
198
+ return @manager.service.query(@type).where(@type => self)
199
+ end
200
+
201
+ # Returns a simple, readable representation of the list
202
+ #
203
+ # puts list
204
+ # => "My new list: 5 genes"
205
+ #
206
+ def to_s
207
+ return "#{@name}: #{@size} #{@type}s"
208
+ end
209
+
210
+ # Returns a detailed representation of the list, useful for debugging.
211
+ def inspect
212
+ return "<#{self.class.name} @name=#{@name.inspect} @size=#{@size} @type=#{@type.inspect} @description=#{@description.inspect} @title=#{@title.inspect} @dateCreated=#{@dateCreated.inspect} @authorized=#{@authorized.inspect} @tags=#{@tags.inspect}>"
213
+ end
214
+
215
+ # Update the name of the list, making sure that the name is also
216
+ # changed in the respective service.
217
+ #
218
+ # list.name = "My new list"
219
+ #
220
+ # If a list is created without a name, it will be considered as a "temporary"
221
+ # list until it is given one. All temporary lists are deleted from the webapp
222
+ # when the program exits.
223
+ #
224
+ def name=(new_name)
225
+ return if (@name == new_name)
226
+ uri = URI.parse(@manager.service.root + Service::LIST_RENAME_PATH)
227
+ params = @manager.service.params.merge("oldname" => @name, "newname" => new_name)
228
+ res = Net::HTTP.post_form(uri, params)
229
+ @manager.process_list_creation_response(res)
230
+ @name = new_name
231
+ end
232
+
233
+ # Delete this list from the webservice. After this method is called this object
234
+ # should not be used again, and any attempt to do so will cause errors.
235
+ def delete
236
+ @manager.delete_lists(self)
237
+ @size = 0
238
+ @name = nil
239
+ end
240
+
241
+ # Add other items to this list. The other items can be identifiers in the same
242
+ # form as were used to create this list orginally (strings, or arrays or files).
243
+ # Or other lists or queries can be used to add items to the list. Any combination of these
244
+ # elements is possible.
245
+ #
246
+ # list.add("Roughened", other_list, a_query)
247
+ #
248
+ def add(*others)
249
+ unionables, ids = classify_others(others)
250
+ unless unionables.empty?
251
+ if unionables.size == 1
252
+ append_list(unionables.first)
253
+ else
254
+ append_lists(unionables)
255
+ end
256
+ end
257
+ unless ids.empty?
258
+ ids.each {|x| append_ids(x)}
259
+ end
260
+ return self
261
+ end
262
+
263
+ # Add the other item to the list, exactly as in List#add
264
+ def <<(other)
265
+ return add(other)
266
+ end
267
+
268
+ # Remove items as specified by the arguments from this list. As in
269
+ # List#add these others can be identifiers specified by strings or arrays
270
+ # or files, or other lists or queries.
271
+ #
272
+ # list.remove("eve", sub_list)
273
+ #
274
+ # If the items were not in the list in the first place, no error will be raised,
275
+ # and the size of this list will simply not change.
276
+ #
277
+ def remove(*others)
278
+ unionables, ids = classify_others(others)
279
+ unless ids.empty?
280
+ unionables += ids.map {|x| @manager.create_list(x, @type)}
281
+ end
282
+ unless unionables.empty?
283
+ myname = @name
284
+ new_list = @manager.subtract([self], unionables, @tags, nil, @description)
285
+ self.delete
286
+ @size = new_list.size
287
+ @name = new_list.name
288
+ @description = new_list.description
289
+ @dateCreated = new_list.dateCreated
290
+ @tags = new_list.tags
291
+ self.name = myname
292
+ end
293
+ return self
294
+ end
295
+
296
+ private
297
+
298
+ # Used to interpret arguments to add and remove
299
+ def classify_others(others)
300
+ unionables = []
301
+ ids = []
302
+ others.each do |o|
303
+ case o
304
+ when List
305
+ unionables << o
306
+ when o.respond_to?(:list_upload_uri)
307
+ unionables << o
308
+ else
309
+ ids << o
310
+ end
311
+ end
312
+ return [unionables, ids]
313
+ end
314
+
315
+ # Used to handle the responses returned by queries used to update the list
316
+ # by add and remove
317
+ def handle_response(res)
318
+ new_list = @manager.process_list_creation_response(res)
319
+ @unmatched_identifiers += new_list.unmatched_identifiers
320
+ @size = new_list.size
321
+ end
322
+
323
+ # Add a List to this List
324
+ def append_list(list)
325
+ q = (list.is_a?(List)) ? list.list_query : list
326
+ params = q.params.merge(@manager.service.params).merge("listName" => @name)
327
+ uri = URI.parse(q.list_append_uri)
328
+ res = Net::HTTP.post_form(uri, params)
329
+ handle_response(res)
330
+ end
331
+
332
+ # Add a collection of lists and queries to this List
333
+ def append_lists(lists)
334
+ addendum = @manager.union_of(lists)
335
+ append_list(addendum)
336
+ end
337
+
338
+ # Add items defined by ids to this list
339
+ def append_ids(ids)
340
+ uri = URI.parse(@manager.service.root + Service::LIST_APPEND_PATH)
341
+ params = {"name" => @name}
342
+ if ids.is_a?(File)
343
+ f = ids
344
+ elsif ids.is_a?(Array)
345
+ f = StringIO.new(ids.map {|x| '"' + x.gsub(/"/, '\"') + '"'}.join(" "))
346
+ elsif File.readable?(ids.to_s)
347
+ f = File.open(ids, "r")
348
+ else
349
+ f = StringIO.new(ids.to_s)
350
+ end
351
+ req = Net::HTTP::Post.new(uri.path + "?" + @manager.params_to_query_string(params))
352
+ req.body_stream = f
353
+ req.content_type = "text/plain"
354
+ req.content_length = f.size
355
+
356
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
357
+ http.request(req)
358
+ end
359
+ handle_response(res)
360
+ f.close
361
+ end
362
+
363
+ end
364
+
365
+ # == Synopsis
366
+ #
367
+ # An internal class for managing lists throughout the lifetime of a program.
368
+ # The main Service object delegates list functionality to this class.
369
+ #
370
+ # # Creation
371
+ # list = service.create_list("path/to/some/file.txt", "Gene", "my-favourite-genes")
372
+ # # Retrieval
373
+ # other_list = service.list("my-previously-saved-list")
374
+ # # Combination
375
+ # intersection = service.intersection_of([list, other_list])
376
+ # # Deletion
377
+ # service.delete_lists(list, other_list)
378
+ #
379
+ # == Description
380
+ #
381
+ # This class contains logic for reading and updating the lists available
382
+ # to a given user at a webservice. This class in particular is responsible for
383
+ # parsing list responses, and performing the operations that combine lists into
384
+ # new result sets (intersection, union, symmetric difference, subtraction).
385
+ #
386
+ #:include:contact_header.rdoc
387
+ #
388
+ class ListManager
389
+
390
+ # The name given by default to all lists you do not explicitly name
391
+ #
392
+ # l = service.create_list("genes.txt", "Gene")
393
+ # puts l.name
394
+ # => "my_list_1"
395
+ #
396
+ DEFAULT_LIST_NAME = "my_list"
397
+
398
+ # The description given by default to all new lists for which you
399
+ # do not provide a description explicitly. The purpose of this
400
+ # is to help you identify automatically created lists in you profile.
401
+ DEFAULT_DESCRIPTION = "Created with InterMine Ruby Webservice Client"
402
+
403
+ # The service this manager belongs to
404
+ attr_reader :service
405
+
406
+ # The temporary lists created in this session. These will be deleted
407
+ # at program exit.
408
+ attr_reader :temporary_lists
409
+
410
+ # Construct a new ListManager.
411
+ #
412
+ # You will never need to call this constructor yourself.
413
+ #
414
+ def initialize(service)
415
+ @service = service
416
+ @lists = {}
417
+ @temporary_lists = []
418
+ do_at_exit(self)
419
+ end
420
+
421
+
422
+ # Get the lists currently available in the webservice.
423
+ def lists
424
+ refresh_lists
425
+ return @lists.values
426
+ end
427
+
428
+ # Get the names of the lists currently available in the webservice
429
+ def list_names
430
+ refresh_lists
431
+ return @lists.keys.sort
432
+ end
433
+
434
+ # Get a list by name. Returns nil if the list does not exist.
435
+ def list(name)
436
+ refresh_lists
437
+ return @lists[name]
438
+ end
439
+
440
+ # Gets all lists with the given tags. If more than one tag is supplied,
441
+ # then a list must have all given tags to be returned.
442
+ #
443
+ # tagged = service.get_lists_with_tags("tagA", "tagB")
444
+ #
445
+ def get_lists_with_tags(*tags)
446
+ return lists.select do |l|
447
+ union = l.tags | tags
448
+ union.size == l.tags.size
449
+ end
450
+ end
451
+
452
+ # Update the stored record of lists. This method is
453
+ # called before all list retrieval methods.
454
+ def refresh_lists
455
+ lists = JSON.parse(@service.get_list_data)
456
+ @lists = {}
457
+ lists["lists"].each {|hash|
458
+ l = List.new(hash, self)
459
+ @lists[l.name] = l
460
+ }
461
+ end
462
+
463
+ # === Create a new List with the given content.
464
+ #
465
+ # Creates a new List and stores it on the appropriate
466
+ # webservice:
467
+ #
468
+ # Arguments:
469
+ # [+content+] Can be a string with delimited identifiers, an Array of
470
+ # identifiers, a File object containing identifiers, or
471
+ # a name of an unopened readable file containing identifiers.
472
+ # It can also be another List (in which case the list is cloned)
473
+ # or a query that describes a result set.
474
+ # [+type+] Required when identifiers are being given (but not when the
475
+ # content is a PathQuery::Query or a List. This should be the kind
476
+ # of object to look for (such as "Gene").
477
+ # [+tags+] An Array of tags to apply to the new list. If a list is supplied as
478
+ # the content, these tags will be added to the existing tags.
479
+ # [+name+] The name of the new list. One will be generated if none is provided.
480
+ # Lists created with generated names are considered temporary and will
481
+ # be deleted upon program exit.
482
+ # [+description+] An informative description of the list
483
+ #
484
+ # # With Array of Ids
485
+ # list = service.create_list(%{eve bib zen})
486
+ #
487
+ # # From a file
488
+ # list = service.create_list("path/to/some/file.txt", "Gene", [], "my-stored-genes")
489
+ #
490
+ # # With a query
491
+ # list = service.create_list(service.query("Gene").select(:id).where(:length => {"<" => 500}))
492
+ #
493
+ #
494
+ def create_list(content, type=nil, tags=[], name=nil, description=nil)
495
+ name ||= get_unused_list_name
496
+ description ||= DEFAULT_DESCRIPTION
497
+
498
+ if content.is_a?(List)
499
+ tags += content.tags
500
+ response = create_list_from_query(content.list_query, tags, name, description)
501
+ elsif content.respond_to?(:list_upload_uri)
502
+ response = create_list_from_query(content, tags, name, description)
503
+ else
504
+ response = create_list_from_ids(content, type, tags, name, description)
505
+ end
506
+
507
+ return process_list_creation_response(response)
508
+ end
509
+
510
+ # Deletes the given lists from the webservice. The lists can be supplied
511
+ # as List objects, or as their names as Strings.
512
+ #
513
+ # Raises errors if problems occur with the deletion of these lists, including
514
+ # if the lists do not exist.
515
+ #
516
+ def delete_lists(*lists)
517
+ lists.map {|x| x.is_a?(List) ? x.name : x.to_s}.uniq.each do |name|
518
+ uri = URI.parse(@service.root + Service::LISTS_PATH)
519
+ params = {"name" => name}
520
+ req = Net::HTTP::Delete.new(uri.path + "?" + params_to_query_string(params))
521
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
522
+ http.request(req)
523
+ end
524
+ check_response_for_error(res)
525
+ end
526
+ refresh_lists
527
+ end
528
+
529
+ # Create a new list in the webservice from the symmetric difference of two
530
+ # or more lists, and return a List object that represents it.
531
+ #
532
+ # See ListManager#create_list for an explanation of tags, name and description
533
+ def symmetric_difference_of(lists=[], tags=[], name=nil, description=nil)
534
+ do_commutative_list_operation(Service::LIST_DIFFERENCE_PATH, "Symmetric difference", lists, tags, name, description)
535
+ end
536
+
537
+ # Create a new list in the webservice from the union of two
538
+ # or more lists, and return a List object that represents it.
539
+ #
540
+ # See ListManager#create_list for an explanation of tags, name and description
541
+ def union_of(lists=[], tags=[], name=nil, description=nil)
542
+ do_commutative_list_operation(Service::LIST_UNION_PATH, "Union", lists, tags, name, description)
543
+ end
544
+
545
+ # Create a new list in the webservice from the intersection of two
546
+ # or more lists, and return a List object that represents it.
547
+ #
548
+ # See ListManager#create_list for an explanation of tags, name and description
549
+ def intersection_of(lists=[], tags=[], name=nil, description=nil)
550
+ do_commutative_list_operation(Service::LIST_INTERSECTION_PATH, "Intersection", lists, tags, name, description)
551
+ end
552
+
553
+ # Create a new list in the webservice by subtracting all the elements in the
554
+ # 'delenda' lists from all the elements in the 'reference' lists,
555
+ # and return a List object that represents it.
556
+ #
557
+ # See ListManager#create_list for an explanation of tags, name and description
558
+ def subtract(references=[], delenda=[], tags=[], name=nil, description=nil)
559
+ ref_names = make_list_names(references)
560
+ del_names = make_list_names(delenda)
561
+ name ||= get_unused_list_name
562
+ description ||= "Subtraction of #{del_names[0 .. -2].join(", ")} and #{del_names.last} from #{ref_names[0 .. -2].join(", ")} and #{ref_names.last}"
563
+ uri = URI.parse(@service.root + Service::LIST_SUBTRACTION_PATH)
564
+ params = @service.params.merge("name" => name, "description" => description, "references" => ref_names.join(';'),
565
+ "subtract" => del_names.join(';'), "tags" => tags.join(';'))
566
+ res = Net::HTTP.post_form(uri, params)
567
+ return process_list_creation_response(res)
568
+ end
569
+
570
+ # Common code to all list requests for interpreting the response
571
+ # from the webservice.
572
+ def process_list_creation_response(response)
573
+ check_response_for_error(response)
574
+ new_list = JSON.parse(response.body)
575
+ new_name = new_list["listName"]
576
+ failed_matches = new_list["unmatchedIdentifiers"] || []
577
+ refresh_lists
578
+ ret = list(new_name)
579
+ ret.unmatched_identifiers.replace(failed_matches)
580
+ return ret
581
+ end
582
+
583
+ # only handles single value keys!
584
+ def params_to_query_string(p)
585
+ return @service.params.merge(p).map { |k,v| "#{k}=#{CGI::escape(v.to_s)}" }.join('&')
586
+ end
587
+
588
+ private
589
+
590
+ # Clean up after itself by deleting
591
+ # any temporary lists left lying about
592
+ def do_at_exit(this)
593
+ at_exit do
594
+ unless this.temporary_lists.empty?
595
+ this.lists.each do |x|
596
+ begin
597
+ x.delete if this.temporary_lists.include?(x.name)
598
+ rescue
599
+ # Ignore errors here.
600
+ end
601
+ end
602
+ end
603
+ end
604
+ end
605
+
606
+ # Transform a collection of objects containing Lists, Queries and Strings
607
+ # into a collection of Strings with list names.
608
+ #
609
+ # Raises errors if an object cannot be resolved to an accessible
610
+ # list.
611
+ def make_list_names(objs)
612
+ current_names = list_names
613
+ return objs.map do |x|
614
+ case x
615
+ when List
616
+ x.name
617
+ when x.respond_to?(:list_upload_uri)
618
+ create_list(x).name
619
+ when current_names.include?(x.to_s)
620
+ x.to_s
621
+ else
622
+ raise ArgumentError, "#{x} is not a list you can access"
623
+ end
624
+ end
625
+ end
626
+
627
+ # Common code behind the operation of union, intersection and symmetric difference operations.
628
+ def do_commutative_list_operation(path, operation, lists, tags=[], name=nil, description=nil)
629
+ list_names = make_list_names(lists)
630
+ name ||= get_unused_list_name
631
+ description ||= "#{operation} of #{list_names[0 .. -2].join(", ")} and #{list_names.last}"
632
+
633
+ uri = URI.parse(@service.root + path)
634
+ params = @service.params.merge(
635
+ "name" => name, "lists" => list_names.join(";"),
636
+ "description" => description, "tags" => tags.join(';')
637
+ )
638
+ res = Net::HTTP::post_form(uri, params)
639
+ return process_list_creation_response(res)
640
+ end
641
+
642
+ # Error checking routine.
643
+ def check_response_for_error(response)
644
+ case response
645
+ when Net::HTTPSuccess
646
+ # Ok
647
+ else
648
+ begin
649
+ container = JSON.parse(response.body)
650
+ raise ServiceError, container["error"]
651
+ rescue
652
+ response.error!
653
+ end
654
+ end
655
+ end
656
+
657
+ # Routine for creating a list from a PathQuery::Query
658
+ def create_list_from_query(query, tags=[], name=nil, description=nil)
659
+ uri = query.list_upload_uri
660
+ list_params = {
661
+ "listName" => name,
662
+ "description" => description,
663
+ "tags" => tags.join(";")
664
+ }
665
+ service_params = @service.params
666
+ params = query.params.merge(list_params).merge(service_params)
667
+ return Net::HTTP.post_form(URI.parse(uri), params)
668
+ end
669
+
670
+ # Routine for creating a List in a webservice from a list of Ids.
671
+ def create_list_from_ids(ids, type, tags=[], name=nil, description=nil)
672
+ if @service.model.get_cd(type).nil?
673
+ raise ArgumentError, "Invalid type (#{type.inspect})"
674
+ end
675
+ uri = URI.parse(@service.root + Service::LISTS_PATH)
676
+ list_params = {
677
+ "name" => name,
678
+ "description" => description,
679
+ "tags" => tags.join(";"),
680
+ "type" => type
681
+ }
682
+ if ids.is_a?(File)
683
+ f = ids
684
+ elsif ids.is_a?(Array)
685
+ f = StringIO.new(ids.map {|x| '"' + x.gsub(/"/, '\"') + '"'}.join(" "))
686
+ elsif File.readable?(ids.to_s)
687
+ f = File.open(ids, "r")
688
+ else
689
+ f = StringIO.new(ids.to_s)
690
+ end
691
+ req = Net::HTTP::Post.new(uri.path + "?" + params_to_query_string(list_params))
692
+ req.body_stream = f
693
+ req.content_type = "text/plain"
694
+ req.content_length = f.size
695
+
696
+ res = Net::HTTP.start(uri.host, uri.port) do |http|
697
+ http.request(req)
698
+ end
699
+ f.close
700
+ return res
701
+ end
702
+
703
+ # Helper to get an unused default name
704
+ def get_unused_list_name(no=1)
705
+ name = DEFAULT_LIST_NAME + "_" + no.to_s
706
+ names = list_names
707
+ while names.include?(name)
708
+ no += 1
709
+ name = DEFAULT_LIST_NAME + "_" + no.to_s
710
+ end
711
+ @temporary_lists.push(name)
712
+ return name
713
+ end
714
+
715
+ end
716
+ end