td 0.7.2 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
data/ChangeLog CHANGED
@@ -1,4 +1,10 @@
1
1
 
2
+ == 2011-08-15 version 0.7.3
3
+
4
+ * show-jobs: removed --from and --around options
5
+ * changed library namespace from TD to TreasureData
6
+
7
+
2
8
  == 2011-08-15 version 0.7.2
3
9
 
4
10
  * Supports TD_API_KEY and TD_CONFIG_PATH environment variable
data/lib/td/api.rb CHANGED
@@ -1,356 +1,401 @@
1
- require 'time'
2
- require 'td/api_iface'
3
- require 'td/error'
4
1
 
5
- module TD
2
+ module TreasureData
6
3
 
7
- class API
8
- def self.authenticate(user, password)
9
- iface = APIInterface.new(nil)
10
- apikey = iface.authenticate(user, password)
11
- new(apikey)
12
- end
13
4
 
14
- def self.server_status
15
- iface = APIInterface.new(nil)
16
- iface.server_status
17
- end
5
+ class APIError < StandardError
6
+ end
7
+
8
+ class AuthError < APIError
9
+ end
10
+
11
+ class AlreadyExistsError < APIError
12
+ end
18
13
 
14
+ class NotFoundError < APIError
15
+ end
16
+
17
+
18
+ class API
19
19
  def initialize(apikey)
20
- @iface = APIInterface.new(apikey)
20
+ require 'json'
21
+ @apikey = apikey
21
22
  end
22
23
 
23
- attr_reader :iface
24
+ # TODO error check & raise appropriate errors
24
25
 
25
- def apikey
26
- @iface.apikey
26
+ attr_reader :apikey
27
+
28
+ def self.validate_database_name(name)
29
+ name = name.to_s
30
+ if name.empty?
31
+ raise "Empty name is not allowed"
32
+ end
33
+ if name.length < 3 || 32 < name.length
34
+ raise "Name must be 3 to 32 characters, got #{name.length} characters."
35
+ end
36
+ unless name =~ /^([a-z0-9_]+)$/
37
+ raise "Name must consist only of alphabets, numbers, '_'."
38
+ end
27
39
  end
28
40
 
29
- def server_status
30
- @iface.server_status
41
+ def self.validate_table_name(name)
42
+ validate_database_name(name)
31
43
  end
32
44
 
33
- # => true
34
- def create_database(db_name)
35
- @iface.create_database(db_name)
45
+ ####
46
+ ## Database API
47
+ ##
48
+
49
+ # => [name:String]
50
+ def list_databases
51
+ code, body, res = get("/v3/database/list")
52
+ if code != "200"
53
+ raise_error("List databases failed", res)
54
+ end
55
+ # TODO format check
56
+ js = JSON.load(body)
57
+ names = js["databases"].map {|dbinfo| dbinfo['name'] }
58
+ return names
36
59
  end
37
60
 
38
61
  # => true
39
- def delete_database(db_name)
40
- @iface.delete_database(db_name)
62
+ def delete_database(db)
63
+ code, body, res = post("/v3/database/delete/#{e db}")
64
+ if code != "200"
65
+ raise_error("Delete database failed", res)
66
+ end
67
+ return true
41
68
  end
42
69
 
43
- # => [Database]
44
- def databases
45
- names = @iface.list_databases
46
- names.map {|db_name|
47
- Database.new(self, db_name)
48
- }
70
+ # => true
71
+ def create_database(db)
72
+ code, body, res = post("/v3/database/create/#{e db}")
73
+ if code != "200"
74
+ raise_error("Create database failed", res)
75
+ end
76
+ return true
49
77
  end
50
78
 
