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.
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
+