gooddata 0.6.10 → 0.6.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -0
  3. data/.travis.yml +4 -0
  4. data/CHANGELOG.md +6 -0
  5. data/README.md +1 -0
  6. data/gooddata.gemspec +38 -40
  7. data/lib/gooddata/bricks/base_downloader.rb +1 -1
  8. data/lib/gooddata/bricks/middleware/base_middleware.rb +36 -0
  9. data/lib/gooddata/bricks/middleware/bulk_salesforce_middleware.rb +8 -38
  10. data/lib/gooddata/bricks/middleware/decode_params_middleware.rb +14 -0
  11. data/lib/gooddata/bricks/middleware/fs_upload_middleware.rb +7 -6
  12. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +7 -5
  13. data/lib/gooddata/bricks/middleware/logger_middleware.rb +1 -1
  14. data/lib/gooddata/bricks/middleware/restforce_middleware.rb +20 -14
  15. data/lib/gooddata/bricks/middleware/undot_params_middleware.rb +33 -0
  16. data/lib/gooddata/cli/commands/api_cmd.rb +1 -1
  17. data/lib/gooddata/cli/commands/auth_cmd.rb +1 -1
  18. data/lib/gooddata/cli/commands/console_cmd.rb +2 -2
  19. data/lib/gooddata/cli/commands/process_cmd.rb +54 -7
  20. data/lib/gooddata/cli/commands/project_cmd.rb +9 -9
  21. data/lib/gooddata/cli/commands/projects_cmd.rb +1 -1
  22. data/lib/gooddata/cli/commands/run_ruby_cmd.rb +24 -7
  23. data/lib/gooddata/cli/commands/scaffold_cmd.rb +2 -2
  24. data/lib/gooddata/cli/commands/user_cmd.rb +1 -1
  25. data/lib/gooddata/cli/hooks.rb +3 -3
  26. data/lib/gooddata/commands/datasets.rb +1 -1
  27. data/lib/gooddata/commands/project.rb +2 -2
  28. data/lib/gooddata/commands/role.rb +1 -1
  29. data/lib/gooddata/commands/runners.rb +2 -2
  30. data/lib/gooddata/connection.rb +2 -2
  31. data/lib/gooddata/core/nil_logger.rb +1 -1
  32. data/lib/gooddata/core/rest.rb +12 -8
  33. data/lib/gooddata/data/guesser.rb +1 -1
  34. data/lib/gooddata/exceptions/attr_element_not_found.rb +1 -1
  35. data/lib/gooddata/extensions/enumerable.rb +1 -1
  36. data/lib/gooddata/extensions/hash.rb +20 -0
  37. data/lib/gooddata/helpers/csv_helper.rb +1 -1
  38. data/lib/gooddata/helpers/global_helpers.rb +59 -1
  39. data/lib/gooddata/mixins/md_lock.rb +83 -0
  40. data/lib/gooddata/mixins/md_object_indexer.rb +1 -1
  41. data/lib/gooddata/mixins/md_object_query.rb +1 -1
  42. data/lib/gooddata/mixins/md_relations.rb +0 -9
  43. data/lib/gooddata/models/dashboard_builder.rb +1 -1
  44. data/lib/gooddata/models/domain.rb +2 -2
  45. data/lib/gooddata/models/empty_result.rb +5 -5
  46. data/lib/gooddata/models/execution.rb +74 -0
  47. data/lib/gooddata/models/execution_detail.rb +74 -0
  48. data/lib/gooddata/models/membership.rb +1 -1
  49. data/lib/gooddata/models/metadata/attribute.rb +4 -6
  50. data/lib/gooddata/models/metadata/dashboard.rb +2 -0
  51. data/lib/gooddata/models/metadata/fact.rb +2 -2
  52. data/lib/gooddata/models/metadata/metric.rb +4 -1
  53. data/lib/gooddata/models/metadata/report.rb +84 -34
  54. data/lib/gooddata/models/metadata/report_definition.rb +28 -17
  55. data/lib/gooddata/models/metadata.rb +1 -1
  56. data/lib/gooddata/models/model.rb +1 -1
  57. data/lib/gooddata/models/process.rb +70 -54
  58. data/lib/gooddata/models/profile.rb +47 -10
  59. data/lib/gooddata/models/project.rb +74 -30
  60. data/lib/gooddata/models/project_blueprint.rb +9 -10
  61. data/lib/gooddata/models/project_builder.rb +2 -2
  62. data/lib/gooddata/models/project_creator.rb +4 -4
  63. data/lib/gooddata/models/report_data_result.rb +1 -1
  64. data/lib/gooddata/models/schedule.rb +39 -32
  65. data/lib/gooddata/models/to_manifest.rb +5 -5
  66. data/lib/gooddata/models/to_wire.rb +3 -3
  67. data/lib/gooddata/rest/client.rb +64 -31
  68. data/lib/gooddata/rest/connection.rb +7 -7
  69. data/lib/gooddata/rest/connections/dummy_connection.rb +5 -5
  70. data/lib/gooddata/rest/connections/rest_client_connection.rb +106 -44
  71. data/lib/gooddata/rest/object.rb +1 -1
  72. data/lib/gooddata/version.rb +1 -1
  73. data/spec/bricks/bricks_spec.rb +67 -0
  74. data/spec/bricks/default-config.json +8 -0
  75. data/spec/data/gooddata_version_process/gooddata_version.rb +3 -0
  76. data/spec/data/gooddata_version_process/gooddata_version.zip +0 -0
  77. data/spec/data/ruby_params_process/ruby_params.rb +3 -0
  78. data/spec/helpers/process_helper.rb +12 -0
  79. data/spec/helpers/schedule_helper.rb +21 -0
  80. data/spec/integration/create_project_spec.rb +19 -0
  81. data/spec/integration/full_process_schedule_spec.rb +129 -8
  82. data/spec/integration/full_project_spec.rb +52 -2
  83. data/spec/spec_helper.rb +1 -0
  84. data/spec/unit/core/rest_spec.rb +76 -3
  85. data/spec/unit/helpers_spec.rb +43 -0
  86. data/spec/unit/models/metric_spec.rb +33 -0
  87. data/spec/unit/models/params_spec.rb +96 -0
  88. data/spec/unit/models/profile_spec.rb +16 -0
  89. data/spec/unit/models/schedule_spec.rb +12 -3
  90. metadata +350 -187
  91. data/lib/gooddata/helper/class_helper.rb +0 -1
  92. data/lib/gooddata/helper/helpers.rb +0 -8
