module SequenceServer

Top level module / namespace.

Define constants used by SequenceServer module

Define Config class.

Define Database class.

This file defines all possible exceptions that can be thrown by SequenceServer on startup.

Exceptions only ever inform another entity (downstream code or users) of an issue. Exceptions may or may not be recoverable.

Error classes should be seen as: the error code (class name), human readable message (to_s method), and necessary attributes to act on the error.

We define as many error classes as needed to be precise about the issue, thus making it easy for downstream code (bin/sequenceserver or config.ru) to act on them.

Define Sequence class.

Define version number.

Constants

DOTDIR

Constant for denoting the path ~/.sequenceserver

Database

Captures a directory containing FASTA files and BLAST databases.

Formatting a FASTA for use with BLAST+ will create 3 or 6 files, collectively referred to as a BLAST database.

It is important that formatted BLAST database files have the same dirname and basename as the source FASTA for SequenceServer to be able to tell formatted FASTA from unformatted. And that FASTA files be formatted with `parse_seqids` option of `makeblastdb` for sequence retrieval to work.

SequenceServer will always place BLAST database files alongside input FASTA, and use `parse_seqids` option of `makeblastdb` to format databases.

MINIMUM_BLAST_VERSION

Use a fixed minimum version of BLAST+

Sequence

Provides simple sequence processing utilities via class methods. Instance of the class serves as a simple data object to captures sequences fetched from BLAST databases.

NOTE:

What all do we need to consistently construct FASTA from `blastdbcmd's`
output?

It would seem rather straightforward. But it's not.

FASTA format:

  >defline
  actual sequence

where,

  defline = >id title

ID of a sequence fetched from nr database should look like this:

               sequence id    -> self.seqid
               -------------
                  accession   -> self.accession
                  ----------
  gi|322796550|gb|EFZ19024.1| -> self.id
     ---------
     gi number                -> self.gi

while for local databases, the id should be the exact same, as in the
original FASTA file:

  SI2.2.0_06267 -> self.id == self.seqid == self.accession.
VERSION

Attributes

config[R]

Public Class Methods

call(env) click to toggle source

Rack-interface.

Inject our logger in the env and dispatch request to our controller.

# File lib/sequenceserver.rb, line 94
def call(env)
  env['rack.logger'] = logger
  Routes.call(env)
end
environment() click to toggle source
# File lib/sequenceserver.rb, line 20
def environment
  ENV['RACK_ENV']
end
init(config = {}) click to toggle source
# File lib/sequenceserver.rb, line 36
def init(config = {})
  @config = Config.new(config)

  init_binaries
  init_database
  load_extension
  check_num_threads
  self

  # We don't validate port and host settings. If SequenceServer is run
  # self-hosted, bind will fail on incorrect values. If SequenceServer
  # is run via Apache+Passenger, we don't need to worry.
end
irb() click to toggle source

Run SequenceServer interactively.

# File lib/sequenceserver.rb, line 100
def irb
  ARGV.clear
  require 'irb'
  IRB.setup nil
  IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
  require 'irb/ext/multi-irb'
  IRB.irb nil, self
end
logger() click to toggle source
# File lib/sequenceserver.rb, line 32
def logger
  @logger ||= Logger.new(STDERR, verbose?)
end
on_start() click to toggle source
# File lib/sequenceserver.rb, line 69
def on_start
  puts '** SequenceServer is ready.'
  puts "   Go to #{server_url} in your browser and start BLASTing!"
  puts '   Press CTRL+C to quit.'
  open_in_browser(server_url)
end
on_stop() click to toggle source
# File lib/sequenceserver.rb, line 76
def on_stop
  puts
  puts '** Thank you for using SequenceServer :).'
  puts '   Please cite: '
  puts '       Priyam A, Woodcroft BJ, Rai V, Moghul I, Munagala A, Ter F,'
  puts '       Chowdhary H, Pieniak I, Maynard LJ, Gibbins MA, Moon H,'
  puts '       Davis-Richardson A, Uludag M, Watson-Haigh N, Challis R,'
  puts '       Nakamura H, Favreau E, Gómez EA, Pluskal T, Leonard G,'
  puts '       Rumpf W & Wurm Y.'
  puts '       Sequenceserver: A modern graphical user interface for'
  puts '       custom BLAST databases.'
  puts '       Molecular Biology and Evolution (2019)'
end
root() click to toggle source
# File lib/sequenceserver.rb, line 28
def root
  File.dirname(File.dirname(__FILE__))
end
run() click to toggle source

Run SequenceServer as a self-hosted server using Thin webserver.

# File lib/sequenceserver.rb, line 53
def run
  check_host
  Server.run(self)
rescue Errno::EADDRINUSE
  puts "** Could not bind to port #{config[:port]}."
  puts "   Is SequenceServer already accessible at #{server_url}?"
  puts '   No? Try running SequenceServer on another port, like so:'
  puts
  puts '       sequenceserver -p 4570.'
rescue Errno::EACCES
  puts "** Need root privilege to bind to port #{config[:port]}."
  puts '   It is not advisable to run SequenceServer as root.'
  puts '   Please use Apache/Nginx to bind to a privileged port.'
  puts '   Instructions available on http://sequenceserver.com.'