51
- # => Database
52
- def database(db_name)
53
- names = @iface.list_databases
54
- names.each {|n|
55
- if n == db_name
56
- return Database.new(self, db_name)
57
- end
79
+
80
+ ####
81
+ ## Table API
82
+ ##
83
+
84
+ # => {name:String => [type:Symbol, count:Integer]}
85
+ def list_tables(db)
86
+ code, body, res = get("/v3/table/list/#{e db}")
87
+ if code != "200"
88
+ raise_error("List tables failed", res)
89
+ end
90
+ # TODO format check
91
+ js = JSON.load(body)
92
+ result = {}
93
+ js["tables"].map {|m|
94
+ name = m['name']
95
+ type = (m['type'] || '?').to_sym
96
+ count = (m['count'] || 0).to_i # TODO?
97
+ result[name] = [type, count]
58
98
  }
59
- raise NotFoundError, "Database '#{db_name}' does not exist"
99
+ return result
60
100
  end
61
101
 
62
102
  # => true
63
- def create_table(db_name, table_name, type)
64
- @iface.create_table(db_name, table_name, type)
103
+ def create_table(db, table, type)
104
+ code, body, res = post("/v3/table/create/#{e db}/#{e table}/#{type}")
105
+ if code != "200"
106
+ raise_error("Create #{type} table failed", res)
107
+ end
108
+ return true
65
109
  end
66
110
 
67
111
  # => true
68
- def create_log_table(db_name, table_name)
69
- create_table(db_name, table_name, :log)
112
+ def create_log_table(db, table)
113
+ create_table(db, table, :log)
70
114
  end
71
115
 
72
116
  # => true
73
- def create_item_table(db_name, table_name)
74
- create_table(db_name, table_name, :item)
117
+ def create_item_table(db, table)
118
+ create_table(db, table, :item)
75
119
  end
76
120
 
77
121
  # => type:Symbol
78
- def delete_table(db_name, table_name)
79
- @iface.delete_table(db_name, table_name)
80
- end
81
-
82
- # => [Table]
83
- def tables(db_name)
84
- m = @iface.list_tables(db_name)
85
- m.map {|table_name,(type,count)|
86
- Table.new(self, db_name, table_name, type, count)
87
- }
122
+ def delete_table(db, table)
123
+ code, body, res = post("/v3/table/delete/#{e db}/#{e table}")
124
+ if code != "200"
125
+ raise_error("Drop table failed", res)
126
+ end
127
+ # TODO format check
128
+ js = JSON.load(body)
129
+ type = (js['type'] || '?').to_sym
130
+ return type
88
131
  end
89
132
 
90
- # => Table
91
- def table(db_name, table_name)
92
- m = @iface.list_tables(db_name)
93
- m.each_pair {|name,(type,count)|
94
- if name == table_name
95
- return Table.new(self, db_name, name, type, count)
96
- end
97
- }
98
- raise NotFoundError, "Table '#{db_name}.#{table_name}' does not exist"
99
- end
100
133
 
101
- # => Job
102
- def query(db_name, q)
103
- job_id = @iface.hive_query(q, db_name)
104
- Job.new(self, job_id, :hive, q) # TODO url
105
- end
134
+ ####
135
+ ## Job API
136
+ ##
106
137
 
107
- # => [Job=]
108
- def jobs(from=nil, to=nil)
109
- js = @iface.list_jobs(from, to)
110
- js.map {|job_id,type,status,query,start_at,end_at|
111
- Job.new(self, job_id, type, query, status, nil, nil, start_at, end_at)
138
+ # => [(jobId:String, type:Symbol, status:String, start_at:String, end_at:String)]
139
+ def list_jobs(from=0, to=nil)
140
+ params = {}
141
+ params['from'] = from.to_s if from
142
+ params['to'] = to.to_s if to
143
+ code, body, res = get("/v3/job/list", params)
144
+ if code != "200"
145
+ raise_error("List jobs failed", res)
146
+ end
147
+ # TODO format check
148
+ js = JSON.load(body)
149
+ result = []
150
+ js['jobs'].each {|m|
151
+ job_id = m['job_id']
152
+ type = (m['type'] || '?').to_sym
153
+ status = m['status']
154
+ query = m['query']
155
+ start_at = m['start_at']
156
+ end_at = m['end_at']
157
+ result << [job_id, type, status, query, start_at, end_at]
112
158
  }
159
+ return result
113
160
  end
114
161
 
115
- # => Job
116
- def job(job_id)
117
- job_id = job_id.to_s
118
- type, query, status, url, debug, start_at, end_at = @iface.show_job(job_id)
119
- Job.new(self, job_id, type, query, status, url, debug, start_at, end_at)
120
- end
121
-
122
- # => type:Symbol, url:String
123
- def job_status(job_id)
124
- type, query, status, url, debug, start_at, end_at = @iface.show_job(job_id)
125
- return query, status, url, debug, start_at, end_at
162
+ # => (type:Symbol, status:String, result:String, url:String)
163
+ def show_job(job_id)
164
+ code, body, res = get("/v3/job/show/#{e job_id}")
165
+ if code != "200"
166
+ raise_error("Show job failed", res)
167
+ end
168
+ # TODO format check
169
+ js = JSON.load(body)
170
+ # TODO debug
171
+ type = (js['type'] || '?').to_sym # TODO
172
+ query = js['query']
173
+ status = js['status']
174
+ debug = js['debug']
175
+ url = js['url']
176
+ start_at = js['start_at']
177
+ end_at = js['end_at']
178
+ return [type, query, status, url, debug, start_at, end_at]
126
179
  end
127
180
 
128
- # => result:[{column:String=>value:Object]
129
181
  def job_result(job_id)
130
- @iface.job_result(job_id)
182
+ require 'msgpack'
183
+ code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'})
184
+ if code != "200"
185
+ raise_error("Get job result failed", res)
186
+ end
187
+ result = []
188
+ MessagePack::Unpacker.new.feed_each(body) {|row|
189
+ result << row
190
+ }
191
+ return result
131
192
  end
132
193
 
133
- # => result:String
134
194
  def job_result_format(job_id, format)
135
- @iface.job_result_format(job_id, format)
195
+ # TODO chunked encoding
196
+ code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>format})
197
+ if code != "200"
198
+ raise_error("Get job result failed", res)
199
+ end
200
+ return body
136
201
  end