@@ -2,6 +2,10 @@
2
2
 
3
3
  require_relative '../rest/resource'
4
4
  require_relative '../extensions/hash'
5
+ require_relative '../mixins/rest_resource'
6
+ require_relative '../helpers/global_helpers'
7
+
8
+ require_relative 'execution'
5
9
 
6
10
  module GoodData
7
11
  class Schedule < Rest::Resource
@@ -11,6 +15,9 @@ module GoodData
11
15
  alias_method :raw_data, :json
12
16
  alias_method :to_hash, :json
13
17
 
18
+ include GoodData::Mixin::RestResource
19
+ root_key :schedule
20
+
14
21
  class << self
15
22
  # Looks for schedule
16
23
  # @param id [String] URL, ID of schedule or :all
@@ -58,11 +65,11 @@ module GoodData
58
65
  # Creates new schedules from parameters passed
59
66
  #
60
67
  # @param process_id [String] Process ID
61
- # @param cron [String] Cron Settings
68
+ # @param trigger [String|GoodData::Schedule] Trigger of schedule. Can be cron string or reference to another schedule.
62
69
  # @param executable [String] Execution executable
63
70
  # @param options [Hash] Optional options
64
71
  # @return [GoodData::Schedule] New GoodData::Schedule instance
65
- def create(process_id, cron, executable, options = {})
72
+ def create(process_id, trigger, executable, options = {})
66
73
  c = client(options)
67
74
  fail ArgumentError, 'No :client specified' if c.nil?
68
75
 
@@ -75,50 +82,39 @@ module GoodData
75
82
  default_opts = {
76
83
  :type => 'MSETL',
77
84
  :timezone => 'UTC',
78
- :cron => cron,
79
85
  :params => {
80
- :process_id => process_id,
81
- :executable => executable
86
+ :PROCESS_ID => process_id,
87
+ :EXECUTABLE => executable
82
88
  },
83
- :hidden_params => {},
89
+ :hiddenParams => {},
84
90
  :reschedule => options[:reschedule] || 0
85
91
  }
86
92
 
