precog 1.0.0.pre2
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.
- 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
|
+
|