solvebio 1.6.1 → 1.7.0

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.
Files changed (119) hide show
  1. data/.bumpversion.cfg +6 -0
  2. data/.gitignore +5 -4
  3. data/.travis.yml +1 -1
  4. data/Gemfile +3 -0
  5. data/README.md +34 -34
  6. data/Rakefile +1 -18
  7. data/bin/solvebio.rb +14 -16
  8. data/installer +64 -0
  9. data/lib/solvebio.rb +50 -11
  10. data/lib/solvebio/acccount.rb +4 -0
  11. data/lib/solvebio/annotation.rb +11 -0
  12. data/lib/solvebio/api_operations.rb +147 -0
  13. data/lib/solvebio/api_resource.rb +32 -0
  14. data/lib/solvebio/cli.rb +75 -0
  15. data/lib/solvebio/cli/auth.rb +106 -0
  16. data/lib/solvebio/cli/credentials.rb +54 -0
  17. data/lib/{cli → solvebio/cli}/irb.rb +0 -23
  18. data/lib/solvebio/cli/irbrc.rb +48 -0
  19. data/lib/solvebio/cli/tutorial.rb +12 -0
  20. data/lib/solvebio/client.rb +149 -0
  21. data/lib/solvebio/dataset.rb +60 -0
  22. data/lib/solvebio/dataset_field.rb +12 -0
  23. data/lib/solvebio/depository.rb +38 -0
  24. data/lib/solvebio/depository_version.rb +40 -0
  25. data/lib/solvebio/errors.rb +64 -0
  26. data/lib/solvebio/filter.rb +315 -0
  27. data/lib/solvebio/list_object.rb +73 -0
  28. data/lib/solvebio/locale.rb +43 -0
  29. data/lib/solvebio/query.rb +341 -0
  30. data/lib/solvebio/sample.rb +54 -0
  31. data/lib/solvebio/singleton_api_resource.rb +25 -0
  32. data/lib/solvebio/solve_object.rb +164 -0
  33. data/lib/solvebio/tabulate.rb +589 -0
  34. data/lib/solvebio/user.rb +4 -0
  35. data/lib/solvebio/util.rb +59 -0
  36. data/lib/solvebio/version.rb +3 -0
  37. data/solvebio.gemspec +10 -18
  38. data/test/helper.rb +6 -2
  39. data/test/solvebio/data/.gitignore +1 -0
  40. data/test/solvebio/data/.netrc +6 -0
  41. data/test/{data → solvebio/data}/netrc-save +0 -0
  42. data/test/solvebio/data/sample.vcf.gz +0 -0
  43. data/test/solvebio/data/test_creds +3 -0
  44. data/test/solvebio/test_annotation.rb +45 -0
  45. data/test/solvebio/test_client.rb +29 -0
  46. data/test/solvebio/test_conversion.rb +14 -0
  47. data/test/solvebio/test_credentials.rb +67 -0
  48. data/test/solvebio/test_dataset.rb +52 -0
  49. data/test/solvebio/test_depository.rb +24 -0
  50. data/test/solvebio/test_depositoryversion.rb +22 -0
  51. data/test/solvebio/test_error.rb +31 -0
  52. data/test/solvebio/test_filter.rb +86 -0
  53. data/test/solvebio/test_query.rb +282 -0
  54. data/test/solvebio/test_query_batch.rb +38 -0
  55. data/test/solvebio/test_query_init.rb +30 -0
  56. data/test/solvebio/test_query_tabulate.rb +73 -0
  57. data/test/solvebio/test_ratelimit.rb +31 -0
  58. data/test/solvebio/test_resource.rb +29 -0
  59. data/test/solvebio/test_sample_access.rb +60 -0
  60. data/test/solvebio/test_sample_download.rb +20 -0
  61. data/test/solvebio/test_tabulate.rb +129 -0
  62. data/test/solvebio/test_util.rb +39 -0
  63. metadata +100 -85
  64. data/Makefile +0 -17
  65. data/demo/README.md +0 -14
  66. data/demo/cheatsheet.rb +0 -31
  67. data/demo/dataset/facets.rb +0 -13
  68. data/demo/dataset/field.rb +0 -13
  69. data/demo/depository/README.md +0 -24
  70. data/demo/depository/all.rb +0 -13
  71. data/demo/depository/retrieve.rb +0 -13
  72. data/demo/depository/versions-all.rb +0 -13
  73. data/demo/query/query-filter.rb +0 -30
  74. data/demo/query/query.rb +0 -13
  75. data/demo/query/range-filter.rb +0 -18
  76. data/demo/test-api.rb +0 -98
  77. data/lib/cli/auth.rb +0 -122
  78. data/lib/cli/help.rb +0 -13
  79. data/lib/cli/irbrc.rb +0 -54
  80. data/lib/cli/options.rb +0 -75
  81. data/lib/client.rb +0 -154
  82. data/lib/credentials.rb +0 -67
  83. data/lib/errors.rb +0 -81
  84. data/lib/filter.rb +0 -312
  85. data/lib/locale.rb +0 -47
  86. data/lib/main.rb +0 -46
  87. data/lib/query.rb +0 -414
  88. data/lib/resource/annotation.rb +0 -23
  89. data/lib/resource/apiresource.rb +0 -241
  90. data/lib/resource/dataset.rb +0 -91
  91. data/lib/resource/datasetfield.rb +0 -37
  92. data/lib/resource/depository.rb +0 -50
  93. data/lib/resource/depositoryversion.rb +0 -69
  94. data/lib/resource/main.rb +0 -123
  95. data/lib/resource/sample.rb +0 -75
  96. data/lib/resource/solveobject.rb +0 -122
  97. data/lib/resource/user.rb +0 -5
  98. data/lib/tabulate.rb +0 -706
  99. data/lib/util.rb +0 -29
  100. data/test/Makefile +0 -9
  101. data/test/data/sample.vcf.gz +0 -0
  102. data/test/test-annotation.rb +0 -46
  103. data/test/test-auth.rb +0 -58
  104. data/test/test-client.rb +0 -27
  105. data/test/test-conversion.rb +0 -13
  106. data/test/test-dataset.rb +0 -42
  107. data/test/test-depository.rb +0 -35
  108. data/test/test-error.rb +0 -36
  109. data/test/test-filter.rb +0 -70
  110. data/test/test-netrc.rb +0 -52
  111. data/test/test-query-batch.rb +0 -40
  112. data/test/test-query-init.rb +0 -29
  113. data/test/test-query-paging.rb +0 -102
  114. data/test/test-query.rb +0 -71
  115. data/test/test-resource.rb +0 -40
  116. data/test/test-sample-access.rb +0 -59
  117. data/test/test-sample-download.rb +0 -20
  118. data/test/test-tabulate.rb +0 -131
  119. data/test/test-util.rb +0 -42