87
- inject_schema = {
88
- :hidden_params => 'hiddenParams'
89
- }
90
-
91
- inject_params = {
92
- :process_id => 'PROCESS_ID',
93
- :executable => 'EXECUTABLE'
94
- }
95
-
96
- default_params = default_opts[:params].reduce({}) do |new_hash, (k, v)|
97
- key = inject_params[k] || k
98
- new_hash[key] = v
99
- new_hash
93
+ if trigger =~ /[a-fA-Z0-9]{24}/
94
+ default_opts[:triggerScheduleId] = trigger
95
+ elsif trigger.is_a?(GoodData::Schedule)
96
+ default_opts[:triggerScheduleId] = trigger.obj_id
97
+ else
98
+ default_opts[:cron] = trigger
100
99
  end
101
100
 
102
- default = default_opts.reduce({}) do |new_hash, (k, v)|
103
- key = inject_schema[k] || k
104
- new_hash[key] = v
105
- new_hash
101
+ if options.key?(:hidden_params)
102
+ options[:hiddenParams] = options[:hidden_params]
103
+ options.delete :hidden_params
106
104
  end
107
105
 
108
- default[:params] = default_params
109
-
110
106
  json = {
111
- 'schedule' => default.merge(options.except(:project, :client))
107
+ 'schedule' => default_opts.deep_merge(options.except(:project, :client))
112
108
  }
113
109
 
114
- tmp = json['schedule'][:params]['PROCESS_ID']
110
+ tmp = json['schedule'][:params][:PROCESS_ID]
115
111
  fail 'Process ID has to be provided' if tmp.nil? || tmp.empty?
116
112
 
117
- tmp = json['schedule'][:params]['EXECUTABLE']
113
+ tmp = json['schedule'][:params][:EXECUTABLE]
118
114
  fail 'Executable has to be provided' if tmp.nil? || tmp.empty?
119
115
 
120
- tmp = json['schedule'][:cron]
121
- fail 'Cron schedule has to be provided' if tmp.nil? || tmp.empty?
116
+ tmp = json['schedule'][:cron] || json['schedule'][:triggerScheduleId]
117
+ fail 'trigger schedule has to be provided' if !tmp || tmp.nil? || tmp.empty?
122
118
 
123
119
  tmp = json['schedule'][:timezone]
124
120
  fail 'A timezone has to be provided' if tmp.nil? || tmp.empty?
@@ -126,6 +122,16 @@ module GoodData
126
122
  tmp = json['schedule'][:type]
127
123
  fail 'Schedule type has to be provided' if tmp.nil? || tmp.empty?
128
124
 
125
+ params = json['schedule'][:params]
126
+ params = GoodData::Helpers.encode_params(params, false)
127
+ json['schedule'][:params] = params
128
+
129
+ hidden_params = json['schedule'][:hiddenParams]
130
+ if hidden_params && !hidden_params.empty?
131
+ hidden_params = GoodData::Helpers.encode_params(json['schedule'][:hiddenParams], true)
132
+ json['schedule'][:hiddenParams] = hidden_params
133
+ end
134
+
129
135
  url = "/gdc/projects/#{project.pid}/schedules"
130
136
  res = c.post url, json
131
137
 
@@ -190,9 +196,10 @@ module GoodData
190
196
  :execution => {}
191
197
  }
192
198
  execution = client.post(execution_url, data)
193
- client.poll_on_response(execution['execution']['links']['self']) do |body|
199
+ res = client.poll_on_response(execution['execution']['links']['self']) do |body|
194
200
  body['execution'] && (body['execution']['status'] == 'RUNNING' || body['execution']['status'] == 'SCHEDULED')
195
201
  end
202
+ client.create(GoodData::Execution, res, client: client, project: project)
196
203
  end
197
204
 
198
205
  # Returns execution URL
@@ -300,7 +307,7 @@ module GoodData
300
307
  #
301
308
  # @return [Array] Raw Executions JSON
302
309
  def executions
303
- if @json
310
+ if @json # rubocop:disable Style/GuardClause
304
311
  url = @json['schedule']['links']['executions']
305
312
  res = client.get url
306
313
  res['executions']['items']
@@ -10,7 +10,7 @@ module GoodData
10
10
  # @param attribute [Hash] Attribute or Anchor
11
11
  # @param mode [String] Mode of the load. Either FULL or INCREMENTAL
12
12
  # @return [Hash] Manifest for a particular reference
