gooddata 0.6.16 → 0.6.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/lib/gooddata/cli/commands/project_cmd.rb +1 -1
  4. data/lib/gooddata/core/logging.rb +15 -5
  5. data/lib/gooddata/core/rest.rb +4 -28
  6. data/lib/gooddata/helpers/global_helpers.rb +14 -138
  7. data/lib/gooddata/helpers/global_helpers_params.rb +145 -0
  8. data/lib/gooddata/mixins/md_object_indexer.rb +2 -2
  9. data/lib/gooddata/models/domain.rb +1 -1
  10. data/lib/gooddata/models/execution.rb +29 -1
  11. data/lib/gooddata/models/from_wire.rb +6 -0
  12. data/lib/gooddata/models/from_wire_parse.rb +125 -0
  13. data/lib/gooddata/models/metadata/attribute.rb +1 -1
  14. data/lib/gooddata/models/metadata/label.rb +11 -10
  15. data/lib/gooddata/models/model.rb +4 -0
  16. data/lib/gooddata/models/profile.rb +12 -2
  17. data/lib/gooddata/models/project.rb +6 -3
  18. data/lib/gooddata/models/project_blueprint.rb +4 -4
  19. data/lib/gooddata/models/project_creator.rb +8 -10
  20. data/lib/gooddata/models/report_data_result.rb +4 -2
  21. data/lib/gooddata/models/schedule.rb +121 -66
  22. data/lib/gooddata/models/to_wire.rb +12 -3
  23. data/lib/gooddata/models/user_filters/user_filter_builder.rb +3 -234
  24. data/lib/gooddata/models/user_filters/user_filter_builder_create.rb +115 -0
  25. data/lib/gooddata/models/user_filters/user_filter_builder_execute.rb +133 -0
  26. data/lib/gooddata/rest/client.rb +27 -13
  27. data/lib/gooddata/rest/connection.rb +102 -23
  28. data/lib/gooddata/version.rb +1 -1
  29. data/spec/data/gd_gse_data_blueprint.json +1 -0
  30. data/spec/data/test_project_model_spec.json +5 -2
  31. data/spec/data/wire_models/model_view.json +3 -0
  32. data/spec/data/wire_test_project.json +8 -1
  33. data/spec/integration/full_project_spec.rb +1 -1
  34. data/spec/unit/core/connection_spec.rb +16 -0
  35. data/spec/unit/core/logging_spec.rb +54 -6
  36. data/spec/unit/models/domain_spec.rb +10 -4
  37. data/spec/unit/models/execution_spec.rb +102 -0
  38. data/spec/unit/models/from_wire_spec.rb +11 -2
  39. data/spec/unit/models/model_spec.rb +2 -2
  40. data/spec/unit/models/project_blueprint_spec.rb +1 -1
  41. data/spec/unit/models/schedule_spec.rb +34 -24
  42. data/spec/unit/models/to_wire_spec.rb +9 -1
  43. metadata +8 -3
@@ -20,6 +20,8 @@ module GoodData
20
20
  # Constants
21
21
  #################################
22
22
  DEFAULT_CONNECTION_IMPLEMENTATION = GoodData::Rest::Connection
23
+ DEFAULT_SLEEP_INTERVAL = 10
24
+ DEFAULT_POLL_TIME_LIMIT = 5 * 60 * 60 # 5 hours
23
25
 
24
26
  #################################
25
27
  # Class variables
@@ -207,6 +209,10 @@ module GoodData
207
209
  @stats
208
210
  end
209
211
 
212
+ def generate_request_id
213
+ @connection.generate_request_id
214
+ end
215
+
210
216
  #######################
211
217
  # Rest
212
218
  #######################
@@ -231,8 +237,9 @@ module GoodData
231
237
  project = GoodData::Project[p, opts]
232
238
  fail ArgumentError, 'Wrong :project specified' if project.nil?
233
239
 
234
- u = URI(project.links['uploads'])
235
- URI.join(u.to_s.chomp(u.path.to_s), '/project-uploads/', "#{project.pid}/")
240
+ url = project.links['uploads']
241
+ fail 'Project WebDAV not supported in this Data Center' unless url
242
+ url
236
243
  end
237
244
 
238
245
  def user_webdav_path(opts = { :project => GoodData.project })
