precog 1.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/precog.rb +385 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0f07093cd36069946ad7110456b91cc58ba009a8
|
4
|
+
data.tar.gz: e756fc697131e4a30341836595b3bb1a5a2c21fa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 206dfe29cdd566d09404b9b9af31f9f346dfb71086f3db615a7b22c9f2e7ce9e97df792b0f442a2839074b3df7bc002199df9b169772eb12c29229d4dee507de
|
7
|
+
data.tar.gz: 9ce1006e3aeb2385fbc5ebbd6e5ec580c9745dc51dedbf2925193c8e17ca1e69bbafb670bb57ee7da9ad3d66cc4833f7a57bd9172040541c570920bc7785c9ae
|
data/lib/precog.rb
ADDED
@@ -0,0 +1,385 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/https'
|
5
|
+
|
6
|
+
require 'json'
|
7
|
+
|
8
|
+
module Precog
|
9
|
+
|
10
|
+
# The Precog Beta HTTPS service. This is also the default Precog service
|
11
|
+
# used when one isn't specified. If you signed up for a beta account, this
|
12
|
+
# is the services you'll want to use.
|
13
|
+
DEFAULT_HOST = 'beta.precog.com'
|
14
|
+
|
15
|
+
# Precog API version being used.
|
16
|
+
VERSION = 1
|
17
|
+
|
18
|
+
# Struct used to flag use of the CSV format with the specified line separator,
|
19
|
+
# record delimiter and literal quote. The default separator is <tt>\n</tt>, the
|
20
|
+
# default delimiter is <tt>,</tt>, while the default quote is <tt>"</tt>.
|
21
|
+
# These defaults are precisely what are specified in the DEFAULT_CSV value.
|
22
|
+
CSV = Struct.new :separator, :delimiter, :quote
|
23
|
+
DEFAULT_CSV = CSV.new("\n", ',', '"')
|
24
|
+
|
25
|
+
# Struct used to return account information from the Client class.
|
26
|
+
AccountInfo = Struct.new :api_key, :account_id, :email
|
27
|
+
|
28
|
+
# A simple REST client for storing data in Precog and querying it with Quirrel.
|
29
|
+
#
|
30
|
+
# This provides methods to upload files to
|
31
|
+
# your virtual file system (in Precog), append records/events to the VFS,
|
32
|
+
# delete data, and run Quirrel queries on your data. Additionally, you can
|
33
|
+
# also create new accounts and get the account details for existing accounts.
|
34
|
+
#
|
35
|
+
# All methods are blocking, which means that the method returns when the
|
36
|
+
# server has replied with the answer.
|
37
|
+
class Client
|
38
|
+
attr_reader :api_key, :account_id, :host, :base_path, :port
|
39
|
+
|
40
|
+
# Builds a new client to connect to precog services. Accepts an optional
|
41
|
+
# +Hash+ of options of the following type:
|
42
|
+
#
|
43
|
+
# * :host (String) The Precog service endpoint (default: <tt>'beta.precog.com'</tt>)
|
44
|
+
# * :base_path (String) The default root for all actions (default: <tt>"#{account_id}/"</tt>)
|
45
|
+
# * :secure (Boolean) Flag indicating whether or not to use HTTPS (default: +true+)
|
46
|
+
# * :port (Fixnum) Service endpoint port to use (default: <tt>443</tt>)
|
47
|
+
#
|
48
|
+
# Arguments:
|
49
|
+
# api_key: (String)
|
50
|
+
# account_id: (String)
|
51
|
+
# options: (Hash)
|
52
|
+
def initialize(api_key, account_id, options = {})
|
53
|
+
options[:host] ||= DEFAULT_HOST
|
54
|
+
options[:base_path] ||= "#{account_id}/"
|
55
|
+
options[:secure] = true if options[:secure].nil?
|
56
|
+
options[:port] ||= options[:secure] ? 443 : 80
|
57
|
+
|
58
|
+
@api_key = api_key
|
59
|
+
@account_id = account_id
|
60
|
+
@host = options[:host]
|
61
|
+
@base_path = options[:base_path]
|
62
|
+
@secure = options[:secure]
|
63
|
+
@port = options[:port]
|
64
|
+
end
|
65
|
+
|
66
|
+
def secure?
|
67
|
+
@secure
|
68
|
+
end
|
69
|
+
|
70
|
+
# Creates a new account ID, accessible by the specified email address and
|
71
|
+
# password, or returns the existing account ID. You _must_ provide a
|
72
|
+
# service that uses HTTPS to use this service, otherwise an exception will
|
73
|
+
# be thrown. Returns an instance of AccountInfo.
|
74
|
+
#
|
75
|
+
# Accepts the same options as new.
|
76
|
+
#
|
77
|
+
# Arguments:
|
78
|
+
# email: (String)
|
79
|
+
# password: (String)
|
80
|
+
# profile: (Object)
|
81
|
+
# options: (Hash)
|
82
|
+
def self.create_account(email, password, profile, options = {})
|
83
|
+
stub_client = Client.new(nil, nil, options) # invalid stub client not meant to escape
|
84
|
+
|
85
|
+
raise 'create_account requires https' unless stub_client.secure?
|
86
|
+
|
87
|
+
body = {
|
88
|
+
:email => email,
|
89
|
+
:password => password,
|
90
|
+
:profile => profile
|
91
|
+
}
|
92
|
+
|
93
|
+
results = JSON.parse Precog.post(stub_client, "/accounts/v#{VERSION}/accounts/", body.to_json, {}).body
|
94
|
+
account_id = results["accountId"]
|
95
|
+
account_details(email, password, account_id, options)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Retrieves the details about a particular account. This call is the
|
99
|
+
# primary mechanism by which you can retrieve your master API key. You
|
100
|
+
# *must* provide a service that uses HTTPS to use this service,
|
101
|
+
# otherwise an exception will be thrown. Returns an instance of AccountInfo.
|
102
|
+
#
|
103
|
+
# Accepts the same options as new.
|
104
|
+
#
|
105
|
+
# Arguments:
|
106
|
+
# email: (String)
|
107
|
+
# password: (String)
|
108
|
+
# account_id: (String)
|
109
|
+
# options: (Hash)
|
110
|
+
def self.account_details(email, password, account_id, options = {})
|
111
|
+
stub_client = Client.new(nil, account_id, options) # invalid stub client not meant to escape
|
112
|
+
|
113
|
+
raise 'account_details requires https' unless stub_client.secure?
|
114
|
+
|
115
|
+
resp = Precog.get_auth(stub_client, "/accounts/v#{VERSION}/accounts/#{account_id}", { 'Content-Type' => 'application/json' }, email, password)
|
116
|
+
results = JSON.parse resp.body
|
117
|
+
AccountInfo.new(results["apiKey"], account_id, email)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Store the object data as a record in Precog. It is serialized by the
|
121
|
+
# +to_json+ function. Returns a pair, <tt>[ingested, errors]</tt>, consisting of
|
122
|
+
# a +Fixnum+ and an +Array+ of errors.
|
123
|
+
#
|
124
|
+
# Note: Calling this method guarantees the object is stored in the Precog
|
125
|
+
# transaction log.
|
126
|
+
#
|
127
|
+
# Arguments:
|
128
|
+
# path: (String)
|
129
|
+
# data: (Object)
|
130
|
+
def append(path, data)
|
131
|
+
append_all(path, [data])
|
132
|
+
end
|
133
|
+
|
134
|
+
# Append a collection of records in Precog.
|
135
|
+
#
|
136
|
+
# Arguments:
|
137
|
+
# path: (String)
|
138
|
+
# collection: (Array)
|
139
|
+
def append_all(path, collection)
|
140
|
+
append_raw(path, :json, collection.to_json)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Appends all the events in +data+, a string whose format
|
144
|
+
# is described by the +format+ argument, to path in the virtual
|
145
|
+
# file-system. The +format+ must be in the following set:
|
146
|
+
#
|
147
|
+
# * :json (raw, well-formed JSON)
|
148
|
+
# * :json_stream (well-formed JSON values separated by newlines)
|
149
|
+
# * :csv (comma-separated values, delimited by newlines using the " character for quoting)
|
150
|
+
# * <tt>CSV.new(...)</tt> (an instance of the CSV struct)
|
151
|
+
def append_raw(path, format, data)
|
152
|
+
path = relativize_path path
|
153
|
+
|
154
|
+
content_type = if format == :json then
|
155
|
+
'application/json'
|
156
|
+
elsif format == :json_stream
|
157
|
+
'application/x-json-stream'
|
158
|
+
elsif CSV === format
|
159
|
+
'text/csv'
|
160
|
+
elsif format == :csv
|
161
|
+
format = DEFAULT_CSV
|
162
|
+
'text/csv'
|
163
|
+
else
|
164
|
+
raise "invalid format: #{format}" # todo
|
165
|
+
end
|
166
|
+
|
167
|
+
csv_params = if CSV == format then
|
168
|
+
{ :escape => CSV.escape, :delimiter => CSV.delimiter, :quote => CSV.quote }
|
169
|
+
else
|
170
|
+
{}
|
171
|
+
end
|
172
|
+
|
173
|
+
header = { 'Content-Type' => content_type }
|
174
|
+
params = { :apiKey => api_key, :receipt => true, :mode => 'batch' }.merge csv_params
|
175
|
+
resp = Precog.post(self, "/ingest/v#{VERSION}/fs/#{path}", data, header, params)
|
176
|
+
|
177
|
+
results = JSON.parse resp.body
|
178
|
+
[results["ingested"], results["errors"]]
|
179
|
+
end
|
180
|
+
|
181
|
+
# Appends all the events in +file+, a file whose format is described by
|
182
|
+
# +format+ (see append_raw), to +path+ in the virtual file-system.
|
183
|
+
#
|
184
|
+
# For instance, to ingest a CSV file, you could do something like:
|
185
|
+
#
|
186
|
+
# client = Precog.new(api_key, account_id)
|
187
|
+
# csv_file = '/path/to/my.csv'
|
188
|
+
# client.append_from_file('my.csv', :csv, csv_file)
|
189
|
+
def append_from_file(path, format, file)
|
190
|
+
path = relativize_path path
|
191
|
+
|
192
|
+
content_type = if format == :json then
|
193
|
+
'application/json'
|
194
|
+
elsif format == :json_stream
|
195
|
+
'application/x-json-stream'
|
196
|
+
elsif CSV === format
|
197
|
+
'text/csv'
|
198
|
+
elsif format == :csv
|
199
|
+
format = DEFAULT_CSV
|
200
|
+
'text/csv'
|
201
|
+
else
|
202
|
+
raise "invalid format: #{format}" # todo
|
203
|
+
end
|
204
|
+
|
205
|
+
csv_params = if CSV == format then
|
206
|
+
{ :escape => CSV.escape, :delimiter => CSV.delimiter, :quote => CSV.quote }
|
207
|
+
else
|
208
|
+
{}
|
209
|
+
end
|
210
|
+
|
211
|
+
header = { 'Content-Type' => content_type }
|
212
|
+
params = { :apiKey => api_key, :receipt => true, :mode => 'batch' }.merge csv_params
|
213
|
+
|
214
|
+
File.open(file, 'r') do |stream|
|
215
|
+
back = []
|
216
|
+
stream.each_line do |chunk|
|
217
|
+
resp = Precog.post(self, "/ingest/v#{VERSION}/fs/#{path}", chunk, header, params)
|
218
|
+
|
219
|
+
results = JSON.parse resp.body
|
220
|
+
back << [results["ingested"], results["errors"]]
|
221
|
+
end
|
222
|
+
back
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Uploads the records in +file+ to +path+. This is equivalent
|
227
|
+
# to first _deleting the data_ at the VFS path (using delete), then
|
228
|
+
# calling append_from_file.
|
229
|
+
def upload_file(path, format, file)
|
230
|
+
delete path
|
231
|
+
append_from_file(path, format, file)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Deletes the data stored at the specified path. This does NOT do a
|
235
|
+
# recursive delete. It'll only delete the data the path specified, all
|
236
|
+
# other data in sub-paths of +path+ will remain intact.
|
237
|
+
def delete(path)
|
238
|
+
path = relativize_path path
|
239
|
+
|
240
|
+
Precog.connect self do |http|
|
241
|
+
uri = Addressable::URI.new
|
242
|
+
uri.query_values = { :apiKey => api_key }
|
243
|
+
|
244
|
+
http.delete "/ingest/v#{VERSION}/fs/#{path}?#{uri.query}"
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Executes a synchronous query relative to the specified base path. The
|
249
|
+
# HTTP connection will remain open for as long as the query is evaluating
|
250
|
+
# (potentially minutes).
|
251
|
+
#
|
252
|
+
# Not recommended for long-running queries, because if the connection is
|
253
|
+
# interrupted, there will be no way to retrieve the results of the query.
|
254
|
+
#
|
255
|
+
# Returns a triple of errors, warnings and an +Array+ of data representing
|
256
|
+
# the query results.
|
257
|
+
def query(path, query)
|
258
|
+
path = relativize_path path
|
259
|
+
params = { :apiKey => api_key, :q => query, :format => 'detailed' }
|
260
|
+
resp = Precog.get(self, "/analytics/v#{VERSION}/fs/#{path}", { 'Content-Type' => 'application/json' }, params)
|
261
|
+
output = JSON.parse resp.body
|
262
|
+
[output["errors"], output["warnings"], output["data"]]
|
263
|
+
end
|
264
|
+
|
265
|
+
# Runs an asynchronous query against Precog. An async query is a query
|
266
|
+
# that simply returns a Job ID, rather than the query results. You can
|
267
|
+
# then periodically poll for the results of the job/query.
|
268
|
+
#
|
269
|
+
# This does _NOT_ run the query in a new thread. It will still block
|
270
|
+
# the current thread until the server responds.
|
271
|
+
#
|
272
|
+
# An example of using query_async to poll for results
|
273
|
+
# could look like:
|
274
|
+
#
|
275
|
+
# client = ...
|
276
|
+
# query = client.query_async("foo/", "min(//bar)")
|
277
|
+
#
|
278
|
+
# result = nil
|
279
|
+
# result = query.parsed until result
|
280
|
+
# errors, warnings, data = result
|
281
|
+
# min = data.first
|
282
|
+
# puts "Minimum is: #{min}"
|
283
|
+
#
|
284
|
+
# This is ideal for long running queries.
|
285
|
+
#
|
286
|
+
# Returns a Query object.
|
287
|
+
def query_async(path, query)
|
288
|
+
path = relativize_path path
|
289
|
+
params = { :apiKey => api_key, :q => query, :prefixPath => path }
|
290
|
+
resp = Precog.post(self, "/analytics/v#{VERSION}/queries", '', { 'Content-Type' => 'application/json' }, params)
|
291
|
+
output = JSON.parse resp.body
|
292
|
+
Query.new(self, output['jobId'])
|
293
|
+
end
|
294
|
+
|
295
|
+
private
|
296
|
+
|
297
|
+
def relativize_path(path)
|
298
|
+
(base_path + path).gsub(/\/+/, '/')
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# Accessor object for a currently-running asynchronous query. This class
|
303
|
+
# provides methods to access the status of the job, as well as retrieve the
|
304
|
+
# results once the job has completed.
|
305
|
+
class Query
|
306
|
+
attr_reader :client, :qid
|
307
|
+
|
308
|
+
# Creates a new Query with the given Client and batch query ID. This is
|
309
|
+
# invoked by the query_async method in Client.
|
310
|
+
#
|
311
|
+
# Arguments:
|
312
|
+
# client: (Client)
|
313
|
+
# qid: (String)
|
314
|
+
def initialize(client, qid)
|
315
|
+
@client = client
|
316
|
+
@qid = qid
|
317
|
+
end
|
318
|
+
|
319
|
+
# NOT IMPLEMENTED
|
320
|
+
def status
|
321
|
+
raise 'not implemented'
|
322
|
+
end
|
323
|
+
|
324
|
+
# This polls Precog for the completion of an async query. If the query
|
325
|
+
# has completed, then a triple of errors, warnings and data is returned.
|
326
|
+
# Otherwise, +nil+ is returned.
|
327
|
+
def parsed
|
328
|
+
params = { :apiKey => client.api_key }
|
329
|
+
resp = Precog.get(client, "/analytics/v#{VERSION}/queries/#{qid}", { 'Content-Type' => 'application/json' }, params)
|
330
|
+
data = resp.body
|
331
|
+
|
332
|
+
if data
|
333
|
+
output = JSON.parse data
|
334
|
+
[output["errors"], output["warnings"], output["data"]]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class << self
|
340
|
+
def post(client, path, body, header, params = {}) # :nodoc:
|
341
|
+
connect client do |http|
|
342
|
+
uri = Addressable::URI.new
|
343
|
+
uri.query_values = params
|
344
|
+
|
345
|
+
http.post("#{path}?#{uri.query}", body, header)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def get(client, path, header, params = {}) # :nodoc:
|
350
|
+
get_auth(client, path, header, nil, nil, params)
|
351
|
+
end
|
352
|
+
|
353
|
+
def get_auth(client, path, header, user, pass, params = {}) # :nodoc:
|
354
|
+
connect client do |http|
|
355
|
+
uri = Addressable::URI.new
|
356
|
+
uri.query_values = params
|
357
|
+
|
358
|
+
req = Net::HTTP::Get.new("#{path}?#{uri.query}")
|
359
|
+
|
360
|
+
header.each do |key, value|
|
361
|
+
req[key] = value
|
362
|
+
end
|
363
|
+
|
364
|
+
req.basic_auth(user, pass) if user && pass
|
365
|
+
|
366
|
+
http.request req
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def connect(client) # :nodoc:
|
371
|
+
http = Net::HTTP.new(client.host, client.port)
|
372
|
+
http.use_ssl = client.secure?
|
373
|
+
http.start
|
374
|
+
|
375
|
+
result = nil
|
376
|
+
begin
|
377
|
+
result = yield http
|
378
|
+
ensure
|
379
|
+
http.finish
|
380
|
+
end
|
381
|
+
|
382
|
+
result
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: precog
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Spiewak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2013-04-22 00:00:00 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: addressable
|
16
|
+
prerelease: false
|
17
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: "2.3"
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: *id001
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: rspec
|
26
|
+
prerelease: false
|
27
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ~>
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: "2.13"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id002
|
34
|
+
description: Client library for the Precog platform
|
35
|
+
email: daniel@precog.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- lib/precog.rb
|
44
|
+
homepage: https://www.precog.com
|
45
|
+
licenses:
|
46
|
+
- MIT
|
47
|
+
metadata: {}
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --main
|
52
|
+
- lib/precog.rb
|
53
|
+
require_paths:
|
54
|
+
- lib
|
55
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">"
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 1.3.1
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.0.3
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: Precog Client
|
72
|
+
test_files: []
|
73
|
+
|