gooddata 0.5.16 → 0.6.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.
- data/Gemfile +2 -0
- data/bin/gooddata +291 -8
- data/gooddata.gemspec +14 -5
- data/lib/gooddata/client.rb +34 -5
- data/lib/gooddata/commands/api.rb +27 -30
- data/lib/gooddata/commands/process.rb +137 -0
- data/lib/gooddata/commands/profile.rb +5 -5
- data/lib/gooddata/commands/projects.rb +107 -40
- data/lib/gooddata/commands/runners.rb +37 -0
- data/lib/gooddata/commands/scaffold.rb +30 -0
- data/lib/gooddata/connection.rb +31 -19
- data/lib/gooddata/extract.rb +1 -1
- data/lib/gooddata/goodzilla/goodzilla.rb +40 -0
- data/lib/gooddata/model.rb +418 -138
- data/lib/gooddata/models/attribute.rb +24 -0
- data/lib/gooddata/models/dashboard.rb +60 -0
- data/lib/gooddata/models/data_result.rb +4 -6
- data/lib/gooddata/models/data_set.rb +20 -0
- data/lib/gooddata/models/display_form.rb +7 -0
- data/lib/gooddata/models/fact.rb +17 -0
- data/lib/gooddata/models/metadata.rb +69 -17
- data/lib/gooddata/models/metric.rb +90 -0
- data/lib/gooddata/models/process.rb +112 -0
- data/lib/gooddata/models/profile.rb +1 -1
- data/lib/gooddata/models/project.rb +85 -29
- data/lib/gooddata/models/report.rb +45 -0
- data/lib/gooddata/models/report_definition.rb +139 -0
- data/lib/gooddata/version.rb +1 -1
- data/lib/templates/bricks/brick.rb.erb +7 -0
- data/lib/templates/bricks/main.rb.erb +4 -0
- data/spec/goodzilla_spec.rb +57 -0
- data/spec/model_dsl_spec.rb +22 -0
- data/test/test_commands.rb +1 -1
- data/test/test_model.rb +6 -6
- metadata +137 -16
- data/bin/igd.rb +0 -33
- data/lib/gooddata/command.rb +0 -75
- data/lib/gooddata/commands/help.rb +0 -104
- data/lib/gooddata/commands/version.rb +0 -7
- data/test/helper.rb +0 -13
@@ -0,0 +1,137 @@
|
|
1
|
+
module GoodData::Command
|
2
|
+
class Process
|
3
|
+
|
4
|
+
def self.list(options={})
|
5
|
+
# with project usage
|
6
|
+
processes = GoodData::Process[:all]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.get(options={})
|
10
|
+
id = options[:process_id]
|
11
|
+
fail "Unspecified process id" if id.nil?
|
12
|
+
GoodData::Process[id]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.with_deploy(dir, options={}, &block)
|
16
|
+
verbose = options[:verbose] || false
|
17
|
+
if block
|
18
|
+
begin
|
19
|
+
res = deploy_graph(dir, options)
|
20
|
+
block.call(res)
|
21
|
+
ensure
|
22
|
+
# self_link = res["process"]["links"]["self"]
|
23
|
+
# GoodData.delete(self_link)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
deploy_graph(dir, options)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.deploy_graph(dir, options={})
|
31
|
+
dir = Pathname(dir) || fail("Directory is not specified")
|
32
|
+
fail "\"#{dir}\" is not a directory" unless dir.directory?
|
33
|
+
project_id = options[:project_id] || fail("Project Id has to be specified")
|
34
|
+
|
35
|
+
|
36
|
+
type = options[:type] || fail("Type of deployment is not specified")
|
37
|
+
deploy_name = "AAAAA"
|
38
|
+
verbose = options[:verbose] || false
|
39
|
+
project_pid = options[:project_pid]
|
40
|
+
puts HighLine::color("Deploying #{dir}", HighLine::BOLD) if verbose
|
41
|
+
res = nil
|
42
|
+
|
43
|
+
Tempfile.open("deploy-graph-archive") do |temp|
|
44
|
+
|
45
|
+
Zip::OutputStream.open(temp.path) do |zio|
|
46
|
+
Dir.glob(dir + "**/*") do |item|
|
47
|
+
puts "including #{item}" if verbose
|
48
|
+
unless File.directory?(item)
|
49
|
+
zio.put_next_entry(item)
|
50
|
+
zio.print IO.read(item)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
GoodData.connection.upload(temp.path)
|
56
|
+
process_id = options[:process]
|
57
|
+
|
58
|
+
data = {
|
59
|
+
:process => {
|
60
|
+
:name => deploy_name,
|
61
|
+
:path => "/uploads/#{File.basename(temp.path)}",
|
62
|
+
:type => type
|
63
|
+
}
|
64
|
+
}
|
65
|
+
res = if process_id.nil?
|
66
|
+
GoodData.post("/gdc/projects/#{project_pid}/dataload/processes", data)
|
67
|
+
else
|
68
|
+
GoodData.put("/gdc/projects/#{project_pid}/dataload/processes/#{process_id}", data)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
puts HighLine::color("Deploy DONE #{dir}", HighLine::BOLD) if verbose
|
72
|
+
res
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.execute_process(link, dir, options={})
|
76
|
+
dir = Pathname(dir)
|
77
|
+
type = :ruby
|
78
|
+
if type == :ruby
|
79
|
+
result = GoodData.post(link, {
|
80
|
+
:execution => {
|
81
|
+
:graph => dir + "main.rb",
|
82
|
+
:params => {}
|
83
|
+
}
|
84
|
+
})
|
85
|
+
begin
|
86
|
+
GoodData.poll(result, "executionTask")
|
87
|
+
rescue RestClient::RequestFailed => e
|
88
|
+
|
89
|
+
ensure
|
90
|
+
result = GoodData.get(result["executionTask"]["links"]["detail"])
|
91
|
+
if result["executionDetail"]["status"] == "ERROR"
|
92
|
+
fail "Runing process failed. You can look at a log here #{result["executionDetail"]["logFileName"]}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
result
|
96
|
+
else
|
97
|
+
result = GoodData.post(link, {
|
98
|
+
:execution => {
|
99
|
+
:graph => dir + "graphs/main.grf",
|
100
|
+
:params => {}
|
101
|
+
}
|
102
|
+
})
|
103
|
+
begin
|
104
|
+
GoodData.poll(result, "executionTask")
|
105
|
+
rescue RestClient::RequestFailed => e
|
106
|
+
|
107
|
+
ensure
|
108
|
+
result = GoodData.get(result["executionTask"]["links"]["detail"])
|
109
|
+
if result["executionDetail"]["status"] == "ERROR"
|
110
|
+
fail "Runing process failed. You can look at a log here #{result["executionDetail"]["logFileName"]}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
result
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.run(dir, options={})
|
118
|
+
email = options[:email]
|
119
|
+
verbose = options[:v]
|
120
|
+
|
121
|
+
dir = Pathname(dir)
|
122
|
+
|
123
|
+
with_deploy(dir, options.merge(:name => "Temporary deploy[#{dir}][#{options[:project_name]}]")) do |deploy_response|
|
124
|
+
puts HighLine::color("Executing", HighLine::BOLD) if verbose
|
125
|
+
# if email.nil?
|
126
|
+
# result = execute_process(deploy_response["process"]["links"]["executions"], dir, options)
|
127
|
+
# else
|
128
|
+
# create_email_channel(options) do |channel_response|
|
129
|
+
# subscribe_on_finish(:success, channel_response, deploy_response, options)
|
130
|
+
result = execute_process(deploy_response["process"]["links"]["executions"], dir)
|
131
|
+
# end
|
132
|
+
# end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
@@ -1,51 +1,118 @@
|
|
1
|
-
module GoodData
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
puts "%s %s" % [project.uri, project.title]
|
8
|
-
end
|
9
|
-
end
|
10
|
-
alias :index :list
|
1
|
+
module GoodData::Command
|
2
|
+
class Projects
|
3
|
+
|
4
|
+
def self.list
|
5
|
+
GoodData::Project.all
|
6
|
+
end
|
11
7
|
|
12
|
-
|
13
|
-
|
8
|
+
def self.create(options={})
|
9
|
+
title = options[:title]
|
10
|
+
summary = options[:summary]
|
11
|
+
template = options[:template]
|
12
|
+
token = options[:token]
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
template = ask "Project template", :default => ''
|
14
|
+
GoodData::Project.create(:title => title, :summary => summary, :template => template, :auth_token => token)
|
15
|
+
end
|
18
16
|
|
19
|
-
|
17
|
+
def self.show(id)
|
18
|
+
GoodData::Project[id]
|
19
|
+
end
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
def self.clone(project_id, options)
|
22
|
+
with_data = options[:with_data]
|
23
|
+
with_users = options[:with_users]
|
24
|
+
title = options[:title]
|
25
|
+
|
26
|
+
export = {
|
27
|
+
:exportProject => {
|
28
|
+
:exportUsers => with_users ? 1 : 0,
|
29
|
+
:exportData => with_data ? 1 : 0
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
result = GoodData.post("/gdc/md/#{project_id}/maintenance/export", export)
|
34
|
+
export_token = result["exportArtifact"]["token"]
|
35
|
+
status_url = result["exportArtifact"]["status"]["uri"]
|
23
36
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
37
|
+
state = GoodData.get(status_url)["taskState"]["status"]
|
38
|
+
while state == "RUNNING"
|
39
|
+
sleep 5
|
40
|
+
result = GoodData.get(status_url)
|
41
|
+
state = result["taskState"]["status"]
|
29
42
|
end
|
30
43
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
44
|
+
old_project = GoodData::Project[project_id]
|
45
|
+
project_uri = self.create(options.merge({:title => "Clone of #{old_project.title}"}))
|
46
|
+
new_project = GoodData::Project[project_uri]
|
47
|
+
|
48
|
+
import = {
|
49
|
+
:importProject => {
|
50
|
+
:token => export_token
|
51
|
+
}
|
52
|
+
}
|
53
|
+
result = GoodData.post("/gdc/md/#{new_project.obj_id}/maintenance/import", import)
|
54
|
+
status_url = result["uri"]
|
55
|
+
state = GoodData.get(status_url)["taskState"]["status"]
|
56
|
+
while state == "RUNNING"
|
57
|
+
sleep 5
|
58
|
+
result = GoodData.get(status_url)
|
59
|
+
state = result["taskState"]["status"]
|
48
60
|
end
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.delete(project_id)
|
65
|
+
p = GoodData::Project[project_id]
|
66
|
+
p.delete
|
49
67
|
end
|
68
|
+
|
50
69
|
end
|
51
70
|
end
|
71
|
+
|
72
|
+
# module GoodData
|
73
|
+
# module Command
|
74
|
+
# class Projects
|
75
|
+
# class << self
|
76
|
+
# def list
|
77
|
+
# Project.all
|
78
|
+
# end
|
79
|
+
# alias :index :list
|
80
|
+
#
|
81
|
+
# def create
|
82
|
+
# title = ask "Project name"
|
83
|
+
# summary = ask "Project summary"
|
84
|
+
# template = ask "Project template", :default => ''
|
85
|
+
#
|
86
|
+
# project = Project.create :title => title, :summary => summary, :template => template
|
87
|
+
#
|
88
|
+
# puts "Project '#{project.title}' with id #{project.uri} created successfully!"
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# def show
|
92
|
+
# id = args.shift rescue nil
|
93
|
+
# raise(CommandFailed, "Specify the project key you wish to show.") if id.nil?
|
94
|
+
# connect
|
95
|
+
# pp Project[id].to_json
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# def delete
|
99
|
+
# raise(CommandFailed, "Specify the project key(s) for the project(s) you wish to delete.") if args.size == 0
|
100
|
+
# connect
|
101
|
+
# while args.size > 0
|
102
|
+
# id = args.shift
|
103
|
+
# project = Project[id]
|
104
|
+
# ask "Do you want to delete the project '#{project.title}' with id #{project.uri}", :answers => %w(y n) do |answer|
|
105
|
+
# case answer
|
106
|
+
# when 'y' then
|
107
|
+
# puts "Deleting #{project.title}..."
|
108
|
+
# project.delete
|
109
|
+
# puts "Project '#{project.title}' with id #{project.uri} deleted successfully!"
|
110
|
+
# when 'n' then
|
111
|
+
# puts "Aborting..."
|
112
|
+
# end
|
113
|
+
# end
|
114
|
+
# end
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
# end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module GoodData::Command
|
2
|
+
class Runners
|
3
|
+
|
4
|
+
def self.run_ruby(brick_dir, options={})
|
5
|
+
pid = options[:project]
|
6
|
+
fail "You have to specify a project ID" if pid.nil?
|
7
|
+
fail "You have to specify directory of the brick ran" if brick_dir.nil?
|
8
|
+
|
9
|
+
params = if options[:params]
|
10
|
+
JSON.parse(File.read(options[:params]), :symbolize_names => true)
|
11
|
+
else
|
12
|
+
{}
|
13
|
+
end
|
14
|
+
|
15
|
+
GoodData.connection.connect!
|
16
|
+
sst = GoodData.connection.cookies[:cookies]["GDCAuthSST"]
|
17
|
+
|
18
|
+
logger_stream = STDOUT
|
19
|
+
script_body = <<-script_body
|
20
|
+
require 'fileutils'
|
21
|
+
FileUtils::cd(\"#{brick_dir}\") do\
|
22
|
+
require 'bundler/setup'
|
23
|
+
eval(File.read(\"main.rb\")).call({
|
24
|
+
:gdc_sst => \"#{sst}\",
|
25
|
+
:gdc_project => \"#{pid}\"
|
26
|
+
}.merge(#{params}))
|
27
|
+
end
|
28
|
+
script_body
|
29
|
+
|
30
|
+
|
31
|
+
Bundler.with_clean_env do
|
32
|
+
system("ruby", "-e", script_body)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module GoodData::Command
|
2
|
+
class Scaffold
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def brick(name)
|
6
|
+
|
7
|
+
require 'erubis'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
templates_path = Pathname(__FILE__) + "../../../templates"
|
11
|
+
|
12
|
+
FileUtils.mkdir(name)
|
13
|
+
FileUtils.cd(name) do
|
14
|
+
input = File.read(templates_path + 'bricks/brick.rb.erb')
|
15
|
+
eruby = Erubis::Eruby.new(input)
|
16
|
+
File.open("brick.rb", 'w') do |f|
|
17
|
+
f.write(eruby.result())
|
18
|
+
end
|
19
|
+
|
20
|
+
input = File.read(templates_path + 'bricks/main.rb.erb')
|
21
|
+
eruby = Erubis::Eruby.new(input)
|
22
|
+
File.open("main.rb", 'w') do |f|
|
23
|
+
f.write(eruby.result())
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/gooddata/connection.rb
CHANGED
@@ -34,7 +34,7 @@ module GoodData
|
|
34
34
|
TOKEN_PATH = '/gdc/account/token'
|
35
35
|
STAGE_PATH = '/uploads/'
|
36
36
|
|
37
|
-
attr_reader(:auth_token)
|
37
|
+
attr_reader(:auth_token, :url)
|
38
38
|
attr_accessor :status
|
39
39
|
|
40
40
|
|
@@ -70,22 +70,16 @@ module GoodData
|
|
70
70
|
#
|
71
71
|
# * +username+ - The GoodData account username
|
72
72
|
# * +password+ - The GoodData account password
|
73
|
-
def initialize(username, password,
|
73
|
+
def initialize(username, password, options = {})
|
74
74
|
@status = :not_connected
|
75
75
|
@username = username
|
76
76
|
@password = password
|
77
|
-
@url =
|
77
|
+
@url = options[:server] || DEFAULT_URL
|
78
78
|
@auth_token = options.delete(:auth_token)
|
79
79
|
@options = options
|
80
80
|
|
81
|
-
@server =
|
82
|
-
|
83
|
-
:headers => {
|
84
|
-
:content_type => :json,
|
85
|
-
:accept => [ :json, :zip ],
|
86
|
-
:user_agent => GoodData.gem_version_string,
|
87
|
-
}
|
88
|
-
|
81
|
+
@server = create_server_connection(@url, @options)
|
82
|
+
|
89
83
|
end
|
90
84
|
|
91
85
|
# Returns the user JSON object of the currently logged in GoodData user account.
|
@@ -106,7 +100,7 @@ module GoodData
|
|
106
100
|
#
|
107
101
|
# Connection.new(username, password).get '/gdc/projects'
|
108
102
|
def get(path, options = {})
|
109
|
-
GoodData.logger.debug "GET #{path}"
|
103
|
+
GoodData.logger.debug "GET #{@server}#{path}"
|
110
104
|
ensure_connection
|
111
105
|
b = Proc.new { @server[path].get cookies }
|
112
106
|
process_response(options, &b)
|
@@ -126,7 +120,7 @@ module GoodData
|
|
126
120
|
# Connection.new(username, password).post '/gdc/projects', { ... }
|
127
121
|
def post(path, data, options = {})
|
128
122
|
payload = data.is_a?(Hash) ? data.to_json : data
|
129
|
-
GoodData.logger.debug "POST #{path}, payload: #{payload}"
|
123
|
+
GoodData.logger.debug "POST #{@server}#{path}, payload: #{payload}"
|
130
124
|
ensure_connection
|
131
125
|
b = Proc.new { @server[path].post payload, cookies }
|
132
126
|
process_response(options, &b)
|
@@ -146,7 +140,7 @@ module GoodData
|
|
146
140
|
# Connection.new(username, password).put '/gdc/projects', { ... }
|
147
141
|
def put(path, data, options = {})
|
148
142
|
payload = data.is_a?(Hash) ? data.to_json : data
|
149
|
-
GoodData.logger.debug "PUT #{path}, payload: #{payload}"
|
143
|
+
GoodData.logger.debug "PUT #{@server}#{path}, payload: #{payload}"
|
150
144
|
ensure_connection
|
151
145
|
b = Proc.new { @server[path].put payload, cookies }
|
152
146
|
process_response(options, &b)
|
@@ -164,7 +158,7 @@ module GoodData
|
|
164
158
|
#
|
165
159
|
# Connection.new(username, password).delete '/gdc/project/1'
|
166
160
|
def delete(path, options = {})
|
167
|
-
GoodData.logger.debug "DELETE #{path}"
|
161
|
+
GoodData.logger.debug "DELETE #{@server}#{path}"
|
168
162
|
ensure_connection
|
169
163
|
b = Proc.new { @server[path].delete cookies }
|
170
164
|
process_response(options, &b)
|
@@ -187,6 +181,11 @@ module GoodData
|
|
187
181
|
@status == :logged_in
|
188
182
|
end
|
189
183
|
|
184
|
+
def url=(url=nil)
|
185
|
+
@url = url || DEFAULT_URL
|
186
|
+
@server = create_server_connection(@url, @options)
|
187
|
+
end
|
188
|
+
|
190
189
|
# The connection will automatically be established once it's needed, which it
|
191
190
|
# usually is when either the user, get, post or delete method is called. If you
|
192
191
|
# want to force a connection (or a re-connect) you can use this method.
|
@@ -197,11 +196,11 @@ module GoodData
|
|
197
196
|
# Uploads a file to GoodData server
|
198
197
|
# /uploads/ resources are special in that they use a different
|
199
198
|
# host and a basic authentication.
|
200
|
-
def upload(file, dir = nil)
|
199
|
+
def upload(file, dir = nil, options={})
|
201
200
|
ensure_connection
|
202
201
|
# We should have followed a link. If it was correct.
|
202
|
+
|
203
203
|
stage_url = @options[:webdav_server] || @url.sub(/\./, '-di.')
|
204
|
-
|
205
204
|
# Make a directory, if needed
|
206
205
|
if dir then
|
207
206
|
url = stage_url + STAGE_PATH + dir + '/'
|
@@ -239,17 +238,20 @@ module GoodData
|
|
239
238
|
dir = "."
|
240
239
|
end
|
241
240
|
|
241
|
+
payload = options[:stream] ? "file" : File.read(file)
|
242
|
+
filename = options[:filename] || options[:stream] ? "randome-filename.txt" : File.basename(file)
|
243
|
+
|
242
244
|
# Upload the file
|
243
245
|
RestClient::Request.execute(
|
244
246
|
:method => :put,
|
245
|
-
:url => stage_url + STAGE_PATH + dir + '/' +
|
247
|
+
:url => stage_url + STAGE_PATH + dir + '/' + filename,
|
246
248
|
:user => @username,
|
247
249
|
:password => @password,
|
248
250
|
:timeout => @options[:timeout],
|
249
251
|
:headers => {
|
250
252
|
:user_agent => GoodData.gem_version_string,
|
251
253
|
},
|
252
|
-
:payload =>
|
254
|
+
:payload => payload
|
253
255
|
)
|
254
256
|
end
|
255
257
|
|
@@ -271,6 +273,16 @@ module GoodData
|
|
271
273
|
|
272
274
|
private
|
273
275
|
|
276
|
+
def create_server_connection(url, options)
|
277
|
+
RestClient::Resource.new url,
|
278
|
+
:timeout => options[:timeout],
|
279
|
+
:headers => {
|
280
|
+
:content_type => :json,
|
281
|
+
:accept => [ :json, :zip ],
|
282
|
+
:user_agent => GoodData.gem_version_string,
|
283
|
+
}
|
284
|
+
end
|
285
|
+
|
274
286
|
def ensure_connection
|
275
287
|
connect if @status == :not_connected
|
276
288
|
end
|