td 0.7.0

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