ruby-factual 0.0.5 → 0.0.6
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/lib/factual.rb +153 -40
- data/test/unit/adapter.rb +53 -0
- data/test/unit/table.rb +51 -0
- metadata +11 -24
data/lib/factual.rb
CHANGED
@@ -1,26 +1,55 @@
|
|
1
|
-
|
1
|
+
# A Ruby Lib for using Facutal API
|
2
|
+
#
|
3
|
+
# For more information, visit http://github.com/factual/ruby-lib (TODO),
|
4
|
+
# and {Factual Developer Tools}[http://www.factual.com/devtools]
|
5
|
+
#
|
6
|
+
# Author:: Forrest Cao (mailto:forrest@factual.com)
|
7
|
+
# Copyright:: Copyright (c) 2010 {Factual Inc}[http://www.factual.com].
|
8
|
+
# License:: GPL
|
9
|
+
|
10
|
+
require 'net/http'
|
2
11
|
require 'json'
|
3
12
|
require 'uri'
|
4
13
|
|
5
14
|
module Factual
|
15
|
+
# The start point of using Factual API
|
6
16
|
class Api
|
17
|
+
|
18
|
+
# To initialize a Factual::Api, you will have to get an api_key from {Factual Developer Tools}[http://www.factual.com/developers/api_key]
|
19
|
+
#
|
20
|
+
# Params: opts as a hash
|
21
|
+
# * <tt>opts[:api_key]</tt> required
|
22
|
+
# * <tt>opts[:debug]</tt> optional, default is false. If you set it as true, it will print the Factual Api Call URLs on the screen
|
23
|
+
# * <tt>opts[:version]</tt> optional, default value is 2, just do not change it
|
24
|
+
# * <tt>opts[:domain]</tt> optional, default value is www.factual.com (only configurable by Factual employees)
|
25
|
+
#
|
26
|
+
# Sample:
|
27
|
+
# api = Factual::Api.new(:api_key => MY_API_KEY, :debug => true)
|
7
28
|
def initialize(opts)
|
8
29
|
@api_key = opts[:api_key]
|
9
|
-
@version = opts[:version]
|
30
|
+
@version = opts[:version] || 2
|
10
31
|
@domain = opts[:domain] || 'www.factual.com'
|
11
|
-
@
|
32
|
+
@debug = opts[:debug]
|
33
|
+
|
34
|
+
@adapter = Adapter.new(@api_key, @version, @domain, @debug)
|
12
35
|
end
|
13
36
|
|
37
|
+
# Get a Factual::Table object by inputting the table_key
|
38
|
+
#
|
39
|
+
# Sample:
|
40
|
+
# api.get_table('g9R1u2')
|
14
41
|
def get_table(table_key)
|
15
42
|
Table.new(table_key, @adapter)
|
16
43
|
end
|
17
44
|
end
|
18
45
|
|
46
|
+
# This class holds the metadata of a Factual table. The filter and sort methods are to filter and/or sort
|
47
|
+
# the table data before calling a find_one or each_row.
|
19
48
|
class Table
|
20
|
-
attr_accessor :name, :description, :rating, :source, :creator, :total_row_count, :created_at, :updated_at, :fields, :geo_enabled, :downloadable
|
21
|
-
|
49
|
+
attr_accessor :name, :key, :description, :rating, :source, :creator, :total_row_count, :created_at, :updated_at, :fields, :geo_enabled, :downloadable
|
50
|
+
attr_reader :adapter # :nodoc:
|
22
51
|
|
23
|
-
def initialize(table_key, adapter)
|
52
|
+
def initialize(table_key, adapter) # :nodoc:
|
24
53
|
@table_key = table_key
|
25
54
|
@adapter = adapter
|
26
55
|
@schema = adapter.schema(@table_key)
|
@@ -37,54 +66,92 @@ module Factual
|
|
37
66
|
end
|
38
67
|
end
|
39
68
|
|
69
|
+
# Define table filters, it can be chained before +find_one+ or +each_row+.
|
70
|
+
#
|
71
|
+
# The params can be:
|
72
|
+
# * simple hash for equal filter
|
73
|
+
# * nested hash with filter operators
|
74
|
+
#
|
75
|
+
# Samples:
|
76
|
+
# table.filter(:state => 'CA').find_one # hash
|
77
|
+
# table.filter(:state => 'CA', :city => 'LA').find_one # multi-key hash
|
78
|
+
# table.filter(:state => {"$has" => 'A'}).find_one # nested hash
|
79
|
+
# table.filter(:state => {"$has" => 'A'}, :city => {"$ew" => 'A'}).find_one # multi-key nested hashes
|
80
|
+
#
|
81
|
+
# For more detail inforamtion about filter syntax, please look up at {Server API Doc for Filter}[http://wiki.developer.factual.com/Filter]
|
40
82
|
def filter(filters)
|
41
83
|
@filters = filters
|
42
84
|
return self
|
43
85
|
end
|
44
86
|
|
45
|
-
|
87
|
+
# Define table sorts, it can be chained before +find_one+ or +each_row+.
|
88
|
+
#
|
89
|
+
# The params can be:
|
90
|
+
# * a hash with single key
|
91
|
+
# * single-key hashes, only the first 2 sorts will work. (secondary sort will be supported in next release of Factual API)
|
92
|
+
#
|
93
|
+
# Samples:
|
94
|
+
# table.sort(:state => 1).find_one # hash with single key
|
95
|
+
# table.sort({:state => 1}, {:abbr => -1}).find_one # single-key hashes
|
96
|
+
# For more detail inforamtion about sort syntax, please look up at {Server API Doc for Sort (TODO)}[http://wiki.developer.factual.com/Sort]
|
97
|
+
def sort(*sorts)
|
46
98
|
@sorts = sorts
|
47
99
|
return self
|
48
100
|
end
|
49
101
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
102
|
+
# Find the first row (a Factual::Row object) of the table with filters and/or sorts.
|
103
|
+
#
|
104
|
+
# Samples:
|
105
|
+
# * <tt>table.filter(:state => 'CA').find_one</tt>
|
106
|
+
# * <tt>table.filter(:state => 'CA').sort(:city => 1).find_one</tt>
|
107
|
+
def find_one
|
108
|
+
resp = @adapter.read_table(@table_key, @filters, @sorts, 1)
|
109
|
+
row_data = resp["data"].first
|
110
|
+
|
111
|
+
if row_data
|
112
|
+
return Row.new(self, row_data)
|
113
|
+
else
|
114
|
+
return nil
|
56
115
|
end
|
116
|
+
end
|
57
117
|
|
58
|
-
|
118
|
+
# An iterator on each row (a Factual::Row object) of the filtered and/or sorted table data
|
119
|
+
#
|
120
|
+
# Samples:
|
121
|
+
# table.filter(:state => 'CA').sort(:city => 1).each do |row|
|
122
|
+
# puts row.inspect
|
123
|
+
# end
|
124
|
+
def each_row
|
125
|
+
resp = @adapter.read_table(@table_key, @filters, @sorts)
|
59
126
|
|
60
|
-
@total_rows = resp["
|
61
|
-
rows = resp["
|
127
|
+
@total_rows = resp["total_rows"]
|
128
|
+
rows = resp["data"]
|
62
129
|
|
63
|
-
# TODO iterator
|
64
130
|
rows.each do |row_data|
|
65
131
|
row = Row.new(self, row_data)
|
66
132
|
yield(row) if block_given?
|
67
133
|
end
|
68
134
|
end
|
69
135
|
|
70
|
-
|
71
|
-
|
72
|
-
def get_field_id(field_ref)
|
73
|
-
@fields.each do |f|
|
74
|
-
return f['id'] if f['field_ref'] == field_ref.to_s
|
75
|
-
end
|
136
|
+
# TODO
|
137
|
+
def add_row(values)
|
76
138
|
end
|
77
139
|
|
140
|
+
private
|
141
|
+
|
78
142
|
def camelize(str)
|
79
143
|
s = str.to_s.split("_").collect{ |w| w.capitalize }.join
|
80
144
|
s[0].chr.downcase + s[1..-1]
|
81
145
|
end
|
82
146
|
end
|
83
147
|
|
148
|
+
# This class holds the subject_key, subject (in array) and facts (Factual::Fact objects) of a Factual Subject.
|
149
|
+
#
|
150
|
+
# The subject_key and subject array can be accessable directly from attributes, and you can get a fact by <tt>row[field_ref]</tt>.
|
84
151
|
class Row
|
85
|
-
|
152
|
+
attr_reader :subject_key, :subject
|
86
153
|
|
87
|
-
def initialize(table, row_data)
|
154
|
+
def initialize(table, row_data) # :nodoc:
|
88
155
|
@subject_key = row_data[0]
|
89
156
|
|
90
157
|
@table = table
|
@@ -105,6 +172,11 @@ module Factual
|
|
105
172
|
end
|
106
173
|
end
|
107
174
|
|
175
|
+
# Get a Factual::Fact object by field_ref
|
176
|
+
#
|
177
|
+
# Sample:
|
178
|
+
# city_info = table.filter(:state => 'CA').find_one
|
179
|
+
# city_info['city_name']
|
108
180
|
def [](field_ref)
|
109
181
|
@facts_hash[field_ref]
|
110
182
|
end
|
@@ -115,10 +187,11 @@ module Factual
|
|
115
187
|
end
|
116
188
|
end
|
117
189
|
|
190
|
+
# This class holds the subject_key, value, field_ref field (field metadata in hash). The input method is for suggesting a new value for the fact.
|
118
191
|
class Fact
|
119
|
-
|
192
|
+
attr_reader :value, :subject_key, :field_ref, :field
|
120
193
|
|
121
|
-
def initialize(table, subject_key, field, value)
|
194
|
+
def initialize(table, subject_key, field, value) # :nodoc:
|
122
195
|
@value = value
|
123
196
|
@field = field
|
124
197
|
@subject_key = subject_key
|
@@ -127,64 +200,104 @@ module Factual
|
|
127
200
|
@adapter = table.adapter
|
128
201
|
end
|
129
202
|
|
130
|
-
def field_ref
|
203
|
+
def field_ref # :nodoc:
|
131
204
|
@field["field_ref"]
|
132
205
|
end
|
133
206
|
|
207
|
+
# To input a new value to the fact
|
208
|
+
#
|
209
|
+
# Parameters:
|
210
|
+
# * +value+
|
211
|
+
# * <tt>opts[:source]</tt> the source of an input, can be a URL or something else
|
212
|
+
# * <tt>opts[:comment]</tt> the comment of an input
|
213
|
+
#
|
214
|
+
# Sample:
|
215
|
+
# fact.input('new value', :source => 'http://website.com', :comment => 'because it is new value.'
|
134
216
|
def input(value, opts={})
|
135
217
|
return false if value.nil?
|
136
218
|
|
137
219
|
hash = opts.merge({
|
138
220
|
:subjectKey => @subject_key,
|
139
|
-
:fieldId
|
140
|
-
:value
|
221
|
+
:fieldId => @field['id'],
|
222
|
+
:value => value
|
141
223
|
})
|
142
|
-
query_string = hash.to_a.collect{ |k,v| URI.escape(k.to_s) + '=' + URI.escape(v.to_s) }.join('&')
|
143
224
|
|
144
|
-
@adapter.
|
225
|
+
@adapter.input(@table_key, hash)
|
145
226
|
return true
|
146
227
|
end
|
147
228
|
|
229
|
+
# Just return the value
|
148
230
|
def to_s
|
149
231
|
@value
|
150
232
|
end
|
151
233
|
|
234
|
+
# Just return the value
|
152
235
|
def inspect
|
153
236
|
@value
|
154
237
|
end
|
155
238
|
end
|
156
239
|
|
157
240
|
|
158
|
-
class Adapter
|
241
|
+
class Adapter # :nodoc:
|
159
242
|
CONNECT_TIMEOUT = 30
|
160
243
|
|
161
|
-
def initialize(api_key, version, domain)
|
162
|
-
@
|
244
|
+
def initialize(api_key, version, domain, debug=false)
|
245
|
+
@domain = domain
|
246
|
+
@base = "/api/v#{version}/#{api_key}"
|
247
|
+
@debug = debug
|
163
248
|
end
|
164
249
|
|
165
250
|
def api_call(url)
|
166
251
|
api_url = @base + url
|
252
|
+
puts "[Factual API Call] http://#{@domain}#{api_url}" if @debug
|
167
253
|
|
254
|
+
json = "{}"
|
168
255
|
begin
|
169
|
-
|
170
|
-
|
256
|
+
Net::HTTP.start(@domain, 80) do |http|
|
257
|
+
response = http.get(api_url)
|
258
|
+
json = response.body
|
171
259
|
end
|
172
|
-
curl.http_get
|
173
260
|
rescue Exception => e
|
174
261
|
raise ApiError.new(e.to_s + " when getting " + api_url)
|
175
262
|
end
|
176
263
|
|
177
|
-
resp = JSON.parse(
|
264
|
+
resp = JSON.parse(json)
|
178
265
|
raise ApiError.new(resp["error"]) if resp["status"] == "error"
|
179
266
|
return resp
|
180
267
|
end
|
181
268
|
|
182
269
|
def schema(table_key)
|
183
|
-
|
270
|
+
url = "/tables/#{table_key}/schema.json"
|
271
|
+
resp = api_call(url)
|
272
|
+
|
184
273
|
return resp["schema"]
|
185
274
|
end
|
275
|
+
|
276
|
+
def read_table(table_key, filters=nil, sorts=nil, limit=999)
|
277
|
+
filters_query = "&filters=" + filters.to_json if filters
|
278
|
+
|
279
|
+
if sorts
|
280
|
+
sorts = sorts[0] if sorts.length == 1
|
281
|
+
sorts_query = "&sort=" + sorts.to_json
|
282
|
+
end
|
283
|
+
|
284
|
+
url = "/tables/#{table_key}/read.jsaml?limit=#{limit}" + filters_query.to_s + sorts_query.to_s
|
285
|
+
resp = api_call(url)
|
286
|
+
|
287
|
+
return resp["response"]
|
288
|
+
end
|
289
|
+
|
290
|
+
def input(table_key, params)
|
291
|
+
query_string = params.to_a.collect{ |k,v| URI.escape(k.to_s) + '=' + URI.escape(v.to_s) }.join('&')
|
292
|
+
|
293
|
+
url = "/tables/#{table_key}/input.js?" + query_string
|
294
|
+
resp = api_call(url)
|
295
|
+
|
296
|
+
return resp['response']
|
297
|
+
end
|
186
298
|
end
|
187
299
|
|
300
|
+
# Exception class for Factual Api Errors
|
188
301
|
class ApiError < Exception
|
189
302
|
end
|
190
303
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'test/unit/helper'
|
2
|
+
require 'lib/factual'
|
3
|
+
|
4
|
+
class AdapterTest < Factual::TestCase
|
5
|
+
def setup
|
6
|
+
@adapter = Factual::Adapter.new(API_KEY, API_VERSION, API_DOMAIN, DEBUG_MODE)
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_corret_request
|
10
|
+
url = "/tables/#{TABLE_KEY}/schema.json"
|
11
|
+
|
12
|
+
assert_nothing_raised do
|
13
|
+
resp = @adapter.api_call(url)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_wrong_request
|
18
|
+
url = "/tables/#{WRONG_KEY}/schema.json"
|
19
|
+
|
20
|
+
assert_raise Factual::ApiError do
|
21
|
+
resp = @adapter.api_call(url)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_getting_schema
|
26
|
+
schema = @adapter.schema(TABLE_KEY)
|
27
|
+
|
28
|
+
assert_not_nil schema
|
29
|
+
assert_equal schema['name'], TABLE_NAME
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_reading_table
|
33
|
+
resp = @adapter.read_table(TABLE_KEY)
|
34
|
+
assert_equal resp['total_rows'], TOTAL_ROWS
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_reading_table_with_filter
|
38
|
+
resp = @adapter.read_table(TABLE_KEY, {:two_letter_abbrev => 'CA'})
|
39
|
+
assert_equal resp['total_rows'], 1
|
40
|
+
end
|
41
|
+
|
42
|
+
def test_inputting
|
43
|
+
params = {
|
44
|
+
:subjectKey => SUBJECT_KEY,
|
45
|
+
:value => 'sample text',
|
46
|
+
:fieldId => STATE_FIELD_ID
|
47
|
+
}
|
48
|
+
|
49
|
+
assert_raise Factual::ApiError do
|
50
|
+
@adapter.input(TABLE_KEY, params)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/test/unit/table.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'lib/factual'
|
2
|
+
require 'test/unit/helper'
|
3
|
+
|
4
|
+
class TableTest < Factual::TestCase
|
5
|
+
def setup
|
6
|
+
api = Factual::Api.new(:api_key => API_KEY, :debug => DEBUG_MODE)
|
7
|
+
|
8
|
+
@table = api.get_table(TABLE_KEY)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_metadata
|
12
|
+
assert_equal @table.name, TABLE_NAME
|
13
|
+
assert_equal @table.creator, TABLE_OWNER
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_each_row
|
17
|
+
states = []
|
18
|
+
@table.each_row do |state_info|
|
19
|
+
fact = state_info['state']
|
20
|
+
states << fact.value
|
21
|
+
end
|
22
|
+
|
23
|
+
assert_equal states.length, TOTAL_ROWS
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_filtering
|
27
|
+
row = @table.filter(:two_letter_abbrev => 'WA').find_one
|
28
|
+
assert_equal row["state"].value, "Washington"
|
29
|
+
|
30
|
+
row = @table.filter(:two_letter_abbrev => { '$has' => 'a' }).sort(:state => 1).find_one
|
31
|
+
assert_equal row["state"].value, "California"
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_sorting
|
35
|
+
row = @table.sort(:state => 1).find_one
|
36
|
+
assert_equal row["state"].value, "California"
|
37
|
+
|
38
|
+
assert_raise Factual::ApiError do
|
39
|
+
# secondary sort will be supported in next release
|
40
|
+
row = @table.sort({:state => 1}, {:test_field1 => 1}).find_one
|
41
|
+
row["state"].value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_row
|
46
|
+
row = @table.find_one
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_fact
|
50
|
+
end
|
51
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-factual
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 6
|
10
|
+
version: 0.0.6
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Forrest Cao
|
@@ -15,29 +15,13 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-10-20 00:00:00 +08:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
|
-
- !ruby/object:Gem::Dependency
|
22
|
-
name: curb
|
23
|
-
prerelease: false
|
24
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
|
-
requirements:
|
27
|
-
- - ">="
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
hash: 27
|
30
|
-
segments:
|
31
|
-
- 0
|
32
|
-
- 3
|
33
|
-
- 4
|
34
|
-
version: 0.3.4
|
35
|
-
type: :runtime
|
36
|
-
version_requirements: *id001
|
37
21
|
- !ruby/object:Gem::Dependency
|
38
22
|
name: json
|
39
23
|
prerelease: false
|
40
|
-
requirement: &
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
41
25
|
none: false
|
42
26
|
requirements:
|
43
27
|
- - ">="
|
@@ -49,7 +33,7 @@ dependencies:
|
|
49
33
|
- 0
|
50
34
|
version: 1.2.0
|
51
35
|
type: :runtime
|
52
|
-
version_requirements: *
|
36
|
+
version_requirements: *id001
|
53
37
|
description: ""
|
54
38
|
email: forrest@factual.com
|
55
39
|
executables: []
|
@@ -61,6 +45,8 @@ extra_rdoc_files:
|
|
61
45
|
files:
|
62
46
|
- README.md
|
63
47
|
- lib/factual.rb
|
48
|
+
- test/unit/adapter.rb
|
49
|
+
- test/unit/table.rb
|
64
50
|
has_rdoc: true
|
65
51
|
homepage: http://github.com/forrestc/ruby-factual
|
66
52
|
licenses: []
|
@@ -101,5 +87,6 @@ rubygems_version: 1.3.7
|
|
101
87
|
signing_key:
|
102
88
|
specification_version: 3
|
103
89
|
summary: ""
|
104
|
-
test_files:
|
105
|
-
|
90
|
+
test_files:
|
91
|
+
- test/unit/adapter.rb
|
92
|
+
- test/unit/table.rb
|