salesforcebulk 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -15
  3. data/lib/salesforce_bulk.rb +1 -5
  4. data/lib/salesforce_bulk/client.rb +56 -59
  5. data/lib/salesforce_bulk/version.rb +1 -1
  6. metadata +46 -76
  7. data/.gitignore +0 -4
  8. data/.travis.yml +0 -10
  9. data/Gemfile +0 -3
  10. data/LICENSE +0 -20
  11. data/Rakefile +0 -22
  12. data/salesforcebulk.gemspec +0 -30
  13. data/test/fixtures/batch_create_request.csv +0 -3
  14. data/test/fixtures/batch_create_response.xml +0 -13
  15. data/test/fixtures/batch_info_list_response.xml +0 -27
  16. data/test/fixtures/batch_info_response.xml +0 -14
  17. data/test/fixtures/batch_result_list_response.csv +0 -3
  18. data/test/fixtures/config.yml +0 -5
  19. data/test/fixtures/invalid_batch_error.xml +0 -5
  20. data/test/fixtures/invalid_error.xml +0 -5
  21. data/test/fixtures/invalid_job_error.xml +0 -5
  22. data/test/fixtures/invalid_session_error.xml +0 -5
  23. data/test/fixtures/job_abort_request.xml +0 -1
  24. data/test/fixtures/job_abort_response.xml +0 -25
  25. data/test/fixtures/job_close_request.xml +0 -1
  26. data/test/fixtures/job_close_response.xml +0 -25
  27. data/test/fixtures/job_create_request.xml +0 -1
  28. data/test/fixtures/job_create_response.xml +0 -25
  29. data/test/fixtures/job_info_response.xml +0 -25
  30. data/test/fixtures/login_error.xml +0 -1
  31. data/test/fixtures/login_request.xml +0 -1
  32. data/test/fixtures/login_response.xml +0 -39
  33. data/test/fixtures/query_result_list_response.xml +0 -1
  34. data/test/fixtures/query_result_response.csv +0 -5
  35. data/test/lib/test_batch.rb +0 -252
  36. data/test/lib/test_batch_result.rb +0 -36
  37. data/test/lib/test_core_extensions.rb +0 -15
  38. data/test/lib/test_initialization.rb +0 -80
  39. data/test/lib/test_job.rb +0 -247
  40. data/test/lib/test_query_result_collection.rb +0 -86
  41. data/test/test_helper.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b6f6595658ab7d407acd1f74cb96b8b40ae9a303
4
- data.tar.gz: d9750c85f6cad1d42a6c81f108d6de8fb1558748
3
+ metadata.gz: 7b79868012c05ed38e161bcb12bb95eb0382e5b3
4
+ data.tar.gz: c3a7e13b192b7b2ceafe17a2214935428d9ff3a9
5
5
  SHA512:
