precog 1.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/precog.rb +385 -0
  3. 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
+