end
verbose?() click to toggle source
# File lib/sequenceserver.rb, line 24
def verbose?
  @verbose ||= (environment == 'development')
end

Private Class Methods

assert_blast_installed_and_compatible() click to toggle source
# File lib/sequenceserver.rb, line 185
def assert_blast_installed_and_compatible
  fail BLAST_NOT_INSTALLED unless command? 'blastdbcmd'
  version = `blastdbcmd -version`.split[1]
  fail BLAST_NOT_EXECUTABLE if !$CHILD_STATUS.success? || version.empty?
  if (parse_version(version) <=> parse_version(MINIMUM_BLAST_VERSION)) < 0
    fail BLAST_NOT_COMPATIBLE, version
  end
end
check_host() click to toggle source

Check and warn user if host is 0.0.0.0 (default).

# File lib/sequenceserver.rb, line 156
def check_host
  # rubocop:disable Style/GuardClause
  if config[:host] == '0.0.0.0'
    logger.warn 'Will listen on all interfaces (0.0.0.0).' \
                ' Consider using 127.0.0.1 (--host option).'
  end
  # rubocop:enable Style/GuardClause
end
check_num_threads() click to toggle source
# File lib/sequenceserver.rb, line 143
def check_num_threads
  num_threads = Integer(config[:num_threads])
  fail NUM_THREADS_INCORRECT unless num_threads > 0

  logger.debug "Will use #{num_threads} threads to run BLAST."
  if num_threads > 256
    logger.warn "Number of threads set at #{num_threads} is unusually high."
  end
rescue
  raise NUM_THREADS_INCORRECT
end
command?(command) click to toggle source

Return `true` if the given command exists and is executable.

# File lib/sequenceserver.rb, line 218
def command?(command)
  system("which #{command} > /dev/null 2>&1")
end
export_bin_dir() click to toggle source

Export NCBI BLAST+ bin dir to PATH environment variable.

# File lib/sequenceserver.rb, line 178
def export_bin_dir
  bin_dir = config[:bin]
  return unless bin_dir
  return if ENV['PATH'].split(':').include? bin_dir
  ENV['PATH'] = "#{bin_dir}:#{ENV['PATH']}"
end
init_binaries() click to toggle source
# File lib/sequenceserver.rb, line 111
def init_binaries
  if config[:bin]
    config[:bin] = File.expand_path config[:bin]
    unless File.exist?(config[:bin]) && File.directory?(config[:bin])
      fail BIN_DIR_NOT_FOUND, config[:bin]
    end
    logger.debug("Will use NCBI BLAST+ at: #{config[:bin]}")
    export_bin_dir
  else
    logger.debug('Will use NCBI BLAST+ at: $PATH')
  end

  assert_blast_installed_and_compatible
end
init_database() click to toggle source
# File lib/sequenceserver.rb, line 126
def init_database
  fail DATABASE_DIR_NOT_SET unless config[:database_dir]

  config[:database_dir] = File.expand_path(config[:database_dir])
  unless File.exist?(config[:database_dir]) &&
         File.directory?(config[:database_dir])
    fail DATABASE_DIR_NOT_FOUND, config[:database_dir]
  end

  logger.debug("Will use BLAST+ databases at: #{config[:database_dir]}")
  Database.scan_databases_dir
  Database.each do |database|
    logger.debug("Found #{database.type} database '#{database.title}'" \
                 " at '#{database.name}'")
  end
end
load_extension() click to toggle source
# File lib/sequenceserver.rb, line 165
def load_extension
  return unless config[:require]

  config[:require] = File.expand_path config[:require]
  unless File.exist?(config[:require]) && File.file?(config[:require])
    fail EXTENSION_FILE_NOT_FOUND, config[:require]
  end

  logger.debug("Loading extension: #{config[:require]}")
  require config[:require]
end
open_in_browser(server_url) click to toggle source
# File lib/sequenceserver.rb, line 200
def open_in_browser(server_url)
  return if using_ssh? || verbose?
  if RUBY_PLATFORM =~ /linux/ && xdg?
    system "xdg-open #{server_url}"
  elsif RUBY_PLATFORM =~ /darwin/
    system "open #{server_url}"
  end
end
parse_version(version_string) click to toggle source
# File lib/sequenceserver.rb, line 222
def parse_version(version_string)
  version_string.split('.').map(&:to_i)
end
server_url() click to toggle source
# File lib/sequenceserver.rb, line 194
def server_url
  host = config[:host]
  host = 'localhost' if host == '127.0.0.1' || host == '0.0.0.0'
  "http://#{host}:#{config[:port]}"
end
using_ssh?() click to toggle source
# File lib/sequenceserver.rb, line 209
def using_ssh?
  true if ENV['SSH_CLIENT'] || ENV['SSH_TTY'] || ENV['SSH_CONNECTION']
end
xdg?() click to toggle source
# File lib/sequenceserver.rb, line 213
def xdg?
  true if ENV['DISPLAY'] && command?('xdg-open')
end