freebase 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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