@@ -257,17 +264,13 @@ module GoodData
257
264
  # @return [Hash] Result of polling
258
265
  def poll_on_code(link, options = {})
259
266
  code = options[:code] || 202
260
- sleep_interval = options[:sleep_interval] || DEFAULT_SLEEP_INTERVAL
261
- response = get(link, :process => false)
267
+ process = options[:process]
262
268
 
263
- while response.code == code
264
- sleep sleep_interval
265
- GoodData::Rest::Client.retryable(:tries => 3, :refresh_token => proc { connection.refresh_token }) do
266
- sleep sleep_interval
267
- response = get(link, :process => false)
268
- end
269
+ response = poll_on_response(link, options.merge(:process => false)) do |resp|
270
+ resp.code == code
269
271
  end
270
- if options[:process] == false
272
+
273
+ if process == false
271
274
  response
272
275
  else
273
276
  get(link)
@@ -285,12 +288,23 @@ module GoodData
285
288
  # @return [Hash] Result of polling
286
289
  def poll_on_response(link, options = {}, &bl)
287
290
  sleep_interval = options[:sleep_interval] || DEFAULT_SLEEP_INTERVAL
288
- response = get(link)
291
+ time_limit = options[:time_limit] || DEFAULT_POLL_TIME_LIMIT
292
+
293
+ # by default the response is processed
294
+ process = options[:process]
295
+
296
+ # get the first status and start the timer
297
+ response = get(link, :process => process)
298
+ poll_start = Time.now
299
+
289
300
  while bl.call(response)
301
+ if time_limit && (Time.now - poll_start > time_limit)
302
+ fail "The time limit #{time_limit} secs for polling on #{link} is over"
303
+ end
290
304
  sleep sleep_interval
291
305
  GoodData::Rest::Client.retryable(:tries => 3, :refresh_token => proc { connection.refresh_token }) do
292
306
  sleep sleep_interval
293
- response = get(link)
307
+ response = get(link, :process => process)
294
308
  end
295
309
  end
296
310
  response
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'terminal-table'
4
+ require 'securerandom'
4
5
 
5
6
  require_relative '../version'
6
7
  require_relative '../exceptions/exceptions'
@@ -9,7 +10,7 @@ module GoodData
9
10
  module Rest
10
11
  # Wrapper of low-level HTTP/REST client/library
11
12
  class Connection
12
- DEFAULT_URL = 'https://secure.gooddata.com'
13
+ DEFAULT_URL = ENV['GD_SERVER'] || 'https://secure.gooddata.com'
13
14
  LOGIN_PATH = '/gdc/account/login'
14
15
  TOKEN_PATH = '/gdc/account/token'
15
16
  KEYS_TO_SCRUB = [:password, :verifyPassword, :authorizationToken]
@@ -20,9 +21,13 @@ module GoodData
20
21
  :user_agent => GoodData.gem_version_string
21
22
  }
22
23
 
24
+ DEFAULT_WEBDAV_HEADERS = {
25
+ :user_agent => GoodData.gem_version_string
26
+ }
27
+
23
28
  DEFAULT_LOGIN_PAYLOAD = {
24
29
  :headers => DEFAULT_HEADERS,
25
- :verify_ssl => OpenSSL::SSL::VERIFY_NONE
30
+ :verify_ssl => true
26
31
  }
27
32
 
