gooddata 0.6.16 → 0.6.17

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.
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
  }