6
- metadata.gz: abe409976e507aa19ff46d3519088e84656f039aa1521def05daa495f114fe4c04bf4835f47baabcc69bf12da52ee3198cd1a950a0fd30eb472bea336acf2dab
7
- data.tar.gz: 87ec0fa2cd572bdf23f624099bfa6ea43c512835672bff6e2aead8be09f4d05a7dce574fcbfaa9cc2285d99c18aaa2a39ff8469eb2726be02aad74636607993e
6
+ metadata.gz: 0c14b55e95b378444129d266c48fafa0421f033f46250105504a0ac8f5b150e73b6873eedffc566d3abca5b1782e1cd469ac320165a0fb6ff7c28b53473bfa0a
7
+ data.tar.gz: 8ec9210c38ab0ccb04fa341c85ba88cfd8ffe01631babfcf6ef30fed96a06e82634b3ac27bfbc981bc771f6c24b61d3cccf2dc562de3540d21a7b0b742a70ad2
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- SalesforceBulk is an easy to use Ruby gem for connecting to and using the [Salesforce Bulk API](http://www.salesforce.com/us/developer/docs/api_asynch/index.htm). This is a rewrite and separate release of Jorge Valdivia's salesforce_bulk gem (renamed `salesforcebulk`) with full unit tests and full API capability (e.g. adding multiple batches per job). This gem was built on Ruby 1.8.7, 1.9.2, and 1.9.3.
5
+ SalesforceBulk is an easy to use Ruby gem for connecting to and using the [Salesforce Bulk API](http://www.salesforce.com/us/developer/docs/api_asynch/index.htm). This is a rewrite and separate release of Jorge Valdivia's salesforce_bulk gem (renamed `salesforcebulk`) with full unit tests and full API capability (e.g. adding multiple batches per job). This gem was built on Ruby 2.0.0, 2.1.6, and 2.2.2.
6
6
 
7
7
  ## Installation
8
8
 
@@ -21,6 +21,14 @@ To contribute, fork this repo, create a topic branch, make changes, then send a
21
21
  bundle install
22
22
  rake
23
23
 
24
+ To run the test suite on all gemfiles with your current ruby version, use:
25
+
26
+ bundle exec rake wwtd:local
27
+
28
+ To run the full test suite with different gemfiles and ruby versions, use:
29
+
30
+ bundle exec rake wwtd
31
+
24
32
  ## Configuration and Initialization
25
33
 
26
34
  ### Basic Configuration
@@ -28,7 +36,7 @@ To contribute, fork this repo, create a topic branch, make changes, then send a
28
36
  When retrieving a password you will also be given a security token. Combine the two into a single value as the API treats this as your real password.
29
37
 
30
38
  require 'salesforce_bulk'
31
-
39
+
32
40
  client = SalesforceBulk::Client.new(username: 'MyUsername', password: 'MyPasswordWithSecurtyToken')
33
41
  client.authenticate
34
42
 
@@ -47,7 +55,7 @@ Create a YAML file with the content below. Only `username` and `password` is req
47
55
  Then in a Ruby script:
48
56
 
49
57
  require 'salesforce_bulk'
50
-
58
+
51
59
  client = SalesforceBulk::Client.new("config/salesforce_bulk.yml")
52
60
  client.authenticate
53
61
 
@@ -59,13 +67,13 @@ An important note about the data in any of the examples below: each hash in a da
59
67
 
60
68
  data1 = [{:Name__c => 'Test 1'}, {:Name__c => 'Test 2'}]
61
69
  data2 = [{:Name__c => 'Test 3'}, {:Name__c => 'Test 4'}]
62
-
70
+
63
71
  job = client.add_job(:insert, :MyObject__c)
64
-
72
+
65
73
  # easily add multiple batches to a job
66
74
  batch = client.add_batch(job.id, data1)
67
75
  batch = client.add_batch(job.id, data2)
68
-
76
+
69
77
  job = client.close_job(job.id) # or use the abort_job(id) method
70
78
 
71
79
  ### Adding a Job
@@ -90,7 +98,7 @@ For any operation you should be able to specify a concurrency mode. The default
90
98
  The Job object has various properties such as status, created time, number of completed and failed batches and various other values.
91
99
 
92
100
  job = client.job_info(jobId) # returns a Job object
93
-
101
+
94
102
  puts "Job #{job.id} is closed." if job.closed? # other: open?, aborted?
95
103
 
96
104
  ### Retrieving Info for a single Batch
@@ -98,13 +106,13 @@ The Job object has various properties such as status, created time, number of co
98
106
  The Batch object has various properties such as status, created time, number of processed and failed records and various other values.
99
107
 
100
108
  batch = client.batch_info(jobId, batchId) # returns a Batch object
101
-
109
+
102
110
  puts "Batch #{batch.id} is in progress." if batch.in_progress?
103
111
 
104
112
  ### Retrieving Info for all Batches
105
113
 
106
114
  batches = client.batch_info_list(jobId) # returns an Array of Batch objects
107
-
115
+
108
116
  batches.each do |batch|
109
117
  puts "Batch #{batch.id} completed." if batch.completed? # other: failed?, in_progress?, queued?
110
118
  end
@@ -116,7 +124,7 @@ To verify that a batch completed successfully or failed call the `batch_info` or
116
124
  The object returned from the following example only applies to the operations: `delete`, `insert`, `update` and `upsert`. Query results are handled differently.
117
125
 
118
126
  results = client.batch_result(jobId, batchId) # returns an Array of BatchResult objects
119
-
127
+
120
128
  results.each do |result|
121
129
  puts "Item #{result.id} had an error of: #{result.error}" if result.error?
122
130
  end
@@ -129,18 +137,18 @@ Query results are handled differently as its possible that a single batch could
129
137
 
130
138
  # returns a QueryResultCollection object (an Array)
131
139
  results = client.batch_result(jobId, batchId)
132
-
140
+
133
141
  while results.any?
134
-
142
+
135
143
  # Assuming query was: SELECT Id, Name, CustomField__c FROM Account
136
144
  results.each do |result|
137
145
  puts result[:Id], result[:Name], result[:CustomField__c]
138
146
  end
139
-
147
+
140
148
  puts "Another set is available." if results.next?
141
-
149
+
142
150
  results.next
143
-
151
+
144
152
  end
145
153
 
146
154
  Note: By reviewing the API docs and response format my understanding was that the API would return multiple results sets for a single batch if the query was to large but this does not seem to be the case in my live testing. It seems to be capped at 10000 records (as it when inserting data) but I haven't been able to verify through the documentation. If you know anything about that your input is appreciated. In the meantime the gem was built to support multiple result sets for a query batch but seems that will change which will simplify that method.
@@ -153,6 +161,16 @@ Note: By reviewing the API docs and response format my understanding was that th
153
161
 
154
162
  ## Version History
155
163
 
164
+ **2.0.0** (April 25, 2015)
165
+
166
+ * Dropped support for Ruby 1.8 and Ruby 1.9
167
+ * Added support for Ruby 2.0, 2.1 and 2.2
168
+ * Added support for Rails 4.0, 4.1 an 4.2
169
+ * Changed test_helper to avoid requiring test_unit (removed in Ruby 2.2)
170
+ * Replaced Test::Unit::TestCase with ActiveSupport::TestCase
171
+ * Bumped shoulda and losen dependencies on minitest
172
+ * All changes in PR's #13, #14, #15, #16 - thanks [@pschambacher](https://github.com/pschambacher)
173
+
156
174
  **1.4.0** (June 1, 2014)
157
175
 
158
176
  * Added state_message to Batch class (#11 - thanks [@bethesque](https://github.com/bethesque))
@@ -1,10 +1,6 @@
1
1
  require 'net/https'
2
2
  require 'xmlsimple'
3
- if RUBY_VERSION < '1.9'
4
- require 'fastercsv'
5
- else
6
- require 'csv'
7
- end
3
+ require 'csv'
8
4
  require 'active_support'
9
5
  require 'active_support/core_ext/object/blank'
10
6
  require 'active_support/core_ext/hash/keys'
@@ -1,45 +1,42 @@
1
1
  module SalesforceBulk
2
- if RUBY_VERSION < "1.9"
3
- CSV = ::FasterCSV
4
- end
5
2
 
6
3
  # Interface for operating the Salesforce Bulk REST API
7
4
  class Client
8
5
  # The host to use for authentication. Defaults to login.salesforce.com.
9
6
  attr_accessor :login_host
10
-
7
+
11
8
  # The instance host to use for API calls. Determined from login response.
12
9
  attr_accessor :instance_host
13
-
10
+
14
11
  # The Salesforce password
15
12
  attr_accessor :password
16
-
13
+
17
14
  # The Salesforce username
18
15
  attr_accessor :username
19
-
16
+
20
17
  # The API version the client is using. Defaults to 24.0.
21
18
  attr_accessor :version
22
-
19
+
23
20
  def initialize(options={})
24
21
  if options.is_a?(String)
25
22
  options = YAML.load_file(options)
26
23
  options.symbolize_keys!
27
24
  end
28
-
25
+
29
26
  options = {:login_host => 'login.salesforce.com', :version => 24.0}.merge(options)
30
-
27
+
31
28
  options.assert_valid_keys(:username, :password, :login_host, :version)
32
-
29
+
33
30
  self.username = options[:username]
34
31
  self.password = "#{options[:password]}"
35
32
  self.login_host = options[:login_host]
36
33
  self.version = options[:version]
37
-
34
+
38
35
  @api_path_prefix = "/services/async/#{version}/"
39
36
  @valid_operations = [:delete, :insert, :update, :upsert, :query]
40
37
  @valid_concurrency_modes = ['Parallel', 'Serial']
41
38
  end
42
-
39
+
43
40
  def authenticate
44
41
  xml = '<?xml version="1.0" encoding="utf-8"?>'
45
42
  xml += '<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
@@ -52,63 +49,63 @@ module SalesforceBulk
52
49
  xml += "</n1:login>"
53
50
  xml += "</env:Body>"
54
51
  xml += "</env:Envelope>\n"
55
-
52
+
56
53
  response = http_post("/services/Soap/u/#{version}", xml, 'Content-Type' => 'text/xml', 'SOAPAction' => 'login')
57
-
54
+
58
55
  data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
59
56
  result = data['Body']['loginResponse']['result']
60
-
57
+
61
58
  @session_id = result['sessionId']
62
-
59
+
63
60
  self.instance_host = "#{instance_id(result['serverUrl'])}.salesforce.com"
64
61
  self
65
62
  end
66
-
63
+
67
64
  def abort_job(jobId)
68
65
  xml = '<?xml version="1.0" encoding="utf-8"?>'
69
66
  xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
70
67
  xml += "<state>Aborted</state>"
71
68
  xml += "</jobInfo>"
72
-
69
+
73
70
  response = http_post("job/#{jobId}", xml)
74
71
  data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
75
72
  Job.new_from_xml(data)
76
73
  end
77
-
74
+
78
75
  def add_batch(jobId, data)
79
76
  body = data
80
-
77
+
81
78
  if data.is_a?(Array)
82
79
  raise ArgumentError, "Data set exceeds 10000 record limit by #{data.length - 10000}" if data.length > 10000
83
-
80
+
84
81
  keys = data.first.keys
85
82
  body = keys.to_csv
86
-
83
+
87
84
  data.each do |item|
88
85
  item_values = keys.map { |key| item[key] }
89
86
  body += item_values.to_csv
90
87
  end
91
88
  end
92
-
93
- # Despite the content for a query operation batch being plain text we
89
+
90
+ # Despite the content for a query operation batch being plain text we
94
91
  # still have to specify CSV content type per API docs.
95
92
  response = http_post("job/#{jobId}/batch", body, "Content-Type" => "text/csv; charset=UTF-8")
96
93
  result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
97
94
  Batch.new_from_xml(result)
98
95
  end
99
-
96
+
100
97
  def add_job(operation, sobject, options={})
101
98
  operation = operation.to_s.downcase.to_sym
102
-
99
+
103
100
  raise ArgumentError.new("Invalid operation: #{operation}") unless @valid_operations.include?(operation)
104
-
101
+
105
102
  options.assert_valid_keys(:external_id_field_name, :concurrency_mode)
106
-
103
+
107
104
  if options[:concurrency_mode]
108
105
  concurrency_mode = options[:concurrency_mode].capitalize
109
106
  raise ArgumentError.new("Invalid concurrency mode: #{concurrency_mode}") unless @valid_concurrency_modes.include?(concurrency_mode)
110
107
  end
111
-
108
+
112
109
  xml = '<?xml version="1.0" encoding="utf-8"?>'
113
110
  xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
114
111
  xml += "<operation>#{operation}</operation>"
@@ -117,16 +114,16 @@ module SalesforceBulk
117
114
  xml += "<concurrencyMode>#{options[:concurrency_mode]}</concurrencyMode>" if options[:concurrency_mode]
118
115
  xml += "<contentType>CSV</contentType>"
119
116
  xml += "</jobInfo>"
120
-
117
+
121
118
  response = http_post("job", xml)
122
119
  data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
123
120
  job = Job.new_from_xml(data)
124
121
  end
125
-
122
+
126
123
  def batch_info_list(jobId)
127
124
  response = http_get("job/#{jobId}/batch")
128
125
  result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
129
-
126
+
130
127
  if result['batchInfo'].is_a?(Array)
131
128
  result['batchInfo'].collect do |info|
132
129
  Batch.new_from_xml(info)
@@ -135,73 +132,73 @@ module SalesforceBulk
135
132
  [Batch.new_from_xml(result['batchInfo'])]
136
133
  end
137
134
  end
138
-
135
+
139
136
  def batch_info(jobId, batchId)
140
137
  response = http_get("job/#{jobId}/batch/#{batchId}")
141
138
  result = XmlSimple.xml_in(response.body, 'ForceArray' => false)
142
139
  Batch.new_from_xml(result)
143
140
  end
144
-
141
+
145
142
  def batch_result(jobId, batchId)
146
143
  response = http_get("job/#{jobId}/batch/#{batchId}/result")
147
-
144
+
148
145
  if response.body =~ /<.*?>/m
149
146
  result = XmlSimple.xml_in(response.body)
150
-
147
+
151
148
  if result['result'].present?
152
149
  results = query_result(jobId, batchId, result['result'].first)
153
-
150
+
154
151
  collection = QueryResultCollection.new(self, jobId, batchId, result['result'].first, result['result'])
155
152
  collection.replace(results)
156
153
  end
157
154
  else
158
155
  result = BatchResultCollection.new(jobId, batchId)
159
-
156
+
160
157
  CSV.parse(response.body, :headers => true) do |row|
161
158
  result << BatchResult.new(row[0], row[1].to_b, row[2].to_b, row[3])
162
159
  end
163
-
160
+
164
161
  result
165
162
  end
166
163
  end
167
-
164
+
168
165
  def query_result(job_id, batch_id, result_id)
169
166
  headers = {"Content-Type" => "text/csv; charset=UTF-8"}
170
167
  response = http_get("job/#{job_id}/batch/#{batch_id}/result/#{result_id}", headers)
171
-
168
+
172
169
  lines = response.body.lines.to_a
173
170
  headers = CSV.parse_line(lines.shift).collect { |header| header.to_sym }
174
-
171
+
175
172
  result = []
176
-
173
+
177
174
  #CSV.parse(lines.join, :headers => headers, :converters => [:all, lambda{|s| s.to_b if s.kind_of? String }]) do |row|
178
175
  CSV.parse(lines.join, :headers => headers) do |row|
179
176
  result << Hash[row.headers.zip(row.fields)]
180
177
  end
181
-
178
+
182
179
  result
183
180
  end
184
-
181
+
185
182
  def close_job(jobId)
186
183
  xml = '<?xml version="1.0" encoding="utf-8"?>'
187
184
  xml += '<jobInfo xmlns="http://www.force.com/2009/06/asyncapi/dataload">'
188
185
  xml += "<state>Closed</state>"
189
186
  xml += "</jobInfo>"
190
-
187
+
191
188
  response = http_post("job/#{jobId}", xml)
192
189
  data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
193
190
  Job.new_from_xml(data)
194
191
  end
195
-
192
+
196
193
  def job_info(jobId)
197
194
  response = http_get("job/#{jobId}")
198
195
  data = XmlSimple.xml_in(response.body, 'ForceArray' => false)
199
196
  Job.new_from_xml(data)
200
197
  end
201
-
198
+
202
199
  def http_post(path, body, headers={})
203
200
  headers = {'Content-Type' => 'application/xml'}.merge(headers)
204
-
201
+
205
202
  if @session_id
206
203
  headers['X-SFDC-Session'] = @session_id
207
204
  host = instance_host
@@ -209,41 +206,41 @@ module SalesforceBulk
209
206
  else
210
207
  host = self.login_host
211
208
  end
212
-
209
+
213
210
  response = https_request(host).post(path, body, headers)
214
-
211
+
215
212
  if response.is_a?(Net::HTTPSuccess)
216
213
  response
217
214
  else
218
215
  raise SalesforceError.new(response)
219
216
  end
220
217
  end
221
-
218
+
222
219
  def http_get(path, headers={})
223
220
  path = "#{@api_path_prefix}#{path}"
224
-
221
+
225
222
  headers = {'Content-Type' => 'application/xml'}.merge(headers)
226
-
223
+
227
224
  if @session_id
228
225
  headers['X-SFDC-Session'] = @session_id
229
226
  end
230
-
227
+
231
228
  response = https_request(self.instance_host).get(path, headers)
232
-
229
+
233
230
  if response.is_a?(Net::HTTPSuccess)
234
231
  response
235
232
  else
236
233
  raise SalesforceError.new(response)
237
234
  end
238
235
  end
239
-
236
+
240
237
  def https_request(host)
241
238
  req = Net::HTTP.new(host, 443)
242
239
  req.use_ssl = true
243
240
  req.verify_mode = OpenSSL::SSL::VERIFY_NONE
244
241
  req
245
242
  end
246
-
243
+
247
244
  def instance_id(url)
248
245
  url.match(/:\/\/([a-zA-Z0-9\-\.]{2,}).salesforce/)[1]
249
246
  end