td 0.8.0 → 0.9.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.
- data/ChangeLog +6 -0
- data/README.rdoc +19 -2
- data/bin/td +2 -1
- data/lib/td/command/common.rb +21 -64
- data/lib/td/command/{database.rb → db.rb} +42 -31
- data/lib/td/command/help.rb +19 -0
- data/lib/td/command/import.rb +5 -7
- data/lib/td/command/job.rb +235 -0
- data/lib/td/command/list.rb +214 -85
- data/lib/td/command/query.rb +4 -234
- data/lib/td/command/schema.rb +96 -0
- data/lib/td/command/server.rb +1 -3
- data/lib/td/command/table.rb +17 -95
- data/lib/td/command/td.rb +1 -1
- data/lib/td/version.rb +1 -1
- metadata +24 -6
- data/lib/td/command/account.rb +0 -84
data/ChangeLog
CHANGED
data/README.rdoc
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
= Treasure Data command line tool
|
2
2
|
|
3
|
+
This CUI utility wraps the td-client-ruby (https://github.com/treasure-data/td-client-ruby),
|
4
|
+
the REST API for managing databases and jobs on the Treasure Data Cloud.
|
5
|
+
|
6
|
+
For more about Treasure Data, see <http://treasure-data.com/>.
|
7
|
+
|
8
|
+
For full documentation see <http://docs.treasure-data.com/>.
|
9
|
+
|
3
10
|
= Getting Started
|
4
11
|
|
5
12
|
Install td command as a gem.
|
@@ -10,9 +17,19 @@ See help message for details.
|
|
10
17
|
|
11
18
|
> td
|
12
19
|
|
20
|
+
You need to authorize the account, before executing any other commands.
|
21
|
+
|
22
|
+
> td account
|
13
23
|
|
14
|
-
|
24
|
+
= Sample Workflow
|
25
|
+
|
26
|
+
> td account -f # authorize an account
|
27
|
+
user: k@treasure-data.com
|
28
|
+
password: **********
|
29
|
+
> td database:create mydb # create a database
|
30
|
+
> td table:create mydb www_access # create a table
|
31
|
+
|
32
|
+
= Copyright
|
15
33
|
|
16
34
|
Copyright:: Copyright (c) 2011 Treasure Data Inc.
|
17
35
|
License:: Apache License, Version 2.0
|
18
|
-
|
data/bin/td
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# -*- coding: utf-8 -*-
|
3
|
-
|
3
|
+
require 'rubygems' unless defined?(gem)
|
4
|
+
gem 'td-client'
|
4
5
|
here = File.dirname(__FILE__)
|
5
6
|
$LOAD_PATH << File.expand_path(File.join(here, '..', 'lib'))
|
6
7
|
require 'td/command/td'
|
data/lib/td/command/common.rb
CHANGED
@@ -9,62 +9,10 @@ autoload :Schema, 'td/client'
|
|
9
9
|
autoload :Job, 'td/client'
|
10
10
|
|
11
11
|
module Command
|
12
|
-
private
|
13
|
-
def cmd_opt(name, *args)
|
14
|
-
if args.last.to_s =~ /_$/
|
15
|
-
multi = true
|
16
|
-
args.push args.pop.to_s[0..-2]+'...'
|
17
|
-
elsif args.last.to_s =~ /_\?$/
|
18
|
-
multi = true
|
19
|
-
args.push args.pop.to_s[0..-3]+'...?'
|
20
|
-
end
|
21
|
-
|
22
|
-
req_args, opt_args = args.partition {|a| a.to_s !~ /\?$/ }
|
23
|
-
opt_args = opt_args.map {|a| a.to_s[0..-2].to_sym }
|
24
|
-
args = req_args + opt_args
|
25
|
-
|
26
|
-
args_line = req_args.map {|a| "<#{a}>" }
|
27
|
-
args_line.concat opt_args.map {|a| "[#{a}]" }
|
28
|
-
args_line = args_line.join(' ')
|
29
|
-
|
30
|
-
description = List.get_description(name)
|
31
12
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
usage: #{$prog} #{name} #{args_line}
|
36
|
-
|
37
|
-
description:
|
38
|
-
#{description.split("\n").map {|l| " #{l}" }.join("\n")}
|
39
|
-
EOF
|
40
|
-
|
41
|
-
(class<<op;self;end).module_eval do
|
42
|
-
define_method(:cmd_usage) do |msg|
|
43
|
-
puts op.to_s
|
44
|
-
puts ""
|
45
|
-
puts "error: #{msg}" if msg
|
46
|
-
exit 1
|
47
|
-
end
|
48
|
-
|
49
|
-
define_method(:cmd_parse) do
|
50
|
-
begin
|
51
|
-
parse!(ARGV)
|
52
|
-
if ARGV.length < req_args.length || (!multi && ARGV.length > args.length)
|
53
|
-
cmd_usage nil
|
54
|
-
end
|
55
|
-
if ARGV.length <= 1
|
56
|
-
ARGV[0]
|
57
|
-
else
|
58
|
-
ARGV
|
59
|
-
end
|
60
|
-
rescue
|
61
|
-
cmd_usage $!
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
end
|
66
|
-
|
67
|
-
op
|
13
|
+
private
|
14
|
+
def get_option(name)
|
15
|
+
List.get_option(name)
|
68
16
|
end
|
69
17
|
|
70
18
|
def get_client
|
@@ -95,32 +43,41 @@ EOF
|
|
95
43
|
end
|
96
44
|
end
|
97
45
|
|
98
|
-
def
|
46
|
+
def get_database(client, db_name)
|
99
47
|
begin
|
100
48
|
return client.database(db_name)
|
101
49
|
rescue
|
102
50
|
cmd_debug_error $!
|
103
51
|
$stderr.puts $!
|
104
|
-
$stderr.puts "Use '#{$prog}
|
52
|
+
$stderr.puts "Use '#{$prog} database:list' to show the list of databases."
|
105
53
|
exit 1
|
106
54
|
end
|
107
55
|
db
|
108
56
|
end
|
109
57
|
|
110
|
-
def
|
111
|
-
|
58
|
+
def parse_table_ident(table_ident)
|
59
|
+
db_name, table_name = table_ident.split('.', 2)
|
60
|
+
unless table_name
|
61
|
+
$stderr.puts "Invalid table identifier '#{table_ident}'; expected 'DB.TABLE'"
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
return db_name, table_name
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_table(client, db_name, table_name)
|
68
|
+
db = get_database(client, db_name)
|
112
69
|
begin
|
113
70
|
table = db.table(table_name)
|
114
71
|
rescue
|
115
72
|
$stderr.puts $!
|
116
|
-
$stderr.puts "Use '#{$prog}
|
73
|
+
$stderr.puts "Use '#{$prog} table:list #{db_name}' to show the list of tables."
|
117
74
|
exit 1
|
118
75
|
end
|
119
|
-
if type && table.type != type
|
120
|
-
|
121
|
-
end
|
76
|
+
#if type && table.type != type
|
77
|
+
# $stderr.puts "Table '#{db_name}.#{table_name} is not a #{type} table but a #{table.type} table"
|
78
|
+
#end
|
122
79
|
table
|
123
80
|
end
|
81
|
+
|
124
82
|
end
|
125
83
|
end
|
126
|
-
|
@@ -2,14 +2,52 @@
|
|
2
2
|
module TreasureData
|
3
3
|
module Command
|
4
4
|
|
5
|
-
def
|
6
|
-
op = cmd_opt 'create-database', :db_name
|
5
|
+
def db_show(op)
|
7
6
|
db_name = op.cmd_parse
|
8
7
|
|
9
8
|
client = get_client
|
10
9
|
|
10
|
+
db = get_database(client, db_name)
|
11
|
+
|
12
|
+
rows = []
|
13
|
+
db.tables.each {|table|
|
14
|
+
pschema = table.schema.fields.map {|f|
|
15
|
+
"#{f.name}:#{f.type}"
|
16
|
+
}.join(', ')
|
17
|
+
rows << {:Table => table.name, :Type => table.type.to_s, :Count => table.count.to_s, :Schema=>pschema.to_s}
|
18
|
+
}
|
19
|
+
rows = rows.sort_by {|map|
|
20
|
+
[map[:Type].size, map[:Table]]
|
21
|
+
}
|
22
|
+
|
23
|
+
puts cmd_render_table(rows, :fields => [:Table, :Type, :Count, :Schema])
|
24
|
+
end
|
25
|
+
|
26
|
+
def db_list(op)
|
27
|
+
op.cmd_parse
|
28
|
+
|
29
|
+
client = get_client
|
30
|
+
dbs = client.databases
|
31
|
+
|
32
|
+
rows = []
|
33
|
+
dbs.each {|db|
|
34
|
+
rows << {:Name => db.name}
|
35
|
+
}
|
36
|
+
puts cmd_render_table(rows, :fields => [:Name])
|
37
|
+
|
38
|
+
if dbs.empty?
|
39
|
+
$stderr.puts "There are no databases."
|
40
|
+
$stderr.puts "Use '#{$prog} create-database <db_name>' to create a database."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def db_create(op)
|
45
|
+
db_name = op.cmd_parse
|
46
|
+
|
11
47
|
API.validate_database_name(db_name)
|
12
48
|
|
49
|
+
client = get_client
|
50
|
+
|
13
51
|
begin
|
14
52
|
client.create_database(db_name)
|
15
53
|
rescue AlreadyExistsError
|
@@ -21,11 +59,7 @@ module Command
|
|
21
59
|
$stderr.puts "Use '#{$prog} create-log-table #{db_name} <table_name>' to create a table."
|
22
60
|
end
|
23
61
|
|
24
|
-
def
|
25
|
-
op = cmd_opt 'drop-database', :db_name
|
26
|
-
|
27
|
-
op.banner << "\noptions:\n"
|
28
|
-
|
62
|
+
def db_delete(op)
|
29
63
|
force = false
|
30
64
|
op.on('-f', '--force', 'clear tables and delete the database', TrueClass) {|b|
|
31
65
|
force = true
|
@@ -46,35 +80,12 @@ module Command
|
|
46
80
|
db.delete
|
47
81
|
rescue NotFoundError
|
48
82
|
$stderr.puts "Database '#{db_name}' does not exist."
|
49
|
-
|
83
|
+
exit 1
|
50
84
|
end
|
51
85
|
|
52
86
|
$stderr.puts "Database '#{db_name}' is deleted."
|
53
87
|
end
|
54
88
|
|
55
|
-
def show_databases
|
56
|
-
op = cmd_opt 'show-databases'
|
57
|
-
op.cmd_parse
|
58
|
-
|
59
|
-
client = get_client
|
60
|
-
|
61
|
-
dbs = client.databases
|
62
|
-
|
63
|
-
rows = []
|
64
|
-
dbs.each {|db|
|
65
|
-
rows << {:Name => db.name}
|
66
|
-
}
|
67
|
-
puts cmd_render_table(rows, :fields => [:Name])
|
68
|
-
|
69
|
-
if dbs.empty?
|
70
|
-
$stderr.puts "There are no databases."
|
71
|
-
$stderr.puts "Use '#{$prog} create-database <db_name>' to create a database."
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
alias show_dbs show_databases
|
76
|
-
alias create_db create_database
|
77
|
-
alias drop_db drop_database
|
78
89
|
end
|
79
90
|
end
|
80
91
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module TreasureData
|
3
|
+
module Command
|
4
|
+
|
5
|
+
def help(op)
|
6
|
+
cmd = op.cmd_parse
|
7
|
+
|
8
|
+
op = List.get_option(cmd)
|
9
|
+
unless op
|
10
|
+
$stderr.puts "'#{cmd}' is not a td command. Run '#{$prog}' to show the list."
|
11
|
+
List.show_guess(cmd)
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
|
15
|
+
puts op.message
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
data/lib/td/command/import.rb
CHANGED
@@ -16,17 +16,13 @@ module Command
|
|
16
16
|
# TODO import-item
|
17
17
|
# TODO tail
|
18
18
|
|
19
|
-
def
|
20
|
-
op = cmd_opt 'import', :db_name, :table_name, :files_
|
21
|
-
|
19
|
+
def table_import(op)
|
22
20
|
op.banner << "\nsupported formats:\n"
|
23
21
|
op.banner << " apache\n"
|
24
22
|
op.banner << " syslog\n"
|
25
23
|
op.banner << " msgpack\n"
|
26
24
|
op.banner << " json\n"
|
27
25
|
|
28
|
-
op.banner << "\noptions:\n"
|
29
|
-
|
30
26
|
format = 'apache'
|
31
27
|
time_key = nil
|
32
28
|
|
@@ -54,7 +50,8 @@ module Command
|
|
54
50
|
time_key = s
|
55
51
|
}
|
56
52
|
|
57
|
-
|
53
|
+
table_ident, *paths = op.cmd_parse
|
54
|
+
db_name, table_name = parse_table_ident(table_ident)
|
58
55
|
|
59
56
|
client = get_client
|
60
57
|
|
@@ -81,7 +78,7 @@ module Command
|
|
81
78
|
parser = TextParser.new(names, regexp, time_format)
|
82
79
|
end
|
83
80
|
|
84
|
-
|
81
|
+
get_table(client, db_name, table_name)
|
85
82
|
|
86
83
|
require 'zlib'
|
87
84
|
|
@@ -176,6 +173,7 @@ module Command
|
|
176
173
|
file.each_line {|line|
|
177
174
|
i += 1
|
178
175
|
begin
|
176
|
+
line.rstrip!
|
179
177
|
m = @regexp.match(line)
|
180
178
|
unless m
|
181
179
|
raise "invalid log format at #{path}:#{i}"
|
@@ -0,0 +1,235 @@
|
|
1
|
+
|
2
|
+
module TreasureData
|
3
|
+
module Command
|
4
|
+
|
5
|
+
def job_list(op)
|
6
|
+
page = 0
|
7
|
+
skip = 0
|
8
|
+
|
9
|
+
op.on('-p', '--page PAGE', 'skip N pages', Integer) {|i|
|
10
|
+
page = i
|
11
|
+
}
|
12
|
+
op.on('-s', '--skip N', 'skip N jobs', Integer) {|i|
|
13
|
+
skip = i
|
14
|
+
}
|
15
|
+
|
16
|
+
max = op.cmd_parse
|
17
|
+
|
18
|
+
max = (max || 20).to_i
|
19
|
+
|
20
|
+
client = get_client
|
21
|
+
|
22
|
+
if page
|
23
|
+
skip += max * page
|
24
|
+
end
|
25
|
+
jobs = client.jobs(skip, skip+max-1)
|
26
|
+
|
27
|
+
rows = []
|
28
|
+
jobs.each {|job|
|
29
|
+
start = job.start_at
|
30
|
+
finish = job.end_at
|
31
|
+
if start
|
32
|
+
if !finish
|
33
|
+
finish = Time.now.utc
|
34
|
+
end
|
35
|
+
e = finish.to_i - start.to_i
|
36
|
+
elapsed = ''
|
37
|
+
if e >= 3600
|
38
|
+
elapsed << "#{e/3600}h "
|
39
|
+
e %= 3600
|
40
|
+
elapsed << "% 2dm " % (e/60)
|
41
|
+
e %= 60
|
42
|
+
elapsed << "% 2dsec" % e
|
43
|
+
elsif e >= 60
|
44
|
+
elapsed << "% 2dm " % (e/60)
|
45
|
+
e %= 60
|
46
|
+
elapsed << "% 2dsec" % e
|
47
|
+
else
|
48
|
+
elapsed << "% 2dsec" % e
|
49
|
+
end
|
50
|
+
else
|
51
|
+
elapsed = ''
|
52
|
+
end
|
53
|
+
elapsed = "% 10s" % elapsed # right aligned
|
54
|
+
|
55
|
+
rows << {:JobID => job.job_id, :Status => job.status, :Query => job.query.to_s, :Start => start, :Elapsed => elapsed}
|
56
|
+
}
|
57
|
+
|
58
|
+
puts cmd_render_table(rows, :fields => [:JobID, :Status, :Start, :Elapsed, :Query])
|
59
|
+
end
|
60
|
+
|
61
|
+
def job_show(op)
|
62
|
+
verbose = nil
|
63
|
+
wait = false
|
64
|
+
output = nil
|
65
|
+
format = 'tsv'
|
66
|
+
|
67
|
+
op.on('-v', '--verbose', 'show logs', TrueClass) {|b|
|
68
|
+
verbose = b
|
69
|
+
}
|
70
|
+
op.on('-w', '--wait', 'wait for finishing the job', TrueClass) {|b|
|
71
|
+
wait = b
|
72
|
+
}
|
73
|
+
op.on('-o', '--output PATH', 'write result to the file') {|s|
|
74
|
+
output = s
|
75
|
+
}
|
76
|
+
op.on('-f', '--format FORMAT', 'format of the result to write to the file (tsv, csv, json or msgpack)') {|s|
|
77
|
+
unless ['tsv', 'csv', 'json', 'msgpack'].include?(s)
|
78
|
+
raise "Unknown format #{s.dump}. Supported format: tsv, csv, json, msgpack"
|
79
|
+
end
|
80
|
+
format = s
|
81
|
+
}
|
82
|
+
|
83
|
+
job_id = op.cmd_parse
|
84
|
+
|
85
|
+
client = get_client
|
86
|
+
|
87
|
+
job = client.job(job_id)
|
88
|
+
|
89
|
+
puts "JobID : #{job.job_id}"
|
90
|
+
puts "URL : #{job.url}"
|
91
|
+
puts "Status : #{job.status}"
|
92
|
+
puts "Query : #{job.query}"
|
93
|
+
|
94
|
+
if wait && !job.finished?
|
95
|
+
wait_job(job)
|
96
|
+
if job.success?
|
97
|
+
puts "Result :"
|
98
|
+
show_result(job, output, format)
|
99
|
+
end
|
100
|
+
|
101
|
+
else
|
102
|
+
if job.success?
|
103
|
+
puts "Result :"
|
104
|
+
show_result(job, output, format)
|
105
|
+
end
|
106
|
+
|
107
|
+
if verbose
|
108
|
+
puts ""
|
109
|
+
puts "cmdout:"
|
110
|
+
job.debug['cmdout'].to_s.split("\n").each {|line|
|
111
|
+
puts " "+line
|
112
|
+
}
|
113
|
+
puts ""
|
114
|
+
puts "stderr:"
|
115
|
+
job.debug['stderr'].to_s.split("\n").each {|line|
|
116
|
+
puts " "+line
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
$stderr.puts "Use '-v' option to show detailed messages." unless verbose
|
122
|
+
end
|
123
|
+
|
124
|
+
def job_kill(op)
|
125
|
+
job_id = op.cmd_parse
|
126
|
+
|
127
|
+
client = get_client
|
128
|
+
|
129
|
+
client.kill_job(job_id)
|
130
|
+
# TODO error
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
def wait_job(job)
|
135
|
+
$stderr.puts "running..."
|
136
|
+
|
137
|
+
cmdout_lines = 0
|
138
|
+
stderr_lines = 0
|
139
|
+
|
140
|
+
until job.finished?
|
141
|
+
sleep 2
|
142
|
+
|
143
|
+
job.update_status!
|
144
|
+
|
145
|
+
cmdout = job.debug['cmdout'].to_s.split("\n")[cmdout_lines..-1] || []
|
146
|
+
stderr = job.debug['stderr'].to_s.split("\n")[stderr_lines..-1] || []
|
147
|
+
(cmdout + stderr).each {|line|
|
148
|
+
puts " "+line
|
149
|
+
}
|
150
|
+
cmdout_lines += cmdout.size
|
151
|
+
stderr_lines += stderr.size
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def show_result(job, output, format)
|
156
|
+
if output
|
157
|
+
write_result(job, output, format)
|
158
|
+
puts "written to #{output} in #{format} format"
|
159
|
+
else
|
160
|
+
render_result(job)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def write_result(job, output, format)
|
165
|
+
case format
|
166
|
+
when 'json'
|
167
|
+
require 'json'
|
168
|
+
first = true
|
169
|
+
File.open(output, "w") {|f|
|
170
|
+
f.write "["
|
171
|
+
job.result_each {|row|
|
172
|
+
if first
|
173
|
+
first = false
|
174
|
+
else
|
175
|
+
f.write ","
|
176
|
+
end
|
177
|
+
f.write row.to_json
|
178
|
+
}
|
179
|
+
f.write "]"
|
180
|
+
}
|
181
|
+
|
182
|
+
when 'msgpack'
|
183
|
+
File.open(output, "w") {|f|
|
184
|
+
f.write job.result_format('msgpack')
|
185
|
+
}
|
186
|
+
|
187
|
+
when 'csv'
|
188
|
+
require 'json'
|
189
|
+
require 'csv'
|
190
|
+
CSV.open(output, "w") {|writer|
|
191
|
+
job.result_each {|row|
|
192
|
+
writer << row.map {|col| col.is_a?(String) ? col.to_s : col.to_json }
|
193
|
+
}
|
194
|
+
}
|
195
|
+
|
196
|
+
when 'tsv'
|
197
|
+
require 'json'
|
198
|
+
File.open(output, "w") {|f|
|
199
|
+
job.result_each {|row|
|
200
|
+
first = true
|
201
|
+
row.each {|col|
|
202
|
+
if first
|
203
|
+
first = false
|
204
|
+
else
|
205
|
+
f.write "\t"
|
206
|
+
end
|
207
|
+
f.write col.is_a?(String) ? col.to_s : col.to_json
|
208
|
+
}
|
209
|
+
f.write "\n"
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
213
|
+
else
|
214
|
+
raise "Unknown format #{format.inspect}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def render_result(job)
|
219
|
+
require 'json'
|
220
|
+
rows = []
|
221
|
+
job.result_each {|row|
|
222
|
+
# TODO limit number of rows to show
|
223
|
+
rows << row.map {|v|
|
224
|
+
if v.is_a?(String)
|
225
|
+
v.to_s
|
226
|
+
else
|
227
|
+
v.to_json
|
228
|
+
end
|
229
|
+
}
|
230
|
+
}
|
231
|
+
puts cmd_render_table(rows, :max_width=>10000)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|