28
33
  RETRYABLE_ERRORS = [
@@ -51,7 +56,8 @@ module GoodData
51
56
  def retryable(options = {}, &_block)
52
57
  opts = { :tries => 1, :on => RETRYABLE_ERRORS }.merge(options)
53
58
 
54
- retry_exception, retries = opts[:on], opts[:tries]
59
+ retry_exception = opts[:on]
60
+ retries = opts[:tries]
55
61
 
56
62
  unless retry_exception.is_a?(Array)
57
63
  retry_exception = [retry_exception]
@@ -79,7 +85,10 @@ module GoodData
79
85
  end
80
86
  end
81
87
 
82
- attr_reader :cookies
88
+ attr_reader :request_params
89
+
90
+ # backward compatibility
91
+ alias_method :cookies, :request_params
83
92
  attr_reader :stats
84
93
  attr_reader :user
85
94
 
@@ -87,13 +96,12 @@ module GoodData
87
96
  @stats = {}
88
97
  @opts = opts
89
98
 
90
- @headers = DEFAULT_HEADERS.dup
99
+ headers = opts[:headers] || {}
100
+ @webdav_headers = DEFAULT_WEBDAV_HEADERS.merge(headers)
101
+
91
102
  @user = nil
92
103
  @server = nil
93
-
94
104
  @opts = opts
95
- headers = opts[:headers] || {}
96
- @headers.merge! headers
97
105
 
98
106
  # Initialize cookies
99
107
  reset_cookies!
@@ -104,7 +112,11 @@ module GoodData
104
112
  # Connect using username and password
105
113
  def connect(username, password, options = {})
106
114
  server = options[:server] || DEFAULT_URL
107
- @server = RestClient::Resource.new server, DEFAULT_LOGIN_PAYLOAD
115
+
116
+ options = DEFAULT_LOGIN_PAYLOAD.merge(options)
117
+ headers = options[:headers] || {}
118
+
119
+ @server = RestClient::Resource.new server, DEFAULT_LOGIN_PAYLOAD.merge(headers)
108
120
 
109
121
  # Install at_exit handler first
110
122
  unless @at_exit_handler_installed
@@ -125,6 +137,7 @@ module GoodData
125
137
  refresh_token :dont_reauth => true
126
138
  else
127
139
  credentials = Connection.construct_login_payload(username, password)
140
+ generate_session_id
128
141
  @auth = post(LOGIN_PATH, credentials)['userLogin']
129
142
 
130
143
  refresh_token :dont_reauth => true
@@ -138,6 +151,7 @@ module GoodData
138
151
  url = @auth['state']
139
152
 
140
153
  begin
154
+ clear_session_id
141
155
  delete url if url
142
156
  rescue RestClient::Unauthorized
143
157
  GoodData.logger.info 'Already disconnected'
@@ -189,7 +203,7 @@ module GoodData
189
203
  :url => url
190
204
  }.merge(cookies)
191
205
 
192
- if where.is_a?(IO)
206
+ if where.is_a?(IO) || where.is_a?(StringIO)
193
207
  RestClient::Request.execute(raw) do |chunk, _x, response|
194
208
  if response.code.to_s != '200'
195
209
  fail ArgumentError, "Error downloading #{url}. Got response: #{response.code} #{response} #{response.body}"
@@ -238,7 +252,15 @@ module GoodData
238
252
  def delete(uri, options = {})
239
253
  GoodData.logger.debug "DELETE: #{@server.url}#{uri}"
240
254
  profile "DELETE #{uri}" do
241
- b = proc { @server[uri].delete cookies }
255
+ b = proc do
256
+ begin
257
+ @server[uri].delete(fresh_request_params(options[:request_id]))
258
+ rescue RestClient::Exception => e
259
+ # log the error if it happens
260
+ GoodData.logger.error(e.inspect)
261
+ raise e
262
+ end
263
+ end
242
264
  process_response(options, &b)
243
265
  end
244
266
  end
@@ -249,7 +271,15 @@ module GoodData
249
271
  def get(uri, options = {}, &user_block)
250
272
  GoodData.logger.debug "GET: #{@server.url}#{uri}"
251
273
  profile "GET #{uri}" do
252
- b = proc { @server[uri].get(cookies, &user_block) }
274
+ b = proc do
275
+ begin
276
+ @server[uri].get(fresh_request_params(options[:request_id]), &user_block)
277
+ rescue RestClient::Exception => e
278
+ # log the error if it happens
279
+ GoodData.logger.error(e.inspect)
280
+ raise e
281
+ end
282
+ end
253
283
  process_response(options, &b)
254
284
  end
255
285
  end
@@ -261,7 +291,15 @@ module GoodData
261
291
  payload = data.is_a?(Hash) ? data.to_json : data
262
292
  GoodData.logger.debug "PUT: #{@server.url}#{uri}, #{scrub_params(data, KEYS_TO_SCRUB)}"
263
293
  profile "PUT #{uri}" do
264
- b = proc { @server[uri].put payload, cookies }
294
+ b = proc do
295
+ begin
296
+ @server[uri].put(payload, fresh_request_params(options[:request_id]))
297
+ rescue RestClient::Exception => e
298
+ # log the error if it happens
299
+ GoodData.logger.error(e.inspect)
300
+ raise e
301
+ end
302
+ end
265
303
  process_response(options, &b)