13
- def self.attribute_to_manifest(project, dataset, a, mode)
13
+ def self.attribute_to_manifest(_project, dataset, a, mode)
14
14
  [{
15
15
  'referenceKey' => 1,
16
16
  'populates' => [GoodData::Model.identifier_for(dataset, a.merge(type: :primary_label))],
@@ -71,7 +71,7 @@ module GoodData
71
71
  # @param reference [Hash] Reference
72
72
  # @param mode [String] Mode of the load. Either FULL or INCREMENTAL
73
73
  # @return [Hash] Manifest for a particular date reference
74
- def self.date_ref_to_manifest(project, dataset, reference, mode)
74
+ def self.date_ref_to_manifest(project, _dataset, reference, mode)
75
75
  referenced_dataset = ProjectBlueprint.find_date_dimension(project, reference[:dataset])
76
76
  [{
77
77
  'populates' => [GoodData::Model.identifier_for(referenced_dataset, type: :date_ref)],
@@ -89,7 +89,7 @@ module GoodData
89
89
  # @param fact [Hash] Fact
90
90
  # @param mode [String] Mode of the load. Either FULL or INCREMENTAL
91
91
  # @return [Hash] Manifest for a particular fact
92
- def self.fact_to_manifest(project, dataset, fact, mode)
92
+ def self.fact_to_manifest(_project, dataset, fact, mode)
93
93
  [{
94
94
  'populates' => [GoodData::Model.identifier_for(dataset, fact)],
95
95
  'mode' => mode,
@@ -104,7 +104,7 @@ module GoodData
104
104
  # @param label [Hash] Label
105
105
  # @param mode [String] Mode of the load. Either FULL or INCREMENTAL
106
106
  # @return [Hash] Manifest for a particular label
107
- def self.label_to_manifest(project, dataset, label, mode)
107
+ def self.label_to_manifest(_project, dataset, label, mode)
108
108
  a = DatasetBlueprint.attribute_for_label(dataset, label)
109
109
  [{
110
110
  'populates' => [GoodData::Model.identifier_for(dataset, label, a)],
@@ -145,7 +145,7 @@ module GoodData
145
145
  # @param reference [Hash] Reference
146
146
  # @param mode [String] Mode of the load. Either FULL or INCREMENTAL
147
147
  # @return [Hash] Manifest for a particular reference
148
- def self.reference_to_manifest(project, dataset, reference, mode)
148
+ def self.reference_to_manifest(project, _dataset, reference, mode)
149
149
  referenced_dataset = ProjectBlueprint.find_dataset(project, reference[:dataset])
150
150
  anchor = DatasetBlueprint.anchor(referenced_dataset)
151
151
  [{
@@ -11,7 +11,7 @@ module GoodData
11
11
  # @param project [Hash] Project blueprint hash represenation
12
12
  # @param dataset [Hash] Dataset blueprint hash represenation
13
13
  # @return [Hash] Manifest for a particular reference
14
- def self.anchor_to_wire(project, dataset)
14
+ def self.anchor_to_wire(_project, dataset)
15
15
  if DatasetBlueprint.anchor(dataset)
16
16
  attribute_to_wire(dataset, DatasetBlueprint.anchor(dataset))
17
17
  else
@@ -29,7 +29,7 @@ module GoodData
29
29
  # @param project [Hash] Project blueprint hash represenation
30
30
  # @param dataset [Hash] Dataset blueprint hash represenation
31
31
  # @return [Hash] Manifest for a particular reference
32
- def self.attributes_to_wire(project, dataset)
32
+ def self.attributes_to_wire(_project, dataset)
33
33
  DatasetBlueprint.attributes(dataset).map do |a|
34
34
  attribute_to_wire(dataset, a)
35
35
  end
@@ -85,7 +85,7 @@ module GoodData
85
85
  # @param project [Hash] Project blueprint hash represenation
86
86
  # @param dataset [Hash] Dataset blueprint hash represenation
87
87
  # @return [Hash] Manifest for a particular reference
88
- def self.date_dimensions_to_wire(project, dataset)
88
+ def self.date_dimensions_to_wire(_project, dataset)
89
89
  {
90
90
  dateDimension: {
91
91
  name: dataset[:name],
@@ -35,6 +35,7 @@ module GoodData
35
35
 
36
36
  # TODO: Decide if we need provide direct access to factory
37
37
  attr_reader :factory
38
+ attr_reader :opts
38
39
 
39
40
  include Mixin::Inspector
40
41
  inspector :object_id
@@ -58,7 +59,7 @@ module GoodData
58
59
  # @param username [String] Username to be used for authentication
59
60
  # @param password [String] Password to be used for authentication
60
61
  # @return [GoodData::Rest::Client] Client
61
- def connect(username, password, opts = {})
62
+ def connect(username, password, opts = { :verify_ssl => true })
62
63
  new_opts = opts.dup
63
64
  if username.is_a?(Hash) && username.key?(:sst_token)
64
65
  new_opts[:sst_token] = username[:sst_token]
@@ -92,7 +93,7 @@ module GoodData
92
93
  end
93
94
 
94
95
  def disconnect
95
- if @@instance # rubocop:disable ClassVars
96
+ if @@instance # rubocop:disable ClassVars, Style/GuardClause
96
97
  @@instance.disconnect # rubocop:disable ClassVars
97
98
  @@instance = nil # rubocop:disable ClassVars
98
99
  end
@@ -102,6 +103,21 @@ module GoodData
102
103
  @@instance # rubocop:disable ClassVars
103
104
  end
104
105
 
106
+ # Retry block if exception thrown
107
+ def retryable(options = {}, &_block)
108
+ opts = { :tries => 1, :on => Exception }.merge(options)
109
+
110
+ retry_exception, retries = opts[:on], opts[:tries]
111
+
112
+ begin
113
+ return yield
114
+ rescue retry_exception
115
+ retry if (retries -= 1) > 0
116
+ end
117
+
118
+ yield
119
+ end
120
+
105
121
  alias_method :client, :connection
106
122
  end
107
123
 
@@ -128,8 +144,8 @@ module GoodData
128
144
  @factory = ObjectFactory.new(self)
129
145
  end
130
146
 
131
- def create_project(options = {})
132
- GoodData::Project.create(title: 'Project for schedule testing', auth_token: ConnectionHelper::GD_PROJECT_TOKEN, client: self)
147
+ def create_project(options = { title: 'Project', auth_token: ENV['GD_PROJECT_TOKEN'] })
148
+ GoodData::Project.create(options.merge(client: self))
133
149
  end
134
150
 
135
151
  def create_project_from_blueprint(blueprint, options = {})
@@ -193,8 +209,8 @@ module GoodData
193
209
  # HTTP GET
194
210
  #
195
211
  # @param uri [String] Target URI
196
- def get(uri, opts = {})
197
- @connection.get uri, opts
212
+ def get(uri, opts = {}, & block)
213
+ @connection.get uri, opts, & block
198
214
  end
199
215
 
200
216
  # FIXME: Invstigate _file argument
@@ -237,7 +253,7 @@ module GoodData
237
253
 
238
254
  while response.code == code
239
255
  sleep sleep_interval
240
- retryable(:tries => 3, :on => RestClient::InternalServerError) do
256
+ GoodData::Rest::Client.retryable(:tries => 3, :on => RestClient::InternalServerError) do
241
257
  sleep sleep_interval
242
258
  response = get(link, :process => false)
243
259
  end
@@ -263,7 +279,7 @@ module GoodData
263
279
  response = get(link)
264
280
  while bl.call(response)
265
281
  sleep sleep_interval
266
- retryable(:tries => 3, :on => RestClient::InternalServerError) do
282
+ GoodData::Rest::Client.retryable(:tries => 3, :on => RestClient::InternalServerError) do
267
283
  sleep sleep_interval
268
284
  response = get(link)
269
285
  end
@@ -285,43 +301,60 @@ module GoodData
285
301
  @connection.post uri, data, opts
286
302
  end
287
303
 
288
- # Retry blok if exception thrown
289
- def retryable(options = {}, &block)
290
- opts = { :tries => 1, :on => Exception }.merge(options)
304
+ # Uploads file to staging
305
+ #
306
+ # @param file [String] file to be uploaded
307
+ # @param options [Hash] must contain :staging_url key (file will be uploaded to :staging_url + File.basename(file))
308
+ def upload(file, options = {})
309
+ @connection.upload file, options
310
+ end
291
311
 
292
- retry_exception, retries = opts[:on], opts[:tries]
312
+ # Downloads file from staging
313
+ #
314
+ # @param source_relative_path [String] path relative to @param options[:staging_url]
315
+ # @param target_file_path [String] path to be downloaded to
316
+ # @param options [Hash] must contain :staging_url key (file will be downloaded from :staging_url + source_relative_path)
317
+ def download(source_relative_path, target_file_path, options = {})
318
+ @connection.download source_relative_path, target_file_path, options
319
+ end
293
320
 
294
- begin
295
- return yield
296
- rescue retry_exception
297
- retry if (retries -= 1) > 0
298
- end
321
+ def download_from_user_webdav(source_relative_path, target_file_path, options = {})
322
+ download(source_relative_path, target_file_path, options.merge(
323
+ :directory => options[:directory],
324
+ :staging_url => get_user_webdav_url(options)
325
+ ))
326
+ end
299
327
 
300
- yield
328
+ def upload_to_user_webdav(file, options = {})
329
+ upload(file, options.merge(
330
+ :directory => options[:directory],
331
+ :staging_url => get_user_webdav_url(options)
332
+ ))
301
333
  end
302
334
 
303
- # Uploads file
304
- def upload(file, options = {})
305
- @connection.upload file, options
335
+ def with_project(pid, &block)
336
+ GoodData.with_project(pid, client: self, &block)
306
337
  end
307
338
 
308
- def upload_to_user_webdav(file, options = {})
339
+ ###################### PRIVATE ######################
340
+
341
+ private
342
+
343
+ def get_user_webdav_url(options = {})
309
344
  p = options[:project]
310
345
  fail ArgumentError, 'No :project specified' if p.nil?
311
346
 
312
- project = GoodData::Project[p, options]
347
+ project = options[:project] || GoodData::Project[p, options]
313
348
  fail ArgumentError, 'Wrong :project specified' if project.nil?
314
349
 
315
350
  u = URI(project.links['uploads'])
316
- url = URI.join(u.to_s.chomp(u.path.to_s), '/uploads/')
317
- upload(file, options.merge(
318
- :directory => options[:directory],
319
- :staging_url => url
320
- ))
321
- end
351
+ us = u.to_s
352
+ ws = options[:client].opts[:webdav_server]
353
+ if !us.empty? && !us.downcase.start_with?('http') && !ws.empty?
354
+ u = URI.join(ws, us)
355
+ end
322
356
 
323
- def with_project(pid, &block)
324
- GoodData.with_project(pid, client: self, &block)
357
+ URI.join(u.to_s.chomp(u.path.to_s), '/uploads/')
325
358
  end
326
359
  end
327
360
  end
@@ -80,7 +80,7 @@ module GoodData
80
80
  reset_cookies!
81
81
  end
82
82
 
83
- def refresh_token(options = {})
83
+ def refresh_token(_options = {})
84
84
  begin # rubocop:disable RedundantBegin
85
85
  get TOKEN_PATH, :dont_reauth => true # avoid infinite loop GET fails with 401
86
86
  rescue Exception => e # rubocop:disable RescueException
@@ -99,28 +99,28 @@ module GoodData
99
99
  # HTTP DELETE
100
100
  #
101
101
  # @param uri [String] Target URI
102
- def delete(uri, options = {})
102
+ def delete(uri, _options = {})
103
103
  fail NotImplementedError "DELETE #{uri}"
104
104
  end
105
105
 
106
106
  # HTTP GET
107
107
  #
108
108
  # @param uri [String] Target URI
109
- def get(uri, options = {})
109
+ def get(uri, _options = {})
110
110
  fail NotImplementedError "GET #{uri}"
111
111
  end
112
112
 
113
113
  # HTTP PUT
114
114
  #
115
115
  # @param uri [String] Target URI
116
- def put(uri, data, options = {})
116
+ def put(uri, _data, _options = {})
117
117
  fail NotImplementedError "PUT #{uri}"
118
118
  end
119
119
 
120
120
  # HTTP POST
121
121
  #
122
122
  # @param uri [String] Target URI
123
- def post(uri, data, options = {})
123
+ def post(uri, _data, _options = {})
124
124
  fail NotImplementedError "POST #{uri}"
125
125
  end
126
126
 
@@ -132,7 +132,7 @@ module GoodData
132
132
  end
133
133
 
134
134
  def stats_table(values = stats)
135
- sorted = values.sort_by { |k, v| v[:avg] }
135
+ sorted = values.sort_by { |_k, v| v[:avg] }
136
136
  Terminal::Table.new :headings => %w(title avg min max total calls) do |t|
137
137
  sorted.each do |l|
138
138
  row = [
@@ -179,7 +179,7 @@ module GoodData
179
179
  keys = keys.reduce([]) { |a, e| a.concat([e.to_s, e.to_sym]) }
180
180
 
181
181
  new_params = params.deep_dup
182
- GoodData::Helpers.hash_dfs(new_params) do |k, key|
182
+ GoodData::Helpers.hash_dfs(new_params) do |k, _key|
183
183
  keys.each do |key_to_scrub|
184
184
  k[key_to_scrub] = ('*' * k[key_to_scrub].length) if k && k.key?(key_to_scrub) && k[key_to_scrub]
185
185
  end
@@ -12,7 +12,7 @@ module GoodData
12
12
  end
13
13
 
14
14
  # Connect using username and password
15
- def connect(username, password)
15
+ def connect(_username, _password)
16
16
  end
17
17
 
18
18
  # Disconnect
@@ -22,28 +22,28 @@ module GoodData
22
22
  # HTTP DELETE
23
23
  #
24
24
  # @param uri [String] Target URI
25
- def delete(uri, options = {})
25
+ def delete(uri, _options = {})
26
26
  puts "DELETE #{uri}"
27
27
  end
28
28
 
29
29
  # HTTP GET
30
30
  #
31
31
  # @param uri [String] Target URI
32
- def get(uri, options = {})
32
+ def get(uri, _options = {})
33
33
  puts "GET #{uri}"
34
34
  end
35
35
 
36
36
  # HTTP PUT
37
37
  #
38
38
  # @param uri [String] Target URI
39
- def put(uri, data, options = {})
39
+ def put(uri, _data, _options = {})
40
40
  puts "PUT #{uri}"
41
41
  end
42
42
 
43
43
  # HTTP POST
44
44
  #
45
45
  # @param uri [String] Target URI
46
- def post(uri, data, options = {})
46
+ def post(uri, _data, _options = {})
47
47
  puts "POST #{uri}"
48
48
  end
49
49
  end
@@ -44,10 +44,10 @@ module GoodData
44
44
  # HTTP GET
45
45
  #
46
46
  # @param uri [String] Target URI
47
- def get(uri, options = {})
47
+ def get(uri, options = {}, &user_block)
48
48
  GoodData.logger.debug "GET: #{@server.url}#{uri}"
49
49
  profile "GET #{uri}" do
50
- b = proc { @server[uri].get cookies }
50
+ b = proc { @server[uri].get(cookies, &user_block) }
51
51
  process_response(options, &b)
52
52
  end
53
53
  end
@@ -77,67 +77,129 @@ module GoodData
77
77
  end
78
78
 
79
79
  # Uploads a file to GoodData server
80
- # /uploads/ resources are special in that they use a different
81
- # host and a basic authentication.
82
80
  def upload(file, options = {})
83
- dir = options[:directory] || ''
84
- staging_uri = options[:staging_url].to_s
85
- url = dir.empty? ? staging_uri : URI.join(staging_uri, "#{dir}/").to_s
81
+ def do_stream_file(uri, filename, options = {})
82
+ puts "uploading the file #{uri}"
83
+
84
+ to_upload = File.new(filename)
85
+ cookies_str = cookies[:cookies].map { |cookie| "#{cookie[0]}=#{cookie[1]}" }.join(';')
86
+ req = Net::HTTP::Put.new(uri.path, 'User-Agent' => GoodData.gem_version_string, 'Cookie' => cookies_str)
87
+ req.content_length = to_upload.size
88
+ req.body_stream = to_upload
89
+ http = Net::HTTP.new(uri.host, uri.port)
90
+ http.use_ssl = true
91
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
92
+ response = http.start { |client| client.request(req) }
93
+ case response
94
+ when Net::HTTPSuccess then
95
+ true
96
+ when Net::HTTPUnauthorized
97
+ refresh_token
98
+ do_stream_file uri, filename, options
99
+ else
100
+ fail "Can't upload file to webdav. Path: #{uri}, response code: #{response.code}, response body: #{response.body}"
101
+ end
102
+ end
86
103
 
87
- # Make a directory, if needed
88
- unless dir.empty?
104
+ def webdav_dir_exists?(url)
89
105
  method = :get
90
106
  GoodData.logger.debug "#{method}: #{url}"
91
- begin
92
- # first check if it does exits
107
+
108
+ b = proc do
109
+ raw = {
110
+ :method => method,
111
+ :url => url,
112
+ :headers => @headers
113
+ }
114
+ begin
115
+ RestClient::Request.execute(raw.merge(cookies))
116
+ rescue RestClient::Exception => e
117
+ false if e.http_code == 404
118
+ end
119
+ end
120
+
121
+ process_with_tt_refresh(&b)
122
+ end
123
+
124
+ def create_webdav_dir_if_needed(url)
125
+ return if webdav_dir_exists?(url)
126
+
127
+ method = :mkcol
128
+ GoodData.logger.debug "#{method}: #{url}"
129
+ b = proc do
93
130
  raw = {
94
131
  :method => method,
95
132
  :url => url,
96
- # :timeout => @options[:timeout],
97
133
  :headers => @headers
98
134
  }.merge(cookies)
99
135
  RestClient::Request.execute(raw)
100
- rescue RestClient::Exception => e
101
- if e.http_code == 404
102
- method = :mkcol
103
- GoodData.logger.debug "#{method}: #{url}"
104
- raw = {
105
- :method => method,
106
- :url => url,
107
- # :timeout => @options[:timeout],
108
- :headers => @headers
109
- }.merge(cookies)
110
- RestClient::Request.execute(raw)
136
+ end
137
+
138
+ process_with_tt_refresh(&b)
139
+ end
140
+
141
+ dir = options[:directory] || ''
142
+ staging_uri = options[:staging_url].to_s
143
+ url = dir.empty? ? staging_uri : URI.join(staging_uri, "#{dir}/").to_s
144
+
145
+ # Make a directory, if needed
146
+ create_webdav_dir_if_needed url unless dir.empty?
147
+
148
+ webdav_filename = options[:filename] || File.basename(file)
149
+ do_stream_file URI.join(url, CGI.escape(webdav_filename)), file
150
+ end
151
+
152
+ def download(what, where, options = {})
153
+ dir = options[:directory] || ''
154
+ staging_uri = options[:staging_url].to_s
155
+
156
+ base_url = dir.empty? ? staging_uri : URI.join(staging_uri, "#{dir}/").to_s
157
+ url = URI.join(base_url, CGI.escape(what)).to_s
158
+
159
+ b = proc do
160
+ raw = {
161
+ :headers => {
162
+ :user_agent => GoodData.gem_version_string
163
+ },
164
+ :method => :get,
165
+ :url => url
166
+ }.merge(cookies)
167
+
168
+ if where.is_a?(String)
169
+ File.open(where, 'w') do |f|
170
+ RestClient::Request.execute(raw) do |chunk, _x, _y|
171
+ f.write chunk
172
+ end
173
+ end
174
+
175
+ else
176
+ # Assume it is a IO stream
177
+ RestClient::Request.execute(raw) do |chunk, _x, _y|
178
+ where.write chunk
111
179
  end
112
180
  end
113
181
  end
114
182
 
115
- payload = options[:stream] ? 'file' : File.read(file)
116
- filename = options[:filename] || options[:stream] ? 'randome-filename.txt' : File.basename(file)
117
-
118
- # Upload the file
119
- # puts "uploading the file #{URI.join(url, filename).to_s}"
120
- raw = {
121
- :method => :put,
122
- :url => URI.join(url, filename).to_s,
123
- # :timeout => @options[:timeout],
124
- :headers => {
125
- :user_agent => GoodData.gem_version_string
126
- },
127
- :payload => payload,
128
- :raw_response => true,
129
- # :user => @username,
130
- # :password => @password
131
- }.merge(cookies)
132
- RestClient::Request.execute(raw)
133
- true
183
+ process_with_tt_refresh(&b)
184
+ end
185
+
186
+ private
187
+
188
+ def process_with_tt_refresh(&block)
189
+ block.call
190
+ rescue RestClient::Unauthorized
191
+ refresh_token
192
+ block.call
134
193
  end
135
194
 
136
195
  private
137
196
 
138
197
  def process_response(options = {}, &block)
139
198
  begin
140
- response = block.call
199
+ # Simply try again when ConnectionReset, ConnectionRefused etc.. (see e.g. MSF-7591)
200
+ response = GoodData::Rest::Client.retryable(:tries => 2, :on => SystemCallError) do
201
+ block.call
202
+ end
141
203
  rescue RestClient::Unauthorized
142
204
  raise $ERROR_INFO if options[:dont_reauth]
143
205
  refresh_token
@@ -150,7 +212,7 @@ module GoodData
150
212
 
151
213
  if content_type == 'application/json' || content_type == 'application/json;charset=UTF-8'
152
214
  result = response.to_str == '""' ? {} : MultiJson.load(response.to_str)
153
- GoodData.logger.debug "Response: #{result.inspect}"
215
+ GoodData.logger.debug "Request ID: #{response.headers[:x_gdc_request]} - Response: #{result.inspect}"
154
216
  elsif ['text/plain;charset=UTF-8', 'text/plain; charset=UTF-8', 'text/plain'].include?(content_type)
155
217
  result = response
156
218
  GoodData.logger.debug 'Response: plain text'