intermine 0.98.01

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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