266
304
  end
267
305
  end
@@ -273,7 +311,15 @@ module GoodData
273
311
  GoodData.logger.debug "POST: #{@server.url}#{uri}, #{scrub_params(data, KEYS_TO_SCRUB)}"
274
312
  profile "POST #{uri}" do
275
313
  payload = data.is_a?(Hash) ? data.to_json : data
276
- b = proc { @server[uri].post payload, cookies }
314
+ b = proc do
315
+ begin
316
+ @server[uri].post(payload, fresh_request_params(options[:request_id]))
317
+ rescue RestClient::Exception => e
318
+ # log the error if it happens
319
+ GoodData.logger.error(e.inspect)
320
+ raise e
321
+ end
322
+ end
277
323
  process_response(options, &b)
278
324
  end
279
325
  end
@@ -282,7 +328,7 @@ module GoodData
282
328
  #
283
329
  # @return uri [String] SST token
284
330
  def sst_token
285
- cookies[:cookies]['GDCAuthSST']
331
+ request_params[:cookies]['GDCAuthSST']
286
332
  end
287
333
 
288
334
  def stats_table(values = stats)
@@ -306,7 +352,7 @@ module GoodData
306
352
  #
307
353
  # @return uri [String] TT token
308
354
  def tt_token
309
- cookies[:cookies]['GDCAuthTT']
355
+ request_params[:cookies]['GDCAuthTT']
310
356
  end
311
357
 
312
358
  # Uploads a file to GoodData server
@@ -315,7 +361,7 @@ module GoodData
315
361
  puts "uploading the file #{uri}"
316
362
 
317
363
  to_upload = File.new(filename)
318
- cookies_str = cookies[:cookies].map { |cookie| "#{cookie[0]}=#{cookie[1]}" }.join(';')
364
+ cookies_str = request_params[:cookies].map { |cookie| "#{cookie[0]}=#{cookie[1]}" }.join(';')
319
365
  req = Net::HTTP::Put.new(uri.path, 'User-Agent' => GoodData.gem_version_string, 'Cookie' => cookies_str)
320
366
  req.content_length = to_upload.size
321
367
  req.body_stream = to_upload
@@ -338,10 +384,10 @@ module GoodData
338
384
  raw = {
339
385
  :method => method,
340
386
  :url => url,
341
- :headers => @headers
342
- }
387
+ :headers => @webdav_headers
388
+ }.merge(cookies)
343
389
  begin
344
- RestClient::Request.execute(raw.merge(cookies))
390
+ RestClient::Request.execute(raw)
345
391
  rescue RestClient::Exception => e
346
392
  false if e.http_code == 404
347
393
  end
@@ -363,7 +409,7 @@ module GoodData
363
409
  raw = {
364
410
  :method => method,
365
411
  :url => url,
366
- :headers => @headers
412
+ :headers => @webdav_headers
367
413
  }.merge(cookies)
368
414
  RestClient::Request.execute(raw)
369
415
  end
@@ -384,10 +430,43 @@ module GoodData
384
430
  do_stream_file URI.join(url, CGI.escape(webdav_filename)), file
385
431
  end
386
432
 
433
+ def generate_request_id
434
+ "#{session_id}:#{call_id}"
435
+ end
436
+
387
437
  private
388
438
 
439
+ ID_LENGTH = 16
440
+
441
+ def generate_string
442
+ SecureRandom.urlsafe_base64(ID_LENGTH)
443
+ end
444
+
445
+ # generate session id to be passed as the first part to
446
+ # x_gdc_request header
447
+ def session_id
448
+ @session_id ||= generate_string
449
+ end
450
+
451
+ def call_id
452
+ generate_string
453
+ end
454
+
455
+ def generate_session_id
456
+ @session_id = generate_string
457
+ end
458
+
459
+ def clear_session_id
460
+ @session_id = nil
461
+ end
462
+
463
+ # request heders with freshly generated request id
464
+ def fresh_request_params(request_id = nil)
465
+ @request_params.merge(:x_gdc_request => request_id || generate_request_id)
466
+ end
467
+
389
468
  def merge_cookies!(cookies)
390
- @cookies[:cookies].merge! cookies
469
+ @request_params[:cookies].merge! cookies
391
470
  end