@@ -1,312 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require_relative 'main'
3
-
4
- # SolveBio::Filter objects.
5
- #
6
- # Makes it easier to create filters cumulatively using ``&`` (and),
7
- # ``|`` (or) and ``~`` (not) operations.
8
- #
9
- # == Example
10
- #
11
- # require 'solvebio'
12
-
13
- # f = SolveBio::Filter.new #=> <Filter []>
14
-
15
- # f &= SolveBio::Filter.new :price => 'Free' #=> <Filter [[:price, "Free"]]>
16
-
17
- # f |= SolveBio::Filter.new :style => 'Mexican' #=> <Filter [{:or=>[[:price, "Free"], [:style, "Mexican"]]}]>
18
- #
19
- # The final result is a filter that can be used in a query which match es
20
- # "price = 'Free' or style = 'Mexican'".
21
- #
22
- # By default, each key/value pairs are AND'ed together. However, you can change that
23
- # to OR by passing in +:or+ as the last argument.
24
- #
25
- # * `<field>='value` matches if the field is term filter (exact term)
26
- # * `<field>__in=[<item1>, ...]` matches any of the terms <item1> and so on
27
- # * `<field>__range=[<start>, <end>]` matches anything from <start> to <end>
28
- # * `<field>__between=[<start>, <end>]` matches anything between <start> to <end> not include either <start> or <end>
29
- #
30
- # String terms are not analyzed and are always assumed to be exact matches.
31
- #
32
- # Numeric columns can be selected by range using:
33
- #
34
- # * `<field>__gt`: greater than
35
- # * `<field>__gte`: greater than or equal to
36
- # * `<field>__lt`: less than
37
- # * `<field>__lte`: less than or equal to
38
- #
39
- # Field action examples:
40
- #
41
- # dataset.query(:gene__in => ['BRCA', 'GATA3'],
42
- # :chr => '3',
43
- # :start__gt => 10000,
44
- # :end__lte => 20000)
45
-
46
- class SolveBio::Filter
47
-
48
- attr_accessor :filters
49
-
50
- # Creates a new Filter, the first argument is expected to be Hash or an Array.
51
- def initialize(filters={}, conn=:and)
52
- if filters.kind_of?(Hash)
53
- @filters = SolveBio::Filter.
54
- normalize(filters.keys.sort.map{|key| [key, filters[key]]})
55
- elsif filters.kind_of?(Array)
56
- @filters = SolveBio::Filter.normalize(filters)
57
- elsif filters.kind_of?(SolveBio::Filter)
58
- @filters = SolveBio::Filter.deep_copy(filters.filters)
59
- return self
60
- else
61
- raise TypeError, "Invalid filter type #{filters.class}"
62
- end
63
- @filters = [{conn => @filters}] if filters.size > 1
64
- self
65
- end
66
-
67
- def inspect
68
- return "<SolveBio::Filter #{@filters.inspect}>"
69
- end
70
-
71
- def empty?
72
- @filters.empty?
73
- end
74
-
75
- # Deep copy
76
- def clone
77
- SolveBio::Filter.deep_copy(self)
78
- end
79
-
80
- # OR and AND will create a new Filter, with the filters from both Filter
81
- # objects combined with the connector `conn`.
82
- # FIXME: should we allow a default conn parameter?
83
- def combine(other, conn=:and)
84
-
85
- return other.clone if self.empty?
86
-
87
- if other.empty?
88
- return self.clone
89
- elsif self.filters[0].member?(conn)
90
- f = self.clone
91
- f.filters[0][conn] += other.filters
92
- elsif other.filters[0].member?(conn)
93
- f = other.clone
94
- f.filters[0][conn] += self.filters
95
- else
96
- f = initialize(self.clone.filters + other.filters, conn)
97
- end
98
-
99
- return f
100
- end
101
-
102
- def |(other)
103
- return self.combine(other, :or)
104
- end
105
-
106
- def &(other)
107
- return self.combine(other, :and)
108
- end
109
-
110
- def ~()
111
- f = self.clone
112
-
113
- # not of null filter is null fiter
114
- return f if f.empty?
115
-
116
- # length of self_filters should never be more than 1
117
- filters = f.filters.first
118
- if filters.kind_of?(Hash) and
119
- filters.member?(:not)
120
- # The filters are already a single dictionary
121
- # containing a 'not'. Swap out the 'not'
122
- f.filters = [filters[:not]]
123
- else
124
- # 'not' blocks can contain only dicts or a single tuple filter
125
- # so we get the first element from the filter list
126
- f.filters = [{:not => filters}]
127
- end
128
-
129
- return f
130
- end
131
-
132
- # Checks and normalizes filter array tuples
133
- def self.normalize(ary)
134
- ary.map do |tuple|
135
- unless tuple.kind_of?(Array)
136
- raise(TypeError,
137
- "Invalid filter element #{tuple.class}; want Array")
138
- end
139
- unless tuple.size == 2
140
- raise(TypeError,
141
- "filter element size must be 2; is #{tuple.size}")
142
- end
143
- key, value = tuple
144
- if key.to_s =~ /.+__(.+)$/
145
- op = $1
146
- unless %w(gt gte lt lte in range between).member?(op)
147
- raise(TypeError,
148
- "Invalid field operation #{op} in #{key}")
149
- end
150
- case op
151
- when 'gt', 'gte', 'lt', 'lte'
152
- begin
153
- value = Float(value)
154
- rescue
155
- raise(TypeError,
156
- "Invalid field value #{value} for #{key}; " +
157
- "should be a number")
158
- end
159
- tuple = [key, value]
160
- when 'range', 'between'
161
- if value.kind_of?(Range)
162
- value = [value.min, value.max]
163
- end
164
- unless value.kind_of?(Array)
165
- raise(TypeError,
166
- "Invalid field value #{value} for #{key}; " +
167
- "should be an array")
168
- end
169
- unless value.size == 2
170
- raise(TypeError,
171
- "Invalid field value #{value} for #{key}; " +
172
- "array should have exactly two values")
173
- end
174
- if value.first > value.last
175
- raise(IndexError,
176
- "Invalid field value #{value} for #{key}; " +
177
- "start value not greater than end value")
178
- end
179
-
180
- # FIXME: Should we check that value contains only numbers?
181
- tuple = [key, value]
182
- when 'in'
183
- unless value.kind_of?(Array)
184
- raise(TypeError,
185
- "Invalid field value #{value} for #{key}; " +
186
- "should be an array")
187
- end
188
-
189
- end
190
- end
191
- tuple
192
- end
193
- end
194
-
195
- def self.deep_copy(obj)
196
- Marshal.load(Marshal.dump(obj))
197
- end
198
-
199
- # Takes an Array of filter items and returns an Array that can be
200
- # passed off (when converted to JSON) to a SolveBio client filter
201
- # parameter. As such, the output format is highly dependent on
202
- # the SolveBio API format.
203
- #
204
- # The filter items can be either a SolveBio::Filter, or Hash of
205
- # the right form, or an Array of the right form.
206
- def self.process_filters(filters)
207
- rv = []
208
- filters.each do |f|
209
- if f.kind_of?(SolveBio::Filter)
210
- if f.filters
211
- rv << process_filters(f.filters)
212
- next
213
- end
214
- elsif f.kind_of?(Hash)
215
- key = f.keys[0]
216
- val = f[key]
217
-
218
- if val.kind_of?(Hash)
219
- filter_filters = process_filters(val)
220
- if filter_filters.size == 1
221
- filter_filters = filter_filters[0]
222
- end
223
- rv << {key => filter_filters}
224
- else
225
- rv << {key => process_filters(val)}
226
- end
227
- elsif f.kind_of?(Array)
228
- rv << f
229
- else
230
- raise TypeError, "Invalid filter class #{f.class}"
231
- end
232
- end
233
- return rv
234
- end
235
-
236
-
237
- end
238
-
239
- # Helper class that generates Range Filters from UCSC-style ranges.
240
- class SolveBio::RangeFilter < SolveBio::Filter
241
- SUPPORTED_BUILDS = ['hg18', 'hg19', 'hg38']
242
-
243
- # Handles UCSC-style range queries (hg19:chr1:100-200)
244
- def self.from_string(string, overlap=false)
245
- begin
246
- build, chromosome, pos = string.split(':')
247
- rescue ValueError
248
- raise ValueError,
249
- 'Please use UCSC-style format: "hg19:chr2:1000-2000"'
250
- end
251
-
252
- if pos.member?('-')
253
- start, last = pos.replace(',', '').split('-')
254
- else
255
- start = last = pos.replace(',', '')
256
- end
257
-
258
- return self.new(build, chromosome, start, last, overlap=overlap)
259
- end
260
-
261
- # Shortcut to do range queries on supported datasets.
262
- def initialize(build, chromosome, start, last, overlap=false)
263
- if !SUPPORTED_BUILDS.member?(build.downcase)
264
- msg = "Build #{build} not supported for range filters. " +
265
- "Supported builds are: #{SUPPORTED_BUILDS.join(', ')}"
266
- raise Exception, msg
267
- end
268
-
269
- f = SolveBio::Filter.new({"#{build}_start__range" => [start, last]})
270
-
271
- if overlap
272
- f |= SolveBio::Filter.
273
- new({"#{build}_end__range" => [start, last]})
274
- else
275
- f &= SolveBio::Filter.
276
- new({"#{build}_end__range" => [start, last]})
277
- end
278
-
279
- f &= SolveBio::Filter.
280
- new({"#{build}_chromosome" => chromosome.sub('chr', '')})
281
- @filters = f.filters
282
- end
283
-
284
- def inspect
285
- return "<RangeFilter #{@filters}>"
286
- end
287
- end
288
-
289
-
290
- # Demo/test code
291
- if __FILE__ == $0
292
- filters =
293
- SolveBio::Filter.new(:omim_id => 144650) |
294
- SolveBio::Filter.new(:omim_id => 144600) |
295
- SolveBio::Filter.new(:omim_id => 145300)
296
- puts filters.inspect
297
- puts SolveBio::Filter.process_filters([[:omim_id, nil]]).inspect
298
- f = SolveBio::Filter.new
299
- puts "%s, empty?: %s" % [f.inspect, f.empty?]
300
- f_not = ~f
301
- puts "%s, empty?: %s" % [f_not.inspect, f_not.empty?]
302
- f2 = SolveBio::Filter.new({:style => 'Mexican', :price => 'Free'})
303
- puts "%s, empty? %s" % [f2.inspect, f2.empty?]
304
- f2_not = ~f2
305
- puts "%s, empty? %s" % [f2_not.inspect, f2_not.empty?]
306
- # FIXME: using a hash means we can't repeat chr1. Is this intended?
307
- f2_or = SolveBio::Filter.new({:chr1 => '3', :chr2 => '4'}, :or)
308
- puts "%s, empty %s" % [f2_or.inspect, f2_or.empty?]
309
- f2_or = SolveBio::Filter.new({:chr1 => '3'}) | SolveBio::Filter.new({:chr2 => '4'})
310
- puts "%s, empty %s" % [f2_or.inspect, f2_or.empty?]
311
- puts((f2_or & f2).inspect)
312
- end
@@ -1,47 +0,0 @@
1
- require_relative 'main'
2
- module SolveBio::Locale
3
-
4
- # Used only if r18n-core is not around
5
- @thousands_sep = ','
6
- @locale = ENV['LANG'] || ENV['LC_NUMERIC'] || 'en_US.UTF-8'
7
- def thousands_sep
8
- @thousands_sep
9
- end
10
- def thousands_sep=(value)
11
- @thousands_sep = value
12
- end
13
-
14
- begin
15
- old_verbose = $VERBOSE
16
- $VERBOSE = false
17
- require 'r18n-core'
18
- R18n.set(@locale)
19
- $VERBOSE = old_verbose
20
- have_r18n = true
21
- rescue LoadError
22
- have_r18n = false
23
- end
24
- if have_r18n
25
- def pretty_int(num)
26
- R18n::l(num)
27
- end
28
- else
29
- def pretty_int(num)
30
- num.to_s.reverse.scan(/\d{1,3}/).join(@thousands_sep).reverse
31
- end
32
- end
33
-
34
- module_function :pretty_int
35
-
36
- end
37
-
38
- class Fixnum
39
- include SolveBio::Locale
40
- def pretty_int
41
- SolveBio::Locale.pretty_int(self)
42
- end
43
- end
44
-
45
- if __FILE__ == $0
46
- puts 10000.pretty_int
47
- end
@@ -1,46 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # SolveBio Ruby Client
3
- # ~~~~~~~~~~~~~~~~~~~
4
- #
5
- # This is the Ruby client & library for the SolveBio API.
6
- #
7
- # Have questions or comments? email us at: contact@solvebio.com
8
-
9
- require 'logger'
10
- require 'fileutils'
11
-
12
- module SolveBio
13
-
14
- VERSION = '1.6.1'
15
- @api_key = ENV['SOLVEBIO_API_KEY']
16
- logfile =
17
- if ENV['SOLVEBIO_LOGFILE']
18
- ENV['SOLVEBIO_LOGFILE']
19
- else
20
- dir = File::expand_path '~/.solvebio'
21
- FileUtils.mkdir_p(dir) unless File.exist? dir
22
- File::expand_path File.join(dir, 'solvebio.log')
23
- end
24
- @logger = Logger.new(logfile)
25
- API_HOST = ENV['SOLVEBIO_API_HOST'] || 'https://api.solvebio.com'
26
-
27
- # Config info in reports and requests. Encapsulate more?
28
- RUBY_VERSION = RbConfig::CONFIG['RUBY_PROGRAM_VERSION']
29
- RUBY_IMPLEMENTATION = RbConfig::CONFIG['RUBY_SO_NAME']
30
- #PLATFORM = ???
31
- #PROCESSOR = ???
32
- ARCHITECTURE = RbConfig::CONFIG['arch']
33
-
34
- def logger
35
- @logger
36
- end
37
- def api_key
38
- @api_key
39
- end
40
- def api_key=(value)
41
- @api_key = value
42
- end
43
-
44
- module_function :logger, :api_key, :api_key=
45
-
46
- end
@@ -1,414 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- require 'pp'
3
- require_relative 'client'
4
- require_relative 'filter'
5
- require_relative 'locale'
6
- require_relative 'tabulate'
7
-
8
- # A Query API request wrapper that generates a request from Filter
9
- # objects, and can iterate through streaming result sets.
10
- class SolveBio::PagingQuery
11
-
12
- include Enumerable
13
-
14
- MAXIMUM_LIMIT ||= 100
15
-
16
- attr_accessor :filters
17
- attr_reader :dataset_id
18
-
19
- def initialize(dataset_id, params={})
20
- @dataset_id = dataset_id
21
-
22
- begin
23
- @limit = Integer(dataset_id)
24
- rescue
25
- raise TypeError, "'dataset_id' parameter must an Integer"
26
- end
27
-
28
- @data_url = "/v1/datasets/#{dataset_id}/data"
29
-
30
- @total = @results = @response = nil
31
- reset_range_window
32
-
33
- # results per request
34
- @limit = MAXIMUM_LIMIT
35
- begin
36
- @limit = Integer(params[:limit])
37
- rescue
38
- raise TypeError, "'limit' parameter must an Integer >= 0"
39
- end if params.member?(:limit)
40
-
41
- @result_class = params[:result_class] || Hash
42
- @debug = params[:debug] || false
43
- @fields = params[:fields]
44
- @filters = []
45
-
46
- # parameter error checking
47
- if @limit < 0
48
- raise RangeError, "'limit' parameter must be >= 0"
49
- end
50
- self
51
- end
52
-
53
- def total
54
- warmup('Query total')
55
- @total = @response["total"]
56
- end
57
-
58
- def clone(filters=[])
59
- result =
60
- initialize(@dataset_id,
61
- {
62
- :limit => @limit,
63
- :total => total, # This causes an HTTP request
64
- :result_class => @result_class,
65
- :debug => @debug,
66
- :fields => @fields
67
- })
68
-
69
- result.filters += @filters unless @filters.empty?
70
- result.filters += filters unless filters.empty?
71
-
72
- return result
73
- end
74
-
75
- # Returns this Query instance with the query args combined with
76
- # existing set with AND.
77
- #
78
- # kwargs are simply passed to a new SolveBio::Filter object and
79
- # combined to any other filters with AND.
80
- #
81
- # By default, everything is combined using AND. If you provide
82
- # multiple filters in a single filter call, those are ANDed
83
- # together. If you provide multiple filters in multiple filter
84
- # calls, those are ANDed together.
85
- #
86
- # If you want something different, use the F class which supports
87
- # ``&`` (and), ``|`` (or) and ``~`` (not) operators. Then call
88
- # filter once with the resulting Filter instance.
89
- def filter(params={}, conn=:and)
90
- if filters.kind_of?(SolveBio::Filter)
91
- return Marshal.load(Marshal.dump(params.filters))
92
- else
93
- return clone(SolveBio::Filter.new(params, conn).filters)
94
- end
95
- end
96
-
97
- # Shortcut to do range queries on supported datasets.
98
- def range(chromosome, start, last, strand=nil, overlap=true)
99
- # TODO: ensure dataset supports range queries?
100
- return self.
101
- clone([self.new(chromosome, start, last, strand, overlap)])
102
- end
103
-
104
- def size
105
- warmup('PagingQuery size')
106
- return @total
107
- end
108
- alias_method :length, :size
109
-
110
- def empty?
111
- warmup('empty?')
112
- return @total == 0
113
- end
114
-
115
- # Convert SolveBio::QueryPaging object to a String type
116
- def to_s
117
- if total == 0 or @limit == 0
118
- return 'query returned 0 results'
119
- end
120
-
121
- sorted_items = SolveBio::Tabulate.
122
- tabulate(self[0].to_a.sort_by{|x| x[0]})
123
- msg =
124
- "\n%s\n\n... %s more results." %
125
- [sorted_items, ['Fields', 'Data'], ['right', 'left'],
126
- (@total - 1).pretty_int]
127
- return msg
128
- end
129
-
130
- def to_pp
131
- if total == 0 or @limit == 0
132
- return 'query returned 0 results'
133
- end
134
- msg = "\n#{self[0].pretty_inspect}\n" +
135
- "\n... #{(@total-1).pretty_int} more results."
136
- return msg
137
- end
138
-
139
- # Convert SolveBio::QueryPaging object to a Hash type
140
- def to_h
141
- self[0]
142
- end
143
-
144
- def inspect
145
- return '<%s: @dataset_id=%s, @total=%s, @limit=%s, @debug=%s>' %
146
- [self.class, @dataset_id, @total ? @total : '?',
147
- @limit, @debug]
148
- end
149
-
150
- # warmup result set...
151
- def warmup(what)
152
- unless @response
153
- SolveBio::logger.debug("warmup #{what}")
154
- execute
155
- end
156
- end
157
-
158
-
159
- # FIXME: consider creating instance variables from
160
- # a response object and then using attr_reader to make that
161
- # visible. This is instead of:
162
- # # One hacky way to define attributes (methods) on an object.
163
- # # Replaces Python's __getattr__
164
- # def method_missing(meth, *args, &block)
165
- # if @response.nil?
166
- # logger.debug('warmup ([]): %s' % key)
167
- # execute
168
- # end
169
-
170
- # if @response.member?(meth)
171
- # return @response[meth]
172
- # end
173
-
174
- # msg = "'%s' object has no attribute '%s'" % [self.class, meth]
175
- # raise NoMethodError, msg
176
- # end
177
-
178
- # Retrieve an item or range from the set of results
179
- def [](key)
180
- # warmup result set...
181
- warmup("[#{key}]")
182
-
183
- unless [Range, Fixnum].member?(key.class)
184
- raise TypeError, "Expecting index value to be a Range or Fixnum; is #{key.class}"
185
- end
186
- if @limit < 0
187
- raise IndexError, 'Indexing not supporting when limit < 0.'
188
- end
189
- if key.kind_of?(Range)
190
- if key.begin < 0 or key.end < 0
191
- raise IndexError, 'Negative indexing is not supported'
192
- end
193
- if key.begin > key.end
194
- raise IndexError, 'Backwards indexing is not supported'
195
- end
196
- elsif key < 0
197
- raise IndexError, 'Negative indexing is not supported'
198
- end
199
-
200
- # FIXME: is it right that we can assume that the results are in
201
- # @results. Do I need another index check?
202
-
203
- result =
204
- if key.kind_of?(Range)
205
- @results[(0...key.end - key.begin)]
206
- else
207
- @request_range = self.to_range(key)
208
- @results[0]
209
- end
210
- # reset request range
211
- @request_range = (0..Float::INFINITY)
212
- return result
213
- end
214
-
215
- # "each" must be defined in an Enumerator. Allows the Query object
216
- # to be an iterable. Iterates through the internal cache using a
217
- # cursor.
218
- def each(*pass)
219
- return self unless block_given?
220
- i = 0
221
-
222
- @delta = @request_range.end - @request_range.begin
223
- while i < total and i < @delta
224
- i_offset = i + @request_range.begin
225
- if @window_range.include?(i_offset)
226
- result_start = i_offset - @window_range.begin
227
- SolveBio::logger.debug(' PagingQuery window range: [%s...%s]' %
228
- [result_start, result_start + 1])
229
- else
230
- SolveBio::logger.debug('executing query. offset/limit: %6d/%d' %
231
- [i_offset, @limit])
232
- execute({:offset => i_offset, :limit => @limit})
233
- result_start = i % @limit
234
- end
235
- yield @results[result_start]
236
- @delta = @request_range.end - @request_range.begin
237
- i += 1
238
- end
239
- return self
240
- end
241
-
242
- # range operations
243
- def to_range(range_or_idx)
244
- return range_or_idx.kind_of?(Range) ? range_or_idx :
245
- (range_or_idx..range_or_idx + 1)
246
- end
247
-
248
- def reset_request_range
249
- @request_range = (0..Float::INFINITY)
250
- end
251
-
252
- def reset_range_window
253
- @window = []
254
- @window_range = (0..Float::INFINITY)
255
- reset_request_range
256
- end
257
-
258
- def build_query
259
- q = {
260
- :limit => @limit,
261
- :debug => @debug
262
- }
263
-
264
- if @filters
265
- filters = SolveBio::Filter.process_filters(@filters)
266
- if filters.size > 1
267
- q[:filters] = [{:and => filters}]
268
- else
269
- q[:filters] = filters
270
- end
271
- end
272
-
273
- if @fields
274
- q[:fields] = @fields
275
- end
276
-
277
- return q
278
- end
279
-
280
- # Executes a query and returns the request parameters and response.
281
- def execute(params={})
282
- _params = build_query()
283
- _params.merge!(params)
284
- SolveBio::logger.debug("querying dataset: #{_params}")
285
-
286
- @response = SolveBio::Client.client.post(@data_url, _params)
287
- @total = @response['total']
288
- SolveBio::logger.
289
- debug("query response took: #{@response['took']} ms, " +
290
- "total: #{@total}")
291
-
292
- # update window
293
- offset = _params[:offset] || 0
294
- @results = @response['results']
295
- @window = @results
296
- @window_range = (offset ... offset + @results.size)
297
-
298
- return _params, @response
299
- end
300
- end
301
-
302
- class SolveBio::Query < SolveBio::PagingQuery
303
- def initialize(dataset_id, params={})
304
- super
305
- return self
306
- end
307
-
308
- def total
309
- warmup('Query total')
310
- @total
311
- end
312
-
313
- def size
314
- warmup('Query size')
315
- [@total, @results.size].min
316
- end
317
- alias_method :length, :size
318
-
319
- # "each" must be defined in an Enumerator. Allows the Query object
320
- # to be an iterable. Iterates through the internal cache using a
321
- # cursor.
322
- def each(*pass)
323
- return self unless block_given?
324
- i = 0
325
- while i < size and i < @limit
326
- i_offset = i + @request_range.begin
327
- if @window_range.include?(i_offset)
328
- result_start = i_offset - @window_range.begin
329
- SolveBio::logger.debug(' Query window range: [%s...%s]' %
330
- [result_start, result_start + 1])
331
- else
332
- SolveBio::logger.debug('executing query. offset/limit: %6d/%d' %
333
- [i_offset, @limit])
334
- execute({:offset => i_offset, :limit => @limit})
335
- result_start = i % @limit
336
- end
337
- yield @results[result_start]
338
- i += 1
339
- end
340
- return self
341
- end
342
-
343
- def [](key)
344
- # Note: super does other parameter checks.
345
- if key.kind_of?(Fixnum) and key >= @window_range.end
346
- raise IndexError, "Invalid index #{key} >= #{@window_range.end}"
347
- end
348
- super[key]
349
- # FIXME: Dunno why the above isn't enough.
350
- @results[key]
351
- end
352
- end
353
-
354
-
355
- # BatchQuery accepts a list of Query objects and executes them
356
- # in a single request to /v1/batch_query.
357
- class SolveBio::BatchQuery
358
- # Expects a list of Query objects.
359
- def initialize(queries)
360
- unless queries.kind_of?(Array)
361
- queries = [queries]
362
- end
363
-
364
- @queries = queries
365
- end
366
-
367
- def build_query
368
- query = {:queries => []}
369
-
370
- @queries.each do |i|
371
- q = i.build_query
372
- q.merge!(:dataset => i.dataset_id)
373
- query[:queries] << q
374
- end
375
-
376
- return query
377
- end
378
-
379
- def execute(params={})
380
- _params = build_query()
381
- _params.merge!(params)
382
- response = SolveBio::Client.client.post('/v1/batch_query', _params)
383
- return response
384
- end
385
- end
386
-
387
- # Demo/test code
388
- if __FILE__ == $0
389
- if SolveBio::api_key
390
- test_dataset_name = 'ClinVar/2.0.0-1/Variants'
391
- require_relative 'solvebio'
392
- require_relative 'errors'
393
- dataset = SolveBio::Dataset.retrieve(test_dataset_name)
394
-
395
- # # A filter
396
- # limit = 5
397
- # results = dataset.query({:paging=>false, :limit => limit}).
398
- # filter({:alternate_alleles => nil})
399
- # puts results.size
400
-
401
- limit = 2
402
- # results = dataset.query({:limit => limit, :paging =>false})
403
- # puts results.size
404
- # results.each_with_index { |val, i|
405
- # puts "#{i}: #{val}"
406
- # }
407
- # puts "#{limit-1}: #{results[limit-1]}"
408
- results = dataset.query({:limit => limit, :paging=>true})
409
- # puts results.size
410
- puts results.to_s
411
- else
412
- puts 'Set SolveBio::api_key to run demo'
413
- end
414
- end