bio-basespace-sdk 0.1.3 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of bio-basespace-sdk might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +479 -482
- data/Rakefile +3 -5
- data/VERSION +1 -1
- data/examples/0_app_triggering.rb +51 -62
- data/examples/1_authentication.rb +14 -99
- data/examples/2_browsing.rb +42 -44
- data/examples/3_accessing_files.rb +72 -77
- data/examples/4_app_result_upload.rb +97 -54
- data/examples/5_purchasing.rb +4 -10
- data/lib/basespace/api/api_client.rb +11 -5
- data/lib/basespace/api/base_api.rb +3 -3
- data/lib/basespace/api/basespace_api.rb +9 -9
- data/lib/basespace/model/project.rb +1 -1
- data/lib/basespace/model/sample.rb +6 -5
- metadata +6 -8
@@ -13,8 +13,8 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
|
16
|
-
# Creating an AppResult and
|
17
|
-
#
|
16
|
+
# Creating an AppResult and Uploading Files
|
17
|
+
# https://github.com/joejimbo/basespace-ruby-sdk#creating-an-appresult-and-uploading-files
|
18
18
|
|
19
19
|
require 'bio-basespace-sdk'
|
20
20
|
|
@@ -39,64 +39,107 @@ unless opts.select{|k,v| v[/^<.*>$/]}.empty?
|
|
39
39
|
exit 1 unless opts
|
40
40
|
end
|
41
41
|
|
42
|
-
#
|
42
|
+
# Initialize a BaseSpace API object:
|
43
43
|
bs_api = BaseSpaceAPI.new(opts['client_id'], opts['client_secret'], opts['basespace_url'], opts['api_version'], opts['app_session_id'], opts['access_token'])
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
#
|
48
|
-
|
45
|
+
### Creating an AppResult ###
|
46
|
+
|
47
|
+
#
|
48
|
+
# Request privileges
|
49
|
+
#
|
50
|
+
|
51
|
+
device_info = bs_api.get_verification_code('browse global')
|
52
|
+
link = device_info['verification_with_code_uri']
|
53
|
+
puts "Visit the URI within 15 seconds and grant access:"
|
54
|
+
puts link
|
55
|
+
host = RbConfig::CONFIG['host_os']
|
56
|
+
case host
|
57
|
+
when /mswin|mingw|cygwin/
|
58
|
+
system("start #{link}")
|
59
|
+
when /darwin/
|
60
|
+
system("open #{link}")
|
61
|
+
when /linux/
|
62
|
+
system("xdg-open #{link}")
|
63
|
+
end
|
64
|
+
sleep(15)
|
65
|
+
|
66
|
+
code = device_info['device_code']
|
67
|
+
bs_api.update_privileges(code)
|
68
|
+
|
69
|
+
#
|
70
|
+
# Get a project
|
71
|
+
#
|
72
|
+
|
73
|
+
# NOTE THAT YOUR PROJECT ID WILL MOST LIKELY BE DIFFERENT!
|
74
|
+
# YOU CAN GET IT VIA THE SDK OR FROM THE BASESPACE WEB INTERFACE!
|
75
|
+
# FOR EXAMPLE: my_projects.first.id
|
76
|
+
puts 'NOTE THAT YOU NEED TO UPDATE THE PROJECT ID IN THE EXAMPLE CODE TO MATCH A PROJECT OF YOURS!'
|
77
|
+
prj = bs_api.get_project_by_id('469469')
|
78
|
+
|
79
|
+
#
|
80
|
+
# List the current analyses for the project
|
81
|
+
#
|
49
82
|
|
50
|
-
# Assuming we have write access to the project
|
51
|
-
# we list the current App Results for the project.
|
52
83
|
statuses = ['Running']
|
53
|
-
app_res = prj.get_app_results(bs_api, {}, statuses)
|
54
|
-
puts "
|
55
|
-
puts
|
84
|
+
app_res = prj.get_app_results(bs_api, {}, statuses)
|
85
|
+
puts "AppResult instances: #{app_res.map { |r| r.to_s }.join(', ')}"
|
56
86
|
|
57
87
|
#
|
58
|
-
#
|
88
|
+
# Request project creation privileges
|
89
|
+
#
|
90
|
+
|
91
|
+
device_info = bs_api.get_verification_code("create project #{prj.id}")
|
92
|
+
link = device_info['verification_with_code_uri']
|
93
|
+
puts "Visit the URI within 15 seconds and grant access:"
|
94
|
+
puts link
|
95
|
+
host = RbConfig::CONFIG['host_os']
|
96
|
+
case host
|
97
|
+
when /mswin|mingw|cygwin/
|
98
|
+
system("start #{link}")
|
99
|
+
when /darwin/
|
100
|
+
system("open #{link}")
|
101
|
+
when /linux/
|
102
|
+
system("xdg-open #{link}")
|
103
|
+
end
|
104
|
+
sleep(15)
|
105
|
+
|
106
|
+
code = device_info['device_code']
|
107
|
+
bs_api.update_privileges(code)
|
108
|
+
|
109
|
+
# NOTE THAT THE APP SESSION ID OF A RUNNING APP MUST BE PROVIDED!
|
110
|
+
app_result = prj.create_app_result(bs_api, "testing", "this is my results", bs_api.app_session_id)
|
111
|
+
puts "AppResult ID: #{app_result.id}"
|
112
|
+
puts "AppResult's AppSession: #{app_result.app_session}"
|
113
|
+
|
114
|
+
#
|
115
|
+
# Change the status of our `AppSession` and add a status-summary as follows:
|
116
|
+
#
|
117
|
+
|
118
|
+
app_result.app_session.set_status(bs_api, 'needsattention', "We worked hard, but encountered some trouble.")
|
119
|
+
|
120
|
+
# Updated status:
|
121
|
+
puts "AppResult's AppSession: #{app_result.app_session}"
|
122
|
+
|
123
|
+
# Set back to running:
|
124
|
+
app_result.app_session.set_status(bs_api, 'running', "Back on track")
|
125
|
+
|
126
|
+
### Uploading Files ###
|
127
|
+
|
128
|
+
#
|
129
|
+
# Attach a file to the `AppResult` object and upload it:
|
130
|
+
#
|
131
|
+
|
132
|
+
puts 'NOTE: THIS ASSUMES A FILE /tmp/testFile.txt IN YOUR FILESYSTEM!'
|
133
|
+
app_result.upload_file(bs_api, '/tmp/testFile.txt', 'BaseSpaceTestFile.txt', '/mydir/', 'text/plain')
|
134
|
+
|
135
|
+
# Let's see if our new file made it into the cloud:
|
136
|
+
app_result_files = app_result.get_files(bs_api)
|
137
|
+
puts "Files: #{app_result_files.map { |f| f.to_s }.join(', ')}"
|
138
|
+
|
59
139
|
#
|
140
|
+
# Download our newly uploaded file (will be saved as BaseSpaceTestFile.txt):
|
60
141
|
|
61
|
-
|
62
|
-
|
63
|
-
puts
|
64
|
-
puts app_results
|
65
|
-
puts app_results.id
|
66
|
-
puts
|
67
|
-
puts "The app results also comes with a reference to our AppSession"
|
68
|
-
my_app_session = app_results.app_session
|
69
|
-
puts my_app_session
|
70
|
-
puts
|
71
|
-
|
72
|
-
# We can change the status of our AppSession and add a status-summary as follows.
|
73
|
-
my_app_session.set_status(bs_api, 'needsattention', "We worked hard, but encountered some trouble.")
|
74
|
-
puts "After a change of status of the app sessions we get #{my_app_session}"
|
75
|
-
puts
|
76
|
-
# We set our appSession back to running so we can do some more work.
|
77
|
-
my_app_session.set_status(bs_api, 'running', "Back on track")
|
78
|
-
|
79
|
-
|
80
|
-
# Let's list all AppResults again and see if our new object shows up.
|
81
|
-
app_res = prj.get_app_results(bs_api, {}, ['Running'])
|
82
|
-
puts "The updated app results are #{app_res}"
|
83
|
-
app_result2 = bs_api.get_app_result_by_id(app_results.id)
|
84
|
-
puts app_result2
|
85
|
-
puts
|
86
|
-
|
87
|
-
# Now we will make another AppResult and try to upload a file to it
|
88
|
-
app_results2 = prj.create_app_result(bs_api, "My second AppResult", "This one I will upload to")
|
89
|
-
app_results2.upload_file(bs_api, '/home/mkallberg/Desktop/testFile2.txt', 'BaseSpaceTestFile.txt', '/mydir/', 'text/plain')
|
90
|
-
puts "My AppResult number 2 #{app_results2}"
|
91
|
-
puts
|
92
|
-
|
93
|
-
# Let's see if our new file made it.
|
94
|
-
app_result_files = app_results2.get_files(bs_api)
|
95
|
-
puts "These are the files in the appResult"
|
96
|
-
puts app_result_files
|
97
|
-
f = app_result_files.last
|
98
|
-
|
99
|
-
# We can even download our newly uploaded file.
|
100
|
-
f = bs_api.get_file_by_id(f.id)
|
101
|
-
f.download_file(bs_api, '/home/mkallberg/Desktop/')
|
142
|
+
f = bs_api.get_file_by_id(app_result_files.last.id)
|
143
|
+
f.download_file(bs_api, '/tmp/')
|
144
|
+
puts 'NOTE: downloaded \'BaseSpaceTestFile.txt\' into the /tmp directory.'
|
102
145
|
|
data/examples/5_purchasing.rb
CHANGED
@@ -13,19 +13,13 @@
|
|
13
13
|
# See the License for the specific language governing permissions and
|
14
14
|
# limitations under the License.
|
15
15
|
|
16
|
+
# Purchasing
|
17
|
+
# --- Sorry, we have not documented this yet. ---
|
18
|
+
|
16
19
|
require 'bio-basespace-sdk'
|
17
20
|
|
18
21
|
include Bio::BaseSpace
|
19
22
|
|
20
|
-
# This example demonstrates the billing methods of BaseSpace.
|
21
|
-
#
|
22
|
-
# Below a purchase is created, requiring the user to click 'Purchase' in a web
|
23
|
-
# browser. The purchase is then refunded, and the purchase is again retrieved
|
24
|
-
# via the API using the purchase id and tags, which are used by developers
|
25
|
-
# to help clarify exactly what was purchased.
|
26
|
-
#
|
27
|
-
# NOTE You will need to fill client values for your app below!
|
28
|
-
|
29
23
|
opts = {
|
30
24
|
# FILL IN WITH YOUR APP VALUES HERE!
|
31
25
|
'client_id' => '<your client key>', # from dev portal app Credentials tab
|
@@ -48,7 +42,7 @@ unless opts['client_id'] or opts['product_id']
|
|
48
42
|
raise "Please fill in client values (in the script) before running the script"
|
49
43
|
end
|
50
44
|
|
51
|
-
#
|
45
|
+
# Initialize a BaseSpace API object:
|
52
46
|
bill_api = BillingAPI.new(opts['basespace_store_url'], opts['api_version'], opts['app_session_id'], opts['access_token'])
|
53
47
|
|
54
48
|
# Create a non-consumable purchase.
|
@@ -153,7 +153,7 @@ class APIClient
|
|
153
153
|
# [TODO] confirm this works or not
|
154
154
|
#request = urllib2.Request(url, headers)
|
155
155
|
uri = URI.parse(url)
|
156
|
-
request = Net::HTTP::Get.new(uri
|
156
|
+
request = Net::HTTP::Get.new(uri, headers)
|
157
157
|
when 'POST', 'PUT', 'DELETE'
|
158
158
|
if cgi_params
|
159
159
|
force_post_url = url
|
@@ -161,16 +161,22 @@ class APIClient
|
|
161
161
|
end
|
162
162
|
if post_data
|
163
163
|
# [TODO] Do we need to skip String, Integer, Float and bool in Ruby?
|
164
|
-
data = post_data
|
164
|
+
data = post_data # if not [str, int, float, bool].include?(type(post_data))
|
165
165
|
end
|
166
166
|
if force_post
|
167
167
|
response = force_post_call(force_post_url, sent_query_params, headers)
|
168
168
|
else
|
169
|
-
data =
|
169
|
+
data = {} if not data or (data and data.empty?) # temp fix, in case is no data in the file, to prevent post request from failing
|
170
170
|
# [TODO] confirm this works or not
|
171
171
|
#request = urllib2.Request(url, headers, data)#, @timeout)
|
172
172
|
uri = URI.parse(url)
|
173
|
-
request = Net::HTTP::Post.new(uri
|
173
|
+
request = Net::HTTP::Post.new(uri, headers)
|
174
|
+
if data.kind_of?(Hash) then
|
175
|
+
request.set_form_data(data)
|
176
|
+
else
|
177
|
+
request.content_type = 'multipart/form-data'
|
178
|
+
request.body = data
|
179
|
+
end
|
174
180
|
end
|
175
181
|
if ['PUT', 'DELETE'].include?(method) # urllib doesnt do put and delete, default to pycurl here
|
176
182
|
response = put_call(url, query_params, headers, data)
|
@@ -237,7 +243,7 @@ class APIClient
|
|
237
243
|
instance = File.new
|
238
244
|
else
|
239
245
|
# models in BaseSpace
|
240
|
-
klass =
|
246
|
+
klass = ::Bio::BaseSpace.const_get(obj_class)
|
241
247
|
instance = klass.new
|
242
248
|
end
|
243
249
|
|
@@ -140,13 +140,13 @@ class BaseAPI
|
|
140
140
|
# convert list response dict to object type
|
141
141
|
# TODO check that Response is present -- errors sometime don't include
|
142
142
|
# [TODO] check why the Python SDK only uses the first element in the response_objects
|
143
|
-
|
143
|
+
converted = []
|
144
144
|
if response_object = response_objects.first
|
145
145
|
response_object.convert_to_object_list.each do |c|
|
146
|
-
|
146
|
+
converted << @api_client.deserialize(c, my_model)
|
147
147
|
end
|
148
148
|
end
|
149
|
-
return
|
149
|
+
return converted
|
150
150
|
end
|
151
151
|
|
152
152
|
# URL encode a Hash of data values.
|
@@ -162,7 +162,7 @@ class BaseSpaceAPI < BaseAPI
|
|
162
162
|
#
|
163
163
|
# +obj+:: The data object we wish to get access to.
|
164
164
|
# +access_type+:: The type of access (read|write), default is write.
|
165
|
-
# +web+::
|
165
|
+
# +web+:: If the App is web-based, then set this parameter to 'true'. The default value is false, which means that the request is for a device based App.
|
166
166
|
# +redirect_url+:: For the web-based case, a redirection URL.
|
167
167
|
# +state+:: (unclear from Python port)
|
168
168
|
def get_access(obj, access_type = 'write', web = nil, redirect_url = nil, state = nil)
|
@@ -188,7 +188,7 @@ class BaseSpaceAPI < BaseAPI
|
|
188
188
|
#curlCall = 'curl -d "response_type=device_code" -d "client_id=' + @key + '" -d "scope=' + scope + '" ' + @api_server + DEVICE_URL
|
189
189
|
#puts curlCall
|
190
190
|
unless @key
|
191
|
-
raise "This BaseSpaceAPI instance has no client_secret (key) set and no alternative id was supplied for method get_verification_code"
|
191
|
+
raise "This BaseSpaceAPI instance has no client_secret (key) set and no alternative id was supplied for method get_verification_code."
|
192
192
|
end
|
193
193
|
data = {'client_id' => @key, 'scope' => scope, 'response_type' => 'device_code'}
|
194
194
|
return make_curl_request(data, @api_server + DEVICE_URL)
|
@@ -201,7 +201,7 @@ class BaseSpaceAPI < BaseAPI
|
|
201
201
|
# +state+:: An optional state parameter that will passed through to the redirect response.
|
202
202
|
def get_web_verification_code(scope, redirect_url, state = nil)
|
203
203
|
if (not @key)
|
204
|
-
raise "This BaseSpaceAPI instance has no client_id (key) set and no alternative id was supplied for method get_verification_code"
|
204
|
+
raise "This BaseSpaceAPI instance has no client_id (key) set and no alternative id was supplied for method get_verification_code."
|
205
205
|
end
|
206
206
|
data = {'client_id' => @key, 'redirect_uri' => redirect_url, 'scope' => scope, 'response_type' => 'code', "state" => state}
|
207
207
|
return @weburl + WEB_AUTHORIZE + '?' + hash2urlencode(data)
|
@@ -212,7 +212,7 @@ class BaseSpaceAPI < BaseAPI
|
|
212
212
|
# +device_code+:: The device code returned by the verification code method.
|
213
213
|
def obtain_access_token(device_code)
|
214
214
|
if (not @key) or (not @secret)
|
215
|
-
raise "This BaseSpaceAPI instance has either no client_secret or no client_id set and no alternative id was supplied for method get_verification_code"
|
215
|
+
raise "This BaseSpaceAPI instance has either no client_secret or no client_id set and no alternative id was supplied for method get_verification_code."
|
216
216
|
end
|
217
217
|
data = {'client_id' => @key, 'client_secret' => @secret, 'code' => device_code, 'grant_type' => 'device', 'redirect_uri' => 'google.com'}
|
218
218
|
dict = make_curl_request(data, @api_server + TOKEN_URL)
|
@@ -584,7 +584,7 @@ class BaseSpaceAPI < BaseAPI
|
|
584
584
|
sid = query_params['appsessionid']
|
585
585
|
session = get_app_session(sid)
|
586
586
|
unless session.can_work_on
|
587
|
-
raise 'AppSession status must be "running," to create and AppResults. Current status is ' + session.status
|
587
|
+
raise 'AppSession status must be "running," to create and AppResults. Current status is: ' + session.status
|
588
588
|
end
|
589
589
|
end
|
590
590
|
|
@@ -628,7 +628,7 @@ class BaseSpaceAPI < BaseAPI
|
|
628
628
|
# Set force post as this need to use POST though no data is being streamed
|
629
629
|
return single_request(my_model, resource_path, method, query_params, header_params, post_data, verbose, force_post)
|
630
630
|
else
|
631
|
-
post_data = File.open(local_path).read
|
631
|
+
post_data = ::File.open(local_path).read
|
632
632
|
return single_request(my_model, resource_path, method, query_params, header_params, post_data, verbose)
|
633
633
|
end
|
634
634
|
end
|
@@ -664,15 +664,15 @@ class BaseSpaceAPI < BaseAPI
|
|
664
664
|
end
|
665
665
|
|
666
666
|
# Do the download
|
667
|
-
File.open(File.join(local_dir, name), "wb") do |fp|
|
667
|
+
::File.open(::File.join(local_dir, name), "wb") do |fp|
|
668
668
|
http_opts = {}
|
669
|
+
uri = URI.parse(file_url)
|
669
670
|
if uri.scheme == "https"
|
670
671
|
http_opts[:use_ssl] = true
|
671
672
|
end
|
672
|
-
uri = URI.parse(file_url)
|
673
673
|
res = Net::HTTP.start(uri.host, uri.port, http_opts) { |http|
|
674
674
|
# [TODO] Do we need user and pass here also?
|
675
|
-
http.get(uri
|
675
|
+
http.get(uri, header)
|
676
676
|
}
|
677
677
|
fp.print res.body
|
678
678
|
end
|
@@ -57,7 +57,7 @@ class Project < Model
|
|
57
57
|
raise ModelNotInitializedError.new('The project model has not been initialized yet') unless get_attr('Id')
|
58
58
|
end
|
59
59
|
|
60
|
-
# Returns the scope-string to used for requesting BaseSpace access to the object.
|
60
|
+
# Returns the scope-string to be used for requesting BaseSpace access to the object.
|
61
61
|
#
|
62
62
|
# +scope+:: The scope-type that is requested (write|read).
|
63
63
|
def get_access_str(scope = 'write')
|
@@ -28,8 +28,8 @@ class Sample < Model
|
|
28
28
|
'SampleNumber' => 'int',
|
29
29
|
'ExperimentName' => 'str',
|
30
30
|
'HrefFiles' => 'str',
|
31
|
-
|
32
|
-
'IsPairedEnd' => '
|
31
|
+
'AppSession' => 'dict',
|
32
|
+
'IsPairedEnd' => 'bool',
|
33
33
|
'Read1' => 'int',
|
34
34
|
'Read2' => 'int',
|
35
35
|
'NumReadsRaw' => 'int',
|
@@ -42,12 +42,13 @@ class Sample < Model
|
|
42
42
|
'Status' => 'str',
|
43
43
|
'StatusSummary' => 'str',
|
44
44
|
'DateCreated' => 'datetime',
|
45
|
-
'References' => 'dict',
|
45
|
+
'References' => 'dict', # NOTE Is this correct? Because references is a list.
|
46
46
|
'Run' => 'RunCompact',
|
47
47
|
}
|
48
48
|
@attributes = {
|
49
49
|
'Name' => nil, # str
|
50
50
|
'HrefFiles' => nil, # str
|
51
|
+
'AppSession' => nil, # dict
|
51
52
|
'DateCreated' => nil, # datetime
|
52
53
|
'SampleNumber' => nil, # int
|
53
54
|
'Id' => nil, # str
|
@@ -56,7 +57,7 @@ class Sample < Model
|
|
56
57
|
'ExperimentName' => nil, # str
|
57
58
|
'Run' => nil, # RunCompact
|
58
59
|
'HrefGenome' => nil, # str
|
59
|
-
'IsPairedEnd' => nil, #
|
60
|
+
'IsPairedEnd' => nil, # bool
|
60
61
|
'Read1' => nil, # int
|
61
62
|
'Read2' => nil, # int
|
62
63
|
'NumReadsRaw' => nil, # int
|
@@ -95,7 +96,7 @@ class Sample < Model
|
|
95
96
|
# Return the AppResults referenced by this sample.
|
96
97
|
#
|
97
98
|
# Note: the returned AppResult objects do not have their "References" field set,
|
98
|
-
# to get a fully populate AppResult object you must use
|
99
|
+
# to get a fully populate AppResult object you must use get_app_result_by_id in BaseSpaceAPI.
|
99
100
|
#
|
100
101
|
# +api+:: BaseSpaceAPI instance.
|
101
102
|
def get_referenced_app_results(api)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bio-basespace-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joachim Baran
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date: 2013-07-
|
15
|
+
date: 2013-07-29 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: getopt
|
@@ -113,11 +113,9 @@ dependencies:
|
|
113
113
|
- !ruby/object:Gem::Version
|
114
114
|
version: '3.12'
|
115
115
|
description: |-
|
116
|
-
|
116
|
+
BaseSpace Ruby SDK is a Ruby based Software Development Kit to be used in the development of Apps and scripts for working with Illumina's BaseSpace cloud-computing solution for next-gen sequencing data analysis.
|
117
117
|
|
118
118
|
The primary purpose of the SDK is to provide an easy-to-use Ruby environment enabling developers to authenticate a user, retrieve data, and upload data/results from their own analysis to BaseSpace.
|
119
|
-
|
120
|
-
If you haven't already done so, you may wish to familiarize yourself with the general BaseSpace developers documentation (https://developer.basespace.illumina.com/) and create a new BaseSpace App to be used when working through the examples provided in 'examples' folder.
|
121
119
|
email: joachim.baran@gmail.com
|
122
120
|
executables: []
|
123
121
|
extensions: []
|
@@ -211,7 +209,7 @@ rubyforge_project:
|
|
211
209
|
rubygems_version: 2.0.5
|
212
210
|
signing_key:
|
213
211
|
specification_version: 4
|
214
|
-
summary:
|
215
|
-
and scripts for working with Illumina's BaseSpace cloud-computing
|
216
|
-
sequencing data analysis.
|
212
|
+
summary: BaseSpace Ruby SDK is a Ruby based Software Development Kit to be used in
|
213
|
+
the development of Apps and scripts for working with Illumina's BaseSpace cloud-computing
|
214
|
+
solution for next-gen sequencing data analysis.
|
217
215
|
test_files: []
|