137
202
 
138
- # => nil
139
203
  def job_result_each(job_id, &block)
140
- @iface.job_result_each(job_id, &block)
141
- end
142
-
143
- # => time:Flaot
144
- def import(db_name, table_name, format, stream, stream_size=stream.lstat.size)
145
- @iface.import(db_name, table_name, format, stream, stream_size)
146
- end
147
-
148
- def self.validate_database_name(name)
149
- name = name.to_s
150
- if name.empty?
151
- raise "Empty name is not allowed"
152
- end
153
- if name.length < 3 || 32 < name.length
154
- raise "Name must be 3 to 32 characters, got #{name.length} characters."
204
+ # TODO chunked encoding
205
+ require 'msgpack'
206
+ code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>'msgpack'})
207
+ if code != "200"
208
+ raise_error("Get job result failed", res)
155
209
  end
156
- unless name =~ /^([a-z0-9_]+)$/
157
- raise "Name must consist only of alphabets, numbers, '_'."
158
- end
159
- end
160
-
161
- def self.validate_table_name(name)
162
- validate_database_name(name)
163
- end
164
- end
165
-
166
- end
167
-
168
-
169
- module TD
170
-
171
- class APIObject
172
- def initialize(api)
173
- @api = api
174
- end
175
- end
176
-
177
- class Database < APIObject
178
- def initialize(api, db_name, tables=nil)
179
- super(api)
180
- @db_name = db_name
181
- @tables = tables
182
- end
183
-
184
- def name
185
- @db_name
210
+ result = []
211
+ MessagePack::Unpacker.new.feed_each(body) {|row|
212
+ yield row
213
+ }
214
+ nil
186
215
  end
187
216
 
188
- def tables
189
- update_tables! unless @tables
190
- @tables
217
+ def job_result_raw(job_id, format)
218
+ code, body, res = get("/v3/job/result/#{e job_id}", {'format'=>format})
219
+ if code != "200"
220
+ raise_error("Get job result failed", res)
221
+ end
222
+ return body
191
223
  end
192
224
 
193
- def create_table(name, type)
194
- @api.create_table(@db_name, name, type)
225
+ # => jobId:String
226
+ def hive_query(q, db=nil)
227
+ code, body, res = post("/v3/job/issue/hive/#{e db}", {'query'=>q})
228
+ if code != "200"
229
+ raise_error("Query failed", res)
230
+ end
231
+ # TODO format check
232
+ js = JSON.load(body)
233
+ return js['job_id'].to_s
195
234
  end
