freebase 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,25 @@
1
+ = Freebase
2
+ This Ruby-on-Rails plugin provides access to the Freebase API (http://www.freebase.com). Freebase is a collaborative, semantic database similar to Wikipedia only for structured data. Freebase.com provides a JSON-over-HTTP API that this library uses.
3
+
4
+ Currently only reads are implemented. Contributions, API Suggestions, bug reports are welcome!
5
+
6
+ This code is ALPHA. The API will change, features will be added. It probably has a bug or two in it. Use it at your own peril.
7
+
8
+ Author:: Christopher Eppstein (mailto:chris@eppsteins.net)
9
+ Copyright:: Copyright (c) 2007 Christopher Eppstein
10
+ License:: Released under the MIT license
11
+
12
+ == Installation
13
+ Install the plugin:
14
+ script/plugin install svn://rubyforge.org/var/svn/freebaseapi/trunk/freebase
15
+
16
+ Then copy freebase.yml to your rails config directory
17
+
18
+ == Usage Examples
19
+ See the following examples:
20
+ * albums.rb[link:../examples/albums.rb]
21
+
22
+ == Contributors
23
+ * Pat Allan (mailto:pat@freelancing-gods.com) provided code snippets that
24
+ exemplified automatic freebase class creation when the class is first referenced.
25
+
@@ -0,0 +1,17 @@
1
+ # Need to modify the inbuilt Module class, to catch const_missing calls.
2
+ class Module
3
+
4
+ # New version of const_missing which catches requests made to the
5
+ # Metaweb module/namespace - and generates modules to represent
6
+ # data domains if necessary.
7
+ def const_missing_with_freebase_support(class_id)
8
+ if self.name[/^Freebase::Types/]
9
+ new_freebase_type(class_id)
10
+ else
11
+ const_missing_without_freebase_support(class_id)
12
+ end
13
+ end
14
+
15
+ alias_method_chain :const_missing, :freebase_support
16
+
17
+ end
@@ -0,0 +1,160 @@
1
+ # (c) Copyright 2007 Chris Eppstein. All Rights Reserved.
2
+
3
+ require 'rubygems'
4
+ require 'activesupport'
5
+ require 'net/http'
6
+ require 'core_extensions'
7
+
8
+ module Freebase
9
+ end
10
+
11
+ require 'freebase/api'
12
+
13
+ module Freebase
14
+ # This module is a namespace scope for the Freebase domains
15
+ module Types
16
+ # Automagically creates modules for domains and classes for types
17
+ # for matching namespaces to Freebase's domain/type naming structure.
18
+ # Shouldn't need to be called manually because this is called by the module
19
+ # whenever the constant is missing.
20
+ def new_freebase_type(name)
21
+ Freebase::Api::Logger.trace {"new freebase module = #{name}"}
22
+ self.const_set(name, Module.new do
23
+ def new_freebase_type(name)
24
+ Freebase::Api::Logger.trace {"new freebase class = #{name}"}
25
+ klass = self.const_set(name, Class.new(Freebase::Base))
26
+ klass.class_eval do
27
+ cattr_accessor :properties, :schema_loaded
28
+ end
29
+ returning(klass) do |tc|
30
+ tc.load_schema! unless tc.schema_loaded?
31
+ Freebase::Api::Logger.trace { "Attempting Mixin include: #{tc.name.sub(/Types/,"Mixins")}" }
32
+ begin
33
+ tc.send(:include, tc.name.sub(/Types/,"Mixins").constantize)
34
+ rescue NameError => e
35
+ Freebase::Api::Logger.trace "failed: #{e}"
36
+ end
37
+ end
38
+ end
39
+
40
+ module_function :new_freebase_type
41
+ end)
42
+ end
43
+
44
+ module_function :new_freebase_type
45
+ end
46
+
47
+ # Add a module within this module that corresponds to a freebase class within Freebase::Types
48
+ # and the methods will be mixed in automatically. E.g.:
49
+ # module Freebase::Mixins::Music
50
+ # module Track
51
+ # def formatted_length
52
+ # "#{self.length.to_i / 60}:#{sprintf("%02i", self.length.to_i % 60)}"
53
+ # end
54
+ # end
55
+ # end
56
+ #
57
+ # Will be mixed in to the Freebase::Types:Music:Track class
58
+ module Mixins
59
+ end
60
+
61
+ # This is the base class for all dynamically defined Freebase Types.
62
+ class Base < Api::FreebaseResult
63
+ extend Api
64
+ alias_method :attributes, :result
65
+ def self.schema_loaded?
66
+ self.schema_loaded || false
67
+ end
68
+ def self.freebase_type
69
+ @freebase_type ||= self.name["Freebase::Types".length..self.name.length].underscore
70
+ end
71
+ def self.load_schema!
72
+ self.properties = {}
73
+ propobjs = mqlread(:type => '/type/type', :id => self.freebase_type, :properties => [{:name => nil, :id => nil, :type => nil, :expected_type => nil}]).properties
74
+ propobjs.each {|propobj|
75
+ self.properties[propobj.id.split(/\//).last.to_sym] = propobj
76
+ }
77
+ self.schema_loaded = true
78
+ end
79
+ def self.find(*args)
80
+ options = args.extract_options!
81
+ case args.first
82
+ when :first
83
+ raise ArgumentError.new("Too many arguments for find(:first)") if args.size > 1
84
+ find_first(options)
85
+ when :all
86
+ raise ArgumentError.new("Too many arguments for find(:all)") if args.size > 1
87
+ find_all(options)
88
+ end
89
+ end
90
+ def self.add_required_query_attributes(conditions)
91
+ case conditions
92
+ when Array
93
+ conditions.map! {|c| add_required_query_attributes(c)}
94
+ when Hash
95
+ if conditions.delete(:fb_object)
96
+ conditions.reverse_merge!(:type => [], :id => nil) unless conditions.has_key?(:*)
97
+ else
98
+ conditions.reverse_merge!(:type => nil) unless conditions.has_key?(:*)
99
+ end
100
+ conditions.each {|k,v| add_required_query_attributes(v) unless k == :*}
101
+ else
102
+ conditions
103
+ end
104
+ end
105
+ # Don't to call this directly. find(:first, options) will be dispatched here.
106
+ # This method is provided for extensibility
107
+ def self.find_first(options = {})
108
+ conditions = options.fetch(:conditions, {}).reverse_merge(:type => self.freebase_type, :name=>nil, :* => [{}], :limit => 1)
109
+ add_required_query_attributes(conditions)
110
+ self.new(mqlread(conditions, :raw => true))
111
+ end
112
+
113
+ # Don't to call this directly. find(:all, options) will be dispatched here.
114
+ # This method is provided for extensibility
115
+ def self.find_all(options = {})
116
+ query = options.fetch(:conditions, {}).merge(:type => self.freebase_type, :name=>nil, :* => [{}])
117
+ query[:limit] = options[:limit] if options[:limit]
118
+ add_required_query_attributes(query)
119
+ mqlread([query], :raw => true).map{|i| self.new(i)}
120
+ end
121
+
122
+ # ActiveRecord:Base-like to_s for the class
123
+ def self.to_s
124
+ if respond_to?(:properties) && !self.properties.blank?
125
+ %Q{#<#{name} #{self.properties.map{|k,v| "#{k}:#{v.expected_type}"}.join(", ")}>}
126
+ else
127
+ "#<#{name}>"
128
+ end
129
+ end
130
+
131
+ # (re)load all properties of this object
132
+ def reload
133
+ query = {:id => self.id, :type=>self.class.freebase_type, :name=> nil, :limit => 1}
134
+ self.class.properties.each do |k,v|
135
+ query[k] = [{}] unless query.has_key?(k)
136
+ end
137
+ @result = self.class.mqlread(query, :raw => true).symbolize_keys!
138
+ Freebase::Api::Logger.trace { @result.inspect }
139
+ return self
140
+ end
141
+
142
+ # access the properties of this object, lazy loading associations as required.
143
+ def method_missing(name,*args)
144
+ if self.class.properties.has_key?(name)
145
+ reload unless attributes.has_key?(name)
146
+ resultify attributes[name]
147
+ elsif self.class.properties.has_key?((singularized_name = name.to_s.singularize.to_sym))
148
+ reload unless attributes.has_key?(singularized_name)
149
+ resultify attributes[singularized_name]
150
+ else
151
+ super
152
+ end
153
+ end
154
+
155
+ # If the object has a name, return it, otherwise the id.
156
+ def to_s
157
+ respond_to?(:name) ? name : id
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,222 @@
1
+ module Freebase::Api
2
+ # A class for returing errors from the freebase api.
3
+ # For more infomation see the freebase documentation:
4
+ # http://www.freebase.com/view/help/guid/9202a8c04000641f800000000544e139#mqlreaderrors
5
+ class MqlReadError < ArgumentError
6
+ attr_accessor :code, :freebase_message
7
+ def initialize(code,message)
8
+ self.code = code
9
+ self.freebase_message = message
10
+ end
11
+ def message
12
+ "#{code}: #{freebase_message}"
13
+ end
14
+ end
15
+
16
+ # Encapsulates a Freebase result, enables method-based access to the returned values.
17
+ # E.g.
18
+ # result = mqlread(:type => "/music/artist", :name => "The Police", :id => nil)
19
+ # result.id => "/topic/en/the_police"
20
+ class FreebaseResult
21
+
22
+ attr_accessor :result
23
+
24
+ def initialize(result)
25
+ @result = result.symbolize_keys!
26
+ end
27
+
28
+ def id
29
+ @result[:id]
30
+ end
31
+
32
+ # result.type is reserved in ruby. Call result.fb_type to access :type instead.
33
+ def fb_type
34
+ @result[:type]
35
+ end
36
+
37
+ # returns the first element of an array if it is one
38
+ # this for handling generic mql queries like [{}] that return only a single value
39
+ def depluralize(v)
40
+ Array(v).first
41
+ end
42
+
43
+ # converts a returned value from freebase into the corresponding ruby object
44
+ # This is done first by the core data type and then by the type attribute for an object
45
+ # The casing is done using a method dispatch pattern which
46
+ # should make it easy to mix-in new behaviors and type support
47
+ def resultify(v)
48
+ resultify_method = "resultify_#{v.class.to_s.downcase}".to_sym
49
+ v = send(resultify_method, v) if respond_to? resultify_method
50
+ return v
51
+ end
52
+
53
+ # resultifies each value in the array
54
+ def resultify_array(v)
55
+ v.map{|vv| resultify(vv)}
56
+ end
57
+
58
+ # resultifies an object hash
59
+ def resultify_hash(v)
60
+ vtype = indifferent_access(v,:type)
61
+ if value_type? vtype
62
+ resultify_value(vtype,v)
63
+ elsif vtype.blank?
64
+ Logger.debug "What's This: #{v.inspect}"
65
+ FreebaseResult.new(v)
66
+ elsif vtype.is_a? Array
67
+ "Freebase::Types#{vtype.first.classify}".constantize.new(v) #TODO: Union these types
68
+ else
69
+ "Freebase::Types#{vtype.classify}".constantize.new(v)
70
+ end
71
+ end
72
+
73
+ #decides if a type is just an expanded simple value object
74
+ def value_type?(t)
75
+ ['/type/text', '/type/datetime'].include?(t)
76
+ end
77
+
78
+ # dispatches to a value method for the type
79
+ # or returns the simple value if it doesn't exist
80
+ # for example /type/text would dispatch to resultify_value_type_text
81
+ def resultify_value(vtype,v)
82
+ resultify_method = "resultify_value#{vtype.gsub(/\//,'_')}".to_sym
83
+ if respond_to? resultify_method
84
+ send(resultify_method, v)
85
+ else
86
+ indifferent_access(v,:value)
87
+ end
88
+ end
89
+
90
+ #provides method based access to the result properties
91
+ def method_missing(name, *args)
92
+ raise NoMethodError.new(name.to_s) unless args.length == 0
93
+ if @result.has_key?(name)
94
+ resultify @result[name]
95
+ elsif @result.has_key?((singularized_name = name.to_s.singularize.to_sym)) and @result[singularized_name].is_a?(Array)
96
+ resultify @result[singularized_name]
97
+ else
98
+ raise NoMethodError.new(name.to_s)
99
+ end
100
+ end
101
+
102
+ protected
103
+ def indifferent_access(h,k)
104
+ h[k] || h[k.to_s] if (h.has_key?(k) || h.has_key?(k.to_s))
105
+ end
106
+
107
+ end
108
+
109
+ # the configuration class controls access to the freebase.yml configuration file.
110
+ # it will load the rails-environment specific configuration
111
+ class Configuration
112
+
113
+ include Singleton
114
+
115
+ attr_accessor :filename
116
+
117
+ DEFAULTS = {:host => 'sandbox.freebase.com', :debug => true, :trace => false}
118
+
119
+ def initialize
120
+ @configuration = {}.reverse_merge!(DEFAULTS)
121
+ configure_rails if defined?(RAILS_ROOT)
122
+ end
123
+
124
+ def configure_rails
125
+ @filename = "#{RAILS_ROOT}/config/freebase.yml"
126
+ unless File.exists? @filename
127
+ puts "WARNING: #{RAILS_ROOT}/config/freebase.yml configuration file is not found. Using sandbox.freebase.com."
128
+ else
129
+ set_all YAML.load_file(@filename)[RAILS_ENV].symbolize_keys!
130
+ end
131
+ end
132
+
133
+ def set_all(opts = {})
134
+ opts.each {|k,v| self[k] = v}
135
+ end
136
+
137
+ def []=(k,v)
138
+ @configuration[k] = v
139
+ end
140
+
141
+ def [](k)
142
+ @configuration[k]
143
+ end
144
+ end
145
+
146
+ # logging service. Is it a bad idea?
147
+ class Logger
148
+ #TODO: log4r or rails logging?
149
+ [:trace, :debug, :warn, :error].each do |level|
150
+ eval %Q{
151
+ def self.#{level}(message = nil)
152
+ if Configuration.instance[:#{level}]
153
+ puts message || yield
154
+ end
155
+ end
156
+ }
157
+ end
158
+ end
159
+
160
+ SERVICES = { :mqlread => '/api/service/mqlread',
161
+ :mqlwrite => '/api/service/mqlwrite',
162
+ :login => '/api/account/login',
163
+ :upload => '/api/service/upload'
164
+ }
165
+
166
+ # get the service url for the specified service.
167
+ def service_url(svc)
168
+ "http://#{Configuration.instance[:host]}#{SERVICES[svc]}"
169
+ end
170
+
171
+ SERVICES.each_key do |k|
172
+ define_method("#{k}_service_url") do
173
+ service_url(k)
174
+ end
175
+ end
176
+
177
+ # raise an error if the inner response envelope is encoded as an error
178
+ def handle_read_error(inner)
179
+ unless inner['code'].starts_with?('/api/status/ok')
180
+ Logger.error "<<< Received Error: #{inner.inspect}"
181
+ error = inner['messages'][0]
182
+ raise MqlReadError.new(error['code'], error['message'])
183
+ end
184
+ end
185
+
186
+
187
+
188
+ # perform a mqlread and return the results
189
+ # Specify :raw => true if you don't want the results converted into a FreebaseResult object.
190
+ def mqlread(query, options = {})
191
+ Logger.trace {">>> Sending Query: #{query.inspect}"}
192
+ envelope = { :qname => {:query => query }}
193
+ response = http_request mqlread_service_url, :queries => envelope.to_json
194
+ result = ActiveSupport::JSON.decode(response)
195
+ inner = result['qname']
196
+ handle_read_error(inner)
197
+ Logger.trace {"<<< Received Response: #{inner['result'].inspect}"}
198
+ if options[:raw]
199
+ inner['result']
200
+ else
201
+ inner['result'] ? FreebaseResult.new(inner['result']) : nil
202
+ end
203
+ end
204
+
205
+ protected
206
+ def params_to_string(parameters)
207
+ parameters.keys.map {|k| "#{URI.encode(k.to_s)}=#{URI.encode(parameters[k])}" }.join('&')
208
+ end
209
+ def http_request(url, parameters = {})
210
+ params = params_to_string(parameters)
211
+ url << '?'+params unless params.blank?
212
+ returning(Net::HTTP.get_response(URI.parse(url)).body) do |response|
213
+ Logger.trace do
214
+ fname = "#{MD5.md5(params)}.mql"
215
+ open(fname,"w") do |f|
216
+ f << response
217
+ end
218
+ "Wrote response to #{fname}"
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,36 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__)+'/test_helper'
3
+
4
+ class FreebaseTest < Test::Unit::TestCase
5
+ # Replace this with your real tests.
6
+ def test_load_music_artist
7
+ Freebase::Types::Music::Artist
8
+ end
9
+ def test_find_first_artist
10
+ assert_kind_of Freebase::Types::Music::Artist,
11
+ (artist = Freebase::Types::Music::Artist.find(:first,:conditions => {:name => "The Police"}))
12
+ assert_equal 20, artist.albums.size
13
+ end
14
+ def test_association_preloading
15
+ artist = Freebase::Types::Music::Artist.find(:first,
16
+ :conditions => {
17
+ :name => {:value => ARGV[0], :lang => {:name => "English"}},
18
+ :album => [{
19
+ :fb_object => true,
20
+ :name => {:value => nil, :lang => {:name => "English"}},
21
+ :release_date => nil,
22
+ :track => [{
23
+ :fb_object => true,
24
+ :length => nil,
25
+ :name => {:value => nil, :lang => {:name => "English"}}
26
+ }]
27
+ }]
28
+ }
29
+ )
30
+ quiesce(:find, :reload) do
31
+ assert_equal 14, artist.albums.size
32
+ assert_equal 8, artist.albums.first.tracks.size
33
+ #artist.albums.first.producer
34
+ end
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: freebase
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Chris Eppstein
8
+ autorequire: freebase
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-02-03 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.2
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: json
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.1.2
32
+ version:
33
+ description:
34
+ email: chris@eppsteins.net
35
+ executables: []
36
+
37
+ extensions: []
38
+
39
+ extra_rdoc_files:
40
+ - README
41
+ files:
42
+ - lib/core_extensions.rb
43
+ - lib/freebase
44
+ - lib/freebase/api.rb
45
+ - lib/freebase.rb
46
+ - README
47
+ has_rdoc: true
48
+ homepage: http://rubyforge.org/projects/freebaseapi/
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project: freebaseapi
69
+ rubygems_version: 0.9.5
70
+ signing_key:
71
+ specification_version: 2
72
+ summary: Ruby wrapper for the Freebase.com API that makes interacting with freebase.com in your Ruby on Rails application as easy as using Active Record. Freebase is a free, collaborative semantic database.
73
+ test_files:
74
+ - test/freebase_test.rb