td 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,16 @@
1
+
2
+ == 2011-08-06 version 0.7.0
3
+
4
+ * import subcommand accepts UNIX time integer value on --json and --msgpack format
5
+ * Renamed command name (trd -> td)
6
+
7
+ == 2011-07-18 version 0.6.3
8
+
9
+ * show-jobs: shows elapsed time
10
+ * query: updated behavior of the -w option for new api
11
+ * import: supported --json and --msgpack format
12
+ * Added 'version' command
13
+
14
+
15
+ == 2011-06-27 version 0.6.2
16
+
@@ -0,0 +1,18 @@
1
+ = Treasure Data command line tool
2
+
3
+ = Getting Started
4
+
5
+ Install td command as a gem.
6
+
7
+ > gem install td
8
+
9
+ See help message for details.
10
+
11
+ > td
12
+
13
+
14
+ == Copyright
15
+
16
+ Copyright:: Copyright (c) 2011 Treasure Data Inc.
17
+ License:: Apache License, Version 2.0
18
+
data/bin/td ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ #require 'rubygems' unless defined?(gem)
4
+ here = File.dirname(__FILE__)
5
+ $LOAD_PATH << File.expand_path(File.join(here, '..', 'lib'))
6
+ require 'td/command/td'
@@ -0,0 +1,305 @@
1
+ require 'time'
2
+ require 'td/api_iface'
3
+ require 'td/error'
4
+
5
+ module TD
6
+
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
+
14
+ def self.server_status
15
+ iface = APIInterface.new(nil)
16
+ iface.server_status
17
+ end
18
+
19
+ def initialize(apikey)
20
+ @iface = APIInterface.new(apikey)
21
+ end
22
+
23
+ attr_reader :iface
24
+
25
+ def apikey
26
+ @iface.apikey
27
+ end
28
+
29
+ def server_status
30
+ @iface.server_status
31
+ end
32
+
33
+ # => true
34
+ def create_database(db_name)
35
+ @iface.create_database(db_name)
36
+ end
37
+
38
+ # => true
39
+ def delete_database(db_name)
40
+ @iface.delete_database(db_name)
41
+ end
42
+
43
+ # => [Database]
44
+ def databases
45
+ names = @iface.list_databases
46
+ names.map {|db_name|
47
+ Database.new(self, db_name)
48
+ }
49
+ end
50
+
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
58
+ }
59
+ raise NotFoundError, "Database '#{db_name}' does not exist"
60
+ end
61
+
62
+ # => true
63
+ def create_table(db_name, table_name, type)
64
+ @iface.create_table(db_name, table_name, type)
65
+ end
66
+
67
+ # => true
68
+ def create_log_table(db_name, table_name)
69
+ create_table(db_name, table_name, :log)
70
+ end
71
+
72
+ # => true
73
+ def create_item_table(db_name, table_name)
74
+ create_table(db_name, table_name, :item)
75
+ end
76
+
77
+ # => 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
+ }
88
+ end
89
+
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
+
101
+ # => Job
102
+ def query(q, db_name=nil)
103
+ job_id = @iface.hive_query(q, db_name)
104
+ Job.new(self, job_id, :hive, q) # TODO url
105
+ end
106
+
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, nil, start_at, end_at)
112
+ }
113
+ end
114
+
115
+ # => Job
116
+ def job(job_id)
117
+ job_id = job_id.to_s
118
+ type, query, status, result, url, debug, start_at, end_at = @iface.show_job(job_id)
119
+ Job.new(self, job_id, type, query, status, url, result, debug, start_at, end_at)
120
+ end
121
+
122
+ # => type:Symbol, result:String, url:String
123
+ def job_status(job_id)
124
+ type, query, status, result, url, debug, start_at, end_at = @iface.show_job(job_id)
125
+ return query, status, result, url, debug, start_at, end_at
126
+ end
127
+
128
+ # => time:Flaot
129
+ def import(db_name, table_name, format, stream, stream_size=stream.lstat.size)
130
+ @iface.import(db_name, table_name, format, stream, stream_size)
131
+ end
132
+ end
133
+
134
+ end
135
+
136
+
137
+ module TD
138
+
139
+ class APIObject
140
+ def initialize(api)
141
+ @api = api
142
+ end
143
+ end
144
+
145
+ class Database < APIObject
146
+ def initialize(api, db_name, tables=nil)
147
+ super(api)
148
+ @db_name = db_name
149
+ @tables = tables
150
+ end
151
+
152
+ def name
153
+ @db_name
154
+ end
155
+
156
+ def tables
157
+ update_tables! unless @tables
158
+ @tables
159
+ end
160
+
161
+ def create_table(name, type)
162
+ @api.create_table(@db_name, name, type)
163
+ end
164
+
165
+ def create_log_table(name)
166
+ create_table(name, :log)
167
+ end
168
+
169
+ def create_item_table(name)
170
+ create_table(name, :item)
171
+ end
172
+
173
+ def table(table_name)
174
+ @api.table(@db_name, table_name)
175
+ end
176
+
177
+ def delete
178
+ @api.delete_database(@db_name)
179
+ end
180
+
181
+ def update_tables!
182
+ @tables = @api.tables(@db_name)
183
+ end
184
+ end
185
+
186
+ class Table < APIObject
187
+ def initialize(api, db_name, table_name, type, count)
188
+ super(api)
189
+ @db_name = db_name
190
+ @table_name = table_name
191
+ @type = type
192
+ @count = count
193
+ end
194
+
195
+ attr_reader :type, :count
196
+
197
+ def database_name
198
+ @db_name
199
+ end
200
+
201
+ def database
202
+ @api.database(@db_name)
203
+ end
204
+
205
+ def name
206
+ @table_name
207
+ end
208
+
209
+ def identifier
210
+ "#{@db_name}.#{@table_name}"
211
+ end
212
+
213
+ def delete
214
+ @api.delete_table(@db_name, @table_name)
215
+ end
216
+ end
217
+
218
+ class Job < APIObject
219
+ def initialize(api, job_id, type, query, status=nil, url=nil, result=nil, debug=nil, start_at=nil, end_at=nil)
220
+ super(api)
221
+ @job_id = job_id
222
+ @type = type
223
+ @url = url
224
+ @query = query
225
+ @status = status
226
+ @result = result
227
+ @debug = debug
228
+ @start_at = start_at
229
+ @end_at = end_at
230
+ end
231
+
232
+ attr_reader :job_id, :type
233
+
234
+ def wait(timeout=nil)
235
+ # TODO
236
+ end
237
+
238
+ def query
239
+ update_status! unless @query
240
+ @query
241
+ end
242
+
243
+ def status
244
+ update_status! unless @status
245
+ @status
246
+ end
247
+
248
+ def url
249
+ update_status! unless @url
250
+ @url
251
+ end
252
+
253
+ def debug
254
+ update_status! unless @debug
255
+ @debug
256
+ end
257
+
258
+ def start_at
259
+ update_status! unless @start_at
260
+ @start_at && !@start_at.empty? ? Time.parse(@start_at) : nil
261
+ end
262
+
263
+ def end_at
264
+ update_status! unless @end_at
265
+ @end_at && !@end_at.empty? ? Time.parse(@end_at) : nil
266
+ end
267
+
268
+ def result
269
+ return nil unless finished?
270
+ update_status! unless @result
271
+ @result.split("\n").map {|line|
272
+ # TODO format of the result is TSV for now
273
+ line.split("\t")
274
+ }
275
+ end
276
+
277
+ def finished?
278
+ update_status! unless @status
279
+ if @status == "success" || @status == "error"
280
+ return true
281
+ else
282
+ return false
283
+ end
284
+ end
285
+
286
+ def running?
287
+ !finished?
288
+ end
289
+
290
+ def update_status!
291
+ query, status, result, url, debug, start_at, end_at = @api.job_status(@job_id)
292
+ @query = query
293
+ @status = status
294
+ @result = result
295
+ @url = url
296
+ @debug = debug
297
+ @start_at = start_at
298
+ @end_at = end_at
299
+ self
300
+ end
301
+ end
302
+
303
+
304
+ end
305
+
@@ -0,0 +1,323 @@
1
+
2
+ module TD
3
+
4
+
5
+ class APIInterface
6
+ def initialize(apikey)
7
+ require 'json'
8
+ @apikey = apikey
9
+ end
10
+
11
+ # TODO error check & raise appropriate errors
12
+
13
+ attr_reader :apikey
14
+
15
+ ####
16
+ ## Database API
17
+ ##
18
+
19
+ # => [name:String]
20
+ def list_databases
21
+ code, body, res = get("/v3/database/list")
22
+ if code != "200"
23
+ raise_error("List databases failed", res)
24
+ end
25
+ # TODO format check
26
+ js = JSON.load(body)
27
+ names = js["databases"].map {|dbinfo| dbinfo['name'] }
28
+ return names
29
+ end
30
+
31
+ # => true
32
+ def delete_database(db)
33
+ code, body, res = post("/v3/database/delete/#{e db}")
34
+ if code != "200"
35
+ raise_error("Delete database failed", res)
36
+ end
37
+ return true
38
+ end
39
+
40
+ # => true
41
+ def create_database(db)
42
+ code, body, res = post("/v3/database/create/#{e db}")
43
+ if code != "200"
44
+ raise_error("Create database failed", res)
45
+ end
46
+ return true
47
+ end
48
+
49
+
50
+ ####
51
+ ## Table API
52
+ ##
53
+
54
+ # => {name:String => [type:Symbol, count:Integer]}
55
+ def list_tables(db)
56
+ code, body, res = get("/v3/table/list/#{e db}")
57
+ if code != "200"
58
+ raise_error("List tables failed", res)
59
+ end
60
+ # TODO format check
61
+ js = JSON.load(body)
62
+ result = {}
63
+ js["tables"].map {|m|
64
+ name = m['name']
65
+ type = (m['type'] || '?').to_sym
66
+ count = (m['count'] || 0).to_i # TODO?
67
+ result[name] = [type, count]
68
+ }
69
+ return result
70
+ end
71
+
72
+ # => true
73
+ def create_table(db, table, type)
74
+ code, body, res = post("/v3/table/create/#{e db}/#{e table}/#{type}")
75
+ if code != "200"
76
+ raise_error("Create #{type} table failed", res)
77
+ end
78
+ return true
79
+ end
80
+
81
+ # => true
82
+ def create_log_table(db, table)
83
+ create_table(db, table, :log)
84
+ end
85
+
86
+ # => true
87
+ def create_item_table(db, table)
88
+ create_table(db, table, :item)
89
+ end
90
+
91
+ # => type:Symbol
92
+ def delete_table(db, table)
93
+ code, body, res = post("/v3/table/delete/#{e db}/#{e table}")
94
+ if code != "200"
95
+ raise_error("Drop table failed", res)
96
+ end
97
+ # TODO format check
98
+ js = JSON.load(body)
99
+ type = (js['type'] || '?').to_sym
100
+ return type
101
+ end
102
+
103
+
104
+ ####
105
+ ## Job API
106
+ ##
107
+
108
+ # => [(jobId:String, type:Symbol, status:String, start_at:String, end_at:String)]
109
+ def list_jobs(from=0, to=nil)
110
+ params = {}
111
+ params['from'] = from.to_s if from
112
+ params['to'] = to.to_s if to
113
+ code, body, res = get("/v3/job/list", params)
114
+ if code != "200"
115
+ raise_error("List jobs failed", res)
116
+ end
117
+ # TODO format check
118
+ js = JSON.load(body)
119
+ result = []
120
+ js['jobs'].each {|m|
121
+ job_id = m['job_id']
122
+ type = (m['type'] || '?').to_sym
123
+ status = m['status']
124
+ query = m['query']
125
+ start_at = m['start_at']
126
+ end_at = m['end_at']
127
+ result << [job_id, type, status, query, start_at, end_at]
128
+ }
129
+ return result
130
+ end
131
+
132
+ # => (type:Symbol, status:String, result:String, url:String)
133
+ def show_job(job_id)
134
+ code, body, res = get("/v3/job/show/#{e job_id}")
135
+ if code != "200"
136
+ raise_error("Show job failed", res)
137
+ end
138
+ # TODO format check
139
+ js = JSON.load(body)
140
+ # TODO debug
141
+ type = (js['type'] || '?').to_sym # TODO
142
+ query = js['query']
143
+ status = js['status']
144
+ result = js['result']
145
+ debug = js['debug']
146
+ url = js['url']
147
+ start_at = js['start_at']
148
+ end_at = js['end_at']
149
+ return [type, query, status, result, url, debug, start_at, end_at]
150
+ end
151
+
152
+ # => jobId:String
153
+ def hive_query(q, db=nil)
154
+ code, body, res = post("/v3/job/issue/hive/#{e db}", {'query'=>q})
155
+ if code != "200"
156
+ raise_error("Query failed", res)
157
+ end
158
+ # TODO format check
159
+ js = JSON.load(body)
160
+ return js['job_id'].to_s
161
+ end
162
+
163
+
164
+ ####
165
+ ## Import API
166
+ ##
167
+
168
+ # => time:Float
169
+ def import(db, table, format, stream, stream_size=stream.lstat.size)
170
+ code, body, res = put("/v3/table/import/#{e db}/#{e table}/#{format}", stream, stream_size)
171
+ if code[0] != ?2
172
+ raise_error("Import failed", res)
173
+ end
174
+ # TODO format check
175
+ js = JSON.load(body)
176
+ time = js['time'].to_f
177
+ return time
178
+ end
179
+
180
+
181
+ ####
182
+ ## User API
183
+ ##
184
+
185
+ # apikey:String
186
+ def authenticate(user, password)
187
+ code, body, res = post("/v3/user/authenticate", {'user'=>user, 'password'=>password})
188
+ if code != "200"
189
+ raise_error("Authentication failed", res)
190
+ end
191
+ # TODO format check
192
+ js = JSON.load(body)
193
+ apikey = js['apikey']
194
+ return apikey
195
+ end
196
+
197
+ ####
198
+ ## Server Status API
199
+ ##
200
+
201
+ # => status:String
202
+ def server_status
203
+ code, body, res = get('/v3/system/server_status')
204
+ if code != "200"
205
+ return "Server is down (#{code})"
206
+ end
207
+ # TODO format check
208
+ js = JSON.load(body)
209
+ status = js['status']
210
+ return status
211
+ end
212
+
213
+ private
214
+ HOST = ENV['TD_API_SERVER'] || 'api.treasure-data.com'
215
+ PORT = 80
216
+ USE_SSL = false
217
+ BASE_URL = ''
218
+
219
+ def get(url, params=nil)
220
+ http, header = new_http
221
+
222
+ path = BASE_URL + url
223
+ if params && !params.empty?
224
+ path << "?"+params.map {|k,v|
225
+ "#{k}=#{e v}"
226
+ }.join('&')
227
+ end
228
+
229
+ request = Net::HTTP::Get.new(path, header)
230
+
231
+ response = http.request(request)
232
+ return [response.code, response.body, response]
233
+ end
234
+
235
+ def post(url, params=nil)
236
+ http, header = new_http
237
+
238
+ path = BASE_URL + url
239
+
240
+ request = Net::HTTP::Post.new(path, header)
241
+ request.set_form_data(params) if params
242
+
243
+ response = http.request(request)
244
+ return [response.code, response.body, response]
245
+ end
246
+
247
+ def put(url, stream, stream_size)
248
+ http, header = new_http
249
+
250
+ path = BASE_URL + url
251
+
252
+ header['Content-Type'] = 'application/octet-stream'
253
+ header['Content-Length'] = stream_size.to_s
254
+
255
+ request = Net::HTTP::Put.new(url, header)
256
+ if request.respond_to?(:body_stream=)
257
+ request.body_stream = stream
258
+ else # Ruby 1.8
259
+ request.body = stream.read
260
+ end
261
+
262
+ response = http.request(request)
263
+ return [response.code, response.body, response]
264
+ end
265
+
266
+ def new_http
267
+ require 'net/http'
268
+ require 'time'
269
+
270
+ http = Net::HTTP.new(HOST, PORT)
271
+ if USE_SSL
272
+ http.use_ssl = true
273
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
274
+ store = OpenSSL::X509::Store.new
275
+ http.cert_store = store
276
+ end
277
+
278
+ #http.read_timeout = options[:read_timeout]
279
+
280
+ header = {}
281
+ if @apikey
282
+ header['Authorization'] = "TD1 #{apikey}"
283
+ end
284
+ header['Date'] = Time.now.rfc2822
285
+
286
+ return http, header
287
+ end
288
+
289
+ def raise_error(msg, res)
290
+ begin
291
+ js = JSON.load(res.body)
292
+ msg = js['message']
293
+ error_code = js['error_code']
294
+
295
+ if res.code == "404"
296
+ raise NotFoundError, "#{error_code}: #{msg}"
297
+ elsif res.code == "409"
298
+ raise AlreadyExistsError, "#{error_code}: #{msg}"
299
+ else
300
+ raise APIError, "#{error_code}: #{msg}"
301
+ end
302
+
303
+ rescue
304
+ if res.code == "404"
305
+ raise NotFoundError, "#{msg}: #{res.body}"
306
+ elsif res.code == "409"
307
+ raise AlreadyExistsError, "#{msg}: #{res.body}"
308
+ else
309
+ raise APIError, "#{msg}: #{res.body}"
310
+ end
311
+ end
312
+ # TODO error
313
+ end
314
+
315
+ def e(s)
316
+ require 'cgi'
317
+ CGI.escape(s.to_s)
318
+ end
319
+ end
320
+
321
+
322
+ end
323
+