factual-api 0.2 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +5 -0
- data/README.md +6 -12
- data/lib/factual.rb +27 -0
- data/lib/factual/api.rb +52 -0
- data/lib/factual/query.rb +105 -0
- metadata +6 -3
- data/lib/factual_api.rb +0 -239
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -5,8 +5,8 @@ This is the Factual supported Ruby driver for [Factual's public API](http://deve
|
|
5
5
|
# Installation
|
6
6
|
|
7
7
|
gem 'factual-api'
|
8
|
-
require '
|
9
|
-
factual = Factual
|
8
|
+
require 'factual'
|
9
|
+
factual = Factual.new(YOUR_KEY, YOUR_SECRET)
|
10
10
|
|
11
11
|
# Examples
|
12
12
|
|
@@ -37,7 +37,7 @@ This is the Factual supported Ruby driver for [Factual's public API](http://deve
|
|
37
37
|
# 6. Page it
|
38
38
|
query = query.page(2, :per => 10)
|
39
39
|
|
40
|
-
# 7. Finally, get response in
|
40
|
+
# 7. Finally, get response in a hash or array of hashes
|
41
41
|
query.first # return one row
|
42
42
|
query.rows # return many rows
|
43
43
|
|
@@ -56,7 +56,7 @@ This is the Factual supported Ruby driver for [Factual's public API](http://deve
|
|
56
56
|
|
57
57
|
## Resolve
|
58
58
|
|
59
|
-
# Returns resolved entities as
|
59
|
+
# Returns resolved entities as an array of hashes
|
60
60
|
query = factual.resolve("name" => "McDonalds",
|
61
61
|
"address" => "10451 Santa Monica Blvd",
|
62
62
|
"region" => "CA",
|
@@ -67,11 +67,5 @@ This is the Factual supported Ruby driver for [Factual's public API](http://deve
|
|
67
67
|
|
68
68
|
## Schema
|
69
69
|
|
70
|
-
# Returns a
|
71
|
-
factual.table("global").schema
|
72
|
-
|
73
|
-
## Facets
|
74
|
-
|
75
|
-
# Returns number of starbucks in regions thoughout the world
|
76
|
-
factual.table("global").select("country", "region").search("starbucks").min_count(2).facets()
|
77
|
-
|
70
|
+
# Returns a hash of table metadata, including an array of fields
|
71
|
+
factual.table("global").schema
|
data/lib/factual.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
require 'factual/api'
|
3
|
+
require 'factual/query'
|
4
|
+
|
5
|
+
class Factual
|
6
|
+
def initialize(key, secret)
|
7
|
+
@api = Factual::API.new(generate_token(key, secret))
|
8
|
+
end
|
9
|
+
|
10
|
+
def crosswalk(factual_id)
|
11
|
+
Query.new(@api, :crosswalk, "places/crosswalk", :factual_id => factual_id)
|
12
|
+
end
|
13
|
+
|
14
|
+
def resolve(values)
|
15
|
+
Query.new(@api, :resolve, "places/resolve", :values => values)
|
16
|
+
end
|
17
|
+
|
18
|
+
def table(table_id_or_alias)
|
19
|
+
Query.new(@api, :read, "t/#{table_id_or_alias}")
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def generate_token(key, secret)
|
25
|
+
OAuth::AccessToken.new(OAuth::Consumer.new(key, secret))
|
26
|
+
end
|
27
|
+
end
|
data/lib/factual/api.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
class Factual
|
5
|
+
class API
|
6
|
+
API_V3_HOST = "http://api.v3.factual.com"
|
7
|
+
DRIVER_VERSION_TAG = "factual-ruby-driver-1.0"
|
8
|
+
PARAM_ALIASES = { :search => :q }
|
9
|
+
|
10
|
+
def initialize(access_token)
|
11
|
+
@access_token = access_token
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(query)
|
15
|
+
params_with_count = query.params.merge(:include_count => true)
|
16
|
+
handle_request(query.action || :read, query.path, params_with_count)
|
17
|
+
end
|
18
|
+
|
19
|
+
def schema(query)
|
20
|
+
handle_request(:schema, query.path + "/schema", query.params)["view"]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def handle_request(action, path, params)
|
26
|
+
response = make_request(path, params)
|
27
|
+
json = response.body
|
28
|
+
payload = JSON.parse(json)
|
29
|
+
|
30
|
+
raise StandardError.new(payload["message"]) unless payload["status"] == "ok"
|
31
|
+
|
32
|
+
payload["response"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def make_request(path, params)
|
36
|
+
url = "#{API_V3_HOST}/#{path}?#{query_string(params)}"
|
37
|
+
headers = { "X-Factual-Lib" => DRIVER_VERSION_TAG }
|
38
|
+
|
39
|
+
@access_token.get(url, headers)
|
40
|
+
end
|
41
|
+
|
42
|
+
def query_string(params)
|
43
|
+
query_array = params.keys.inject([]) do |array, key|
|
44
|
+
param_alias = PARAM_ALIASES[key.to_sym] || key.to_sym
|
45
|
+
value = params[key].class == Hash ? params[key].to_json : params[key].to_s
|
46
|
+
array << "#{param_alias}=#{URI.escape(value)}"
|
47
|
+
end
|
48
|
+
|
49
|
+
query_array.join("&")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
class Factual
|
2
|
+
class Query
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
DEFAULT_LIMIT = 20
|
6
|
+
VALID_PARAMS = {
|
7
|
+
:read => [ :filters, :search, :geo, :sort, :select, :limit, :offset ],
|
8
|
+
:resolve => [ :values ],
|
9
|
+
:crosswalk => [ :factual_id ],
|
10
|
+
:schema => [ ],
|
11
|
+
:any => [ :include_count ]
|
12
|
+
}
|
13
|
+
|
14
|
+
def initialize(api, action, path, params = {})
|
15
|
+
@api = api
|
16
|
+
@action = action
|
17
|
+
@path = path
|
18
|
+
@params = params
|
19
|
+
@response = nil
|
20
|
+
@schema = nil
|
21
|
+
validate_params
|
22
|
+
end
|
23
|
+
|
24
|
+
# Attribute Readers
|
25
|
+
attr_reader :action
|
26
|
+
|
27
|
+
[:path, :params].each do |attribute|
|
28
|
+
define_method(attribute) do
|
29
|
+
instance_variable_get("@#{attribute}").clone
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Response Methods
|
34
|
+
def each(&block)
|
35
|
+
rows.each { |row| block.call(row) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def last
|
39
|
+
rows.last
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](index)
|
43
|
+
rows[index]
|
44
|
+
end
|
45
|
+
|
46
|
+
def rows
|
47
|
+
(@response ||= @api.execute(self))["data"].clone
|
48
|
+
end
|
49
|
+
|
50
|
+
def total_rows
|
51
|
+
(@response ||= @api.execute(self))["total_row_count"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def schema
|
55
|
+
(@schema ||= @api.schema(self)).clone
|
56
|
+
end
|
57
|
+
|
58
|
+
# Query Modifiers
|
59
|
+
VALID_PARAMS.values.flatten.uniq.each do |param|
|
60
|
+
define_method(param) do |*args|
|
61
|
+
args = args.collect{|arg| arg.is_a?(String) ? arg.strip : arg }
|
62
|
+
value = (args.length == 1) ? args.first : args.join(',')
|
63
|
+
|
64
|
+
new_params = @params.clone
|
65
|
+
new_params[param] = value
|
66
|
+
|
67
|
+
Query.new(@api, @action, @path, new_params)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def sort_desc(*args)
|
72
|
+
columns = args.map { |column| "#{column}:desc" }
|
73
|
+
|
74
|
+
new_params = @params.clone
|
75
|
+
new_params[:sort] = columns.join(',')
|
76
|
+
|
77
|
+
Query.new(@api, @action, @path, new_params)
|
78
|
+
end
|
79
|
+
|
80
|
+
def page(page_number, paging_options = {})
|
81
|
+
limit = (paging_options[:per] || paging_options["per"] || DEFAULT_LIMIT).to_i
|
82
|
+
limit = DEFAULT_LIMIT if limit < 1
|
83
|
+
|
84
|
+
page_number = page_number.to_i
|
85
|
+
page_number = 1 if page_number < 1
|
86
|
+
|
87
|
+
new_params = @params.clone
|
88
|
+
new_params[:limit] = limit
|
89
|
+
new_params[:offset] = (page_number - 1) * limit
|
90
|
+
|
91
|
+
Query.new(@api, @action, @path, new_params)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Validations
|
97
|
+
def validate_params
|
98
|
+
@params.each do |param, val|
|
99
|
+
unless (VALID_PARAMS[@action] + VALID_PARAMS[:any]).include?(param)
|
100
|
+
raise StandardError.new("InvalidArgument #{param} for #{@action}")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
metadata
CHANGED
@@ -2,16 +2,17 @@
|
|
2
2
|
name: factual-api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: "0.
|
5
|
+
version: "0.5"
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
|
+
- Rudiger Lippert
|
8
9
|
- Forrest Cao
|
9
10
|
- Aaron Crow
|
10
11
|
autorequire:
|
11
12
|
bindir: bin
|
12
13
|
cert_chain: []
|
13
14
|
|
14
|
-
date: 2012-01-
|
15
|
+
date: 2012-01-19 00:00:00 +08:00
|
15
16
|
default_executable:
|
16
17
|
dependencies:
|
17
18
|
- !ruby/object:Gem::Dependency
|
@@ -35,7 +36,9 @@ extensions: []
|
|
35
36
|
extra_rdoc_files: []
|
36
37
|
|
37
38
|
files:
|
38
|
-
- lib/
|
39
|
+
- lib/factual/api.rb
|
40
|
+
- lib/factual/query.rb
|
41
|
+
- lib/factual.rb
|
39
42
|
- README.md
|
40
43
|
- CHANGELOG.md
|
41
44
|
has_rdoc: true
|
data/lib/factual_api.rb
DELETED
@@ -1,239 +0,0 @@
|
|
1
|
-
require 'oauth'
|
2
|
-
require 'json'
|
3
|
-
require 'uri'
|
4
|
-
require 'ostruct'
|
5
|
-
|
6
|
-
module Factual
|
7
|
-
class Api
|
8
|
-
API_V3_HOST = "http://api.v3.factual.com"
|
9
|
-
DRIVER_VERSION_TAG = "factual-ruby-driver-1.0"
|
10
|
-
PARAM_ALIASES = { :search => :q }
|
11
|
-
|
12
|
-
# initializers
|
13
|
-
# ----------------
|
14
|
-
def initialize(key, secret)
|
15
|
-
@access_token = OAuth::AccessToken.new(
|
16
|
-
OAuth::Consumer.new(key, secret))
|
17
|
-
end
|
18
|
-
|
19
|
-
# actions
|
20
|
-
# ----------------
|
21
|
-
def crosswalk(factual_id, format = :object)
|
22
|
-
query = Query.new(self, :crosswalk, "places/crosswalk", { :factual_id => factual_id }, format)
|
23
|
-
|
24
|
-
return query
|
25
|
-
end
|
26
|
-
|
27
|
-
def resolve(values, format = :object)
|
28
|
-
query = Query.new(self, :resolve, "places/resolve", { :values => values }, format)
|
29
|
-
|
30
|
-
return query
|
31
|
-
end
|
32
|
-
|
33
|
-
def table(table_id_or_alias, format = :object)
|
34
|
-
query = Query.new(self, :read, "t/#{table_id_or_alias}", Hash.new, format)
|
35
|
-
|
36
|
-
return query
|
37
|
-
end
|
38
|
-
|
39
|
-
# requesting
|
40
|
-
# ----------------
|
41
|
-
def request(path, params)
|
42
|
-
url = "#{API_V3_HOST}/#{path}?#{query_string(params)}"
|
43
|
-
headers = {"X-Factual-Lib" => DRIVER_VERSION_TAG}
|
44
|
-
|
45
|
-
return @access_token.get(url, headers)
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
def query_string(params)
|
51
|
-
arr = []
|
52
|
-
params.each do |param, v|
|
53
|
-
param_alias = PARAM_ALIASES[param.to_sym] || param.to_sym
|
54
|
-
|
55
|
-
v = v.to_json if v.class == Hash
|
56
|
-
arr << "#{param_alias}=#{URI.escape(v.to_s)}"
|
57
|
-
end
|
58
|
-
return arr.join("&")
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
|
-
|
63
|
-
class Query
|
64
|
-
|
65
|
-
# helper functions
|
66
|
-
DEFAULT_LIMIT = 20
|
67
|
-
|
68
|
-
VALID_PARAMS = {
|
69
|
-
:read => [ :filters, :search, :geo, :sort, :select, :limit, :offset ],
|
70
|
-
:resolve => [ :values ],
|
71
|
-
:crosswalk => [ :factual_id ],
|
72
|
-
:facets => [ :filters, :search, :geo, :limit, :select, :min_count ],
|
73
|
-
:schema => [ ],
|
74
|
-
:any => [ :include_count ]
|
75
|
-
}
|
76
|
-
|
77
|
-
attr_accessor :params
|
78
|
-
|
79
|
-
# initializers
|
80
|
-
# ----------------
|
81
|
-
def initialize(api, action, path, params = nil, format = :object)
|
82
|
-
@api = api
|
83
|
-
@action = action
|
84
|
-
@path = path
|
85
|
-
@params = params || Hash.new
|
86
|
-
@format = format
|
87
|
-
end
|
88
|
-
|
89
|
-
# helper functions
|
90
|
-
# ----------------
|
91
|
-
def clone
|
92
|
-
new_query = self.class.new(@api, @action, @path, @params.clone, @format)
|
93
|
-
|
94
|
-
return new_query
|
95
|
-
end
|
96
|
-
|
97
|
-
def set_param(key, value)
|
98
|
-
@params[key] = value
|
99
|
-
end
|
100
|
-
|
101
|
-
# attributes, after 'get'
|
102
|
-
# ----------------
|
103
|
-
def first
|
104
|
-
row_data = read_response["data"].first
|
105
|
-
|
106
|
-
if (@format == :json) # or :object
|
107
|
-
return row_data
|
108
|
-
else
|
109
|
-
return Row.new(row_data)
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
def rows
|
114
|
-
return read_response["data"] if (@format == :json)
|
115
|
-
|
116
|
-
return read_response["data"].collect do |row_data|
|
117
|
-
Row.new(row_data)
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
def total_count
|
122
|
-
read_response["total_row_count"]
|
123
|
-
end
|
124
|
-
|
125
|
-
|
126
|
-
def schema
|
127
|
-
unless @schema_response
|
128
|
-
@path += "/schema"
|
129
|
-
@schema_response = response(:schema)
|
130
|
-
end
|
131
|
-
|
132
|
-
view = @schema_response["view"]
|
133
|
-
fields = view["fields"]
|
134
|
-
|
135
|
-
schema = Table.new(view)
|
136
|
-
if schema && fields
|
137
|
-
schema.fields = fields.collect do |f|
|
138
|
-
Field.new(f)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
|
142
|
-
return schema
|
143
|
-
end
|
144
|
-
|
145
|
-
def facets
|
146
|
-
unless @facets_response
|
147
|
-
@path += "/facets"
|
148
|
-
@facets_response = response(:facets)
|
149
|
-
end
|
150
|
-
columns = @facets_response["data"]
|
151
|
-
|
152
|
-
return Facet.new(columns)
|
153
|
-
end
|
154
|
-
|
155
|
-
# query builder, returns immutable ojbects
|
156
|
-
# ----------------
|
157
|
-
VALID_PARAMS.values.flatten.uniq.each do |param|
|
158
|
-
define_method(param) do |*args|
|
159
|
-
api = self.clone()
|
160
|
-
val = (args.length == 1) ? args.first : args.join(',')
|
161
|
-
|
162
|
-
api.set_param(param, val)
|
163
|
-
|
164
|
-
return api
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
# sugers
|
169
|
-
# ----------------
|
170
|
-
def sort_desc(*args)
|
171
|
-
api = self.clone
|
172
|
-
columns = args.collect{ |col|"#{col}:desc" }
|
173
|
-
api.set_param(:sort, columns.join(','))
|
174
|
-
|
175
|
-
return api
|
176
|
-
end
|
177
|
-
|
178
|
-
def page(page_num, paging_opts = {})
|
179
|
-
limit = (paging_opts[:per] || paging_opts["per"]).to_i
|
180
|
-
limit = DEFAULT_LIMIT if limit < 1
|
181
|
-
|
182
|
-
page_num = page_num.to_i
|
183
|
-
page_num = 1 if page_num < 1
|
184
|
-
offset = (page_num - 1) * limit
|
185
|
-
|
186
|
-
api = self.clone
|
187
|
-
api.set_param(:limit, limit)
|
188
|
-
api.set_param(:offset, offset)
|
189
|
-
|
190
|
-
return api
|
191
|
-
end
|
192
|
-
|
193
|
-
# requesting
|
194
|
-
# ----------------
|
195
|
-
private
|
196
|
-
|
197
|
-
def read_response
|
198
|
-
unless @read_response
|
199
|
-
# always include count for reads
|
200
|
-
@params[:include_count] = true
|
201
|
-
@read_response = response(@action || :read)
|
202
|
-
end
|
203
|
-
|
204
|
-
return @read_response
|
205
|
-
end
|
206
|
-
|
207
|
-
def check_params!(action)
|
208
|
-
@params.each do |param, val|
|
209
|
-
unless (VALID_PARAMS[action] + VALID_PARAMS[:any]).include?(param)
|
210
|
-
raise StandardError.new("InvalidArgument #{param} for #{action}")
|
211
|
-
end
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
def response(action)
|
216
|
-
check_params!(action)
|
217
|
-
|
218
|
-
res = @api.request(@path, @params)
|
219
|
-
|
220
|
-
code = res.code
|
221
|
-
json = res.body
|
222
|
-
payload = JSON.parse(json)
|
223
|
-
|
224
|
-
if payload["status"] == "ok"
|
225
|
-
return payload["response"]
|
226
|
-
else
|
227
|
-
raise StandardError.new(payload["message"])
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
end
|
232
|
-
|
233
|
-
# response classes
|
234
|
-
# ----------------
|
235
|
-
class Row < OpenStruct; end
|
236
|
-
class Facet < OpenStruct; end
|
237
|
-
class Table < OpenStruct; end
|
238
|
-
class Field < OpenStruct; end
|
239
|
-
end
|