196
235
 
197
- def create_log_table(name)
198
- create_table(name, :log)
199
- end
200
236
 
201
- def create_item_table(name)
202
- create_table(name, :item)
203
- end
237
+ ####
238
+ ## Import API
239
+ ##
204
240
 
205
- def table(table_name)
206
- @api.table(@db_name, table_name)
241
+ # => time:Float
242
+ def import(db, table, format, stream, stream_size=stream.lstat.size)
243
+ code, body, res = put("/v3/table/import/#{e db}/#{e table}/#{format}", stream, stream_size)
244
+ if code[0] != ?2
245
+ raise_error("Import failed", res)
246
+ end
247
+ # TODO format check
248
+ js = JSON.load(body)
249
+ time = js['time'].to_f
250
+ return time
207
251
  end
208
252
 
209
- def delete
210
- @api.delete_database(@db_name)
211
- end
212
253
 
213
- def update_tables!
214
- @tables = @api.tables(@db_name)
215
- end
216
- end
254
+ ####
255
+ ## User API
256
+ ##
217
257
 
218
- class Table < APIObject
219
- def initialize(api, db_name, table_name, type, count)
220
- super(api)
221
- @db_name = db_name
222
- @table_name = table_name
223
- @type = type
224
- @count = count
258
+ # apikey:String
259
+ def authenticate(user, password)
260
+ code, body, res = post("/v3/user/authenticate", {'user'=>user, 'password'=>password})
261
+ if code != "200"
262
+ raise_error("Authentication failed", res)
263
+ end
264
+ # TODO format check
265
+ js = JSON.load(body)
266
+ apikey = js['apikey']
267
+ return apikey
225
268
  end
226
269
 
227
- attr_reader :type, :count
270
+ ####
271
+ ## Server Status API
272
+ ##
228
273
 
229
- def database_name
230
- @db_name
231
- end
232
-
233
- def database
234
- @api.database(@db_name)
235
- end
274
+ # => status:String
275
+ def server_status
276
+ code, body, res = get('/v3/system/server_status')
277
+ if code != "200"
278
+ return "Server is down (#{code})"
279
+ end
280
+ # TODO format check
281
+ js = JSON.load(body)
282
+ status = js['status']
283
+ return status
284
+ end
285
+
286
+ private
287
+ host = 'api.treasure-data.com'
288
+ port = 80
289
+ if e = ENV['TD_API_SERVER']
290
+ host, port_ = e.split(':',2)
291
+ port_ = port_.to_i
292
+ port = port_ if port_ != 0
293
+ end
294
+
295
+ HOST = host
296
+ PORT = port
297
+ USE_SSL = false
298
+ BASE_URL = ''
299
+
300
+ def get(url, params=nil)
301
+ http, header = new_http
302
+
303
+ path = BASE_URL + url
304
+ if params && !params.empty?
305
+ path << "?"+params.map {|k,v|
306
+ "#{k}=#{e v}"
307
+ }.join('&')
308
+ end
236
309
 
237
- def name
238
- @table_name
239
- end
310
+ request = Net::HTTP::Get.new(path, header)
240
311
 
241
- def identifier
242
- "#{@db_name}.#{@table_name}"
312
+ response = http.request(request)
313
+ return [response.code, response.body, response]
243
314
  end
244
315
 
245
- def delete
246
- @api.delete_table(@db_name, @table_name)
247
- end
248
- end
316
+ def post(url, params=nil)
317
+ http, header = new_http
249
318
 
250
- class Job < APIObject
251
- def initialize(api, job_id, type, query, status=nil, url=nil, debug=nil, start_at=nil, end_at=nil, result=nil)
252
- super(api)
253
- @job_id = job_id
254
- @type = type
255
- @url = url
256
- @query = query
257
- @status = status
258
- @debug = debug
259
- @start_at = start_at
260
- @end_at = end_at
261
- @result = result
262
- end
319
+ path = BASE_URL + url
263
320
 
264
- attr_reader :job_id, :type
321
+ request = Net::HTTP::Post.new(path, header)
322
+ request.set_form_data(params) if params
265
323
 
