diff options
-rw-r--r-- | Gemfile.lock | 4 | ||||
-rw-r--r-- | lib/compound.rb | 12 | ||||
-rw-r--r-- | lib/dataset.rb | 133 | ||||
-rw-r--r-- | lib/error.rb | 5 | ||||
-rw-r--r-- | lib/opentox-client.rb | 2 | ||||
-rw-r--r-- | lib/opentox.rb | 189 | ||||
-rw-r--r-- | lib/overwrite.rb | 18 | ||||
-rw-r--r-- | lib/task.rb | 27 | ||||
-rw-r--r-- | opentox-client.gemspec | 1 |
9 files changed, 250 insertions, 141 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 6aa15dc..785b3e1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,9 +13,9 @@ GEM specs: addressable (2.2.8) ffi (1.0.11) - mime-types (1.18) + mime-types (1.19) open4 (1.3.0) - rdf (0.3.5.2) + rdf (0.3.7) addressable (>= 2.2.6) rdf-raptor (0.4.1) ffi (>= 1.0) diff --git a/lib/compound.rb b/lib/compound.rb index ce0fdbf..5992ee3 100644 --- a/lib/compound.rb +++ b/lib/compound.rb @@ -40,25 +40,25 @@ module OpenTox # Get InChI # @return [String] InChI string def to_inchi - get(:accept => 'chemical/x-inchi').to_s.chomp if @uri + RestClientWrapper.get(@uri,{},{:accept => 'chemical/x-inchi'}).chomp end # Get (canonical) smiles # @return [String] Smiles string def to_smiles - get(:accept => 'chemical/x-daylight-smiles').chomp + RestClientWrapper.get(@uri,{},{:accept => 'chemical/x-daylight-smiles'}).chomp end # Get sdf # @return [String] SDF string def to_sdf - get(:accept => 'chemical/x-mdl-sdfile').chomp + RestClientWrapper.get(@uri,{},{:accept => 'chemical/x-mdl-sdfile'}).chomp end # Get gif image # @return [image/gif] Image data def to_gif - get("#{CACTUS_URI}#{to_inchi}/image") + RestClientWrapper.get("#{CACTUS_URI}#{to_inchi}/image") end # Get png image @@ -66,7 +66,7 @@ module OpenTox # image = compound.to_png # @return [image/png] Image data def to_png - get(File.join @uri, "image") + RestClientWrapper.get(File.join @uri, "image") end # Get URI of compound image @@ -81,7 +81,7 @@ module OpenTox # @return [String] Compound names def to_names begin - get("#{CACTUS_URI}#{to_inchi}/names").split("\n") + RestClientWrapper.get("#{CACTUS_URI}#{to_inchi}/names").split("\n") rescue "not available" end diff --git a/lib/dataset.rb b/lib/dataset.rb index 8032533..a6c22d0 100644 --- a/lib/dataset.rb +++ b/lib/dataset.rb @@ -3,27 +3,128 @@ module OpenTox # Ruby wrapper for OpenTox Dataset Webservices (http://opentox.org/dev/apis/api-1.2/dataset). class Dataset - def data_entries - # TODO fix for api 1.2 - data_entries = [] - pull - @reload = false - metadata[RDF::OT1.dataEntry].collect{|data_entry| - data_entries << @rdf.to_hash[data_entry] - } - @reload = true - data_entries + attr_accessor :features, :compounds, :data_entries + + def initialize uri=nil, subjectid=nil + super uri, subjectid + @features = [] + @compounds = [] + @data_entries = [] + append RDF.type, RDF::OT.OrderedDataset + end + + def upload filename, wait=true + uri = RestClientWrapper.put(@uri, {:file => File.new(filename)}, {:subjectid => @subjectid}) + OpenTox::Task.new(uri).wait if URI.task?(uri) and wait + end + + def get + super + @features = [] + @compounds = [] + @data_entries = [] + query = RDF::Query.new do + pattern [:uri, RDF.type, RDF::OT.OrderedDataset] + end + if query.execute(@rdf).first # ordered dataset + query = RDF::Query.new do + pattern [:uri, RDF.type, RDF::OT.Compound] + pattern [:uri, RDF::OLO.index, :idx] + end + @compounds = query.execute(@rdf).sort_by{|s| s.idx}.collect{|s| OpenTox::Compound.new s.uri.to_s} + query = RDF::Query.new do + pattern [:uri, RDF.type, RDF::OT.Feature] + pattern [:uri, RDF::OLO.index, :idx] + end + @features = query.execute(@rdf).sort_by{|s| s.idx}.collect{|s| OpenTox::Feature.new(s.uri.to_s)} + numeric_features = @features.collect{|f| f.get; f[RDF.type].include? RDF::OT.NumericFeature} + @compounds.each_with_index do |compound,i| + query = RDF::Query.new do + pattern [:data_entry, RDF::OLO.index, i] + pattern [:data_entry, RDF::OT.values, :values] + pattern [:values, RDF::OT.feature, :feature] + pattern [:feature, RDF::OLO.index, :feature_idx] + pattern [:values, RDF::OT.value, :value] + end + values = query.execute(@rdf).sort_by{|s| s.feature_idx}.collect do |s| + numeric_features[s.feature_idx] ? s.value.to_s.to_f : s.value.to_s + end + @data_entries << values + end + else + query = RDF::Query.new do + pattern [:uri, RDF.type, RDF::OT.Feature] + end + @features = query.execute(@rdf).collect{|s| OpenTox::Feature.new(s.uri.to_s)} + query = RDF::Query.new do + pattern [:data_entry, RDF::OT.compound, :compound] + end + @compounds = query.execute(@rdf).sort_by{|s| s.data_entry}.collect{|s| OpenTox::Compound.new s.compound.to_s} + numeric_features = @features.collect{|f| f.get; f[RDF.type].include? RDF::OT.NumericFeature} + @compounds.each do |compound| + values = [] + @features.each_with_index do |feature,i| + query = RDF::Query.new do + pattern [:data_entry, RDF::OT.compound, RDF::URI.new(compound.uri)] + pattern [:data_entry, RDF::OT.values, :values] + pattern [:values, RDF::OT.feature, RDF::URI.new(feature.uri)] + pattern [:values, RDF::OT.value, :value] + end + value = query.execute(@rdf).first.value.to_s + value = value.to_f if numeric_features[i] + values << value + end + @data_entries << values + end + end end - def compounds - uri = File.join(@uri,"compounds") - RestClientWrapper.get(uri,{},{:accept => "text/uri-list", :subjectid => @subjectid}).split("\n").collect{|uri| OpenTox::Compound.new uri} + def get_metadata + uri = File.join(@uri,"metadata") + begin + parse_ntriples RestClientWrapper.get(uri,{},{:accept => "text/plain", :subjectid => @subjectid}) + rescue # fall back to rdfxml + parse_rdfxml RestClientWrapper.get(uri,{},{:accept => "application/rdf+xml", :subjectid => @subjectid}) + end + metadata end - def features - uri = File.join(@uri,"features") - RestClientWrapper.get(uri,{},{:accept => "text/uri-list", :subjectid => @subjectid}).split("\n").collect{|uri| OpenTox::Feature.new uri} + def << data_entry + compound = data_entry.shift + bad_request_error "Dataset features are empty." unless features + bad_request_error "data_entry size does not match features size." unless data_entry.size == features.size + bad_request_error "First data_entry is not a OpenTox::Compound" unless compound.class == OpenTox::Compound + @compounds << compound + @data_entries << data_entry end + RDF_FORMATS.each do |format| + + # redefine rdf serialization methods + send :define_method, "to_#{format}".to_sym do + # TODO: check, might affect appending to unordered datasets + features.each_with_index do |feature,i| + @rdf << [RDF::URI.new(feature.uri), RDF::URI.new(RDF.type), RDF::URI.new(RDF::OT.Feature)] + @rdf << [RDF::URI.new(feature.uri), RDF::URI.new(RDF::OLO.index), RDF::Literal.new(i)] + end + compounds.each_with_index do |compound,i| + @rdf << [RDF::URI.new(compound.uri), RDF::URI.new(RDF.type), RDF::URI.new(RDF::OT.Compound)] + @rdf << [RDF::URI.new(compound.uri), RDF::URI.new(RDF::OLO.index), RDF::Literal.new(i)] + data_entry_node = RDF::Node.new + @rdf << [RDF::URI.new(@uri), RDF::URI.new(RDF::OT.dataEntry), data_entry_node] + @rdf << [data_entry_node, RDF::URI.new(RDF.type), RDF::URI.new(RDF::OT.DataEntry)] + @rdf << [data_entry_node, RDF::URI.new(RDF::OLO.index), RDF::Literal.new(i)] + @rdf << [data_entry_node, RDF::URI.new(RDF::OT.compound), RDF::URI.new(compound.uri)] + data_entries[i].each_with_index do |value,j| + value_node = RDF::Node.new + @rdf << [data_entry_node, RDF::URI.new(RDF::OT.values), value_node] + @rdf << [value_node, RDF::URI.new(RDF::OT.feature), RDF::URI.new(@features[j].uri)] + @rdf << [value_node, RDF::URI.new(RDF::OT.value), RDF::Literal.new(value)] + end + end + super() + end + + end end end diff --git a/lib/error.rb b/lib/error.rb index 64b0fb1..51451e7 100644 --- a/lib/error.rb +++ b/lib/error.rb @@ -10,7 +10,7 @@ class RuntimeError $logger.error "\n"+self.to_turtle end - # define to_ and self.from_ methods for various rdf formats + # define to_ methods for all RuntimeErrors and various rdf formats RDF_FORMATS.each do |format| send :define_method, "to_#{format}".to_sym do @@ -22,7 +22,7 @@ class RuntimeError subject = RDF::Node.new writer << [subject, RDF.type, RDF::OT.ErrorReport] writer << [subject, RDF::OT.actor, @uri.to_s] - writer << [subject, RDF::OT.message, @message.to_s] + writer << [subject, RDF::OT.message, message.to_s] writer << [subject, RDF::OT.statusCode, @http_code] writer << [subject, RDF::OT.errorCode, self.class.to_s] @@ -80,7 +80,6 @@ module OpenTox class RestCallError < Error attr_accessor :request#, :response def initialize message, request, uri - #def initialize request, response, message @request = request #@response = response super 502, message, uri diff --git a/lib/opentox-client.rb b/lib/opentox-client.rb index 8c19225..ad7d3cc 100644 --- a/lib/opentox-client.rb +++ b/lib/opentox-client.rb @@ -2,6 +2,7 @@ require 'rubygems' require "bundler/setup" require 'rdf' require 'rdf/raptor' +require 'rdf/n3' require "rest-client" require 'uri' require 'yaml' @@ -14,6 +15,7 @@ require "securerandom" RDF::OT = RDF::Vocabulary.new 'http://www.opentox.org/api/1.2#' RDF::OT1 = RDF::Vocabulary.new 'http://www.opentox.org/api/1.1#' RDF::OTA = RDF::Vocabulary.new 'http://www.opentox.org/algorithmTypes.owl#' +RDF::OLO = RDF::Vocabulary.new 'http://purl.org/ontology/olo/core#' CLASSES = ["Generic", "Compound", "Feature", "Dataset", "Algorithm", "Model", "Validation", "Task", "Investigation"] RDF_FORMATS = [:rdfxml,:ntriples,:turtle] diff --git a/lib/opentox.rb b/lib/opentox.rb index f79b51b..2e0f05a 100644 --- a/lib/opentox.rb +++ b/lib/opentox.rb @@ -4,7 +4,7 @@ $logger.level = Logger::DEBUG module OpenTox - attr_accessor :uri, :subjectid, :rdf, :response, :reload + attr_accessor :uri, :subjectid, :rdf # Ruby interface @@ -13,44 +13,86 @@ module OpenTox # @param [optional,String] subjectid # @return [OpenTox] OpenTox object def initialize uri=nil, subjectid=nil - @uri = uri.to_s.chomp - @subjectid = subjectid - @reload = true @rdf = RDF::Graph.new + if uri + @uri = uri.to_s.chomp + else + service = self.class.to_s.split('::').last.downcase + service_uri = eval("$#{service}[:uri]") + bad_request_error "$#{service}[:uri] variable not set. Please set $#{service}[:uri] or use an explicit uri as first constructor argument " unless service_uri + @uri = File.join service_uri, SecureRandom.uuid + end + append RDF.type, eval("RDF::OT."+self.class.to_s.split('::').last) + append RDF::DC.date, DateTime.now + @subjectid = subjectid end - # Load metadata from service - def pull - parse_ntriples RestClientWrapper.get(@uri,{},{:accept => "text/plain", :subjectid => @subjectid}) - rescue # fall back to rdfxml - parse_rdfxml RestClientWrapper.get(@uri,{},{:accept => "application/rdf+xml", :subjectid => @subjectid}) + # Object metadata + # @return [Hash] Object metadata + def metadata + # return plain strings instead of RDF objects + @rdf.to_hash[RDF::URI.new(@uri)].inject({}) { |h, (predicate, values)| h[predicate.to_s] = values.collect{|v| v.to_s}; h } end - # Get object metadata - # @return [Hash] Metadata - def metadata - pull if @reload # force update - @rdf.to_hash[RDF::URI.new(@uri)] + # Metadata values + # @param [String] Predicate URI + # @return [Array, String] Predicate value(s) + def [](predicate) + return nil if metadata[predicate.to_s].nil? + metadata[predicate.to_s].size == 1 ? metadata[predicate.to_s].first : metadata[predicate.to_s] + end + + # Set object metadata + # @param [String] Predicate URI + # @param [Array, String] Predicate value(s) + def []=(predicate,values) + @rdf.delete [RDF::URI.new(@uri.to_s),RDF::URI.new(predicate.to_s),nil] + append predicate.to_s, values + end + + # Append object metadata + # @param [String] Predicate URI + # @param [Array, String] Predicate value(s) + def append(predicate,values) + uri = RDF::URI.new @uri + predicate = RDF::URI.new predicate + [values].flatten.each { |value| @rdf << [uri, predicate, value] } end - # Get metadata values - # @param [RDF] Key from RDF Vocabularies - # @return [Array] Values for supplied key - def [](key) - pull if @reload # force update - result = @rdf.query([RDF::URI.new(@uri),key,nil]).collect{|statement| statement.object} - # TODO: convert to OpenTox objects?? - return nil if result and result.empty? - return result.first.to_s if result.size == 1 - return result.collect{|r| r.to_s} - result + # Get object from webservice + def get wait=true + response = RestClientWrapper.get(@uri,{},{:accept => "text/plain", :subjectid => @subjectid}) + if URI.task?(response) and wait + t = OpenTox::Task.new(uri).wait + response = RestClientWrapper.get(t.resultURI,{},{:accept => "text/plain", :subjectid => @subjectid}) + end + parse_ntriples response + #rescue # fall back to rdfxml + #parse_rdfxml RestClientWrapper.get(@uri,{},{:accept => "application/rdf+xml", :subjectid => @subjectid}) + end + + # Post object to webservice + def post service_uri, wait=true + uri = RestClientWrapper.post service_uri, to_ntriples, { :content_type => "text/plain", :subjectid => @subjectid} + OpenTox::Task.new(uri).wait if URI.task?(uri) and wait + #rescue # fall back to rdfxml + #RestClientWrapper.post service_uri, to_rdfxml, { :content_type => "application/rdf+xml", :subjectid => @subjectid} + end + + # Save object at webservice + def put wait=true + append RDF::DC.modified, DateTime.now + #begin + RestClientWrapper.put @uri.to_s, self.to_ntriples, { :content_type => "text/plain", :subjectid => @subjectid} + #rescue # fall back to rdfxml + #RestClientWrapper.put @uri.to_s, self.to_rdfxml, { :content_type => "application/rdf+xml", :subjectid => @subjectid} + #end + OpenTox::Task.new(uri).wait if URI.task?(uri) and wait end - # Save object at service - def save - put self.to_ntriples, { :content_type => "text/plain"} - rescue # fall back to rdfxml - put self.to_rdfxml, { :content_type => "application/rdf+xml"} + # Delete object at webservice + def delete + RestClientWrapper.delete(@uri.to_s,nil,{:subjectid => @subjectid}) end RDF_FORMATS.each do |format| @@ -72,78 +114,20 @@ module OpenTox end end - def to_yaml - @rdf.to_hash.to_yaml - end - - def to_json - to_hash.to_json - end - - # REST API - def get headers={} - headers[:subjectid] ||= @subjectid - headers[:accept] ||= 'application/rdf+xml' - @response = RestClientWrapper.get @uri, {}, headers - end - - def post payload={}, headers={} - headers[:subjectid] ||= @subjectid - headers[:accept] ||= 'application/rdf+xml' - @response = RestClientWrapper.post(@uri.to_s, payload, headers) - end - - def put payload={}, headers={} - headers[:subjectid] ||= @subjectid - headers[:accept] ||= 'application/rdf+xml' - @response = RestClientWrapper.put(@uri.to_s, payload, headers) - end - - def delete headers={} - headers[:subjectid] ||= @subjectid - @response = RestClientWrapper.delete(@uri.to_s,nil,headers) - end - - # class methods - module ClassMethods - - def all service_uri, subjectid=nil - uris = RestClientWrapper.get(service_uri, {}, :accept => 'text/uri-list').split("\n").compact - uris.collect{|uri| URI.task?(service_uri) ? from_uri(uri, subjectid, false) : from_uri(uri, subjectid)} + def to_turtle # redefine to use prefixes (not supported by RDF::Writer) + prefixes = {:rdf => "http://www.w3.org/1999/02/22-rdf-syntax-ns#"} + ['OT', 'DC', 'XSD', 'OLO'].each{|p| prefixes[p.downcase.to_sym] = eval("RDF::#{p}.to_s") } + turtle = RDF::N3::Writer.for(:turtle).buffer(:prefixes => prefixes) do |writer| + @rdf.each{|statement| writer << statement} end + end - def create service_uri, subjectid=nil - #uri = File.join(service_uri,SecureRandom.uuid) - uri = RestClientWrapper.post(service_uri, {}, {:accept => 'text/uri-list', :subjectid => subjectid}) - URI.task?(service_uri) ? from_uri(uri, subjectid, false) : from_uri(uri, subjectid) + {:title => RDF::DC.title, :dexcription => RDF::DC.description}.each do |method,predicate| + send :define_method, method do + self.[](predicate) end - - def from_file service_uri, filename, subjectid=nil - file = File.new filename - from_uri RestClientWrapper.post(service_uri, {:file => file}, {:subjectid => subjectid, :content_type => file.mime_type, :accept => "text/uri-list"}), subjectid - end - - private - def from_uri uri, subjectid=nil, wait=true - - uri.chomp! - # TODO add waiting task - if URI.task?(uri) and wait - t = OpenTox::Task.new(uri) - t.wait - uri = t.resultURI - end - - # guess class from uri, this is potentially unsafe, but polling metadata from large uri lists is way too slow (and not all service provide RDF.type in their metadata) - result = CLASSES.collect{|s| s if uri =~ /#{s.downcase}/}.compact - if result.size == 1 - klass = result.first - else - klass = OpenTox::Generic.new(uri)[RDF.type] - internal_server_error "Cannot determine class from URI '#{uri} (Candidate classes are #{result.inspect}) or matadata." unless klass - end - # initialize with/without subjectid - subjectid ? eval("#{self}.new(\"#{uri}\", \"#{subjectid}\")") : eval("#{self}.new(\"#{uri}\")") + send :define_method, "#{method}=" do |value| + self.[]=(predicate,value) end end @@ -151,7 +135,12 @@ module OpenTox CLASSES.each do |klass| c = Class.new do include OpenTox - extend OpenTox::ClassMethods + #extend OpenTox::ClassMethods + + def self.all service_uri, subjectid=nil + uris = RestClientWrapper.get(service_uri, {}, :accept => 'text/uri-list').split("\n").compact + uris.collect{|uri| self.new(uri, subjectid)} + end end OpenTox.const_set klass,c end diff --git a/lib/overwrite.rb b/lib/overwrite.rb index d98769e..137fec8 100644 --- a/lib/overwrite.rb +++ b/lib/overwrite.rb @@ -1,3 +1,21 @@ +class Object + # An object is blank if it's false, empty, or a whitespace string. + # For example, "", " ", +nil+, [], and {} are all blank. + def blank? + respond_to?(:empty?) ? empty? : !self + end + + def numeric? + true if Float(self) rescue false + end +end + +module Enumerable + def duplicates + inject({}) {|h,v| h[v]=h[v].to_i+1; h}.reject{|k,v| v==1}.keys + end +end + class String def underscore self.gsub(/::/, '/'). diff --git a/lib/task.rb b/lib/task.rb index 2ca481d..0b0aea2 100644 --- a/lib/task.rb +++ b/lib/task.rb @@ -8,13 +8,11 @@ module OpenTox def self.create service_uri, subjectid=nil, params={} - uri = RDF::URI.new File.join(service_uri,SecureRandom.uuid) + uri = File.join(service_uri,SecureRandom.uuid) task = Task.new uri, subjectid - task.rdf << RDF::Statement.new(uri, RDF.type, RDF::OT.Task) - task.rdf << RDF::Statement.new(uri, RDF::DC.date, RDF::Literal.new(DateTime.now)) - task.rdf << RDF::Statement.new(uri, RDF::OT.hasStatus, RDF::Literal.new("Running")) - params.each {|k,v| task.rdf << RDF::Statement.new(uri, k, v)} - task.save + task[RDF::OT.hasStatus] = "Running" + params.each { |k,v| task[k] = v } + task.put false pid = fork do begin result_uri = yield @@ -53,24 +51,23 @@ module OpenTox end def description - pull - self.[](RDF::DC.description).uniq.first + self.[](RDF::DC.description) end def creator - pull - self.[](RDF::DC.creator).uniq.first + self.[](RDF::DC.creator) end def cancel kill - RestClientWrapper.put(File.join(@uri,'Cancelled'),{}) + self.[]=(RDF::OT.hasStatus, "Cancelled") + put false end def completed(uri) - #puts uri - #not_found_error "Result URI \"#{uri}\" does not exist." unless URI.accessible? uri, @subjectid - RestClientWrapper.put(File.join(@uri,'Completed'),{:resultURI => uri}) + self.[]=(RDF::OT.resultURI, uri) + self.[]=(RDF::OT.hasStatus, "Completed") + put false end # waits for a task, unless time exceeds or state is no longer running @@ -85,6 +82,7 @@ module OpenTox dur = [[(Time.new - start_time)/20.0,0.3].max,300.0].min time_out_error "max wait time exceeded ("+DEFAULT_TASK_MAX_DURATION.to_s+"sec), task: '"+@uri.to_s+"'" if (Time.new > due_to_time) end + get end end @@ -109,6 +107,7 @@ module OpenTox [:hasStatus, :resultURI, :finished_at].each do |method| define_method method do + get response = self.[](RDF::OT[method]) response = self.[](RDF::OT1[method]) unless response # API 1.1 compatibility response diff --git a/opentox-client.gemspec b/opentox-client.gemspec index 75d390b..f9d333a 100644 --- a/opentox-client.gemspec +++ b/opentox-client.gemspec @@ -23,6 +23,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency "rest-client" s.add_runtime_dependency "rdf" s.add_runtime_dependency "rdf-raptor" + s.add_runtime_dependency 'rdf-n3' s.add_runtime_dependency "open4" # external requirements |