gooddata 0.6.7 → 0.6.8
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -1
- data/README.md +10 -2
- data/TODO.md +32 -0
- data/gooddata.gemspec +5 -0
- data/lib/gooddata.rb +4 -0
- data/lib/gooddata/app/app.rb +12 -0
- data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +4 -3
- data/lib/gooddata/bricks/middleware/restforce_middleware.rb +2 -1
- data/lib/gooddata/cli/commands/console_cmd.rb +23 -5
- data/lib/gooddata/cli/commands/domain_cmd.rb +9 -10
- data/lib/gooddata/cli/commands/process_cmd.rb +11 -9
- data/lib/gooddata/cli/commands/project_cmd.rb +25 -27
- data/lib/gooddata/cli/commands/projects_cmd.rb +2 -2
- data/lib/gooddata/cli/commands/run_ruby_cmd.rb +1 -1
- data/lib/gooddata/cli/commands/user_cmd.rb +2 -2
- data/lib/gooddata/cli/hooks.rb +4 -2
- data/lib/gooddata/cli/shared.rb +1 -1
- data/lib/gooddata/cli/terminal.rb +1 -1
- data/lib/gooddata/commands/api.rb +1 -1
- data/lib/gooddata/commands/auth.rb +4 -28
- data/lib/gooddata/commands/domain.rb +9 -4
- data/lib/gooddata/commands/process.rb +26 -23
- data/lib/gooddata/commands/project.rb +74 -50
- data/lib/gooddata/commands/projects.rb +3 -2
- data/lib/gooddata/commands/role.rb +9 -3
- data/lib/gooddata/commands/user.rb +6 -4
- data/lib/gooddata/connection.rb +11 -45
- data/lib/gooddata/core/logging.rb +0 -1
- data/lib/gooddata/core/project.rb +22 -22
- data/lib/gooddata/core/rest.rb +9 -8
- data/lib/gooddata/core/user.rb +0 -11
- data/lib/gooddata/exceptions/project_not_found.rb +1 -0
- data/lib/gooddata/extensions/enumerable.rb +10 -0
- data/lib/gooddata/extensions/hash.rb +25 -0
- data/lib/gooddata/goodzilla/goodzilla.rb +4 -4
- data/lib/gooddata/helper/class_helper.rb +1 -0
- data/lib/gooddata/helper/helpers.rb +8 -0
- data/lib/gooddata/helpers/auth_helpers.rb +41 -0
- data/lib/gooddata/mixins/author.rb +1 -1
- data/lib/gooddata/mixins/contributor.rb +1 -1
- data/lib/gooddata/mixins/data_property_reader.rb +2 -0
- data/lib/gooddata/mixins/data_property_writer.rb +2 -0
- data/lib/gooddata/mixins/inspector.rb +49 -0
- data/lib/gooddata/mixins/md_finders.rb +16 -8
- data/lib/gooddata/mixins/md_id_to_uri.rb +12 -4
- data/lib/gooddata/mixins/md_object_indexer.rb +15 -4
- data/lib/gooddata/mixins/md_object_query.rb +42 -20
- data/lib/gooddata/mixins/md_relations.rb +21 -12
- data/lib/gooddata/mixins/meta_getter.rb +2 -0
- data/lib/gooddata/mixins/meta_property_reader.rb +2 -0
- data/lib/gooddata/mixins/meta_property_writer.rb +2 -0
- data/lib/gooddata/mixins/rest_resource.rb +32 -10
- data/lib/gooddata/mixins/root_key_getter.rb +1 -1
- data/lib/gooddata/models/data_result.rb +3 -1
- data/lib/gooddata/models/domain.rb +31 -22
- data/lib/gooddata/models/empty_result.rb +22 -0
- data/lib/gooddata/models/invitation.rb +11 -9
- data/lib/gooddata/models/links.rb +5 -3
- data/lib/gooddata/models/membership.rb +23 -28
- data/lib/gooddata/models/metadata.rb +35 -35
- data/lib/gooddata/models/metadata/attribute.rb +10 -8
- data/lib/gooddata/models/metadata/dashboard.rb +1 -1
- data/lib/gooddata/models/metadata/fact.rb +3 -3
- data/lib/gooddata/models/metadata/label.rb +4 -4
- data/lib/gooddata/models/metadata/metric.rb +76 -38
- data/lib/gooddata/models/metadata/report.rb +52 -17
- data/lib/gooddata/models/metadata/report_definition.rb +178 -28
- data/lib/gooddata/models/model.rb +13 -6
- data/lib/gooddata/models/process.rb +93 -30
- data/lib/gooddata/models/profile.rb +18 -20
- data/lib/gooddata/models/project.rb +344 -127
- data/lib/gooddata/models/project_creator.rb +32 -22
- data/lib/gooddata/models/project_metadata.rb +26 -14
- data/lib/gooddata/models/project_role.rb +15 -17
- data/lib/gooddata/models/report_data_result.rb +4 -0
- data/lib/gooddata/models/schedule.rb +51 -20
- data/lib/gooddata/models/schema_blueprint.rb +9 -3
- data/lib/gooddata/rest/README.md +37 -0
- data/lib/gooddata/rest/client.rb +318 -0
- data/lib/gooddata/rest/connection.rb +235 -0
- data/lib/gooddata/rest/connections/connections.rb +8 -0
- data/lib/gooddata/rest/connections/dummy_connection.rb +52 -0
- data/lib/gooddata/rest/connections/rest_client_connection.rb +177 -0
- data/lib/gooddata/rest/object.rb +32 -0
- data/lib/gooddata/rest/object_factory.rb +67 -0
- data/lib/gooddata/rest/resource.rb +17 -0
- data/lib/gooddata/rest/rest.rb +20 -0
- data/lib/gooddata/version.rb +1 -1
- data/spec/data/cc/data/source/commits.csv +4 -0
- data/spec/data/cc/data/source/devs.csv +4 -0
- data/spec/data/cc/data/source/repos.csv +3 -0
- data/spec/data/cc/devel.prm +0 -0
- data/spec/data/cc/graph/graph.grf +11 -0
- data/spec/data/cc/workspace.prm +19 -0
- data/spec/data/hello_world_process/hello_world.rb +1 -0
- data/spec/data/hello_world_process/hello_world.zip +0 -0
- data/spec/data/users.csv +12 -12
- data/spec/helpers/connection_helper.rb +6 -0
- data/spec/helpers/process_helper.rb +12 -0
- data/spec/helpers/project_helper.rb +2 -2
- data/spec/integration/command_projects_spec.rb +11 -9
- data/spec/integration/create_from_template_spec.rb +6 -2
- data/spec/integration/full_process_schedule_spec.rb +49 -36
- data/spec/integration/full_project_spec.rb +221 -256
- data/spec/integration/partial_md_export_import_spec.rb +18 -17
- data/spec/logging_in_logging_out_spec.rb +17 -8
- data/spec/spec_helper.rb +4 -2
- data/spec/unit/cli/commands/cmd_api_spec.rb +1 -1
- data/spec/unit/cli/commands/cmd_auth_spec.rb +1 -1
- data/spec/unit/cli/commands/cmd_domain_spec.rb +29 -3
- data/spec/unit/cli/commands/cmd_process_spec.rb +1 -1
- data/spec/unit/cli/commands/cmd_project_spec.rb +1 -1
- data/spec/unit/cli/commands/cmd_role_spec.rb +13 -2
- data/spec/unit/cli/commands/cmd_run_ruby_spec.rb +1 -1
- data/spec/unit/cli/commands/cmd_scaffold_spec.rb +1 -1
- data/spec/unit/cli/commands/cmd_user_spec.rb +1 -1
- data/spec/unit/commands/command_api_spec.rb +0 -19
- data/spec/unit/commands/command_auth_spec.rb +20 -13
- data/spec/unit/commands/command_dataset_spec.rb +2 -2
- data/spec/unit/commands/command_process_spec.rb +24 -21
- data/spec/unit/commands/command_projects_spec.rb +2 -2
- data/spec/unit/commands/command_scaffold_spec.rb +2 -2
- data/spec/unit/commands/command_user_spec.rb +3 -3
- data/spec/unit/core/connection_spec.rb +9 -10
- data/spec/unit/core/project_spec.rb +8 -4
- data/spec/unit/core/rest_spec.rb +6 -6
- data/spec/unit/models/domain_spec.rb +14 -7
- data/spec/unit/models/invitation_spec.rb +2 -2
- data/spec/unit/models/membership_spec.rb +5 -5
- data/spec/unit/models/metric_spec.rb +92 -0
- data/spec/unit/models/profile_spec.rb +25 -21
- data/spec/unit/models/project_blueprint_spec.rb +6 -6
- data/spec/unit/models/project_role_spec.rb +3 -5
- data/spec/unit/models/project_spec.rb +43 -37
- data/spec/unit/models/schedule_spec.rb +58 -107
- data/spec/unit/rest/resource_spec.rb +6 -0
- metadata +87 -10
- data/lib/gooddata/cli/commands/role_cmd.rb +0 -28
- data/lib/gooddata/core/connection.rb +0 -392
- data/lib/gooddata/core/threaded.rb +0 -14
- data/lib/gooddata/models/md_object.rb +0 -25
- data/lib/gooddata/models/metadata/folder.rb +0 -24
- data/spec/unit/models/md_object_spec.rb +0 -55
- data/spec/unit/models/metric.rb +0 -92
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'rest-client'
|
|
4
|
+
|
|
5
|
+
require_relative '../helpers/auth_helpers'
|
|
6
|
+
|
|
7
|
+
require_relative 'connections/connections'
|
|
8
|
+
require_relative 'object_factory'
|
|
9
|
+
|
|
10
|
+
require_relative '../mixins/inspector'
|
|
11
|
+
|
|
12
|
+
module GoodData
|
|
13
|
+
module Rest
|
|
14
|
+
# User's interface to GoodData Platform.
|
|
15
|
+
#
|
|
16
|
+
# MUST provide way to use - DELETE, GET, POST, PUT
|
|
17
|
+
# SHOULD provide way to use - HEAD, Bulk GET ...
|
|
18
|
+
class Client
|
|
19
|
+
#################################
|
|
20
|
+
# Constants
|
|
21
|
+
#################################
|
|
22
|
+
DEFAULT_CONNECTION_IMPLEMENTATION = Connections::RestClientConnection
|
|
23
|
+
|
|
24
|
+
#################################
|
|
25
|
+
# Class variables
|
|
26
|
+
#################################
|
|
27
|
+
@@instance = nil # rubocop:disable ClassVars
|
|
28
|
+
|
|
29
|
+
#################################
|
|
30
|
+
# Getters/Setters
|
|
31
|
+
#################################
|
|
32
|
+
|
|
33
|
+
# Decide if we need provide direct access to connection
|
|
34
|
+
attr_reader :connection
|
|
35
|
+
|
|
36
|
+
# TODO: Decide if we need provide direct access to factory
|
|
37
|
+
attr_reader :factory
|
|
38
|
+
|
|
39
|
+
include Mixin::Inspector
|
|
40
|
+
inspector :object_id
|
|
41
|
+
|
|
42
|
+
#################################
|
|
43
|
+
# Class methods
|
|
44
|
+
#################################
|
|
45
|
+
class << self
|
|
46
|
+
# Globally available way to connect (and create client and set global instance)
|
|
47
|
+
#
|
|
48
|
+
# ## HACK
|
|
49
|
+
# To make transition from old implementation to new one following HACK IS TEMPORARILY ENGAGED!
|
|
50
|
+
#
|
|
51
|
+
# 1. First call of #connect sets the GoodData::Rest::Client.instance (static, singleton instance)
|
|
52
|
+
# 2. There are METHOD functions with same signature as their CLASS counterparts using singleton instance
|
|
53
|
+
#
|
|
54
|
+
# ## Example
|
|
55
|
+
#
|
|
56
|
+
# client = GoodData.connect('jon.smith@goodddata.com', 's3cr3tp4sw0rd')
|
|
57
|
+
#
|
|
58
|
+
# @param username [String] Username to be used for authentication
|
|
59
|
+
# @param password [String] Password to be used for authentication
|
|
60
|
+
# @return [GoodData::Rest::Client] Client
|
|
61
|
+
def connect(username, password, opts = {})
|
|
62
|
+
new_opts = opts.dup
|
|
63
|
+
if username.is_a?(Hash) && username.key?(:sst_token)
|
|
64
|
+
new_opts[:sst_token] = username[:sst_token]
|
|
65
|
+
elsif username.is_a? Hash
|
|
66
|
+
new_opts[:username] = username[:login] || username[:user] || username[:username]
|
|
67
|
+
new_opts[:password] = username[:password]
|
|
68
|
+
elsif username.nil? && password.nil? && (opts.nil? || opts.empty?)
|
|
69
|
+
new_opts = Helpers::AuthHelper.read_credentials
|
|
70
|
+
else
|
|
71
|
+
new_opts[:username] = username
|
|
72
|
+
new_opts[:password] = password
|
|
73
|
+
end
|
|
74
|
+
unless new_opts[:sst_token]
|
|
75
|
+
fail ArgumentError, 'No username specified' if new_opts[:username].nil?
|
|
76
|
+
fail ArgumentError, 'No password specified' if new_opts[:password].nil?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
client = Client.new(new_opts)
|
|
80
|
+
|
|
81
|
+
if client
|
|
82
|
+
at_exit do
|
|
83
|
+
# puts client.connection.stats_table if client && client.connection
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# HACK: This line assigns class instance # if not done yet
|
|
88
|
+
@@instance = client # rubocop:disable ClassVars
|
|
89
|
+
client
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def disconnect
|
|
93
|
+
if @@instance # rubocop:disable ClassVars
|
|
94
|
+
@@instance.disconnect # rubocop:disable ClassVars
|
|
95
|
+
@@instance = nil # rubocop:disable ClassVars
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def connection
|
|
100
|
+
@@instance # rubocop:disable ClassVars
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
alias_method :client, :connection
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Constructor of client
|
|
107
|
+
# @param opts [Hash] Client options
|
|
108
|
+
# @option opts [String] :username Username used for authentication
|
|
109
|
+
# @option opts [String] :password Password used for authentication
|
|
110
|
+
# @option opts :connection_factory Object able to create new instances of GoodData::Rest::Connection
|
|
111
|
+
# @option opts [GoodData::Rest::Connection] :connection Existing GoodData::Rest::Connection
|
|
112
|
+
def initialize(opts)
|
|
113
|
+
# TODO: Decide if we want to pass the options directly or not
|
|
114
|
+
@opts = opts
|
|
115
|
+
|
|
116
|
+
@connection_factory = @opts[:connection_factory] || DEFAULT_CONNECTION_IMPLEMENTATION
|
|
117
|
+
|
|
118
|
+
# TODO: See previous TODO
|
|
119
|
+
# Create connection
|
|
120
|
+
@connection = opts[:connection] || @connection_factory.new(opts)
|
|
121
|
+
|
|
122
|
+
# Connect
|
|
123
|
+
connect
|
|
124
|
+
|
|
125
|
+
# Create factory bound to previously created connection
|
|
126
|
+
@factory = ObjectFactory.new(self)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def domain(domain_name)
|
|
130
|
+
GoodData::Domain[domain_name, :client => self]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def projects(id = :all)
|
|
134
|
+
GoodData::Project[id, client: self]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def processes(id = :all)
|
|
138
|
+
GoodData::Process[id, client: self]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def connect
|
|
142
|
+
username = @opts[:username]
|
|
143
|
+
password = @opts[:password]
|
|
144
|
+
|
|
145
|
+
@connection.connect(username, password, @opts)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def disconnect
|
|
149
|
+
@connection.disconnect
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
#######################
|
|
153
|
+
# Factory stuff
|
|
154
|
+
######################
|
|
155
|
+
def create(klass, data = {}, opts = {})
|
|
156
|
+
@factory.create(klass, data, opts)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def find(klass, opts = {})
|
|
160
|
+
@factory.find(klass, opts)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Gets resource by name
|
|
164
|
+
def resource(res_name)
|
|
165
|
+
puts "Getting resource '#{res_name}'"
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def user
|
|
170
|
+
create(GoodData::Profile, @connection.user)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
#######################
|
|
174
|
+
# Rest
|
|
175
|
+
#######################
|
|
176
|
+
# HTTP DELETE
|
|
177
|
+
#
|
|
178
|
+
# @param uri [String] Target URI
|
|
179
|
+
def delete(uri, opts = {})
|
|
180
|
+
@connection.delete uri, opts
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# HTTP GET
|
|
184
|
+
#
|
|
185
|
+
# @param uri [String] Target URI
|
|
186
|
+
def get(uri, opts = {})
|
|
187
|
+
@connection.get uri, opts
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# FIXME: Invstigate _file argument
|
|
191
|
+
def get_project_webdav_path(_file, opts = { :project => GoodData.project })
|
|
192
|
+
p = opts[:project]
|
|
193
|
+
fail ArgumentError, 'No :project specified' if p.nil?
|
|
194
|
+
|
|
195
|
+
project = GoodData::Project[p, opts]
|
|
196
|
+
fail ArgumentError, 'Wrong :project specified' if project.nil?
|
|
197
|
+
|
|
198
|
+
u = URI(project.links['uploads'])
|
|
199
|
+
URI.join(u.to_s.chomp(u.path.to_s), '/project-uploads/', "#{project.pid}/")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# FIXME: Invstigate _file argument
|
|
203
|
+
def get_user_webdav_path(_file, opts = { :project => GoodData.project })
|
|
204
|
+
p = opts[:project]
|
|
205
|
+
fail ArgumentError, 'No :project specified' if p.nil?
|
|
206
|
+
|
|
207
|
+
project = GoodData::Project[p, opts]
|
|
208
|
+
fail ArgumentError, 'Wrong :project specified' if project.nil?
|
|
209
|
+
|
|
210
|
+
u = URI(project.links['uploads'])
|
|
211
|
+
URI.join(u.to_s.chomp(u.path.to_s), '/uploads/')
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Generalizaton of poller. Since we have quite a variation of how async proceses are handled
|
|
215
|
+
# this is a helper that should help you with resources where the information about "Are we done"
|
|
216
|
+
# is the http code of response. By default we repeat as long as the code == 202. You can
|
|
217
|
+
# change the code if necessary. It expects the URI as an input where it can poll. It returns the
|
|
218
|
+
# value of last poll. In majority of cases these are the data that you need.
|
|
219
|
+
#
|
|
220
|
+
# @param link [String] Link for polling
|
|
221
|
+
# @param options [Hash] Options
|
|
222
|
+
# @return [Hash] Result of polling
|
|
223
|
+
def poll_on_code(link, options = {})
|
|
224
|
+
code = options[:code] || 202
|
|
225
|
+
sleep_interval = options[:sleep_interval] || DEFAULT_SLEEP_INTERVAL
|
|
226
|
+
response = get(link, :process => false)
|
|
227
|
+
|
|
228
|
+
while response.code == code
|
|
229
|
+
sleep sleep_interval
|
|
230
|
+
retryable(:tries => 3, :on => RestClient::InternalServerError) do
|
|
231
|
+
sleep sleep_interval
|
|
232
|
+
response = get(link, :process => false)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
if options[:process] == false
|
|
236
|
+
response
|
|
237
|
+
else
|
|
238
|
+
get(link)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Generalizaton of poller. Since we have quite a variation of how async proceses are handled
|
|
243
|
+
# this is a helper that should help you with resources where the information about "Are we done"
|
|
244
|
+
# is inside the response. It expects the URI as an input where it can poll and a block that should
|
|
245
|
+
# return either true -> 'meaning we are done' or false -> meaning sleep and repeat. It returns the
|
|
246
|
+
# value of last poll. In majority of cases these are the data that you need
|
|
247
|
+
#
|
|
248
|
+
# @param link [String] Link for polling
|
|
249
|
+
# @param options [Hash] Options
|
|
250
|
+
# @return [Hash] Result of polling
|
|
251
|
+
def poll_on_response(link, options = {}, &bl)
|
|
252
|
+
sleep_interval = options[:sleep_interval] || DEFAULT_SLEEP_INTERVAL
|
|
253
|
+
response = get(link)
|
|
254
|
+
while bl.call(response)
|
|
255
|
+
sleep sleep_interval
|
|
256
|
+
retryable(:tries => 3, :on => RestClient::InternalServerError) do
|
|
257
|
+
sleep sleep_interval
|
|
258
|
+
response = get(link)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
response
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# HTTP PUT
|
|
265
|
+
#
|
|
266
|
+
# @param uri [String] Target URI
|
|
267
|
+
def put(uri, data, opts = {})
|
|
268
|
+
@connection.put uri, data, opts
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# HTTP POST
|
|
272
|
+
#
|
|
273
|
+
# @param uri [String] Target URI
|
|
274
|
+
def post(uri, data, opts = {})
|
|
275
|
+
@connection.post uri, data, opts
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Retry blok if exception thrown
|
|
279
|
+
def retryable(options = {}, &block)
|
|
280
|
+
opts = { :tries => 1, :on => Exception }.merge(options)
|
|
281
|
+
|
|
282
|
+
retry_exception, retries = opts[:on], opts[:tries]
|
|
283
|
+
|
|
284
|
+
begin
|
|
285
|
+
return yield
|
|
286
|
+
rescue retry_exception
|
|
287
|
+
retry if (retries -= 1) > 0
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
yield
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Uploads file
|
|
294
|
+
def upload(file, options = {})
|
|
295
|
+
@connection.upload file, options
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def upload_to_user_webdav(file, options = {})
|
|
299
|
+
p = options[:project]
|
|
300
|
+
fail ArgumentError, 'No :project specified' if p.nil?
|
|
301
|
+
|
|
302
|
+
project = GoodData::Project[p, options]
|
|
303
|
+
fail ArgumentError, 'Wrong :project specified' if project.nil?
|
|
304
|
+
|
|
305
|
+
u = URI(project.links['uploads'])
|
|
306
|
+
url = URI.join(u.to_s.chomp(u.path.to_s), '/uploads/')
|
|
307
|
+
upload(file, options.merge(
|
|
308
|
+
:directory => options[:directory],
|
|
309
|
+
:staging_url => url
|
|
310
|
+
))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def with_project(pid, &block)
|
|
314
|
+
GoodData.with_project(pid, client: self, &block)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'terminal-table'
|
|
4
|
+
|
|
5
|
+
require_relative '../version'
|
|
6
|
+
require_relative '../exceptions/exceptions'
|
|
7
|
+
|
|
8
|
+
module GoodData
|
|
9
|
+
module Rest
|
|
10
|
+
# Wrapper of low-level HTTP/REST client/library
|
|
11
|
+
class Connection
|
|
12
|
+
DEFAULT_URL = 'https://secure.gooddata.com'
|
|
13
|
+
LOGIN_PATH = '/gdc/account/login'
|
|
14
|
+
TOKEN_PATH = '/gdc/account/token'
|
|
15
|
+
|
|
16
|
+
DEFAULT_HEADERS = {
|
|
17
|
+
:content_type => :json,
|
|
18
|
+
:accept => [:json, :zip],
|
|
19
|
+
:user_agent => GoodData.gem_version_string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
DEFAULT_LOGIN_PAYLOAD = {
|
|
23
|
+
:headers => DEFAULT_HEADERS
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def construct_login_payload(username, password)
|
|
28
|
+
res = {
|
|
29
|
+
'postUserLogin' => {
|
|
30
|
+
'login' => username,
|
|
31
|
+
'password' => password,
|
|
32
|
+
'remember' => 1
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
res
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :cookies
|
|
40
|
+
attr_reader :stats
|
|
41
|
+
attr_reader :user
|
|
42
|
+
|
|
43
|
+
def initialize(opts)
|
|
44
|
+
@user = nil
|
|
45
|
+
|
|
46
|
+
@stats = {}
|
|
47
|
+
@opts = opts
|
|
48
|
+
|
|
49
|
+
# Initialize cookies
|
|
50
|
+
reset_cookies!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Connect using username and password
|
|
54
|
+
def connect(username, password, options = {})
|
|
55
|
+
# Reset old cookies first
|
|
56
|
+
if options[:sst_token]
|
|
57
|
+
merge_cookies!('GDCAuthSST' => options[:sst_token])
|
|
58
|
+
@user = get(get('/gdc/app/account/bootstrap')['bootstrapResource']['accountSetting']['links']['self'])
|
|
59
|
+
@auth = {}
|
|
60
|
+
refresh_token :dont_reauth => true
|
|
61
|
+
else
|
|
62
|
+
credentials = Connection.construct_login_payload(username, password)
|
|
63
|
+
@auth = post(LOGIN_PATH, credentials, :dont_reauth => true)['userLogin']
|
|
64
|
+
|
|
65
|
+
@user = get(@auth['profile'])
|
|
66
|
+
refresh_token :dont_reauth => true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Disconnect
|
|
71
|
+
def disconnect
|
|
72
|
+
# TODO: Wrap somehow
|
|
73
|
+
url = @auth['state']
|
|
74
|
+
delete url if url
|
|
75
|
+
|
|
76
|
+
@auth = nil
|
|
77
|
+
@server = nil
|
|
78
|
+
@user = nil
|
|
79
|
+
|
|
80
|
+
reset_cookies!
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def refresh_token(options = {})
|
|
84
|
+
begin # rubocop:disable RedundantBegin
|
|
85
|
+
get TOKEN_PATH, :dont_reauth => true # avoid infinite loop GET fails with 401
|
|
86
|
+
rescue Exception => e # rubocop:disable RescueException
|
|
87
|
+
puts e.message
|
|
88
|
+
raise e
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns server URI
|
|
93
|
+
#
|
|
94
|
+
# @return [String] server uri
|
|
95
|
+
def server_url
|
|
96
|
+
@server && @server.url
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# HTTP DELETE
|
|
100
|
+
#
|
|
101
|
+
# @param uri [String] Target URI
|
|
102
|
+
def delete(uri, options = {})
|
|
103
|
+
fail NotImplementedError "DELETE #{uri}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# HTTP GET
|
|
107
|
+
#
|
|
108
|
+
# @param uri [String] Target URI
|
|
109
|
+
def get(uri, options = {})
|
|
110
|
+
fail NotImplementedError "GET #{uri}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# HTTP PUT
|
|
114
|
+
#
|
|
115
|
+
# @param uri [String] Target URI
|
|
116
|
+
def put(uri, data, options = {})
|
|
117
|
+
fail NotImplementedError "PUT #{uri}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# HTTP POST
|
|
121
|
+
#
|
|
122
|
+
# @param uri [String] Target URI
|
|
123
|
+
def post(uri, data, options = {})
|
|
124
|
+
fail NotImplementedError "POST #{uri}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Reader method for SST token
|
|
128
|
+
#
|
|
129
|
+
# @return uri [String] SST token
|
|
130
|
+
def sst_token
|
|
131
|
+
cookies[:cookies]['GDCAuthSST']
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def stats_table(values = stats)
|
|
135
|
+
sorted = values.sort_by { |k, v| v[:avg] }
|
|
136
|
+
Terminal::Table.new :headings => %w(title avg min max total calls) do |t|
|
|
137
|
+
sorted.each do |l|
|
|
138
|
+
row = [
|
|
139
|
+
l[0],
|
|
140
|
+
sprintf('%.3f', l[1][:avg]),
|
|
141
|
+
sprintf('%.3f', l[1][:min]),
|
|
142
|
+
sprintf('%.3f', l[1][:max]),
|
|
143
|
+
sprintf('%.3f', l[1][:total]),
|
|
144
|
+
l[1][:calls]
|
|
145
|
+
]
|
|
146
|
+
t.add_row row
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Reader method for TT token
|
|
152
|
+
#
|
|
153
|
+
# @return uri [String] TT token
|
|
154
|
+
def tt_token
|
|
155
|
+
cookies[:cookies]['GDCAuthTT']
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def merge_cookies!(cookies)
|
|
161
|
+
@cookies[:cookies].merge! cookies
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def profile(title, &block)
|
|
165
|
+
t1 = Time.now
|
|
166
|
+
res = block.call
|
|
167
|
+
t2 = Time.now
|
|
168
|
+
delta = t2 - t1
|
|
169
|
+
|
|
170
|
+
update_stats title, delta
|
|
171
|
+
res
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def reset_cookies!
|
|
175
|
+
@cookies = { :cookies => {} }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# TODO: Store PH_MAP for wildcarding of URLs in reports in separate file
|
|
179
|
+
PH_MAP = [
|
|
180
|
+
['/gdc/projects/{id}/users/{id}/permissions', %r{/gdc/projects/[\w]+/users/[\w]+/permissions}],
|
|
181
|
+
['/gdc/projects/{id}/roles/{id}', %r{/gdc/projects/[\w]+/roles/[\d]+}],
|
|
182
|
+
['/gdc/projects/{id}/model/diff/{id}', %r{/gdc/projects/[\w]+/model/diff/[\w]+}],
|
|
183
|
+
['/gdc/projects/{id}/', %r{/gdc/projects/[\w]+/}],
|
|
184
|
+
['/gdc/projects/{id}', %r{/gdc/projects/[\w]+}],
|
|
185
|
+
['/gdc/md/{id}/using2/{id}/{id}', %r{/gdc/md/[\w]+/using2/[\d]+/[\d]+}],
|
|
186
|
+
['/gdc/md/{id}/usedby2/{id}/{id}', %r{/gdc/md/[\w]+/usedby2/[\d]+/[\d]+}],
|
|
187
|
+
['/gdc/md/{id}/tasks/{id}/status', %r{/gdc/md/[\w]+/tasks/[\w]+/status}],
|
|
188
|
+
['/gdc/md/{id}/obj/{id}/validElements', %r{/gdc/md/[\w]+/obj/[\d]+/validElements(/)?(\?.*)?}],
|
|
189
|
+
['/gdc/md/{id}/obj/{id}/elements', %r{/gdc/md/[\w]+/obj/[\d]+/elements(/)?(\?.*)?}],
|
|
190
|
+
['/gdc/md/{id}/obj/{id}', %r{/gdc/md/[\w]+/obj/[\d]+}],
|
|
191
|
+
['/gdc/md/{id}/etl/task/{id}', %r{/gdc/md/[\w]+/etl/task/[\d]+}],
|
|
192
|
+
['/gdc/md/{id}/dataResult/{id}', %r{/gdc/md/[\w]+/dataResult/[\d]+}],
|
|
193
|
+
['/gdc/md/{id}', %r{/gdc/md/[\w]+}],
|
|
194
|
+
['/gdc/app/projects/{id}/execute', %r{/gdc/app/projects/[\w]+/execute}],
|
|
195
|
+
['/gdc/account/profile/{id}', %r{/gdc/account/profile/[\w]+}],
|
|
196
|
+
['/gdc/account/login/{id}', %r{/gdc/account/login/[\w]+}],
|
|
197
|
+
['/gdc/account/domains/{id}', %r{/gdc/account/domains/[\w\d-]+}]
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
def update_stats(title, delta)
|
|
201
|
+
orig_title = title
|
|
202
|
+
|
|
203
|
+
placeholders = true
|
|
204
|
+
|
|
205
|
+
if placeholders
|
|
206
|
+
PH_MAP.each do |pm|
|
|
207
|
+
break if title.gsub!(pm[1], pm[0])
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
stat = stats[title]
|
|
212
|
+
if stat.nil?
|
|
213
|
+
stat = {
|
|
214
|
+
:min => delta,
|
|
215
|
+
:max => delta,
|
|
216
|
+
:total => 0,
|
|
217
|
+
:avg => 0,
|
|
218
|
+
:calls => 0,
|
|
219
|
+
:entries => []
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
stat[:min] = delta if delta < stat[:min]
|
|
224
|
+
stat[:max] = delta if delta > stat[:max]
|
|
225
|
+
stat[:total] += delta
|
|
226
|
+
stat[:calls] += 1
|
|
227
|
+
stat[:avg] = stat[:total] / stat[:calls]
|
|
228
|
+
|
|
229
|
+
stat[:entries] << orig_title if placeholders
|
|
230
|
+
|
|
231
|
+
stats[title] = stat
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|