sequenceserver 0.7.9 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/sequenceserver.rb +55 -57
- data/lib/sequenceserver/blast.rb +51 -158
- data/lib/sequenceserver/database.rb +8 -2
- data/lib/sequenceserver/helpers.rb +14 -12
- data/lib/sequenceserver/options.rb +132 -0
- data/lib/sequenceserver/sequencehelpers.rb +4 -30
- data/public/css/bootstrap.dropdown.css +29 -0
- data/public/css/bootstrap.icons.css +155 -0
- data/public/css/bootstrap.min.css +399 -314
- data/public/css/bootstrap.modal.css +28 -0
- data/public/css/custom.css +183 -23
- data/public/img/glyphicons-halflings-white.png +0 -0
- data/public/img/glyphicons-halflings.png +0 -0
- data/public/js/bootstrap-typeahead.js +298 -0
- data/public/js/bootstrap.dropdown.js +92 -0
- data/public/js/bootstrap.modal.js +7 -0
- data/public/js/bootstrap.transition.js +7 -0
- data/public/js/jquery.index.js +67 -0
- data/public/js/jquery.scrollspy.js +74 -0
- data/public/js/jquery.ui.js +57 -0
- data/public/js/sequenceserver.blast.js +96 -37
- data/public/js/sequenceserver.js +157 -85
- data/public/js/store.min.js +2 -0
- data/public/js/yaml.js +24 -0
- data/sequenceserver.gemspec +5 -2
- data/tests/run +26 -0
- data/tests/test_sequencehelpers.rb +10 -18
- data/tests/test_sequenceserver_blast.rb +60 -0
- data/views/_options.erb +144 -0
- data/views/search.erb +133 -145
- metadata +135 -52
data/lib/sequenceserver.rb
CHANGED
@@ -93,8 +93,7 @@ module SequenceServer
|
|
93
93
|
# blast methods (executables) and their corresponding absolute path
|
94
94
|
set :binaries, {}
|
95
95
|
|
96
|
-
# list of
|
97
|
-
# 'protein', or 'nucleotide'
|
96
|
+
# list of blast databases indexed by their hash value
|
98
97
|
set :databases, {}
|
99
98
|
end
|
100
99
|
|
@@ -186,10 +185,8 @@ module SequenceServer
|
|
186
185
|
self.databases = scan_blast_db(database, binaries['blastdbcmd']).freeze
|
187
186
|
|
188
187
|
# Log the discovery of databases.
|
189
|
-
databases.each do |
|
190
|
-
|
191
|
-
log.info("Found #{type} database: #{d.title} at #{d.name}")
|
192
|
-
end
|
188
|
+
databases.each do |id, database|
|
189
|
+
log.info("Found #{database.type} database: #{database.title} at #{database.name}")
|
193
190
|
end
|
194
191
|
rescue IOError => error
|
195
192
|
log.fatal("Fail: #{error}")
|
@@ -221,48 +218,60 @@ module SequenceServer
|
|
221
218
|
erb :search
|
222
219
|
end
|
223
220
|
|
224
|
-
|
225
|
-
|
226
|
-
db_type_param = params['db']
|
227
|
-
sequence = params[:sequence]
|
221
|
+
before '/' do
|
222
|
+
pass if params.empty?
|
228
223
|
|
229
|
-
#
|
230
|
-
|
224
|
+
# ensure required params present
|
225
|
+
#
|
226
|
+
# If a required parameter is missing, SequnceServer returns 'Bad Request
|
227
|
+
# (400)' error.
|
228
|
+
#
|
229
|
+
# See Twitter's [Error Codes & Responses][1] page for reference.
|
230
|
+
#
|
231
|
+
# [1]: https://dev.twitter.com/docs/error-codes-responses
|
231
232
|
|
232
|
-
|
233
|
-
|
234
|
-
raise ArgumentError, "No BLAST database selected"
|
233
|
+
if params[:method].nil? or params[:method].empty?
|
234
|
+
halt 400, "No BLAST method provided."
|
235
235
|
end
|
236
|
-
db_type = db_type_param.first.first
|
237
236
|
|
238
|
-
|
239
|
-
|
240
|
-
|
237
|
+
if params[:sequence].nil? or params[:sequence].empty?
|
238
|
+
halt 400, "No input sequence provided."
|
239
|
+
end
|
241
240
|
|
242
|
-
|
243
|
-
|
244
|
-
|
241
|
+
if params[:databases].nil?
|
242
|
+
halt 400, "No BLAST database provided."
|
243
|
+
end
|
245
244
|
|
246
|
-
#
|
247
|
-
sequence_type = type_of_sequences(sequence)
|
248
|
-
settings.log.debug('sequence: ' + sequence)
|
249
|
-
settings.log.debug('input seq type: ' + sequence_type.to_s)
|
250
|
-
settings.log.debug('blast db type: ' + db_type)
|
251
|
-
settings.log.debug('blast method: ' + method)
|
245
|
+
# ensure params are valid #
|
252
246
|
|
253
|
-
|
254
|
-
|
247
|
+
# only allowed blast methods should be used
|
248
|
+
blast_methods = %w|blastn blastp blastx tblastn tblastx|
|
249
|
+
unless blast_methods.include?(params[:method])
|
250
|
+
halt 400, "Unknown BLAST method: #{params[:method]}."
|
255
251
|
end
|
256
252
|
|
257
|
-
# check
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
253
|
+
# check the advanced options are sensible
|
254
|
+
begin #FIXME
|
255
|
+
validate_advanced_parameters(params[:advanced])
|
256
|
+
rescue ArgumentError => error
|
257
|
+
halt 400, "Advanced parameters invalid: #{error}"
|
262
258
|
end
|
263
259
|
|
260
|
+
# log params
|
261
|
+
settings.log.debug('method: ' + params[:method])
|
262
|
+
settings.log.debug('sequence: ' + params[:sequence])
|
263
|
+
settings.log.debug('database: ' + params[:databases].inspect)
|
264
|
+
settings.log.debug('advanced: ' + params[:advanced])
|
265
|
+
end
|
266
|
+
|
267
|
+
post '/' do
|
268
|
+
method = params['method']
|
269
|
+
databases = params[:databases]
|
270
|
+
sequence = params[:sequence]
|
264
271
|
advanced_opts = params['advanced']
|
265
|
-
|
272
|
+
|
273
|
+
# evaluate empty sequence as nil, otherwise as fasta
|
274
|
+
sequence = sequence.empty? ? nil : to_fasta(sequence)
|
266
275
|
|
267
276
|
# blastn implies blastn, not megablast; but let's not interfere if a user
|
268
277
|
# specifies `task` herself
|
@@ -270,29 +279,21 @@ module SequenceServer
|
|
270
279
|
advanced_opts << ' -task blastn '
|
271
280
|
end
|
272
281
|
|
273
|
-
method
|
274
|
-
|
275
|
-
|
276
|
-
settings.databases[db_type][index.to_i].name
|
282
|
+
method = settings.binaries[ method ]
|
283
|
+
databases = params[:databases].map{|index|
|
284
|
+
settings.databases[index].name
|
277
285
|
}
|
278
286
|
|
279
|
-
# run blast
|
280
|
-
blast = Blast.
|
281
|
-
|
282
|
-
settings.log.info('Ran
|
283
|
-
|
284
|
-
# convert blast archive to HTML version
|
285
|
-
blast.convert_blast_archive_to_html_result(settings.binaries['blast_formatter'])
|
286
|
-
# log the command that was run
|
287
|
-
settings.log.info('Ran to get HTML output: ' + blast.command) if settings.logging
|
287
|
+
# run blast and log
|
288
|
+
blast = Blast.new(method, sequence, databases.join(' '), advanced_opts)
|
289
|
+
blast.run!
|
290
|
+
settings.log.info('Ran: ' + blast.command)
|
288
291
|
|
289
|
-
|
290
|
-
|
291
|
-
if request.xhr?
|
292
|
-
return @blast
|
292
|
+
unless blast.success?
|
293
|
+
halt *blast.error
|
293
294
|
end
|
294
295
|
|
295
|
-
|
296
|
+
format_blast_results(blast.result, databases)
|
296
297
|
end
|
297
298
|
|
298
299
|
# get '/get_sequence/?id=sequence_ids&db=retreival_databases'
|
@@ -372,9 +373,6 @@ HEADER
|
|
372
373
|
end
|
373
374
|
|
374
375
|
def format_blast_results(result, databases)
|
375
|
-
raise ArgumentError, 'Problem: empty result! Maybe your query was invalid?' if !result.class == String
|
376
|
-
raise ArgumentError, 'Problem: empty result! Maybe your query was invalid?' if result.empty?
|
377
|
-
|
378
376
|
formatted_result = ''
|
379
377
|
@all_retrievable_ids = []
|
380
378
|
string_of_used_databases = databases.join(' ')
|
data/lib/sequenceserver/blast.rb
CHANGED
@@ -1,200 +1,93 @@
|
|
1
1
|
require 'tempfile'
|
2
|
-
require 'open3'
|
3
2
|
|
4
3
|
module SequenceServer
|
5
|
-
# Simple
|
4
|
+
# Simple BLAST+ wrapper.
|
6
5
|
class Blast
|
7
|
-
# blast method
|
8
|
-
attr_accessor :method
|
9
6
|
|
10
|
-
|
11
|
-
attr_accessor :db
|
12
|
-
|
13
|
-
# query sequence string
|
14
|
-
attr_accessor :qstring
|
15
|
-
|
16
|
-
# query file name
|
17
|
-
attr_accessor :qfile
|
18
|
-
|
19
|
-
# advanced blast options
|
20
|
-
attr_accessor :options
|
7
|
+
ERROR_LINE = /\(CArgException.*\)\s(.*)/
|
21
8
|
|
22
9
|
# command string to be executed
|
23
|
-
attr_reader
|
10
|
+
attr_reader :command
|
24
11
|
|
25
12
|
# result of executing command
|
26
|
-
attr_reader
|
13
|
+
attr_reader :result
|
27
14
|
|
28
|
-
#
|
29
|
-
attr_reader
|
30
|
-
|
31
|
-
# errors if any while executing command
|
32
|
-
attr_reader :error
|
15
|
+
# errors as [status, message]
|
16
|
+
attr_reader :error
|
33
17
|
|
34
18
|
# Initialize a new blast search.
|
35
19
|
# ---
|
36
|
-
# Arguments
|
37
|
-
# * method(String)
|
38
|
-
# *
|
39
|
-
# *
|
40
|
-
#
|
41
|
-
# In the query Hash, use:
|
42
|
-
# * :qfile(String) - to run Blast against a file.
|
43
|
-
# * :qstrin(String) - to run Blast against a string.
|
44
|
-
# * :options(String) - to specify multiple blast options.
|
20
|
+
# Arguments:
|
21
|
+
# * method (String) - blast executable (shell executable, or absolute path)
|
22
|
+
# * query (String) - query string
|
23
|
+
# * databases (String) - database name as returned by 'blastdbcmd -list'
|
24
|
+
# * options (String) - other options
|
45
25
|
#
|
46
|
-
# Either :qfile, or :qstring should be used. If both are given, by design
|
47
|
-
# :qstring will be used to run blast.
|
48
26
|
# ---
|
49
27
|
# Examples:
|
50
28
|
#
|
51
|
-
# b = Blast.new("blastn", "S.cdna.fasta",
|
52
|
-
# b = Blast.new("blastn", "S.cdna.fasta", :qstring => 'ATGTCCGCGAATCGATTGAACGTGCTGGTGACCCTGATGCTCGCCGTCGCGCTTCTTGTG')
|
29
|
+
# b = Blast.new("blastn", 'ATGTCCGCGAATCGATTGAACGTGCTGGTGACCCTGATGCTCGCCGTCGCGCTTCTTGTG', "S.cdna.fasta", "-html -num_threads 4")
|
53
30
|
#
|
54
31
|
# b.run! => true
|
55
32
|
# b.result => "blast output"
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
@
|
65
|
-
|
66
|
-
|
67
|
-
@options =
|
33
|
+
def initialize(method, query, databases, options = nil)
|
34
|
+
@method = method
|
35
|
+
@databases = databases
|
36
|
+
|
37
|
+
# create a tempfile for the given query
|
38
|
+
qfile = Tempfile.new('sequenceserver_query')
|
39
|
+
qfile.puts(query)
|
40
|
+
qfile.close
|
41
|
+
@query = qfile.path
|
42
|
+
|
43
|
+
# Add -outfmt 11 to list of options so that it outputs a blast archive
|
44
|
+
@options = options.to_s
|
45
|
+
@options += " -html"
|
68
46
|
end
|
69
47
|
|
70
48
|
# Run blast everytime it is called. Returns the success
|
71
|
-
# status - true, or false.
|
72
|
-
# need to be set before calling this method, else blast will fail.
|
73
|
-
#
|
74
|
-
# b = Blast.new
|
75
|
-
# b.run! => false
|
76
|
-
#
|
77
|
-
# # set blast method, and db
|
78
|
-
# b.method = 'blastn'
|
79
|
-
# b.db = 'S.cdna.fasta'
|
80
|
-
#
|
81
|
-
# b.run! => false
|
82
|
-
# b.errors => "blast error output"
|
83
|
-
#
|
84
|
-
# # set qfile
|
85
|
-
# b.qfile = 'query1.seq'
|
86
|
-
#
|
87
|
-
# b.run! => true
|
88
|
-
# b.reuslt => "blast output"
|
49
|
+
# status - true, or false.
|
89
50
|
def run!
|
90
|
-
|
91
|
-
return false unless @method
|
92
|
-
|
93
|
-
# create a tempfile if qstring is given
|
94
|
-
if @qstring
|
95
|
-
@tempfile = Tempfile.new('qfile')
|
96
|
-
@tempfile.puts(qstring)
|
97
|
-
@tempfile.close
|
98
|
-
@qfile = @tempfile.path
|
99
|
-
end
|
51
|
+
@result, @error, status = execute(command)
|
100
52
|
|
101
|
-
|
102
|
-
@command = to_s
|
53
|
+
status == 0 and return @success = true
|
103
54
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
@error =
|
55
|
+
if status == 1
|
56
|
+
message = @error.each{|l| l.match(ERROR_LINE) and break Regexp.last_match[1]}
|
57
|
+
message = message || @error
|
58
|
+
@error = [400, message]
|
59
|
+
else
|
60
|
+
@error = [500, @error]
|
108
61
|
end
|
109
62
|
|
110
|
-
|
111
|
-
return @success = @error.empty?
|
112
|
-
|
113
|
-
ensure
|
114
|
-
# delete tempfile if it was created
|
115
|
-
@tempfile.unlink if @tempfile
|
63
|
+
false
|
116
64
|
end
|
117
65
|
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
# b.type => 'blastn'
|
122
|
-
def type
|
123
|
-
@type ||= @method[(@method.rindex('/') + 1)..-1]
|
66
|
+
# The command that will be executed.
|
67
|
+
def command
|
68
|
+
@command ||= "#@method -db '#@databases' -query '#@query' #@options"
|
124
69
|
end
|
125
70
|
|
126
|
-
# Return success status
|
127
|
-
# 'nil' implies that blast has not been run yet.
|
71
|
+
# Return success status.
|
128
72
|
def success?
|
129
73
|
@success
|
130
74
|
end
|
131
75
|
|
132
|
-
|
133
|
-
# the command to be executed.
|
134
|
-
def to_s
|
135
|
-
s = "#@method "
|
136
|
-
s << "-db '#@db' " if @db
|
137
|
-
s << "-query #@qfile " if @qfile
|
138
|
-
s << @options.to_s if @options
|
139
|
-
s
|
140
|
-
end
|
76
|
+
private
|
141
77
|
|
142
|
-
#
|
143
|
-
def
|
144
|
-
|
145
|
-
|
146
|
-
|
78
|
+
# Execute a command and return its stdout, stderr, and exit status.
|
79
|
+
def execute(command)
|
80
|
+
rfile = Tempfile.new('sequenceserver_result')
|
81
|
+
efile = Tempfile.new('sequenceserver_error')
|
82
|
+
[rfile, efile].each {|file| file.close}
|
147
83
|
|
148
|
-
|
149
|
-
|
150
|
-
@blast_archive_tempfile = Tempfile.open('seqserve_formatter')
|
151
|
-
|
152
|
-
# Add -outfmt 11 to list of options so that it outputs a blast archive
|
153
|
-
@options ||= ''
|
154
|
-
@options += " -outfmt 11 -out #{@blast_archive_tempfile.path}"
|
84
|
+
system("#{command} > #{rfile.path} 2> #{efile.path}")
|
85
|
+
status = $?.exitstatus
|
155
86
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
# convert the blast archive to a regular HTML result, stored
|
162
|
-
# as an instance variable Blast#result
|
163
|
-
def convert_blast_archive_to_html_result(blast_formatter_path)
|
164
|
-
@command = "#{blast_formatter_path} -archive #{blast_archive_tempfile.path} -html"
|
165
|
-
|
166
|
-
# execute command and capture both stdout, and stderr
|
167
|
-
Open3.popen3(@command) do |stdin, stdout, stderr|
|
168
|
-
@result = stdout.readlines # convert to string?
|
169
|
-
@error = stderr.readlines
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
class << self
|
174
|
-
# shortcut method to run blast against a query file
|
175
|
-
def blast_file(method, db, qfile, options = nil)
|
176
|
-
b = Blast.new(method, db, :qfile => qfile, :options => options)
|
177
|
-
b.run!
|
178
|
-
b
|
179
|
-
end
|
180
|
-
|
181
|
-
# shortcut method to run blast against a query string
|
182
|
-
def blast_string(method, db, qstring, options = nil)
|
183
|
-
b = Blast.new(method, db, :qstring => qstring, :options => options)
|
184
|
-
b.run!
|
185
|
-
b
|
186
|
-
end
|
187
|
-
|
188
|
-
# shortcut method to run blast with a query string and return a
|
189
|
-
# blast archive, which can then be further processed into other useful
|
190
|
-
# output forms (e.g. HTML, GFF). If it ran successfully, the blast archive
|
191
|
-
# is a Tempfile accessible as an instance variable of the returned
|
192
|
-
# Blast object.
|
193
|
-
def blast_string_to_blast_archive(method, db, qstring, options = nil)
|
194
|
-
b = Blast.new(method, db, :qstring => qstring, :options => options)
|
195
|
-
b.run_to_blast_archive!
|
196
|
-
b
|
197
|
-
end
|
87
|
+
return File.readlines(rfile.path), File.readlines(efile.path), status
|
88
|
+
ensure
|
89
|
+
rfile.unlink
|
90
|
+
efile.unlink
|
198
91
|
end
|
199
92
|
end
|
200
93
|
end
|
@@ -1,7 +1,9 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
1
3
|
module SequenceServer
|
2
|
-
class Database < Struct.new("Database", :name, :title)
|
4
|
+
class Database < Struct.new("Database", :name, :title, :type)
|
3
5
|
def to_s
|
4
|
-
"#{title} #{name}"
|
6
|
+
"#{type}: #{title} #{name}"
|
5
7
|
end
|
6
8
|
|
7
9
|
# Its not very meaningful to compare Database objects, however,
|
@@ -19,5 +21,9 @@ module SequenceServer
|
|
19
21
|
self.name <=> other.name
|
20
22
|
end
|
21
23
|
end
|
24
|
+
|
25
|
+
def hash
|
26
|
+
@hash ||= Digest::MD5.hexdigest(self.name)
|
27
|
+
end
|
22
28
|
end
|
23
29
|
end
|
@@ -92,29 +92,21 @@ module SequenceServer
|
|
92
92
|
db_list.each_line do |line|
|
93
93
|
next if line.empty? # required for BLAST+ 2.2.22
|
94
94
|
type, name, *title = line.split(' ')
|
95
|
-
type = type.downcase
|
95
|
+
type = type.downcase.intern
|
96
96
|
name = name.freeze
|
97
97
|
title = title.join(' ').freeze
|
98
98
|
|
99
99
|
# skip past all but alias file of a NCBI multi-part BLAST database
|
100
|
-
if name
|
100
|
+
if multipart_database_name?(name)
|
101
101
|
log.info(%|Found a multi-part database volume at #{name} - ignoring it.|)
|
102
102
|
next
|
103
103
|
end
|
104
104
|
|
105
105
|
#LOG.info("Found #{type} database: #{title} at #{name}")
|
106
|
-
|
106
|
+
database = Database.new(name, title, type)
|
107
|
+
db[database.hash] = database
|
107
108
|
end
|
108
109
|
|
109
|
-
|
110
|
-
# the erb would fail as calling nil.each_with_index if a dbtype was undefined.
|
111
|
-
db['protein'] = [] unless db.keys.include?('protein')
|
112
|
-
db['nucleotide'] = [] unless db.keys.include?('nucleotide')
|
113
|
-
|
114
|
-
# sort the list of dbs
|
115
|
-
db['protein'].sort!
|
116
|
-
db['nucleotide'].sort!
|
117
|
-
|
118
110
|
db
|
119
111
|
end
|
120
112
|
|
@@ -125,6 +117,16 @@ module SequenceServer
|
|
125
117
|
def command?(command)
|
126
118
|
system("which #{command} > /dev/null 2>&1")
|
127
119
|
end
|
120
|
+
|
121
|
+
# Returns true if the database name appears to be a multi-part database name.
|
122
|
+
#
|
123
|
+
# e.g.
|
124
|
+
# /home/ben/pd.ben/sequenceserver/db/nr.00 => yes
|
125
|
+
# /home/ben/pd.ben/sequenceserver/db/nr => no
|
126
|
+
# /home/ben/pd.ben/sequenceserver/db/img3.5.finished.faa.01 => yes
|
127
|
+
def multipart_database_name?(db_name)
|
128
|
+
!(db_name.match(/.+\/\S+\d{2}$/).nil?)
|
129
|
+
end
|
128
130
|
end
|
129
131
|
|
130
132
|
def self.included(klass)
|