solvebio 1.5.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.
- data/.gitignore +7 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/Gemspec +3 -0
- data/LICENSE +21 -0
- data/Makefile +17 -0
- data/README.md +64 -0
- data/Rakefile +59 -0
- data/bin/solvebio.rb +36 -0
- data/demo/README.md +14 -0
- data/demo/dataset/facets.rb +13 -0
- data/demo/dataset/field.rb +13 -0
- data/demo/depository/README.md +24 -0
- data/demo/depository/all.rb +13 -0
- data/demo/depository/retrieve.rb +13 -0
- data/demo/depository/versions-all.rb +13 -0
- data/demo/query/query-filter.rb +30 -0
- data/demo/query/query.rb +13 -0
- data/demo/query/range-filter.rb +18 -0
- data/demo/test-api.rb +98 -0
- data/lib/apiresource.rb +130 -0
- data/lib/cli/auth.rb +122 -0
- data/lib/cli/help.rb +13 -0
- data/lib/cli/irb.rb +58 -0
- data/lib/cli/irbrc.rb +53 -0
- data/lib/cli/options.rb +75 -0
- data/lib/client.rb +152 -0
- data/lib/credentials.rb +67 -0
- data/lib/errors.rb +81 -0
- data/lib/filter.rb +312 -0
- data/lib/help.rb +46 -0
- data/lib/locale.rb +47 -0
- data/lib/main.rb +37 -0
- data/lib/query.rb +415 -0
- data/lib/resource.rb +414 -0
- data/lib/solvebio.rb +14 -0
- data/lib/solveobject.rb +101 -0
- data/lib/tabulate.rb +706 -0
- data/solvebio.gemspec +75 -0
- data/test/data/netrc-save +6 -0
- data/test/helper.rb +3 -0
- data/test/test-auth.rb +54 -0
- data/test/test-client.rb +27 -0
- data/test/test-error.rb +36 -0
- data/test/test-filter.rb +70 -0
- data/test/test-netrc.rb +42 -0
- data/test/test-query-batch.rb +60 -0
- data/test/test-query-init.rb +29 -0
- data/test/test-query-paging.rb +123 -0
- data/test/test-query.rb +88 -0
- data/test/test-resource.rb +47 -0
- data/test/test-solveobject.rb +27 -0
- data/test/test-tabulate.rb +127 -0
- metadata +158 -0
data/lib/client.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
require 'openssl'
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
require_relative 'credentials'
|
7
|
+
require_relative 'errors'
|
8
|
+
|
9
|
+
# import textwrap
|
10
|
+
|
11
|
+
# A requests-based HTTP client for SolveBio API resources
|
12
|
+
class SolveBio::Client
|
13
|
+
|
14
|
+
attr_reader :headers, :api_host
|
15
|
+
attr_accessor :api_key
|
16
|
+
|
17
|
+
def initialize(api_key=nil, api_host=nil)
|
18
|
+
@api_key = api_key || SolveBio::api_key
|
19
|
+
SolveBio::api_key ||= api_key
|
20
|
+
@api_host = api_host || SolveBio::API_HOST
|
21
|
+
# Mirroring comments from:
|
22
|
+
# http://ruby-doc.org/stdlib-2.1.2/libdoc/net/http/rdoc/Net/HTTP.html
|
23
|
+
# gzip compression is used in preference to deflate
|
24
|
+
# compression, which is used in preference to no compression.
|
25
|
+
@headers = {
|
26
|
+
'Content-Type' => 'application/json',
|
27
|
+
'Accept' => 'application/json',
|
28
|
+
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
29
|
+
'User-Agent' => 'SolveBio Ruby Client %s [%s/%s]' % [
|
30
|
+
SolveBio::VERSION,
|
31
|
+
SolveBio::RUBY_IMPLEMENTATION,
|
32
|
+
SolveBio::RUBY_VERSION
|
33
|
+
]
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def request(method, url, params=nil, raw=false)
|
38
|
+
|
39
|
+
if not @api_host
|
40
|
+
raise SolveBio::Error.new(nil, 'No SolveBio API host is set')
|
41
|
+
elsif not url.start_with?(@api_host)
|
42
|
+
url = URI.join(@api_host, url).to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
uri = URI.parse(url)
|
46
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
47
|
+
|
48
|
+
# Note: there's also read_timeout and ssl_timeout
|
49
|
+
http.open_timeout = 80 # in seconds
|
50
|
+
|
51
|
+
if uri.scheme == 'https'
|
52
|
+
http.use_ssl = true
|
53
|
+
# FIXME? Risky - see
|
54
|
+
# http://www.rubyinside.com/how-to-cure-nethttps-risky-default-https-behavior-4010.html
|
55
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
56
|
+
end
|
57
|
+
|
58
|
+
http.set_debug_output($stderr) if $DEBUG
|
59
|
+
SolveBio::logger.debug('API %s Request: %s' % [method.upcase, url])
|
60
|
+
|
61
|
+
request = nil
|
62
|
+
if ['POST', 'PUT', 'PATCH'].member?(method.upcase)
|
63
|
+
# FIXME? do we need to do something different for
|
64
|
+
# PUT and PATCH?
|
65
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
66
|
+
request.body = params.to_json
|
67
|
+
else
|
68
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
69
|
+
end
|
70
|
+
@headers.each { |k, v| request.add_field(k, v) }
|
71
|
+
request.add_field('Authorization', "Token #{@api_key}") if @api_key
|
72
|
+
response = http.request(request)
|
73
|
+
|
74
|
+
# FIXME: There's probably gzip decompression built in to
|
75
|
+
# net/http. Until I figure out how to get that to work, the
|
76
|
+
# below works.
|
77
|
+
case response
|
78
|
+
when Net::HTTPSuccess then
|
79
|
+
begin
|
80
|
+
if response['Content-Encoding'].eql?( 'gzip' ) then
|
81
|
+
puts "Performing gzip decompression for response body." if $DEBUG
|
82
|
+
sio = StringIO.new( response.body )
|
83
|
+
gz = Zlib::GzipReader.new( sio )
|
84
|
+
response.body = gz.read()
|
85
|
+
end
|
86
|
+
rescue Exception
|
87
|
+
puts "Error occurred (#{$!.message})" if $DEBUG
|
88
|
+
# handle errors
|
89
|
+
raise $!.message
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
status_code = response.code.to_i
|
94
|
+
if status_code < 200 or status_code >= 300
|
95
|
+
handle_api_error(response)
|
96
|
+
end
|
97
|
+
|
98
|
+
if raw
|
99
|
+
return response.body
|
100
|
+
else
|
101
|
+
return JSON.parse(response.body)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def handle_request_error(e)
|
106
|
+
# FIXME: go over this. It is still a rough translation
|
107
|
+
# from the python.
|
108
|
+
err = e.inspect
|
109
|
+
if e.kind_of?(requests.exceptions.RequestException)
|
110
|
+
msg = SolveBio::Error::Default_message
|
111
|
+
else
|
112
|
+
msg = 'Unexpected error communicating with SolveBio. ' +
|
113
|
+
"It looks like there's probably a configuration " +
|
114
|
+
'issue locally. If this problem persists, let us ' +
|
115
|
+
'know at contact@solvebio.com.'
|
116
|
+
end
|
117
|
+
msg = msg + "\n\n(Network error: #{err}"
|
118
|
+
raise SolveBio::Error.new(nil, msg)
|
119
|
+
end
|
120
|
+
|
121
|
+
def handle_api_error(response)
|
122
|
+
if [400, 401, 403, 404].member?(response.code.to_i)
|
123
|
+
raise SolveBio::Error.new(response)
|
124
|
+
else
|
125
|
+
SolveBio::logger.info("API Error: #{response.msg}")
|
126
|
+
raise SolveBio::Error.new(response)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.client
|
131
|
+
@@client ||= SolveBio::Client.new()
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.request(*args)
|
135
|
+
client.request(*args)
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
if __FILE__ == $0
|
141
|
+
puts SolveBio::Client.client.headers
|
142
|
+
puts SolveBio::Client.client.api_host
|
143
|
+
client = SolveBio::Client.new(nil, 'http://google.com')
|
144
|
+
response = client.request('http', 'http://google.com') rescue 'no good'
|
145
|
+
puts response.inspect
|
146
|
+
puts '-' * 30
|
147
|
+
response = client.request('http', 'http://www.google.com') rescue 'nope'
|
148
|
+
puts response.inspect
|
149
|
+
puts '-' * 30
|
150
|
+
response = client.request('http', 'https://www.google.com') rescue 'nope'
|
151
|
+
puts response.inspect
|
152
|
+
end
|
data/lib/credentials.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# Deals with reading netrc credentials
|
4
|
+
require_relative 'main'
|
5
|
+
require 'netrc'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
#
|
9
|
+
# Raised if the credentials are not found.
|
10
|
+
#
|
11
|
+
class CredentialsError < RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
module SolveBio::Credentials
|
15
|
+
|
16
|
+
module_function
|
17
|
+
|
18
|
+
# SolveBio API host -- just the hostname
|
19
|
+
def api_host
|
20
|
+
URI(SolveBio::API_HOST).host
|
21
|
+
end
|
22
|
+
|
23
|
+
def netrc_path
|
24
|
+
path =
|
25
|
+
if ENV['NETRC_PATH']
|
26
|
+
File.join(ENV['NETRC_PATH'], ".netrc")
|
27
|
+
else
|
28
|
+
Netrc.default_path
|
29
|
+
end
|
30
|
+
if not File.exist?(path)
|
31
|
+
raise IOError, "netrc file #{path} not found"
|
32
|
+
end
|
33
|
+
path
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Returns the tuple user / password given a path for the .netrc file.
|
38
|
+
# Raises CredentialsError if no valid netrc file is found.
|
39
|
+
#
|
40
|
+
def get_credentials
|
41
|
+
n = Netrc.read(netrc_path)
|
42
|
+
return n[api_host]
|
43
|
+
rescue Netrc::Error => e
|
44
|
+
raise CredentialsError, "Could not read .netrc file: #{e}"
|
45
|
+
end
|
46
|
+
module_function :get_credentials
|
47
|
+
|
48
|
+
def delete_credentials
|
49
|
+
n = Netrc.read(netrc_path)
|
50
|
+
n.delete(api_host)
|
51
|
+
n.save
|
52
|
+
end
|
53
|
+
|
54
|
+
def save_credentials(email, api_key)
|
55
|
+
n = Netrc.read(netrc_path)
|
56
|
+
# Overwrites any existing credentials
|
57
|
+
n[api_host] = email, api_key
|
58
|
+
n.save
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Demo code
|
63
|
+
if __FILE__ == $0
|
64
|
+
include SolveBio::Credentials
|
65
|
+
puts "authentication: #{netrc_path}"
|
66
|
+
puts "creds", get_credentials
|
67
|
+
end
|
data/lib/errors.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
require_relative 'main'
|
4
|
+
|
5
|
+
class SolveBio::Error < RuntimeError
|
6
|
+
Default_message =
|
7
|
+
'Unexpected error communicating with SolveBio. ' +
|
8
|
+
'If this problem persists, let us know at ' +
|
9
|
+
'contact@solvebio.com.'
|
10
|
+
|
11
|
+
attr_reader :json_body
|
12
|
+
attr_reader :status_code
|
13
|
+
attr_reader :message
|
14
|
+
attr_reader :field_errors
|
15
|
+
|
16
|
+
def initialize( response=nil, message=nil)
|
17
|
+
@json_body = nil
|
18
|
+
@status_code = nil
|
19
|
+
@message = message or Default_message
|
20
|
+
@field_errors = []
|
21
|
+
|
22
|
+
if response
|
23
|
+
@status_code = response.code.to_i
|
24
|
+
@message = response.message
|
25
|
+
begin
|
26
|
+
@json_body = JSON.parse(response.body)
|
27
|
+
rescue
|
28
|
+
@message = '404 Not Found.' if @status_code == 404
|
29
|
+
SolveBio.logger.debug(
|
30
|
+
"API Response (%d): No content." % @status_code)
|
31
|
+
else
|
32
|
+
SolveBio.logger.debug(
|
33
|
+
"API Response (#{@status_code}): #{@json_body}")
|
34
|
+
|
35
|
+
if [400, 401, 403, 404].member?(@status_code)
|
36
|
+
@message = 'Bad request.'
|
37
|
+
|
38
|
+
if @json_body.member?('detail')
|
39
|
+
@message = '%s' % @json_body['detail']
|
40
|
+
end
|
41
|
+
|
42
|
+
if @json_body.member?('non_field_errors')
|
43
|
+
@message = '%s.' % \
|
44
|
+
@json_body['non_field_errors'].join(', ')
|
45
|
+
end
|
46
|
+
|
47
|
+
@json_body.each do |k, v|
|
48
|
+
unless ['detail', 'non_field_errors'].member?(k)
|
49
|
+
v = v.join(', ') if v.kind_of?(Array)
|
50
|
+
@field_errors << ('%s (%s)' % [k, v])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
unless @field_errors.empty?
|
55
|
+
@message += (' The following fields were missing ' +
|
56
|
+
'or invalid: %s' %
|
57
|
+
@field_errors.join(', '))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
@message
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Demo code
|
71
|
+
if __FILE__ == $0
|
72
|
+
puts SolveBio::Error.new
|
73
|
+
puts SolveBio::Error.new(nil, 'Hi there').inspect
|
74
|
+
puts SolveBio::Error.new(nil, 'Hi there').to_s
|
75
|
+
puts SolveBio::Error.new(nil, ['Hello, ', 'again.']).inspect
|
76
|
+
|
77
|
+
require 'net/http'
|
78
|
+
response = Net::HTTPUnauthorized.new('HTTP 1.1', '404', 'No creds')
|
79
|
+
puts SolveBio::Error.new(response).to_s
|
80
|
+
|
81
|
+
end
|
data/lib/filter.rb
ADDED
@@ -0,0 +1,312 @@
|
|
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
|