lib/sequenceserver/blast.rb in sequenceserver-1.0.14 vs lib/sequenceserver/blast.rb in sequenceserver-1.1.0.beta

- old
+ new

@@ -1,168 +1,2 @@ -require 'forwardable' -require 'tempfile' -require 'English' -require 'ox' - -require 'sequenceserver/links' -require 'sequenceserver/blast/exceptions' -require 'sequenceserver/blast/constants' -require 'sequenceserver/blast/formatter' -require 'sequenceserver/blast/report' -require 'sequenceserver/blast/query' -require 'sequenceserver/blast/hit' -require 'sequenceserver/blast/hsp' - -module SequenceServer - # Simple wrapper around BLAST+ search algorithms. - # - # `BLAST::ArgumentError` and `BLAST::RuntimeError` signal errors encountered - # when attempting a BLAST search. - module BLAST - class << self - extend Forwardable - - def_delegators SequenceServer, :config, :logger - - # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity - # rubocop:disable Metrics/MethodLength - def run(params) - pre_process params - validate_blast_params params - - # Compile parameters for BLAST search into a shell executable command. - # - # BLAST method to use. - method = params[:method] - # - # BLAST+ expects query sequence as a file. - qfile = Tempfile.new('sequenceserver_query') - qfile.puts(params[:sequence]) - qfile.close - # - # Retrieve database objects from database id. - databases = Database[params[:databases]] - # - # Concatenate other blast options. - options = params[:advanced].to_s.strip + defaults - # - # blastn implies blastn, not megablast; but let's not interfere if a - # user specifies `task` herself. - options << ' -task blastn' if method == 'blastn' && !(options =~ /task/) - - # Run BLAST search. - # - # Command to execute. - command = "#{method} -db '#{databases.map(&:name).join(' ')}'" \ - " -query '#{qfile.path}' #{options}" - # - # Debugging log. - logger.debug("Executing: #{command}") - # - # Temporary files to capture stdout and stderr. - rfile = Tempfile.new('sequenceserver_blast_result') - efile = Tempfile.new('sequenceserver_blast_error') - [rfile, efile].each(&:close) - # - # Execute. - system("#{command} > #{rfile.path} 2> #{efile.path}") - - # Capture error. - status = $CHILD_STATUS.exitstatus - case status - when 1 # error in query sequence or options; see [1] - efile.open - - # Most of the time BLAST+ generates a verbose error message with - # details we don't require. So we parse out the relevant lines. - error = efile.each_line do |l| - break Regexp.last_match[1] if l.match(ERROR_LINE) - end - - # But sometimes BLAST+ returns the exact/relevant error message. - # Trying to parse such messages returns nil, and we use the error - # message from BLAST+ as it is. - error = efile.rewind && efile.read unless error.is_a? String - - efile.close - fail ArgumentError, error - when 2, 3, 4, 255 # see [1] - efile.open - error = efile.read - efile.close - fail RuntimeError.new(status, error) - end - - Search << rfile - Report.new(File.basename(rfile.path), databases) - end - # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity - # rubocop:enable Metrics/MethodLength - - def pre_process(params) - params[:sequence].strip! unless params[:sequence].nil? - end - - def validate_blast_params(params) - validate_blast_method params[:method] - validate_blast_sequences params[:sequence] - validate_blast_databases params[:databases] - validate_blast_options params[:advanced] - end - - def defaults - " -outfmt 11 -num_threads #{config[:num_threads]}" - end - - def validate_blast_method(method) - return true if ALGORITHMS.include? method - fail ArgumentError, 'BLAST algorithm should be one of:' \ - " #{ALGORITHMS.join(', ')}." - end - - def validate_blast_sequences(sequences) - return true if sequences.is_a?(String) && !sequences.empty? - fail ArgumentError, 'Sequences should be a non-empty string.' - end - - def validate_blast_databases(database_ids) - ids = Database.ids - return true if database_ids.is_a?(Array) && !database_ids.empty? && - (ids & database_ids).length == database_ids.length - fail ArgumentError, 'Database id should be one of:' \ - " #{ids.join("\n")}." - end - - # Advanced options are specified by the user. Here they are checked for - # interference with SequenceServer operations. - # - # Raise ArgumentError if an error has occurred. - def validate_blast_options(options) - return true if !options || (options.is_a?(String) && - options.strip.empty?) - - unless allowed_chars.match(options) - fail ArgumentError, 'Invalid characters detected in options.' - end - - if disallowed_options.match(options) - failedopt = Regexp.last_match[0] - fail ArgumentError, "Option \"#{failedopt}\" is prohibited." - end - - true - end - - def allowed_chars - /\A[a-z0-9\-_\. ']*\Z/i - end - - def disallowed_options - /-out|-html|-outfmt|-db|-query/i - end - end - end -end - -# References -# ---------- -# [1]: http://www.ncbi.nlm.nih.gov/books/NBK1763/ +require_relative 'blast/job' +require_relative 'blast/report'