gooddata 0.6.10 → 0.6.11

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 (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'