266
- def wait(timeout=nil)
267
- # TODO
324
+ response = http.request(request)
325
+ return [response.code, response.body, response]
268
326
  end
269
327
 
270
- def query
271
- update_status! unless @query
272
- @query
273
- end
328
+ def put(url, stream, stream_size)
329
+ http, header = new_http
274
330
 
275
- def status
276
- update_status! unless @status
277
- @status
278
- end
331
+ path = BASE_URL + url
279
332
 
280
- def url
281
- update_status! unless @url
282
- @url
283
- end
333
+ header['Content-Type'] = 'application/octet-stream'
334
+ header['Content-Length'] = stream_size.to_s
284
335
 
285
- def debug
286
- update_status! unless @debug
287
- @debug
288
- end
336
+ request = Net::HTTP::Put.new(url, header)
337
+ if request.respond_to?(:body_stream=)
338
+ request.body_stream = stream
339
+ else # Ruby 1.8
340
+ request.body = stream.read
341
+ end
289
342
 
290
- def start_at
291
- update_status! unless @start_at
292
- @start_at && !@start_at.empty? ? Time.parse(@start_at) : nil
343
+ response = http.request(request)
344
+ return [response.code, response.body, response]
293
345
  end
294
346
 
295
- def end_at
296
- update_status! unless @end_at
297
- @end_at && !@end_at.empty? ? Time.parse(@end_at) : nil
298
- end
347
+ def new_http
348
+ require 'net/http'
349
+ require 'time'
299
350
 
300
- def result
301
- unless @result
302
- return nil unless finished?
303
- @result = @api.job_result(@job_id)
351
+ http = Net::HTTP.new(HOST, PORT)
352
+ if USE_SSL
353
+ http.use_ssl = true
354
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
355
+ store = OpenSSL::X509::Store.new
356
+ http.cert_store = store
304
357
  end
305
- @result
306
- end
307
358
 
308
- def result_format(format)
309
- return nil unless finished?
310
- @api.job_result_format(@job_id, format)
311
- end
359
+ #http.read_timeout = options[:read_timeout]
312
360
 
313
- def result_each(&block)
314
- if @result
315
- @result.each(&block)
316
- else
317
- @api.job_result_each(@job_id, &block)
361
+ header = {}
362
+ if @apikey
363
+ header['Authorization'] = "TD1 #{apikey}"
318
364
  end
319
- nil
320
- end
365
+ header['Date'] = Time.now.rfc2822
321
366
 
322
- def finished?
323
- update_status! unless @status
324
- if @status == "success" || @status == "error"
325
- return true
326
- else
327
- return false
328
- end
367
+ return http, header
329
368
  end
330
369
 
331
- def running?
332
- !finished?
333
- end
370
+ def raise_error(msg, res)
371
+ begin
372
+ js = JSON.load(res.body)
373
+ msg = js['message']
374
+ error_code = js['error_code']
334
375
 
335
- def success?
336
- update_status! unless @status
337
- @status == "success"
338
- end
376
+ if res.code == "404"
377
+ raise NotFoundError, "#{error_code}: #{msg}"
378
+ elsif res.code == "409"
379
+ raise AlreadyExistsError, "#{error_code}: #{msg}"
380
+ else
381
+ raise APIError, "#{error_code}: #{msg}"
382
+ end
339
383
 
340
- def error?
341
- update_status! unless @status
342
- @status == "error"
384
+ rescue
385
+ if res.code == "404"
386
+ raise NotFoundError, "#{msg}: #{res.body}"
387
+ elsif res.code == "409"
388
+ raise AlreadyExistsError, "#{msg}: #{res.body}"
389
+ else
390
+ raise APIError, "#{msg}: #{res.body}"
391
+ end
392
+ end
393
+ # TODO error
343
394
  end
344
395
 
345
- def update_status!
346
- query, status, url, debug, start_at, end_at = @api.job_status(@job_id)
347
- @query = query
348
- @status = status
349
- @url = url
350
- @debug = debug
351
- @start_at = start_at
352
- @end_at = end_at
353
- self
396
+ def e(s)
397
+ require 'cgi'
398
+ CGI.escape(s.to_s)
354
399
  end
355
400
  end
356
401