salesforce_bulk_api_serial_or_parallel 0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b42acc44b480f4bc913f265ca0e5f1ee9d6eb27c
4
+ data.tar.gz: 2fae9dadedce4fea9d26be4828a27c0cbc662304
5
+ SHA512:
6
+ metadata.gz: de7e1e445ecacd7b832830175fa501146de6e210f1b653a3f579f129135993b12a592802716ab3d523a214031f99643928f202bbe35d73d7deca0a7cac3ef32f
7
+ data.tar.gz: 9545be081a46a0f17a1e34ff20c8cf30fcfe9277dded5b21f7cc460fd0431c55edde2f74e751a145a0bec57e73fec52ebd9e6a1e8e9110bcff6ea6f3469eee49
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ .ruby-gemset
5
+ .ruby-version
6
+ pkg/*
7
+ auth_credentials.yml
8
+ *.swp
9
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in salesforce_bulk_api_serial_or_parallel.gemspec
4
+ gemspec
data/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Yatish Mehta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # Salesforce-Bulk-Api
2
+ [![Gem Version](https://badge.fury.io/rb/salesforce_bulk_api.png)](http://badge.fury.io/rb/salesforce_bulk_api)
3
+ ## Overview
4
+
5
+ Salesforce bulk API is a simple ruby gem for connecting to and using the Salesforce Bulk API. It is actually a re-written code from [salesforce_bulk](https://github.com/jorgevaldivia/salesforce_bulk).Written to suit many more other features as well.
6
+
7
+ ## How to use
8
+
9
+ Using this gem is simple and straight forward.
10
+
11
+ To initialize:
12
+
13
+ `sudo gem install salesforce_bulk_api_serial_or_parallel`
14
+
15
+ or add
16
+
17
+ `gem salesforce_bulk_api_serial_or_parallel`
18
+
19
+ in your Gemfile
20
+
21
+ There are two ways to authenticate with SalesForce to use the Bulk API: databasedotcom & restforce.
22
+ Please check out the entire documentation of the gem you decide to use to learn the various ways of authentication.
23
+
24
+ [Databasedotcom](https://github.com/heroku/databasedotcom)
25
+ [Restforce](https://github.com/ejholmes/restforce)
26
+
27
+
28
+ You can use username password combo, OmniAuth, Oauth2
29
+ You can use as many records possible in the Array. Governor limits are taken care of inside the gem.
30
+
31
+
32
+ require 'salesforce_bulk_api_serial_or_parallel'
33
+ client = Databasedotcom::Client.new :client_id => SFDC_APP_CONFIG["client_id"], :client_secret => SFDC_APP_CONFIG["client_secret"] #client_id and client_secret respectively
34
+ client.authenticate :token => "my-oauth-token", :instance_url => "http://na1.salesforce.com" #=> "my-oauth-token"
35
+
36
+ salesforce = SalesforceBulkApi::Api.new(client)
37
+
38
+ OR
39
+
40
+ require 'salesforce_bulk_api_serial_or_parallel'
41
+ client = Restforce.new(
42
+ username: SFDC_APP_CONFIG['SFDC_USERNAME'],
43
+ password: SFDC_APP_CONFIG['SFDC_PASSWORD'],
44
+ security_token: SFDC_APP_CONFIG['SFDC_SECURITY_TOKEN'],
45
+ client_id: SFDC_APP_CONFIG['SFDC_CLIENT_ID'],
46
+ client_secret: SFDC_APP_CONFIG['SFDC_CLIENT_SECRET'].to_i,
47
+ host: SFDC_APP_CONFIG['SFDC_HOST']
48
+ )
49
+ client.authenticate!
50
+ salesforce = SalesforceBulkApi::Api.new(client)
51
+
52
+
53
+ ### Sample operations:
54
+
55
+ # Insert/Create
56
+ # Add as many fields per record as needed.
57
+ new_account = Hash["name" => "Test Account", "type" => "Other"]
58
+ records_to_insert = Array.new
59
+ # You can add as many records as you want here, just keep in mind that Salesforce has governor limits.
60
+ records_to_insert.push(new_account)
61
+ result = salesforce.create("Account", records_to_insert)
62
+ puts "result is: #{result.inspect}"
63
+
64
+ # Update
65
+ updated_account = Hash["name" => "Test Account -- Updated", id => "a00A0001009zA2m"] # Nearly identical to an insert, but we need to pass the salesforce id.
66
+ records_to_update = Array.new
67
+ records_to_update.push(updated_account)
68
+ salesforce.update("Account", records_to_update)
69
+
70
+ # Upsert
71
+ upserted_account = Hash["name" => "Test Account -- Upserted", "External_Field_Name" => "123456"] # Fields to be updated. External field must be included
72
+ records_to_upsert = Array.new
73
+ records_to_upsert.push(upserted_account)
74
+ salesforce.upsert("Account", records_to_upsert, "External_Field_Name") # Note that upsert accepts an extra parameter for the external field name
75
+
76
+ # Delete
77
+ deleted_account = Hash["id" => "a00A0001009zA2m"] # We only specify the id of the records to delete
78
+ records_to_delete = Array.new
79
+ records_to_delete.push(deleted_account)
80
+ salesforce.delete("Account", records_to_delete)
81
+
82
+ # Query
83
+ res = salesforce.query("Account", "select id, name, createddate from Account limit 3") # We just need to pass the sobject name and the query string
84
+
85
+ ### Helpful methods:
86
+
87
+ # Check status of a job via #job_from_id
88
+ job = salesforce.job_from_id('a00A0001009zA2m') # Returns a SalesforceBulkApi::Job instance
89
+ puts "status is: #{job.check_job_status.inspect}"
90
+
91
+ ### Listening to events:
92
+
93
+ # A job is created
94
+ # Useful when you need to store the job_id before any work begins, then if you fail during a complex load scenario, you can wait for your
95
+ # previous job(s) to finish.
96
+ salesforce.on_job_created do |job|
97
+ puts "Job #{job.job_id} created!"
98
+ end
99
+
100
+ ### Throttling API calls:
101
+
102
+ # By default, this gem (and maybe your app driving it) will query job/batch statuses at an unbounded rate. We
103
+ # can fix that, e.g.:
104
+ salesforce.connection.set_status_throttle(30) # only check status of individual jobs/batches every 30 seconds
105
+
106
+ ## Installation
107
+
108
+ sudo gem install salesforce_bulk_api_serial_or_parallel
109
+
110
+ ## Contribute
111
+
112
+ Feel to fork and send Pull request
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'rspec/core/rake_task'
2
+ task :default => :spec
3
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,7 @@
1
+ salesforce:
2
+ client_id: client_id_here
3
+ client_secret: client_secret_here
4
+ user: sf_user@example.com
5
+ passwordandtoken: passandtokenhere
6
+ test_account_id: 0013000000ymMBh
7
+ host: 'login.salesforce.com' # use test.salesforce.com if it is a sandbox
@@ -0,0 +1,98 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'net/https'
4
+ require 'xmlsimple'
5
+ require 'csv'
6
+
7
+ require 'salesforce_bulk_api_serial_or_parallel/version'
8
+ require 'salesforce_bulk_api_serial_or_parallel/concerns/throttling'
9
+ require 'salesforce_bulk_api_serial_or_parallel/job'
10
+ require 'salesforce_bulk_api_serial_or_parallel/connection'
11
+
12
+ module SalesforceBulkApiSerialOrParallel
13
+ class Api
14
+ attr_reader :connection
15
+
16
+ SALESFORCE_API_VERSION = '1.0.1'
17
+
18
+ def initialize(client)
19
+ @connection = SalesforceBulkApiSerialOrParallel::Connection.new(SALESFORCE_API_VERSION, client)
20
+ @listeners = { job_created: [] }
21
+ end
22
+
23
+ def upsert(sobject, records, external_field, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500, concurrency = nil)
24
+ do_operation('upsert', sobject, records, external_field, get_response, timeout, batch_size, send_nulls, no_null_list, concurrency)
25
+ end
26
+
27
+ def update(sobject, records, get_response = false, send_nulls = false, no_null_list = [], batch_size = 10000, timeout = 1500, concurrency = nil)
28
+ do_operation('update', sobject, records, nil, get_response, timeout, batch_size, send_nulls, no_null_list, concurrency)
29
+ end
30
+
31
+ def create(sobject, records, get_response = false, send_nulls = false, batch_size = 10000, timeout = 1500, concurrency = nil)
32
+ do_operation('insert', sobject, records, nil, get_response, timeout, batch_size, send_nulls, concurrency)
33
+ end
34
+
35
+ def delete(sobject, records, get_response = false, batch_size = 10000, timeout = 1500)
36
+ do_operation('delete', sobject, records, nil, get_response, timeout, batch_size, concurrency)
37
+ end
38
+
39
+ def query(sobject, query, batch_size = 10000, timeout = 1500)
40
+ do_operation('query', sobject, query, nil, true, timeout, batch_size)
41
+ end
42
+
43
+ def counters
44
+ {
45
+ http_get: @connection.counters[:get],
46
+ http_post: @connection.counters[:post],
47
+ upsert: get_counters[:upsert],
48
+ update: get_counters[:update],
49
+ create: get_counters[:create],
50
+ delete: get_counters[:delete],
51
+ query: get_counters[:query]
52
+ }
53
+ end
54
+
55
+
56
+ ##
57
+ # Allows you to attach a listener that accepts the created job (which has a useful #job_id field). This is useful
58
+ # for recording a job ID persistently before you begin batch work (i.e. start modifying the salesforce database),
59
+ # so if the load process you are writing needs to recover, it can be aware of previous jobs it started and wait
60
+ # for them to finish.
61
+ def on_job_created(&block)
62
+ @listeners[:job_created] << block
63
+ end
64
+
65
+ def job_from_id(job_id)
66
+ SalesforceBulkApiSerialOrParallel::Job.new(job_id: job_id, connection: @connection)
67
+ end
68
+
69
+ def do_operation(operation, sobject, records, external_field, get_response, timeout, batch_size, send_nulls = false, no_null_list = [], concurrency_mode = nil)
70
+ count operation.to_sym
71
+
72
+ job = SalesforceBulkApiSerialOrParallel::Job.new(
73
+ operation: operation,
74
+ sobject: sobject,
75
+ records: records,
76
+ external_field: external_field,
77
+ connection: @connection
78
+ )
79
+
80
+ job.create_job(batch_size, send_nulls, no_null_list, concurrency_mode)
81
+ @listeners[:job_created].each {|callback| callback.call(job)}
82
+ operation == "query" ? job.add_query() : job.add_batches()
83
+ response = job.close_job
84
+ response.merge!({'batches' => job.get_job_result(get_response, timeout)}) if get_response == true
85
+ response
86
+ end
87
+
88
+ private
89
+ def get_counters
90
+ @counters ||= Hash.new(0)
91
+ end
92
+
93
+ def count(name)
94
+ get_counters[name] += 1
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,60 @@
1
+ module SalesforceBulkApiSerialOrParallel::Concerns
2
+ module Throttling
3
+
4
+ def throttles
5
+ @throttles.dup
6
+ end
7
+
8
+ def add_throttle(&throttling_callback)
9
+ @throttles ||= []
10
+ @throttles << throttling_callback
11
+ end
12
+
13
+ def set_status_throttle(limit_seconds)
14
+ set_throttle_limit_in_seconds(limit_seconds, [:http_method, :path], ->(details) { details[:http_method] == :get })
15
+ end
16
+
17
+ def set_throttle_limit_in_seconds(limit_seconds, throttle_by_keys, only_if)
18
+ add_throttle do |details|
19
+ limit_log = get_limit_log(Time.now - limit_seconds)
20
+ key = extract_constraint_key_from(details, throttle_by_keys)
21
+ last_request = limit_log[key]
22
+
23
+ if !last_request.nil? && only_if.call(details)
24
+ seconds_since_last_request = Time.now.to_f - last_request.to_f
25
+ need_to_wait_seconds = limit_seconds - seconds_since_last_request
26
+ sleep(need_to_wait_seconds) if need_to_wait_seconds > 0
27
+ end
28
+
29
+ limit_log[key] = Time.now
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def extract_constraint_key_from(details, throttle_by_keys)
36
+ hash = {}
37
+ throttle_by_keys.each { |k| hash[k] = details[k] }
38
+ hash
39
+ end
40
+
41
+ def get_limit_log(prune_older_than)
42
+ @limits ||= Hash.new(0)
43
+
44
+ @limits.delete_if do |k, v|
45
+ v < prune_older_than
46
+ end
47
+
48
+ @limits
49
+ end
50
+
51
+ def throttle(details={})
52
+ (@throttles || []).each do |callback|
53
+ args = [details]
54
+ args = args[0..callback.arity]
55
+ callback.call(*args)
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,98 @@
1
+ module SalesforceBulkApiSerialOrParallel
2
+ require 'timeout'
3
+
4
+ class Connection
5
+ include Concerns::Throttling
6
+
7
+ LOGIN_HOST = 'login.salesforce.com'
8
+
9
+ def initialize(api_version, client)
10
+ @client = client
11
+ @api_version = api_version
12
+ @path_prefix = "/services/async/#{@api_version}/"
13
+
14
+ login()
15
+ end
16
+
17
+ def login()
18
+ client_type = @client.class.to_s
19
+ case client_type
20
+ when "Restforce::Data::Client"
21
+ @session_id = @client.options[:oauth_token]
22
+ @server_url = @client.options[:instance_url]
23
+ else
24
+ @session_id = @client.oauth_token
25
+ @server_url = @client.instance_url
26
+ end
27
+ @instance = parse_instance()
28
+ @instance_host = "#{@instance}.salesforce.com"
29
+ end
30
+
31
+ def post_xml(host, path, xml, headers)
32
+ host = host || @instance_host
33
+ if host != LOGIN_HOST # Not login, need to add session id to header
34
+ headers['X-SFDC-Session'] = @session_id
35
+ path = "#{@path_prefix}#{path}"
36
+ end
37
+ i = 0
38
+ begin
39
+ count :post
40
+ throttle(http_method: :post, path: path)
41
+ https(host).post(path, xml, headers).body
42
+ rescue
43
+ i += 1
44
+ if i < 3
45
+ puts "Request fail #{i}: Retrying #{path}"
46
+ retry
47
+ else
48
+ puts "FATAL: Request to #{path} failed three times."
49
+ raise
50
+ end
51
+ end
52
+ end
53
+
54
+ def get_request(host, path, headers)
55
+ host = host || @instance_host
56
+ path = "#{@path_prefix}#{path}"
57
+ if host != LOGIN_HOST # Not login, need to add session id to header
58
+ headers['X-SFDC-Session'] = @session_id;
59
+ end
60
+
61
+ count :get
62
+ throttle(http_method: :get, path: path)
63
+ https(host).get(path, headers).body
64
+ end
65
+
66
+ def https(host)
67
+ req = Net::HTTP.new(host, 443)
68
+ req.use_ssl = true
69
+ req.verify_mode = OpenSSL::SSL::VERIFY_NONE
70
+ req
71
+ end
72
+
73
+ def parse_instance()
74
+ @instance = @server_url.match(/https:\/\/[a-z]{2}[0-9]{1,2}/).to_s.gsub("https://","")
75
+ @instance = @server_url.split(".salesforce.com")[0].split("://")[1] if @instance.nil? || @instance.empty?
76
+ return @instance
77
+ end
78
+
79
+ def counters
80
+ {
81
+ get: get_counters[:get],
82
+ post: get_counters[:post]
83
+ }
84
+ end
85
+
86
+ private
87
+
88
+ def get_counters
89
+ @counters ||= Hash.new(0)
90
+ end
91
+
92
+ def count(http_method)
93
+ get_counters[http_method] += 1
94
+ end
95
+
96
+ end
97
+
98
+ end
@@ -0,0 +1,235 @@
1
+ module SalesforceBulkApiSerialOrParallel
2
+
3
+ class Job
4
+ attr_reader :job_id
5
+
6
+ class SalesforceException < StandardError; end
7
+
8
+ def initialize(args)
9
+ @job_id = args[:job_id]
10
+ @operation = args[:operation]
11
+ @sobject = args[:sobject]
12
+ @external_field = args[:external_field]
13
+ @records = args[:records]
14
+ @connection = args[:connection]
15
+ @batch_ids = []
16
+ @XML_HEADER = '<?xml version="1.0" encoding="utf-8" ?>'
17
+ end
18
+
19
+ def create_job(batch_size, send_nulls, no_null_list, concurrency_mode)
20
+ @batch_size = batch_size
21
+ @send_nulls = send_nulls
22
+ @no_null_list = no_null_list
23
+ @concurrency_mode = concurrency_mode
24
+
25
+ xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
26
+ xml += "<operation>#{@operation}</operation>"
27
+ xml += "<object>#{@sobject}</object>"
28
+ if @concurrency_mode
29
+ xml += "<concurrencyMode>#{@concurrency_mode}</concurrencyMode>"
30
+ end
31
+ # This only happens on upsert
32
+ if !@external_field.nil?
33
+ xml += "<externalIdFieldName>#{@external_field}</externalIdFieldName>"
34
+ end
35
+ xml += "<contentType>XML</contentType>"
36
+ xml += "</jobInfo>"
37
+
38
+ path = "job"
39
+ headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
40
+
41
+ response = @connection.post_xml(nil, path, xml, headers)
42
+ response_parsed = XmlSimple.xml_in(response)
43
+
44
+ # response may contain an exception, so raise it
45
+ raise SalesforceException.new("#{response_parsed['exceptionMessage'][0]} (#{response_parsed['exceptionCode'][0]})") if response_parsed['exceptionCode']
46
+
47
+ @job_id = response_parsed['id'][0]
48
+ end
49
+
50
+ def close_job()
51
+ xml = "#{@XML_HEADER}<jobInfo xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\">"
52
+ xml += "<state>Closed</state>"
53
+ xml += "</jobInfo>"
54
+
55
+ path = "job/#{@job_id}"
56
+ headers = Hash['Content-Type' => 'application/xml; charset=utf-8']
57
+
58
+ response = @connection.post_xml(nil, path, xml, headers)
59
+ XmlSimple.xml_in(response)
60
+ end
61
+
62
+ def add_query
63
+ path = "job/#{@job_id}/batch/"
64
+ headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
65
+
66
+ response = @connection.post_xml(nil, path, @records, headers)
67
+ response_parsed = XmlSimple.xml_in(response)
68
+
69
+ @batch_ids << response_parsed['id'][0]
70
+ end
71
+
72
+ def add_batches
73
+ raise 'Records must be an array of hashes.' unless @records.is_a? Array
74
+ keys = @records.reduce({}) {|h, pairs| pairs.each {|k, v| (h[k] ||= []) << v}; h}.keys
75
+
76
+ @records_dup = @records.clone
77
+
78
+ super_records = []
79
+ (@records_dup.size / @batch_size).to_i.times do
80
+ super_records << @records_dup.pop(@batch_size)
81
+ end
82
+ super_records << @records_dup unless @records_dup.empty?
83
+
84
+ super_records.each do |batch|
85
+ @batch_ids << add_batch(keys, batch)
86
+ end
87
+ end
88
+
89
+ def add_batch(keys, batch)
90
+ xml = "#{@XML_HEADER}<sObjects xmlns=\"http://www.force.com/2009/06/asyncapi/dataload\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
91
+ batch.each do |r|
92
+ xml += create_sobject(keys, r)
93
+ end
94
+ xml += '</sObjects>'
95
+ path = "job/#{@job_id}/batch/"
96
+ headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
97
+ response = @connection.post_xml(nil, path, xml, headers)
98
+ response_parsed = XmlSimple.xml_in(response)
99
+ response_parsed['id'][0] if response_parsed['id']
100
+ end
101
+
102
+ def build_sobject(data)
103
+ xml = '<sObject>'
104
+ data.keys.each do |k|
105
+ if k.is_a?(Hash)
106
+ xml += build_sobject(k)
107
+ elsif data[k] != :type
108
+ xml += "<#{k}>#{data[k]}</#{k}>"
109
+ end
110
+ end
111
+ xml += '</sObject>'
112
+ end
113
+
114
+ def create_sobject(keys, r)
115
+ sobject_xml = '<sObject>'
116
+ keys.each do |k|
117
+ if r[k].is_a?(Hash)
118
+ sobject_xml += "<#{k}>"
119
+ sobject_xml += build_sobject(r[k])
120
+ sobject_xml += "</#{k}>"
121
+ elsif !r[k].to_s.empty?
122
+ sobject_xml += "<#{k}>"
123
+ if r[k].respond_to?(:encode)
124
+ sobject_xml += r[k].encode(:xml => :text)
125
+ elsif r[k].respond_to?(:iso8601) # timestamps
126
+ sobject_xml += r[k].iso8601.to_s
127
+ else
128
+ sobject_xml += r[k].to_s
129
+ end
130
+ sobject_xml += "</#{k}>"
131
+ elsif @send_nulls && !@no_null_list.include?(k)
132
+ sobject_xml += "<#{k} xsi:nil=\"true\"/>"
133
+ end
134
+ end
135
+ sobject_xml += '</sObject>'
136
+ sobject_xml
137
+ end
138
+
139
+ def check_job_status
140
+ path = "job/#{@job_id}"
141
+ headers = Hash.new
142
+ response = @connection.get_request(nil, path, headers)
143
+
144
+ begin
145
+ response_parsed = XmlSimple.xml_in(response) if response
146
+ response_parsed
147
+ rescue StandardError => e
148
+ puts "Error parsing XML response for #{@job_id}"
149
+ puts e
150
+ puts e.backtrace
151
+ end
152
+ end
153
+
154
+ def check_batch_status(batch_id)
155
+ path = "job/#{@job_id}/batch/#{batch_id}"
156
+ headers = Hash.new
157
+
158
+ response = @connection.get_request(nil, path, headers)
159
+
160
+ begin
161
+ response_parsed = XmlSimple.xml_in(response) if response
162
+ response_parsed
163
+ rescue StandardError => e
164
+ puts "Error parsing XML response for #{@job_id}, batch #{batch_id}"
165
+ puts e
166
+ puts e.backtrace
167
+ end
168
+ end
169
+
170
+ def get_job_result(return_result, timeout)
171
+ # timeout is in seconds
172
+ begin
173
+ state = []
174
+ Timeout::timeout(timeout, SalesforceBulkApiSerialOrParallel::JobTimeout) do
175
+ while true
176
+ job_status = self.check_job_status
177
+ if job_status && job_status['state'] && job_status['state'][0] == 'Closed'
178
+ batch_statuses = {}
179
+
180
+ batches_ready = @batch_ids.all? do |batch_id|
181
+ batch_state = batch_statuses[batch_id] = self.check_batch_status(batch_id)
182
+ batch_state && batch_state['state'] && batch_state['state'][0] && !['Queued', 'InProgress'].include?(batch_state['state'][0])
183
+ end
184
+
185
+ if batches_ready
186
+ @batch_ids.each do |batch_id|
187
+ state.insert(0, batch_statuses[batch_id])
188
+ @batch_ids.delete(batch_id)
189
+ end
190
+ end
191
+ break if @batch_ids.empty?
192
+ else
193
+ break
194
+ end
195
+ end
196
+ end
197
+ rescue SalesforceBulkApiSerialOrParallel::JobTimeout => e
198
+ puts 'Timeout waiting for Salesforce to process job batches #{@batch_ids} of job #{@job_id}.'
199
+ puts e
200
+ raise
201
+ end
202
+
203
+ state.each_with_index do |batch_state, i|
204
+ if batch_state['state'][0] == 'Completed' && return_result == true
205
+ state[i].merge!({'response' => self.get_batch_result(batch_state['id'][0])})
206
+ end
207
+ end
208
+ state
209
+ end
210
+
211
+ def get_batch_result(batch_id)
212
+ path = "job/#{@job_id}/batch/#{batch_id}/result"
213
+ headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
214
+
215
+ response = @connection.get_request(nil, path, headers)
216
+ response_parsed = XmlSimple.xml_in(response)
217
+ results = response_parsed['result'] unless @operation == 'query'
218
+
219
+ if(@operation == 'query') # The query op requires us to do another request to get the results
220
+ result_id = response_parsed["result"][0]
221
+ path = "job/#{@job_id}/batch/#{batch_id}/result/#{result_id}"
222
+ headers = Hash.new
223
+ headers = Hash["Content-Type" => "application/xml; charset=UTF-8"]
224
+ response = @connection.get_request(nil, path, headers)
225
+ response_parsed = XmlSimple.xml_in(response)
226
+ results = response_parsed['records']
227
+ end
228
+ results
229
+ end
230
+
231
+ end
232
+
233
+ class JobTimeout < StandardError
234
+ end
235
+ end
@@ -0,0 +1,3 @@
1
+ module SalesforceBulkApiSerialOrParallel
2
+ VERSION = '0.1'
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "salesforce_bulk_api_serial_or_parallel/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'salesforce_bulk_api_serial_or_parallel'
7
+ s.version = SalesforceBulkApiSerialOrParallel::VERSION
8
+ s.authors = ['Brendan Keogh']
9
+ s.email = ['bkeogh123@gmail.com']
10
+
11
+ s.homepage = 'https://github.com/beekermememe/salesforce_bulk_api_serial_or_parallel'
12
+ s.summary = %q{It uses the bulk api of salesforce to communicate with Salesforce CRM}
13
+ s.description = %q{Salesforce Bulk API with governor limits taken care of, this is a fork off of yatish27/salesforce_bulk_api_serial_or_parallel that just adds serial/parallel concurrency support}
14
+
15
+ s.rubyforge_project = 'salesforce_bulk_api_serial_or_parallel'
16
+
17
+ s.add_dependency('json', ['>= 0'])
18
+ s.add_dependency('xml-simple', ['>= 0'])
19
+
20
+ s.add_development_dependency 'rspec'
21
+ s.add_development_dependency 'restforce', '~> 1.5.1'
22
+ s.add_development_dependency 'rake', '~> 10.4.2'
23
+ s.add_development_dependency 'pry'
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
28
+ s.require_paths = ['lib']
29
+
30
+ end
@@ -0,0 +1,193 @@
1
+ require 'spec_helper'
2
+ require 'yaml'
3
+ require 'restforce'
4
+
5
+ describe SalesforceBulkApiSerialOrParallel do
6
+
7
+ before :each do
8
+ auth_hash = YAML.load_file('auth_credentials.yml')
9
+ sfdc_auth_hash = auth_hash['salesforce']
10
+
11
+ @sf_client = Restforce.new(
12
+ username: sfdc_auth_hash['user'],
13
+ password: sfdc_auth_hash['passwordandtoken'],
14
+ client_id: sfdc_auth_hash['client_id'],
15
+ client_secret: sfdc_auth_hash['client_secret'],
16
+ host: sfdc_auth_hash['host'])
17
+ @sf_client.authenticate!
18
+
19
+ @account_id = auth_hash['salesforce']['test_account_id']
20
+
21
+ @api = SalesforceBulkApiSerialOrParallel::Api.new(@sf_client)
22
+ end
23
+
24
+ after :each do
25
+
26
+ end
27
+
28
+ describe 'upsert' do
29
+
30
+ context 'when not passed get_result' do
31
+ it "doesn't return the batches array" do
32
+ res = @api.upsert('Account', [{:Id => @account_id, :Website => 'www.test.com'}], 'Id')
33
+ res['batches'].should be_nil
34
+ end
35
+ end
36
+
37
+ context 'when passed get_result = true' do
38
+ it 'returns the batches array' do
39
+ res = @api.upsert('Account', [{:Id => @account_id, :Website => 'www.test.com'}], 'Id', true)
40
+ res['batches'][0]['response'].is_a? Array
41
+
42
+ res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
43
+ res['batches'][0]['response'][0]['success'].should eq ['true']
44
+ res['batches'][0]['response'][0]['created'].should eq ['false']
45
+
46
+ end
47
+ end
48
+
49
+ context 'when passed send_nulls = true' do
50
+ it 'sets the nil and empty attributes to NULL' do
51
+ @api.update('Account', [{:Id => @account_id, :Website => 'abc123', :Phone => '5678'}], true)
52
+ res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
53
+ res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
54
+ res['batches'][0]['response'][0]['Phone'][0].should eq '5678'
55
+ res = @api.upsert('Account', [{:Id => @account_id, :Website => '', :Phone => nil}], 'Id', true, true)
56
+ res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
57
+ res['batches'][0]['response'][0]['success'].should eq ['true']
58
+ res['batches'][0]['response'][0]['created'].should eq ['false']
59
+ res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
60
+ res['batches'][0]['response'][0]['Website'][0].should eq({"xsi:nil" => "true"})
61
+ res['batches'][0]['response'][0]['Phone'][0].should eq({"xsi:nil" => "true"})
62
+ end
63
+ end
64
+
65
+ context 'when passed send_nulls = true and an array of fields not to null' do
66
+ it 'sets the nil and empty attributes to NULL, except for those included in the list of fields to ignore' do
67
+ @api.update('Account', [{:Id => @account_id, :Website => 'abc123', :Phone => '5678'}], true)
68
+ res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
69
+ res['batches'][0]['response'][0]['Website'][0].should eq 'abc123'
70
+ res['batches'][0]['response'][0]['Phone'][0].should eq '5678'
71
+ res = @api.upsert('Account', [{:Id => @account_id, :Website => '', :Phone => nil}], 'Id', true, true, [:Website, :Phone])
72
+ res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
73
+ res['batches'][0]['response'][0]['success'].should eq ['true']
74
+ res['batches'][0]['response'][0]['created'].should eq ['false']
75
+ res = @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
76
+ res['batches'][0]['response'][0]['Website'][0].should eq('abc123')
77
+ res['batches'][0]['response'][0]['Phone'][0].should eq('5678')
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ describe 'update' do
84
+ context 'when there is not an error' do
85
+ context 'when not passed get_result' do
86
+ it "doesnt return the batches array" do
87
+ res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}])
88
+ res['batches'].should be_nil
89
+ end
90
+ end
91
+
92
+ context 'when passed get_result = true' do
93
+ it 'returns the batches array' do
94
+ res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}], true)
95
+ res['batches'][0]['response'].is_a? Array
96
+ res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
97
+ res['batches'][0]['response'][0]['success'].should eq ['true']
98
+ res['batches'][0]['response'][0]['created'].should eq ['false']
99
+ end
100
+ end
101
+ end
102
+
103
+ context 'when there is an error' do
104
+ context 'when not passed get_result' do
105
+ it "doesn't return the results array" do
106
+ res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'},{:Id => 'abc123', :Website => 'www.test.com'}])
107
+ res['batches'].should be_nil
108
+ end
109
+ end
110
+
111
+ context 'when passed get_result = true with batches' do
112
+ it 'returns the results array' do
113
+ res = @api.update('Account', [{:Id => @account_id, :Website => 'www.test.com'}, {:Id => @account_id, :Website => 'www.test.com'}, {:Id => @account_id, :Website => 'www.test.com'}, {:Id => 'abc123', :Website => 'www.test.com'}], true, false, [], 2)
114
+
115
+ res['batches'][0]['response'][0]['id'][0].should start_with(@account_id)
116
+ res['batches'][0]['response'][0]['success'].should eq ['true']
117
+ res['batches'][0]['response'][0]['created'].should eq ['false']
118
+ res['batches'][0]['response'][1]['id'][0].should start_with(@account_id)
119
+ res['batches'][0]['response'][1]['success'].should eq ['true']
120
+ res['batches'][0]['response'][1]['created'].should eq ['false']
121
+
122
+ res['batches'][1]['response'][0]['id'][0].should start_with(@account_id)
123
+ res['batches'][1]['response'][0]['success'].should eq ['true']
124
+ res['batches'][1]['response'][0]['created'].should eq ['false']
125
+ res['batches'][1]['response'][1].should eq({"errors"=>[{"fields"=>["Id"], "message"=>["Account ID: id value of incorrect type: abc123"], "statusCode"=>["MALFORMED_ID"]}], "success"=>["false"], "created"=>["false"]})
126
+ end
127
+ end
128
+ end
129
+
130
+ end
131
+
132
+ describe 'create' do
133
+ pending
134
+ end
135
+
136
+ describe 'delete' do
137
+ pending
138
+ end
139
+
140
+ describe 'query' do
141
+
142
+ context 'when there are results' do
143
+ it 'returns the query results' do
144
+ res = @api.query('Account', "SELECT id, Name From Account WHERE Name LIKE 'Test%'")
145
+ res['batches'][0]['response'].length.should > 1
146
+ res['batches'][0]['response'][0]['Id'].should_not be_nil
147
+ end
148
+
149
+ context 'and there are multiple batches' do
150
+ # need dev to create > 10k records in dev organization
151
+ it 'returns the query results in a merged hash'
152
+ end
153
+ end
154
+
155
+ context 'when there are no results' do
156
+ it 'returns nil' do
157
+ res = @api.query('Account', "SELECT id From Account WHERE Name = 'ABC'")
158
+ res['batches'][0]['response'].should eq nil
159
+ end
160
+ end
161
+
162
+ context 'when there is an error' do
163
+ it 'returns nil' do
164
+ res = @api.query('Account', "SELECT id From Account WHERE Name = ''ABC'")
165
+ res['batches'][0]['response'].should eq nil
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ describe 'counters' do
172
+ context 'when read operations are called' do
173
+ it 'increments operation count and http GET count' do
174
+ @api.counters[:http_get].should eq 0
175
+ @api.counters[:query].should eq 0
176
+ @api.query('Account', "SELECT Website, Phone From Account WHERE Id = '#{@account_id}'")
177
+ @api.counters[:http_get].should eq 1
178
+ @api.counters[:query].should eq 1
179
+ end
180
+ end
181
+
182
+ context 'when update operations are called' do
183
+ it 'increments operation count and http POST count' do
184
+ @api.counters[:http_post].should eq 0
185
+ @api.counters[:update].should eq 0
186
+ @api.update('Account', [{:Id => @account_id, :Website => 'abc123', :Phone => '5678'}], true)
187
+ @api.counters[:http_post].should eq 1
188
+ @api.counters[:update].should eq 1
189
+ end
190
+ end
191
+ end
192
+
193
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'salesforce_bulk_api_serial_or_parallel'
4
+
5
+ RSpec.configure do |c|
6
+ c.filter_run :focus => true
7
+ c.run_all_when_everything_filtered = true
8
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: salesforce_bulk_api_serial_or_parallel
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Brendan Keogh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: xml-simple
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: restforce
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.5.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.5.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 10.4.2
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 10.4.2
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Salesforce Bulk API with governor limits taken care of, this is a fork
98
+ off of yatish27/salesforce_bulk_api_serial_or_parallel that just adds serial/parallel
99
+ concurrency support
100
+ email:
101
+ - bkeogh123@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - Gemfile
109
+ - LICENCE
110
+ - README.md
111
+ - Rakefile
112
+ - auth_credentials.yml.example
113
+ - lib/salesforce_bulk_api_serial_or_parallel.rb
114
+ - lib/salesforce_bulk_api_serial_or_parallel/concerns/throttling.rb
115
+ - lib/salesforce_bulk_api_serial_or_parallel/connection.rb
116
+ - lib/salesforce_bulk_api_serial_or_parallel/job.rb
117
+ - lib/salesforce_bulk_api_serial_or_parallel/version.rb
118
+ - salesforce_bulk_api_serial_or_parallel.gemspec
119
+ - spec/salesforce_bulk_api_serial_or_parallel/salesforce_bulk_api_serial_or_parallel.rb
120
+ - spec/spec_helper.rb
121
+ homepage: https://github.com/beekermememe/salesforce_bulk_api_serial_or_parallel
122
+ licenses: []
123
+ metadata: {}
124
+ post_install_message:
125
+ rdoc_options: []
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ requirements: []
139
+ rubyforge_project: salesforce_bulk_api_serial_or_parallel
140
+ rubygems_version: 2.6.13
141
+ signing_key:
142
+ specification_version: 4
143
+ summary: It uses the bulk api of salesforce to communicate with Salesforce CRM
144
+ test_files:
145
+ - spec/salesforce_bulk_api_serial_or_parallel/salesforce_bulk_api_serial_or_parallel.rb
146
+ - spec/spec_helper.rb