392
471
 
393
472
  def process_response(options = {}, &block)
@@ -447,7 +526,7 @@ module GoodData
447
526
  end
448
527
 
449
528
  def reset_cookies!
450
- @cookies = { :cookies => {} }
529
+ @request_params = { :cookies => {} }
451
530
  end
452
531
 
453
532
  def scrub_params(params, keys)
@@ -2,7 +2,7 @@
2
2
 
3
3
  # GoodData Module
4
4
  module GoodData
5
- VERSION = '0.6.16'
5
+ VERSION = '0.6.17'
6
6
 
7
7
  class << self
8
8
  # Version
@@ -294,6 +294,7 @@
294
294
  "columns": [{
295
295
  "type": "anchor",
296
296
  "name": "techoppanalysis",
297
+ "folder": "Opportunity Benchmark",
297
298
  "title": "Tech Opp. Analysis",
298
299
  "gd_data_type": "VARCHAR(128)",
299
300
  "gd_type": "GDC.text"
@@ -4,10 +4,12 @@
4
4
  {
5
5
  "type": "dataset",
6
6
  "name": "repos",
7
+ "folder": "Foldered Repos",
7
8
  "columns": [
8
9
  {
9
10
  "type": "anchor",
10
- "name": "repo_id"
11
+ "name": "repo_id",
12
+ "description": "This is anchor description"
11
13
  },
12
14
  {
13
15
  "type": "label",
@@ -42,7 +44,8 @@
42
44
  "columns": [
43
45
  {
44
46
  "type": "fact",
45
- "name": "lines_changed"
47
+ "name": "lines_changed",
48
+ "description": "Fact description"
46
49
  },
47
50
  {
48
51
  "type": "date",
@@ -31,6 +31,7 @@
31
31
  "fact": {
32
32
  "identifier": "fact.stage_history.stage_velocity",
33
33
  "title": "Stage Velocity",
34
+ "description": "Velocity description",
34
35
  "dataType": "DECIMAL(12,2)"
35
36
  }
36
37
  }, {
@@ -1498,6 +1499,7 @@
1498
1499
  "attribute": {
1499
1500
  "identifier": "attr.opp_owner.region",
1500
1501
  "title": "Opp. Owner Region",
1502
+ "description": "Owner Region description",
1501
1503
  "folder": "Opp. Owner",
1502
1504
  "labels": [{
1503
1505
  "label": {
@@ -1601,6 +1603,7 @@
1601
1603
  "attribute": {
1602
1604
  "identifier": "attr.opportunity.id",
1603
1605
  "title": "Opportunity",
1606
+ "description": "This is opportunity attribute description",
1604
1607
  "folder": "Opportunity",
1605
1608
  "labels": [{
1606
1609
  "label": {
@@ -11,6 +11,8 @@
11
11
  "attribute": {
12
12
  "identifier": "attr.repos.repo_id",
13
13
  "title": "Repo",
14
+ "description": "This is anchor description",
15
+ "folder": "Foldered Repos",
14
16
  "labels": [
15
17
  {
16
18
  "label": {
@@ -37,6 +39,7 @@
37
39
  "attribute": {
38
40
  "identifier": "attr.repos.department",
39
41
  "title": "Department",
42
+ "folder": "Foldered Repos",
40
43
  "labels": [
41
44
  {
42
45
  "label": {
@@ -67,6 +70,7 @@
67
70
  "attribute": {
68
71
  "identifier": "attr.devs.dev_id",
69
72
  "title": "Dev",
73
+ "folder": "Devs",
70
74
  "labels": [
71
75
  {
72
76
  "label": {
@@ -106,7 +110,8 @@
106
110
  "anchor": {
107
111
  "attribute": {
108
112
  "identifier": "attr.commits.factsof",
109
- "title": "Records of Commits"
113
+ "title": "Records of Commits",
114
+ "folder": "Commits"
110
115
  }
111
116
  },
112
117
  "attributes": [
@@ -117,6 +122,8 @@
117
122
  "fact": {
118
123
  "identifier": "fact.commits.lines_changed",
119
124
  "title": "Lines Changed",
125
+ "description": "Fact description",
126
+ "folder": "Commits",
120
127
  "dataType": "INT"
121
128
  }
122
129
  }